@attraccess/plugin-wago 0.1.0-nightly.1089.1 → 0.1.0-nightly.1106.2

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
@@ -957,10 +957,10 @@ function sameRuntimeImage(left, right) {
957
957
  const digest = left.image.split("@")[1];
958
958
  return Boolean(digest) && digest === right.image.split("@")[1];
959
959
  }
960
- var import_common11, import_node_path, WagoBuildRuntimeCatalog;
960
+ var import_common12, import_node_path, WagoBuildRuntimeCatalog;
961
961
  var init_build = __esm({
962
962
  "backend/runtime/artifacts/build.ts"() {
963
- import_common11 = require("@nestjs/common");
963
+ import_common12 = require("@nestjs/common");
964
964
  import_node_path = require("node:path");
965
965
  init_catalog();
966
966
  init_verification();
@@ -976,7 +976,7 @@ var init_build = __esm({
976
976
  }
977
977
  async import(upload) {
978
978
  for (const stream of Object.values(upload)) stream.destroy();
979
- throw new import_common11.ConflictException("The deployed server build owns the CC100 runtime. Deploy a new build to change it.");
979
+ throw new import_common12.ConflictException("The deployed server build owns the CC100 runtime. Deploy a new build to change it.");
980
980
  }
981
981
  current() {
982
982
  if (!this.build) {
@@ -992,7 +992,7 @@ var init_build = __esm({
992
992
  }
993
993
  async acquire(digest) {
994
994
  const build = await this.current();
995
- if (digest && digest !== build.digest) throw new import_common11.ConflictException("Runtime does not belong to this server build.");
995
+ if (digest && digest !== build.digest) throw new import_common12.ConflictException("Runtime does not belong to this server build.");
996
996
  return Object.freeze({ ...await super.acquire(build.digest), ...build });
997
997
  }
998
998
  async loadBuild() {
@@ -1097,7 +1097,7 @@ async function writeArtifactStream(path, source, limit) {
1097
1097
  write(chunk, _encoding, callback) {
1098
1098
  bytes += chunk.length;
1099
1099
  if (bytes > limit) {
1100
- callback(new import_common12.BadRequestException("Runtime artifact upload exceeds the size limit"));
1100
+ callback(new import_common13.BadRequestException("Runtime artifact upload exceeds the size limit"));
1101
1101
  return;
1102
1102
  }
1103
1103
  const write = async () => {
@@ -1115,10 +1115,10 @@ async function writeArtifactStream(path, source, limit) {
1115
1115
  }
1116
1116
  return bytes;
1117
1117
  }
1118
- var import_common12, import_node_crypto9, import_node_fs, import_promises, import_node_os, import_node_path2, import_node_stream, import_promises2, digestPattern, hostId, processId, uuidPattern, temporaryPattern, WagoRuntimeArtifactCatalog, WagoRuntimeArtifactsService;
1118
+ var import_common13, import_node_crypto9, import_node_fs, import_promises, import_node_os, import_node_path2, import_node_stream, import_promises2, digestPattern, hostId, processId, uuidPattern, temporaryPattern, WagoRuntimeArtifactCatalog, WagoRuntimeArtifactsService;
1119
1119
  var init_catalog = __esm({
1120
1120
  "backend/runtime/artifacts/catalog.ts"() {
1121
- import_common12 = require("@nestjs/common");
1121
+ import_common13 = require("@nestjs/common");
1122
1122
  import_node_crypto9 = require("node:crypto");
1123
1123
  import_node_fs = require("node:fs");
1124
1124
  import_promises = __toESM(require("node:fs/promises"));
@@ -1147,7 +1147,7 @@ var init_catalog = __esm({
1147
1147
  async acquire(digest) {
1148
1148
  const selected = digest ?? (await this.current())?.digest;
1149
1149
  if (!selected || !digestPattern.test(selected))
1150
- throw new import_common12.ConflictException("Import a verified runtime release first");
1150
+ throw new import_common13.ConflictException("Import a verified runtime release first");
1151
1151
  const root = await this.root();
1152
1152
  await this.verifiedMetadata(root, selected);
1153
1153
  const directory = await this.createTemporaryDirectory((0, import_node_path2.join)(root, "snapshots"), "delivery");
@@ -1191,7 +1191,7 @@ var init_catalog = __esm({
1191
1191
  return result;
1192
1192
  }
1193
1193
  async get(digest) {
1194
- if (!digestPattern.test(digest)) throw new import_common12.ConflictException("Select a verified runtime release.");
1194
+ if (!digestPattern.test(digest)) throw new import_common13.ConflictException("Select a verified runtime release.");
1195
1195
  return this.verifiedMetadata(await this.root(), digest);
1196
1196
  }
1197
1197
  async current() {
@@ -1209,7 +1209,7 @@ var init_catalog = __esm({
1209
1209
  async ingest(upload, publish) {
1210
1210
  if (this.activeImports >= 2) {
1211
1211
  for (const source of Object.values(upload)) source.destroy();
1212
- throw new import_common12.ConflictException("Another runtime import is in progress; retry shortly");
1212
+ throw new import_common13.ConflictException("Another runtime import is in progress; retry shortly");
1213
1213
  }
1214
1214
  this.activeImports++;
1215
1215
  let directory;
@@ -1266,8 +1266,8 @@ var init_catalog = __esm({
1266
1266
  }
1267
1267
  return metadata2;
1268
1268
  } catch (error) {
1269
- if (error instanceof import_common12.ConflictException) throw error;
1270
- throw new import_common12.BadRequestException(
1269
+ if (error instanceof import_common13.ConflictException) throw error;
1270
+ throw new import_common13.BadRequestException(
1271
1271
  "Runtime import failed. Check the release files, size, and manifest compatibility."
1272
1272
  );
1273
1273
  } finally {
@@ -1456,16 +1456,16 @@ var init_catalog = __esm({
1456
1456
  return current ? [current] : [];
1457
1457
  }
1458
1458
  async acquire(digest) {
1459
- if (!await this.current()) throw new import_common12.ConflictException("Build and install the bundled CC100 runtime first.");
1459
+ if (!await this.current()) throw new import_common13.ConflictException("Build and install the bundled CC100 runtime first.");
1460
1460
  return (await this.buildCatalog()).acquire(digest);
1461
1461
  }
1462
1462
  async import(upload) {
1463
1463
  for (const stream of Object.values(upload)) stream.destroy();
1464
- throw new import_common12.ConflictException("The server build owns the CC100 runtime. Custom runtime imports are not supported.");
1464
+ throw new import_common13.ConflictException("The server build owns the CC100 runtime. Custom runtime imports are not supported.");
1465
1465
  }
1466
1466
  };
1467
1467
  WagoRuntimeArtifactsService = __decorateClass([
1468
- (0, import_common12.Injectable)()
1468
+ (0, import_common13.Injectable)()
1469
1469
  ], WagoRuntimeArtifactsService);
1470
1470
  }
1471
1471
  });
@@ -1479,7 +1479,7 @@ module.exports = __toCommonJS(plugin_exports);
1479
1479
 
1480
1480
  // backend/controllers/controller.ts
1481
1481
  var import_plugins_backend_sdk16 = require("@attraccess/plugins-backend-sdk");
1482
- var import_common30 = require("@nestjs/common");
1482
+ var import_common31 = require("@nestjs/common");
1483
1483
 
1484
1484
  // backend/audit/index.ts
1485
1485
  var import_common = require("@nestjs/common");
@@ -1542,6 +1542,17 @@ function parseMeasurement(value) {
1542
1542
  };
1543
1543
  }
1544
1544
 
1545
+ // modbus/channel-enabled.ts
1546
+ var DISABLEABLE_CAPABILITIES = /* @__PURE__ */ new Set(["input", "measurement"]);
1547
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1548
+ function canDisableChannel(capabilities, point) {
1549
+ const list = [...capabilities];
1550
+ const binding = isRecord(point) && isRecord(point.modbus) ? point.modbus : void 0;
1551
+ return Boolean(
1552
+ isRecord(point) && point.hardwareProfile === "modbus" && binding && typeof binding.measurementId === "string" && binding.measurementId && binding.actionId === void 0 && list.length && list.every((capability) => typeof capability === "string" && DISABLEABLE_CAPABILITIES.has(capability))
1553
+ );
1554
+ }
1555
+
1545
1556
  // modbus/model.ts
1546
1557
  var import_ipaddr = __toESM(require_ipaddr());
1547
1558
 
@@ -1695,6 +1706,19 @@ var base = {
1695
1706
  };
1696
1707
  var field = (id, name, address, unit, section, scale = 1, kind = "live") => ({ ...base, id, name, address, unit, section, scale, kind });
1697
1708
  var information = (id, name, address, dataType = "uint16", extra = {}) => ({ ...field(id, name, address, "number", "information"), dataType, ...extra });
1709
+ var WAGO_879_3020_DEFAULT_ENABLED = Object.freeze([
1710
+ "active-power",
1711
+ "import-energy",
1712
+ "export-energy",
1713
+ "voltage-l1",
1714
+ "voltage-l2",
1715
+ "voltage-l3",
1716
+ "current-l1",
1717
+ "current-l2",
1718
+ "current-l3",
1719
+ "frequency",
1720
+ "current"
1721
+ ]);
1698
1722
  function wago8793020Measurements() {
1699
1723
  const original = [
1700
1724
  field("active-power", "Active power", 20498, "watt", "electrical", 1e3),
@@ -2132,18 +2156,40 @@ function isNullableString(value) {
2132
2156
  function operationalWildcardTopic(prefix) {
2133
2157
  return `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/+/#`;
2134
2158
  }
2135
- function parseObject(payload, label) {
2136
- let value;
2159
+ var CONTROLLER_PAYLOAD_MAX_BYTES = 64 * 1024;
2160
+ var CONTROLLER_PAYLOAD_MAX_DEPTH = 8;
2161
+ function exceedsJsonDepth(text, maxDepth) {
2162
+ let depth = 0;
2163
+ let inString = false;
2164
+ for (let index = 0; index < text.length; index += 1) {
2165
+ const char = text[index];
2166
+ if (inString) {
2167
+ if (char === "\\") index += 1;
2168
+ else if (char === '"') inString = false;
2169
+ } else if (char === '"') inString = true;
2170
+ else if (char === "{" || char === "[") {
2171
+ depth += 1;
2172
+ if (depth > maxDepth) return true;
2173
+ } else if (char === "}" || char === "]") depth -= 1;
2174
+ }
2175
+ return false;
2176
+ }
2177
+ function parseControllerJson(payload, label) {
2178
+ if (payload.byteLength > CONTROLLER_PAYLOAD_MAX_BYTES) throw new Error(`${label} is too large`);
2179
+ const text = payload.toString("utf8");
2180
+ if (exceedsJsonDepth(text, CONTROLLER_PAYLOAD_MAX_DEPTH)) throw new Error(`${label} is nested too deeply`);
2137
2181
  try {
2138
- value = JSON.parse(payload.toString("utf8"));
2182
+ return JSON.parse(text);
2139
2183
  } catch {
2140
2184
  throw new Error(`${label} is not valid JSON`);
2141
2185
  }
2186
+ }
2187
+ function parseObject(payload, label) {
2188
+ const value = parseControllerJson(payload, label);
2142
2189
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
2143
2190
  return value;
2144
2191
  }
2145
- function parseHeartbeat(payload) {
2146
- const input = parseObject(payload, "announcement");
2192
+ function heartbeatFromObject(input) {
2147
2193
  const required = ["hardwareId", "protocolVersion", "runtimeVersion"];
2148
2194
  for (const key of required)
2149
2195
  if (typeof input[key] !== "string" || !input[key].trim()) throw new Error(`announcement ${key} is required`);
@@ -2168,8 +2214,8 @@ function parseHeartbeat(payload) {
2168
2214
  };
2169
2215
  }
2170
2216
  function parseAnnouncement(payload) {
2171
- const heartbeat = parseHeartbeat(payload);
2172
- const input = JSON.parse(payload.toString("utf8"));
2217
+ const input = parseObject(payload, "announcement");
2218
+ const heartbeat = heartbeatFromObject(input);
2173
2219
  if (typeof input.pairingCode !== "string" || !input.pairingCode.trim())
2174
2220
  throw new Error("announcement pairingCode is required");
2175
2221
  return { ...heartbeat, pairingCode: input.pairingCode.trim() };
@@ -2367,6 +2413,7 @@ function validateFeedback(value, path, capabilities, currentChannelId, channelsB
2367
2413
  code: "invalid_feedback_channel",
2368
2414
  message: "feedback must reference a distinct input channel"
2369
2415
  });
2416
+ if (feedbackChannel?.enabled === false) errors.push(disabledReferenceError(`${path}.channelId`));
2370
2417
  if (!["match", "inverse"].includes(value.expected))
2371
2418
  errors.push({ path: `${path}.expected`, code: "unsupported_value", message: "expected must be match or inverse" });
2372
2419
  if (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0)
@@ -2378,17 +2425,35 @@ function validateFeedback(value, path, capabilities, currentChannelId, channelsB
2378
2425
  if (!capabilities.has("feedback"))
2379
2426
  errors.push({ path, code: "unsupported_field", message: "feedback requires feedback capability" });
2380
2427
  }
2381
- function validateGuard(value, path, capabilities, channelIds, errors) {
2428
+ function validateGuard(value, path, capabilities, channelIds, errors, disabledChannelIds = /* @__PURE__ */ new Set()) {
2382
2429
  if (value === void 0) return;
2383
2430
  if (!record(value, path, errors)) return;
2384
2431
  exactKeys(value, path, ["channelId", "when"], errors);
2385
2432
  if (typeof value.channelId !== "string" || !channelIds.has(value.channelId))
2386
2433
  errors.push(referenceError(`${path}.channelId`, "logical channel", value.channelId));
2434
+ else if (disabledChannelIds.has(value.channelId)) errors.push(disabledReferenceError(`${path}.channelId`));
2387
2435
  if (!["on", "off"].includes(value.when))
2388
2436
  errors.push({ path: `${path}.when`, code: "unsupported_value", message: "when must be on or off" });
2389
2437
  if (!capabilities.has("guard"))
2390
2438
  errors.push({ path, code: "unsupported_field", message: "guard requires guard capability" });
2391
2439
  }
2440
+ function disabledReferenceError(path) {
2441
+ return { path, code: "disabled_reference", message: "a guard or feedback cannot use a disabled channel" };
2442
+ }
2443
+ function validateEnabled(channel, path, capabilities, pointsById, errors) {
2444
+ if (channel.enabled === void 0) return;
2445
+ if (channel.enabled !== false) {
2446
+ errors.push({ path: `${path}.enabled`, code: "invalid_enabled", message: "enabled must be false or omitted" });
2447
+ return;
2448
+ }
2449
+ const point = typeof channel.physicalPointId === "string" ? pointsById.get(channel.physicalPointId) : void 0;
2450
+ if (!canDisableChannel(capabilities, point))
2451
+ errors.push({
2452
+ path: `${path}.enabled`,
2453
+ code: "invalid_enabled",
2454
+ message: "only Modbus measurement channels without an action can be disabled"
2455
+ });
2456
+ }
2392
2457
  function validateMeasurement(value, path, capabilities, errors) {
2393
2458
  if (value === void 0) return;
2394
2459
  if (!record(value, path, errors)) return;
@@ -2453,7 +2518,7 @@ function collection(value, path, errors) {
2453
2518
  function configurationHash(snapshot) {
2454
2519
  return (0, import_node_crypto.createHash)("sha256").update(canonicalSnapshot(snapshot)).digest("hex");
2455
2520
  }
2456
- function isRecord(value) {
2521
+ function isRecord2(value) {
2457
2522
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2458
2523
  }
2459
2524
  function diffValue(path, previous, current, changes) {
@@ -2464,7 +2529,7 @@ function diffValue(path, previous, current, changes) {
2464
2529
  diffValue(`${path}[${index}]`, previous[index], current[index], changes);
2465
2530
  return;
2466
2531
  }
2467
- if (isRecord(previous) && isRecord(current)) {
2532
+ if (isRecord2(previous) && isRecord2(current)) {
2468
2533
  const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
2469
2534
  for (const key of [...keys].sort()) diffValue(`${path}.${key}`, previous[key], current[key], changes);
2470
2535
  return;
@@ -2494,7 +2559,7 @@ function exactKeys(value, path, allowed, errors, optional = []) {
2494
2559
  );
2495
2560
  }
2496
2561
  function isConfigurationValidationError(value) {
2497
- return isRecord(value) && typeof value.path === "string" && Boolean(value.path.trim()) && typeof value.code === "string" && Boolean(value.code.trim()) && typeof value.message === "string" && Boolean(value.message.trim());
2562
+ return isRecord2(value) && typeof value.path === "string" && Boolean(value.path.trim()) && typeof value.code === "string" && Boolean(value.code.trim()) && typeof value.message === "string" && Boolean(value.message.trim());
2498
2563
  }
2499
2564
  function parseConfigurationReport(value) {
2500
2565
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@@ -2554,11 +2619,14 @@ function validateSnapshot(snapshot) {
2554
2619
  const pointIds = /* @__PURE__ */ new Set();
2555
2620
  const channelIds = /* @__PURE__ */ new Set();
2556
2621
  const channelsById = /* @__PURE__ */ new Map();
2622
+ const pointsById = /* @__PURE__ */ new Map();
2623
+ const disabledChannelIds = /* @__PURE__ */ new Set();
2557
2624
  points.forEach((point, index) => {
2558
2625
  const path = `physicalPoints[${index}]`;
2559
2626
  if (!record(point, path, errors)) return;
2560
2627
  exactKeys(point, path, ["id", "hardwareProfile", "channel", "modbus"], errors, ["modbus"]);
2561
2628
  addId(point.id, `${path}.id`, pointIds, errors);
2629
+ if (typeof point.id === "string" && !pointsById.has(point.id)) pointsById.set(point.id, point);
2562
2630
  enumValue(point.hardwareProfile, `${path}.hardwareProfile`, HARDWARE_PROFILES, errors);
2563
2631
  if (typeof point.channel !== "number" || !Number.isSafeInteger(point.channel) || point.channel < 0)
2564
2632
  errors.push({
@@ -2572,6 +2640,7 @@ function validateSnapshot(snapshot) {
2572
2640
  const item = channel;
2573
2641
  addId(item.id, `logicalChannels[${index}].id`, channelIds, errors);
2574
2642
  if (typeof item.id === "string") channelsById.set(item.id, item);
2643
+ if (typeof item.id === "string" && item.enabled === false) disabledChannelIds.add(item.id);
2575
2644
  }
2576
2645
  });
2577
2646
  channels.forEach((channel, index) => {
@@ -2585,6 +2654,7 @@ function validateSnapshot(snapshot) {
2585
2654
  "physicalPointId",
2586
2655
  "profile",
2587
2656
  "capabilities",
2657
+ "enabled",
2588
2658
  "invert",
2589
2659
  "disconnectPolicy",
2590
2660
  "range",
@@ -2593,7 +2663,8 @@ function validateSnapshot(snapshot) {
2593
2663
  "feedback",
2594
2664
  "measurement"
2595
2665
  ],
2596
- errors
2666
+ errors,
2667
+ ["enabled"]
2597
2668
  );
2598
2669
  if (typeof channel.physicalPointId !== "string" || !pointIds.has(channel.physicalPointId))
2599
2670
  errors.push(referenceError(`${path}.physicalPointId`, "physical point", channel.physicalPointId));
@@ -2605,10 +2676,11 @@ function validateSnapshot(snapshot) {
2605
2676
  code: "invalid_invert",
2606
2677
  message: "invert requires a boolean and an input channel"
2607
2678
  });
2679
+ validateEnabled(channel, path, capabilities, pointsById, errors);
2608
2680
  validateDisconnectPolicy(channel.disconnectPolicy, `${path}.disconnectPolicy`, errors);
2609
2681
  validateRange(channel.range, `${path}.range`, capabilities, errors);
2610
2682
  validatePulse(channel.pulse, `${path}.pulse`, capabilities, errors);
2611
- validateGuard(channel.guard, `${path}.guard`, capabilities, channelIds, errors);
2683
+ validateGuard(channel.guard, `${path}.guard`, capabilities, channelIds, errors, disabledChannelIds);
2612
2684
  validateFeedback(channel.feedback, `${path}.feedback`, capabilities, channel.id, channelsById, errors);
2613
2685
  validateMeasurement(channel.measurement, `${path}.measurement`, capabilities, errors);
2614
2686
  });
@@ -2990,12 +3062,12 @@ function parseManagementInspection(output) {
2990
3062
  }
2991
3063
 
2992
3064
  // backend/commissioning/service.ts
2993
- var import_common28 = require("@nestjs/common");
3065
+ var import_common29 = require("@nestjs/common");
2994
3066
  var import_promises9 = require("node:fs/promises");
2995
3067
  var import_plugins_backend_sdk14 = require("@attraccess/plugins-backend-sdk");
2996
3068
 
2997
3069
  // backend/controllers/service.ts
2998
- var import_common9 = require("@nestjs/common");
3070
+ var import_common10 = require("@nestjs/common");
2999
3071
 
3000
3072
  // backend/controllers/model.ts
3001
3073
  var import_common3 = require("@nestjs/common");
@@ -3046,7 +3118,7 @@ function safeEqual(left, right) {
3046
3118
  }
3047
3119
  function isClaimAcknowledgement(payload, token) {
3048
3120
  try {
3049
- const value = JSON.parse(payload.toString("utf8"));
3121
+ const value = parseObject(payload, "claim acknowledgement");
3050
3122
  return typeof value.acknowledgementToken === "string" && safeEqual(value.acknowledgementToken, token);
3051
3123
  } catch {
3052
3124
  return false;
@@ -3073,6 +3145,11 @@ var MqttSubscriptionError = class extends Error {
3073
3145
  var WagoCredentialOperationUncertainError = class extends import_common3.ConflictException {
3074
3146
  };
3075
3147
 
3148
+ // shared/capabilities.ts
3149
+ var BOUNDED_MESSAGES_CAPABILITY = "bounded-messages-v1";
3150
+ var DROP_REASONS = ["oversized", "overload"];
3151
+ var CHANNEL_ENABLED_CAPABILITY = "channel-enabled-v1";
3152
+
3076
3153
  // shared/clock.ts
3077
3154
  var CONTROLLER_CLOCK_TOLERANCE_MS = 5e3;
3078
3155
 
@@ -3135,6 +3212,147 @@ function admitEnvelope(state, data, category, now) {
3135
3212
  return restart ? "restart" : "accepted";
3136
3213
  }
3137
3214
 
3215
+ // shared/configuration-limits.ts
3216
+ var CONFIGURATION_LIMIT_CODE = "configuration_limit_exceeded";
3217
+ var MAX_CONFIGURATION_BYTES = 1024 * 1024;
3218
+ var DESIRED_ENVELOPE_RESERVE_BYTES = 1024;
3219
+ var MAX_CONFIGURATION_DEPTH = 16;
3220
+ var MAX_IDENTIFIER_CHARS = 256;
3221
+ var MAX_CONFIGURATION_ITEMS = 2048;
3222
+ var MAX_ENABLED_CHANNELS = 256;
3223
+ var MAX_STATUS_REPORT_BYTES = 64 * 1024;
3224
+ var STATUS_ERROR_BUDGET_BYTES = 8 * 1024;
3225
+ var STATUS_FIXED_BYTES = 1024;
3226
+ var encoder = new TextEncoder();
3227
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/;
3228
+ var IDENTIFIER_KEY = /^(?:id|[A-Za-z]*Id)$/;
3229
+ var byteLength = (value) => encoder.encode(JSON.stringify(value)).byteLength;
3230
+ function isRecord3(value) {
3231
+ return !!value && typeof value === "object" && !Array.isArray(value);
3232
+ }
3233
+ function depthExceeds(root, max) {
3234
+ const stack = [[root, 1]];
3235
+ while (stack.length) {
3236
+ const [value, depth] = stack.pop();
3237
+ if (!value || typeof value !== "object") continue;
3238
+ if (depth > max) return true;
3239
+ for (const child of Array.isArray(value) ? value : Object.values(value)) stack.push([child, depth + 1]);
3240
+ }
3241
+ return false;
3242
+ }
3243
+ function identifierViolation(root) {
3244
+ const stack = [[root, "$"]];
3245
+ while (stack.length) {
3246
+ const [value, path] = stack.pop();
3247
+ if (Array.isArray(value)) value.forEach((item, index) => stack.push([item, `${path}[${index}]`]));
3248
+ else if (isRecord3(value))
3249
+ for (const [key, item] of Object.entries(value)) {
3250
+ if (typeof item === "string" && IDENTIFIER_KEY.test(key)) {
3251
+ if (item.length > MAX_IDENTIFIER_CHARS)
3252
+ return { limit: "identifier", path: `${path}.${key}`, reason: "length", max: MAX_IDENTIFIER_CHARS };
3253
+ if (CONTROL_CHARACTERS.test(item))
3254
+ return { limit: "identifier", path: `${path}.${key}`, reason: "control", max: MAX_IDENTIFIER_CHARS };
3255
+ }
3256
+ stack.push([item, `${path}.${key}`]);
3257
+ }
3258
+ }
3259
+ return void 0;
3260
+ }
3261
+ function channelEnabled(channel) {
3262
+ return !isRecord3(channel) || channel.enabled !== false;
3263
+ }
3264
+ var capabilitiesOf = (channel) => Array.isArray(channel.capabilities) ? channel.capabilities : [];
3265
+ function worstCaseStatusReportBytes(snapshot) {
3266
+ const pointIsModbus = /* @__PURE__ */ new Map();
3267
+ for (const point of Array.isArray(snapshot.physicalPoints) ? snapshot.physicalPoints : [])
3268
+ if (isRecord3(point) && typeof point.id === "string" && !pointIsModbus.has(point.id))
3269
+ pointIsModbus.set(point.id, Boolean(point.modbus));
3270
+ let total = STATUS_FIXED_BYTES + STATUS_ERROR_BUDGET_BYTES;
3271
+ for (const channel of Array.isArray(snapshot.logicalChannels) ? snapshot.logicalChannels : []) {
3272
+ if (!isRecord3(channel) || !channelEnabled(channel)) continue;
3273
+ const capabilities = capabilitiesOf(channel);
3274
+ const output = capabilities.includes("output");
3275
+ const digitalInput = !output && capabilities.includes("input") && !(typeof channel.physicalPointId === "string" && pointIsModbus.get(channel.physicalPointId));
3276
+ if (!output && !digitalInput) continue;
3277
+ const id = typeof channel.id === "string" ? channel.id : "";
3278
+ const idBytes = encoder.encode(JSON.stringify(id)).byteLength;
3279
+ total += output ? (idBytes + 7) * 2 + idBytes + 1 : idBytes + 7;
3280
+ }
3281
+ return total;
3282
+ }
3283
+ function configurationLimitViolations(snapshot) {
3284
+ const violations = [];
3285
+ const bytes = (() => {
3286
+ try {
3287
+ return byteLength(snapshot) + DESIRED_ENVELOPE_RESERVE_BYTES;
3288
+ } catch {
3289
+ return Infinity;
3290
+ }
3291
+ })();
3292
+ if (bytes > MAX_CONFIGURATION_BYTES) {
3293
+ violations.push({ limit: "bytes", actual: bytes, max: MAX_CONFIGURATION_BYTES });
3294
+ return violations;
3295
+ }
3296
+ if (depthExceeds(snapshot, MAX_CONFIGURATION_DEPTH)) {
3297
+ violations.push({ limit: "depth", max: MAX_CONFIGURATION_DEPTH });
3298
+ return violations;
3299
+ }
3300
+ const identifier3 = identifierViolation(snapshot);
3301
+ if (identifier3) violations.push(identifier3);
3302
+ if (isRecord3(snapshot)) {
3303
+ for (const [limit, collection3] of [
3304
+ ["points", snapshot.physicalPoints],
3305
+ ["channels", snapshot.logicalChannels]
3306
+ ])
3307
+ if (Array.isArray(collection3) && collection3.length > MAX_CONFIGURATION_ITEMS)
3308
+ violations.push({ limit, actual: collection3.length, max: MAX_CONFIGURATION_ITEMS });
3309
+ if (Array.isArray(snapshot.logicalChannels)) {
3310
+ const enabled = snapshot.logicalChannels.filter(channelEnabled).length;
3311
+ if (enabled > MAX_ENABLED_CHANNELS)
3312
+ violations.push({ limit: "enabled-channels", actual: enabled, max: MAX_ENABLED_CHANNELS });
3313
+ }
3314
+ const report = worstCaseStatusReportBytes(snapshot);
3315
+ if (report > MAX_STATUS_REPORT_BYTES)
3316
+ violations.push({ limit: "status-report", actual: report, max: MAX_STATUS_REPORT_BYTES });
3317
+ }
3318
+ return violations;
3319
+ }
3320
+ var CONFIGURATION_LIMIT_MESSAGES = {
3321
+ bytes: "The configuration is too large to send to the controller (limit 1 MiB). Remove devices, channels or custom profiles.",
3322
+ depth: "The configuration is nested deeper than 16 levels. Simplify custom profiles or remove nested settings.",
3323
+ identifierLength: "An ID is longer than 256 characters. Shorten it.",
3324
+ identifierControl: "An ID contains control characters such as line breaks or tabs. Remove them.",
3325
+ points: "The configuration has more than 2048 physical points. Remove points or devices; each built-in 879-3020 meter adds 131.",
3326
+ channels: "The configuration has more than 2048 channels. Remove channels or devices; each built-in 879-3020 meter adds 131 channels.",
3327
+ enabledChannels: "More than 256 channels are enabled. Disable meter readings you do not need or remove channels; each built-in 879-3020 meter adds 131 channels, of which 11 are enabled by default.",
3328
+ statusReport: "The controller status report could exceed 64 KiB with these digital and output channel IDs. Shorten those IDs or remove channels."
3329
+ };
3330
+ function describeConfigurationLimit(violation) {
3331
+ switch (violation.limit) {
3332
+ case "bytes":
3333
+ return CONFIGURATION_LIMIT_MESSAGES.bytes;
3334
+ case "depth":
3335
+ return CONFIGURATION_LIMIT_MESSAGES.depth;
3336
+ case "identifier":
3337
+ return violation.reason === "length" ? CONFIGURATION_LIMIT_MESSAGES.identifierLength : CONFIGURATION_LIMIT_MESSAGES.identifierControl;
3338
+ case "points":
3339
+ return CONFIGURATION_LIMIT_MESSAGES.points;
3340
+ case "channels":
3341
+ return CONFIGURATION_LIMIT_MESSAGES.channels;
3342
+ case "enabled-channels":
3343
+ return CONFIGURATION_LIMIT_MESSAGES.enabledChannels;
3344
+ case "status-report":
3345
+ return CONFIGURATION_LIMIT_MESSAGES.statusReport;
3346
+ }
3347
+ }
3348
+ function configurationLimitErrors(snapshot, rootPath = "snapshot") {
3349
+ return configurationLimitViolations(snapshot).map((violation) => ({
3350
+ path: violation.limit === "identifier" ? violation.path.replace(/^\$/, rootPath) : rootPath,
3351
+ code: CONFIGURATION_LIMIT_CODE,
3352
+ message: describeConfigurationLimit(violation)
3353
+ }));
3354
+ }
3355
+
3138
3356
  // backend/diagnostics/validation.ts
3139
3357
  var fields = new Set(
3140
3358
  "snapshot 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(
@@ -3145,7 +3363,7 @@ var codes = new Set(
3145
3363
  "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 configuration_commit_failed".split(
3146
3364
  " "
3147
3365
  )
3148
- );
3366
+ ).add(CONFIGURATION_LIMIT_CODE);
3149
3367
  function safeValidationSummaries(value) {
3150
3368
  if (!Array.isArray(value)) return [];
3151
3369
  return value.slice(0, 50).map((item) => {
@@ -3161,6 +3379,18 @@ function safeValidationSummaries(value) {
3161
3379
  }
3162
3380
 
3163
3381
  // backend/diagnostics/store.ts
3382
+ function heartbeatDropCounts(data) {
3383
+ const counts = data.dropCounts;
3384
+ if (!Array.isArray(data.capabilities) || !data.capabilities.includes(BOUNDED_MESSAGES_CAPABILITY) || !counts || typeof counts !== "object" || Array.isArray(counts))
3385
+ return void 0;
3386
+ const result = {};
3387
+ for (const reason of DROP_REASONS) {
3388
+ const value = counts[reason];
3389
+ if (!Number.isSafeInteger(value) || value < 0) return void 0;
3390
+ result[reason] = value;
3391
+ }
3392
+ return result;
3393
+ }
3164
3394
  var faultCodes = /* @__PURE__ */ new Set([
3165
3395
  "measurement_read_failed",
3166
3396
  "device_write_failed",
@@ -3220,8 +3450,8 @@ var WagoDiagnosticsStore = class {
3220
3450
  applyEvents(id, state, kind, data, canonical, metadata2) {
3221
3451
  const { receivedAt } = metadata2;
3222
3452
  if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
3223
- const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
3224
- collection2[data.channelId] = {
3453
+ const collection3 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
3454
+ collection3[data.channelId] = {
3225
3455
  kind: "measurement",
3226
3456
  value: data.value,
3227
3457
  unit: data.unit,
@@ -3334,10 +3564,9 @@ var WagoDiagnosticsStore = class {
3334
3564
  }
3335
3565
  ingest(id, kind, payload) {
3336
3566
  this.prune();
3337
- if (payload.length > 65536) return false;
3338
3567
  let data;
3339
3568
  try {
3340
- data = JSON.parse(payload.toString("utf8"));
3569
+ data = parseControllerJson(payload, "diagnostic message");
3341
3570
  } catch {
3342
3571
  return false;
3343
3572
  }
@@ -3350,7 +3579,7 @@ var WagoDiagnosticsStore = class {
3350
3579
  if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
3351
3580
  if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
3352
3581
  this.controllers.delete(oldest);
3353
- const state = this.read(id);
3582
+ const state = this.working(id);
3354
3583
  if (!this.admitIncoming(id, state, kind, data, canonical)) return false;
3355
3584
  state.touched = this.now();
3356
3585
  const receivedAt = new Date(this.now()).toISOString();
@@ -3360,10 +3589,13 @@ var WagoDiagnosticsStore = class {
3360
3589
  streamId: canonical ? state.activeStream : null,
3361
3590
  sequence: canonical ? data.sequence : null
3362
3591
  };
3363
- if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
3592
+ if (kind === "heartbeat") {
3593
+ state.heartbeatAt = canonical ? data.timestamp : receivedAt;
3594
+ state.dropCounts = heartbeatDropCounts(data);
3595
+ }
3364
3596
  if (kind === "state") this.applyState(state, data, canonical, metadata2);
3365
3597
  this.applyEvents(id, state, kind, data, canonical, metadata2);
3366
- for (const collection2 of [
3598
+ for (const collection3 of [
3367
3599
  state.cumulativeMeasurements,
3368
3600
  state.inputs,
3369
3601
  state.outputs,
@@ -3371,7 +3603,7 @@ var WagoDiagnosticsStore = class {
3371
3603
  state.faults,
3372
3604
  state.acknowledgements
3373
3605
  ]) {
3374
- while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
3606
+ while (Object.keys(collection3).length > MAX_CHANNELS) delete collection3[Object.keys(collection3)[0]];
3375
3607
  }
3376
3608
  state.events.push({ kind, receivedAt });
3377
3609
  state.events = state.events.slice(-50);
@@ -3405,10 +3637,18 @@ var WagoDiagnosticsStore = class {
3405
3637
  if (this.commands.size >= 1024 && oldest !== void 0) this.commands.delete(oldest);
3406
3638
  this.commands.set(id, { controllerId, channelId: channelId2, at: this.now() });
3407
3639
  }
3640
+ /** The last heartbeat time without copying the whole diagnostics state, as `read(id).heartbeatAt`. */
3641
+ heartbeatAt(id) {
3642
+ this.prune();
3643
+ return this.controllers.get(id)?.heartbeatAt;
3644
+ }
3408
3645
  read(id) {
3409
3646
  this.prune();
3410
3647
  const state = this.controllers.get(id);
3411
- const copy = state ? JSON.parse(JSON.stringify(state)) : {
3648
+ return this.retained(state ? JSON.parse(JSON.stringify(state)) : this.empty());
3649
+ }
3650
+ empty() {
3651
+ return {
3412
3652
  touched: 0,
3413
3653
  inputs: {},
3414
3654
  outputs: {},
@@ -3419,6 +3659,20 @@ var WagoDiagnosticsStore = class {
3419
3659
  events: [],
3420
3660
  ...emptyStream()
3421
3661
  };
3662
+ }
3663
+ /**
3664
+ * The copy `ingest` mutates and stores only once the message is admitted. It shares the immutable
3665
+ * sample, fault and acknowledgement records with the stored state instead of deep-copying them on
3666
+ * every message: those records are only ever replaced or deleted, never changed in place. Only the
3667
+ * containers a message can change are copied, so a rejected message still leaves no trace.
3668
+ */
3669
+ working(id) {
3670
+ const state = this.controllers.get(id);
3671
+ if (!state) return this.retained(this.empty());
3672
+ return this.retained({ ...state, retiredStreams: [...state.retiredStreams], watermarks: { ...state.watermarks } });
3673
+ }
3674
+ /** Copies the per-channel collections (shallowly) and drops everything past the retention window. */
3675
+ retained(copy) {
3422
3676
  const cutoff = this.now() - RETENTION_MS;
3423
3677
  for (const field3 of [
3424
3678
  "inputs",
@@ -3430,7 +3684,7 @@ var WagoDiagnosticsStore = class {
3430
3684
  ]) {
3431
3685
  copy[field3] = Object.assign(/* @__PURE__ */ Object.create(null), copy[field3]);
3432
3686
  }
3433
- for (const collection2 of [
3687
+ for (const collection3 of [
3434
3688
  copy.cumulativeMeasurements,
3435
3689
  copy.inputs,
3436
3690
  copy.outputs,
@@ -3438,8 +3692,8 @@ var WagoDiagnosticsStore = class {
3438
3692
  copy.faults,
3439
3693
  copy.acknowledgements
3440
3694
  ]) {
3441
- for (const key of Object.keys(collection2))
3442
- if (Date.parse(collection2[key].receivedAt) < cutoff) delete collection2[key];
3695
+ for (const key of Object.keys(collection3))
3696
+ if (Date.parse(collection3[key].receivedAt) < cutoff) delete collection3[key];
3443
3697
  }
3444
3698
  copy.events = copy.events.filter((event) => Date.parse(event.receivedAt) >= cutoff);
3445
3699
  if (copy.rejection && Date.parse(copy.rejection.receivedAt) < cutoff) delete copy.rejection;
@@ -3452,12 +3706,70 @@ var WagoDiagnosticsStore = class {
3452
3706
  }
3453
3707
  };
3454
3708
 
3709
+ // backend/configuration/channel-enabled.ts
3710
+ var import_common4 = require("@nestjs/common");
3711
+ var CHANNEL_ENABLED_UNSUPPORTED_CODE = "channel_enabled_unsupported";
3712
+ var CHANNEL_ENABLED_UNSUPPORTED_MESSAGE = "This configuration disables channels, but the CC100 runtime on this controller cannot disable channels. Update the CC100 runtime, or enable every channel before publishing.";
3713
+ var LEGACY_MAX_CONFIGURATION_ITEMS = 256;
3714
+ var LEGACY_RUNTIME_LIMIT_MESSAGES = {
3715
+ points: "The CC100 runtime on this controller is older and accepts at most 256 physical points. Update the CC100 runtime to use the higher limits, or remove points or devices.",
3716
+ channels: "The CC100 runtime on this controller is older and accepts at most 256 channels. Update the CC100 runtime to use the higher limits, or remove channels or devices.",
3717
+ statusReport: "The CC100 runtime on this controller is older and counts every channel ID towards its 64 KiB status report. Update the CC100 runtime to use the higher limits, or shorten channel IDs or remove channels."
3718
+ };
3719
+ function controllerCapabilities(capabilities) {
3720
+ if (Array.isArray(capabilities)) return capabilities;
3721
+ try {
3722
+ const parsed = JSON.parse(capabilities);
3723
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
3724
+ } catch {
3725
+ return [];
3726
+ }
3727
+ }
3728
+ var supportsChannelEnabled = (capabilities) => controllerCapabilities(capabilities).includes(CHANNEL_ENABLED_CAPABILITY);
3729
+ function collection2(snapshot, key) {
3730
+ const value = snapshot && typeof snapshot === "object" ? snapshot[key] : void 0;
3731
+ return Array.isArray(value) ? value : [];
3732
+ }
3733
+ function channelEnabledErrors(snapshot, capabilities) {
3734
+ if (collection2(snapshot, "logicalChannels").every(channelEnabled) || supportsChannelEnabled(capabilities)) return [];
3735
+ return [{ path: "$", code: CHANNEL_ENABLED_UNSUPPORTED_CODE, message: CHANNEL_ENABLED_UNSUPPORTED_MESSAGE }];
3736
+ }
3737
+ function legacyWorstCaseStatusReportBytes(snapshot) {
3738
+ const encoder2 = new TextEncoder();
3739
+ let total = STATUS_FIXED_BYTES + STATUS_ERROR_BUDGET_BYTES;
3740
+ for (const channel of collection2(snapshot, "logicalChannels")) {
3741
+ const id = channel && typeof channel === "object" && typeof channel.id === "string" ? channel.id : "";
3742
+ const idBytes = encoder2.encode(JSON.stringify(id)).byteLength;
3743
+ total += (idBytes + 7) * 2 + idBytes + 1;
3744
+ }
3745
+ return total;
3746
+ }
3747
+ function legacyRuntimeLimitErrors(snapshot, capabilities) {
3748
+ if (supportsChannelEnabled(capabilities)) return [];
3749
+ const error = (message) => ({ path: "$", code: CONFIGURATION_LIMIT_CODE, message });
3750
+ const errors = [];
3751
+ if (collection2(snapshot, "physicalPoints").length > LEGACY_MAX_CONFIGURATION_ITEMS)
3752
+ errors.push(error(LEGACY_RUNTIME_LIMIT_MESSAGES.points));
3753
+ if (collection2(snapshot, "logicalChannels").length > LEGACY_MAX_CONFIGURATION_ITEMS)
3754
+ errors.push(error(LEGACY_RUNTIME_LIMIT_MESSAGES.channels));
3755
+ if (legacyWorstCaseStatusReportBytes(snapshot) > MAX_STATUS_REPORT_BYTES)
3756
+ errors.push(error(LEGACY_RUNTIME_LIMIT_MESSAGES.statusReport));
3757
+ return errors;
3758
+ }
3759
+ function runtimeSupportErrors(snapshot, capabilities) {
3760
+ return [...channelEnabledErrors(snapshot, capabilities), ...legacyRuntimeLimitErrors(snapshot, capabilities)];
3761
+ }
3762
+ function requireRuntimeSupport(snapshot, capabilities) {
3763
+ const errors = runtimeSupportErrors(snapshot, capabilities);
3764
+ if (errors.length) throw new import_common4.ConflictException({ message: errors[0].message, code: errors[0].code, errors });
3765
+ }
3766
+
3455
3767
  // backend/controllers/enrollment.ts
3456
- var import_common8 = require("@nestjs/common");
3768
+ var import_common9 = require("@nestjs/common");
3457
3769
  var import_node_crypto7 = require("node:crypto");
3458
3770
 
3459
3771
  // backend/configuration/revisions.ts
3460
- var import_common7 = require("@nestjs/common");
3772
+ var import_common8 = require("@nestjs/common");
3461
3773
 
3462
3774
  // backend/configuration/flow-references.ts
3463
3775
  var import_plugins_backend_sdk = require("@attraccess/plugins-backend-sdk");
@@ -3485,6 +3797,10 @@ function editorMetadata(value) {
3485
3797
  return { names, presets };
3486
3798
  }
3487
3799
  function validateEditorSnapshot(snapshot) {
3800
+ const limits2 = configurationLimitErrors(snapshot, "$");
3801
+ return limits2.length ? limits2 : validateEditorModel(snapshot);
3802
+ }
3803
+ function validateEditorModel(snapshot) {
3488
3804
  const errors = validateSnapshot(snapshot);
3489
3805
  if (errors.length) return errors;
3490
3806
  const value = snapshot;
@@ -3583,6 +3899,8 @@ function selectPresetChanges(snapshot, application, paths, expectedHash) {
3583
3899
  if (errors.length) throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
3584
3900
  return candidate;
3585
3901
  }
3902
+ var CHANNEL_DISABLED_IMPACT_CODE = "channel_disabled";
3903
+ var CHANNEL_DISABLED_IMPACT_MESSAGE = "Channel disabled. Flows reading it will see it as unavailable.";
3586
3904
  function configurationImpacts(previous, current) {
3587
3905
  return (previous?.logicalChannels ?? []).flatMap((channel) => {
3588
3906
  const next = current.logicalChannels.find((item) => item.id === channel.id);
@@ -3590,6 +3908,8 @@ function configurationImpacts(previous, current) {
3590
3908
  const newPoint = current.physicalPoints.find((point) => point.id === next?.physicalPointId);
3591
3909
  if (next && configurationHash({ channel, point: oldPoint }) === configurationHash({ channel: next, point: newPoint }))
3592
3910
  return [];
3911
+ if (next && next.enabled === false && channel.enabled !== false)
3912
+ return [{ channelId: channel.id, code: CHANNEL_DISABLED_IMPACT_CODE, message: CHANNEL_DISABLED_IMPACT_MESSAGE }];
3593
3913
  return [
3594
3914
  {
3595
3915
  channelId: channel.id,
@@ -3626,7 +3946,7 @@ async function configurationFlowImpacts(context, controllerId, previous, current
3626
3946
  }
3627
3947
 
3628
3948
  // backend/configuration/drafts.ts
3629
- var import_common6 = require("@nestjs/common");
3949
+ var import_common7 = require("@nestjs/common");
3630
3950
 
3631
3951
  // backend/controllers/settings.entity.ts
3632
3952
  var import_plugins_backend_sdk2 = require("@attraccess/plugins-backend-sdk");
@@ -3651,7 +3971,7 @@ WagoSettings = __decorateClass([
3651
3971
  ], WagoSettings);
3652
3972
 
3653
3973
  // backend/controllers/registry.ts
3654
- var import_common5 = require("@nestjs/common");
3974
+ var import_common6 = require("@nestjs/common");
3655
3975
 
3656
3976
  // backend/controllers/entity.ts
3657
3977
  var import_plugins_backend_sdk3 = require("@attraccess/plugins-backend-sdk");
@@ -3829,6 +4149,7 @@ function channelLabel(config, channel, snapshot, names, text) {
3829
4149
  const terminal = DIGITAL_TERMINALS.find((item) => item.channel === point.channel)?.label;
3830
4150
  if (terminal && !channelName.includes(terminal)) channelName += ` \xB7 ${terminal}`;
3831
4151
  }
4152
+ if (channel?.enabled === false) channelName += ` (${text("disabled, unavailable", "deaktiviert, nicht verf\xFCgbar")})`;
3832
4153
  return channelName;
3833
4154
  }
3834
4155
  function commandAction(config, channel, text) {
@@ -3981,7 +4302,7 @@ var WagoCommandHandler = class {
3981
4302
  acknowledge(controllerId, payload) {
3982
4303
  let acknowledgement;
3983
4304
  try {
3984
- acknowledgement = JSON.parse(payload.toString("utf8"));
4305
+ acknowledgement = parseControllerJson(payload, "command acknowledgement");
3985
4306
  } catch {
3986
4307
  return;
3987
4308
  }
@@ -4346,7 +4667,7 @@ WagoConfigurationRevision = __decorateClass([
4346
4667
  ], WagoConfigurationRevision);
4347
4668
 
4348
4669
  // backend/controllers/state.ts
4349
- var import_common4 = require("@nestjs/common");
4670
+ var import_common5 = require("@nestjs/common");
4350
4671
  var WagoServiceState = class {
4351
4672
  constructor(context) {
4352
4673
  __publicField(this, "context", context);
@@ -4374,7 +4695,7 @@ var WagoServiceState = class {
4374
4695
  claimedController: async (id) => {
4375
4696
  const controller = await this.claimedController(id);
4376
4697
  if (this.isRuntimeUpdateRequired(id))
4377
- throw new import_common4.ConflictException(
4698
+ throw new import_common5.ConflictException(
4378
4699
  "Runtime update required; outputs are held in failsafe until the server runtime is installed"
4379
4700
  );
4380
4701
  return controller;
@@ -4392,7 +4713,7 @@ var WagoServiceState = class {
4392
4713
  }
4393
4714
  };
4394
4715
  WagoServiceState = __decorateClass([
4395
- __decorateParam(0, (0, import_common4.Inject)(PLUGIN_CONTEXT))
4716
+ __decorateParam(0, (0, import_common5.Inject)(PLUGIN_CONTEXT))
4396
4717
  ], WagoServiceState);
4397
4718
 
4398
4719
  // backend/controllers/registry.ts
@@ -4402,7 +4723,7 @@ var WagoRegistry = class extends WagoServiceState {
4402
4723
  }
4403
4724
  async setSettings(serverId, operationalPrefix) {
4404
4725
  if (serverId !== void 0 && serverId !== null && !await this.context.getMqttServerConfig(serverId))
4405
- throw new import_common5.NotFoundException(`MQTT server ${serverId} not found`);
4726
+ throw new import_common6.NotFoundException(`MQTT server ${serverId} not found`);
4406
4727
  const save = async () => {
4407
4728
  const settings2 = await this.getSettings();
4408
4729
  if (serverId !== void 0) settings2.defaultMqttServerId = serverId;
@@ -4411,7 +4732,7 @@ var WagoRegistry = class extends WagoServiceState {
4411
4732
  if (normalizedPrefix !== settings2.operationalPrefix) {
4412
4733
  const controllers = await this.controllers.find({ where: { trustState: "claimed" } });
4413
4734
  if (controllers.length)
4414
- throw new import_common5.ConflictException("operational MQTT prefix cannot change after a controller has been claimed");
4735
+ throw new import_common6.ConflictException("operational MQTT prefix cannot change after a controller has been claimed");
4415
4736
  settings2.operationalPrefix = normalizedPrefix;
4416
4737
  }
4417
4738
  }
@@ -4444,7 +4765,13 @@ var WagoRegistry = class extends WagoServiceState {
4444
4765
  { qos: 1, retain: false }
4445
4766
  );
4446
4767
  const [revision2] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
4447
- if (revision2 && (revision2.state === "published" || revision2.state === "applied"))
4768
+ const replay = revision2 && (revision2.state === "published" || revision2.state === "applied") ? revision2 : null;
4769
+ const unsupported = replay ? runtimeSupportErrors(JSON.parse(replay.snapshot), controller.capabilities) : [];
4770
+ if (replay && unsupported.length)
4771
+ this.context.logger.warn(
4772
+ `Not replaying WAGO configuration revision ${replay.revision} to controller ${controllerId}: ${unsupported[0].message}`
4773
+ );
4774
+ else if (replay)
4448
4775
  await this.context.mqtt.publish(
4449
4776
  controller.mqttServerId,
4450
4777
  topic,
@@ -4452,9 +4779,9 @@ var WagoRegistry = class extends WagoServiceState {
4452
4779
  protocolVersion: CONFIGURATION_PROTOCOL_VERSION,
4453
4780
  runtimeImageId: desired,
4454
4781
  runtimePolicyToken,
4455
- revision: revision2.revision,
4456
- contentHash: revision2.contentHash,
4457
- snapshot: JSON.parse(revision2.snapshot)
4782
+ revision: replay.revision,
4783
+ contentHash: replay.contentHash,
4784
+ snapshot: JSON.parse(replay.snapshot)
4458
4785
  }),
4459
4786
  { qos: 1, retain: true }
4460
4787
  );
@@ -4487,7 +4814,7 @@ var WagoRegistry = class extends WagoServiceState {
4487
4814
  runtimeVersion: controller.runtimeVersion,
4488
4815
  capabilities: controller.capabilities,
4489
4816
  lastSequence: controller.lastSequence,
4490
- lastHeartbeatAt: this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt,
4817
+ lastHeartbeatAt: this.diagnostics.heartbeatAt(controller.id) ?? controller.lastHeartbeatAt,
4491
4818
  lastSeenAt: controller.lastSeenAt,
4492
4819
  compatibilityError: controller.compatibilityError,
4493
4820
  createdAt: controller.createdAt,
@@ -4504,12 +4831,12 @@ var WagoRegistry = class extends WagoServiceState {
4504
4831
  async manualCommand(controllerId, input, principal) {
4505
4832
  const keys = ["channelId", "action", "value", "expectedConfigurationRevision", "acknowledgementTimeoutSeconds"];
4506
4833
  if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => !keys.includes(key)))
4507
- throw new import_common5.BadRequestException("Invalid manual command");
4834
+ throw new import_common6.BadRequestException("Invalid manual command");
4508
4835
  if (typeof input.channelId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(input.channelId))
4509
- throw new import_common5.BadRequestException("Invalid Logical Channel");
4836
+ throw new import_common6.BadRequestException("Invalid Logical Channel");
4510
4837
  const config = { ...input, controllerId, completionBehavior: "acknowledged" };
4511
4838
  const errors = await this.commands.validate(config, /* @__PURE__ */ new Map(), true);
4512
- if (errors.length) throw new import_common5.BadRequestException("Manual command does not match the applied configuration");
4839
+ if (errors.length) throw new import_common6.BadRequestException("Manual command does not match the applied configuration");
4513
4840
  const commandId = (0, import_node_crypto6.randomUUID)();
4514
4841
  const details = { commandId, channelId: input.channelId, operation: input.action };
4515
4842
  const lifecycle = new WagoAudit(this.context).begin(principal, controllerId, "manual_command", details);
@@ -4572,7 +4899,7 @@ var WagoRegistry = class extends WagoServiceState {
4572
4899
  async refreshNetworkConnection(controllerId) {
4573
4900
  const controller = await this.claimedController(controllerId), settings = await this.getSettings();
4574
4901
  const serverId = controller.mqttServerId;
4575
- if (!serverId) throw new import_common5.ConflictException("Controller MQTT server is unavailable");
4902
+ if (!serverId) throw new import_common6.ConflictException("Controller MQTT server is unavailable");
4576
4903
  const subscriptions = [];
4577
4904
  let retained;
4578
4905
  const active2 = () => !this.destroyed && this.networkSubscriptions.get(controllerId) === subscriptions;
@@ -4656,12 +4983,12 @@ var WagoDrafts = class extends WagoRegistry {
4656
4983
  const draft = await this.draftForPreset(controllerId);
4657
4984
  const snapshot = JSON.parse(draft.snapshot);
4658
4985
  if (previewedDraftHash !== configurationHash(snapshot))
4659
- throw new import_common6.ConflictException("selected preset changes no longer match the configuration draft");
4986
+ throw new import_common7.ConflictException("selected preset changes no longer match the configuration draft");
4660
4987
  const candidate = applyPreset(snapshot, application);
4661
4988
  const diff = configurationDiff(snapshot, candidate);
4662
4989
  const validPaths = new Set(diff.map((change) => change.path));
4663
4990
  if (!Array.isArray(selectedPaths) || selectedPaths.some((path) => !validPaths.has(path)))
4664
- throw new import_common6.ConflictException("selected preset changes no longer match the configuration draft");
4991
+ throw new import_common7.ConflictException("selected preset changes no longer match the configuration draft");
4665
4992
  const updatedSnapshot = applySelectedChanges(snapshot, diff, selectedPaths);
4666
4993
  if (configurationHash(updatedSnapshot) === configurationHash(snapshot)) return draft;
4667
4994
  const provenance = parsePresetProvenance(draft.presetProvenance);
@@ -4716,13 +5043,13 @@ var WagoDrafts = class extends WagoRegistry {
4716
5043
  async draftForPreset(controllerId) {
4717
5044
  await this.claimedController(controllerId);
4718
5045
  const draft = await this.drafts.findOneBy({ controllerId });
4719
- if (!draft) throw new import_common6.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
5046
+ if (!draft) throw new import_common7.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
4720
5047
  return draft;
4721
5048
  }
4722
5049
  async validateDraft(controllerId, snapshot) {
4723
5050
  const draft = await this.getDraft(controllerId);
4724
5051
  if (!draft && snapshot === void 0)
4725
- throw new import_common6.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
5052
+ throw new import_common7.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
4726
5053
  const errors = validateEditorSnapshot(snapshot === void 0 && draft ? JSON.parse(draft.snapshot) : snapshot);
4727
5054
  return { valid: errors.length === 0, errors };
4728
5055
  }
@@ -4732,7 +5059,7 @@ var WagoDrafts = class extends WagoRegistry {
4732
5059
  try {
4733
5060
  return previewConfigurationPreset(source, application);
4734
5061
  } catch (error) {
4735
- throw new import_common6.BadRequestException(error instanceof Error ? error.message : "invalid preset");
5062
+ throw new import_common7.BadRequestException(error instanceof Error ? error.message : "invalid preset");
4736
5063
  }
4737
5064
  }
4738
5065
  presets() {
@@ -4742,7 +5069,7 @@ var WagoDrafts = class extends WagoRegistry {
4742
5069
  return this.withConfigurationLock(controllerId, async () => {
4743
5070
  const previous = await this.getDraft(controllerId);
4744
5071
  if (expectedDraft !== void 0 && (expectedDraft === null ? previous !== null : !previous || expectedDraft.snapshot !== previous.snapshot || expectedDraft.updatedAt !== previous.updatedAt || expectedDraft.presetProvenance !== previous.presetProvenance)) {
4745
- throw new import_common6.ConflictException("Saved draft changed. Reload it before saving your edits.");
5072
+ throw new import_common7.ConflictException("Saved draft changed. Reload it before saving your edits.");
4746
5073
  }
4747
5074
  const validatedMetadata = metadata2 === void 0 ? void 0 : editorMetadata(metadata2);
4748
5075
  const previousMetadata = this.metadataFromProvenance(previous?.presetProvenance);
@@ -4836,10 +5163,11 @@ var WagoRevisions = class extends WagoDrafts {
4836
5163
  const controller = await this.claimedController(controllerId);
4837
5164
  this.requireConfigurationCompatibility(controller);
4838
5165
  const draft = preparedDraft ?? await this.drafts.findOneBy({ controllerId });
4839
- if (!draft) throw new import_common7.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
5166
+ if (!draft) throw new import_common8.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
4840
5167
  const validation = validateEditorSnapshot(JSON.parse(draft.snapshot));
4841
5168
  if (validation.length)
4842
- throw new import_common7.ConflictException({ message: "configuration draft is invalid", errors: validation });
5169
+ throw new import_common8.ConflictException({ message: "configuration draft is invalid", errors: validation });
5170
+ requireRuntimeSupport(JSON.parse(draft.snapshot), controller.capabilities);
4843
5171
  const contentHash = configurationHash(JSON.parse(draft.snapshot));
4844
5172
  const previous = await this.latestRevision(controllerId);
4845
5173
  const impacts = await configurationFlowImpacts(
@@ -4850,11 +5178,11 @@ var WagoRevisions = class extends WagoDrafts {
4850
5178
  );
4851
5179
  const reviewIdentity = this.reviewIdentity(draft, previous, impacts);
4852
5180
  if (reviewedHash !== void 0 && reviewedHash !== reviewIdentity)
4853
- throw new import_common7.ConflictException("draft changed since your review; review it again");
5181
+ throw new import_common8.ConflictException("draft changed since your review; review it again");
4854
5182
  if (draft.reviewedHash !== reviewIdentity)
4855
- throw new import_common7.ConflictException("review the current configuration draft before publishing it");
5183
+ throw new import_common8.ConflictException("review the current configuration draft before publishing it");
4856
5184
  if (impacts.length && !force)
4857
- throw new import_common7.ConflictException({ message: "acknowledge potential flow impacts before publishing", impacts });
5185
+ throw new import_common8.ConflictException({ message: "acknowledge potential flow impacts before publishing", impacts });
4858
5186
  let allocatedRevision;
4859
5187
  const dispatch = (revision2) => {
4860
5188
  allocatedRevision = revision2.revision;
@@ -4886,12 +5214,12 @@ var WagoRevisions = class extends WagoDrafts {
4886
5214
  protocolVersion: controller.protocolVersion,
4887
5215
  capabilities: JSON.parse(controller.capabilities)
4888
5216
  });
4889
- if (incompatibility) throw new import_common7.ConflictException(`Cannot publish configuration: ${incompatibility}`);
5217
+ if (incompatibility) throw new import_common8.ConflictException(`Cannot publish configuration: ${incompatibility}`);
4890
5218
  }
4891
5219
  async reviewDraftWhileLocked(controllerId) {
4892
5220
  await this.claimedController(controllerId);
4893
5221
  const draft = await this.drafts.findOneBy({ controllerId });
4894
- if (!draft) throw new import_common7.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
5222
+ if (!draft) throw new import_common8.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
4895
5223
  const previous = await this.latestRevision(controllerId);
4896
5224
  const impacts = await configurationFlowImpacts(
4897
5225
  this.context,
@@ -4922,7 +5250,7 @@ var WagoRevisions = class extends WagoDrafts {
4922
5250
  try {
4923
5251
  provenance = JSON.stringify({ editor: editorMetadata(metadata2) });
4924
5252
  } catch (error) {
4925
- throw new import_common7.BadRequestException(error instanceof Error ? error.message : "invalid editor metadata");
5253
+ throw new import_common8.BadRequestException(error instanceof Error ? error.message : "invalid editor metadata");
4926
5254
  }
4927
5255
  }
4928
5256
  const serialized = canonicalSnapshot(snapshot);
@@ -4979,27 +5307,30 @@ var WagoRevisions = class extends WagoDrafts {
4979
5307
  }
4980
5308
  async previewRevision(controllerId, revision2) {
4981
5309
  return this.withConfigurationLock(controllerId, async () => {
4982
- await this.claimedController(controllerId);
5310
+ const controller = await this.claimedController(controllerId);
4983
5311
  const selected = await this.revisions.findOneBy({ controllerId, revision: revision2 });
4984
- if (!selected) throw new import_common7.NotFoundException(`WAGO configuration revision ${revision2} not found`);
5312
+ if (!selected) throw new import_common8.NotFoundException(`WAGO configuration revision ${revision2} not found`);
4985
5313
  const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
4986
5314
  const draft = await this.drafts.findOneBy({ controllerId });
5315
+ const snapshot = JSON.parse(selected.snapshot);
4987
5316
  const impacts = await configurationFlowImpacts(
4988
5317
  this.context,
4989
5318
  controllerId,
4990
5319
  current ? JSON.parse(current.snapshot) : null,
4991
- JSON.parse(selected.snapshot)
5320
+ snapshot
4992
5321
  );
4993
5322
  return {
4994
5323
  draftHash: this.rollbackIdentity(draft, current ?? null, selected, impacts),
4995
5324
  impacts,
4996
5325
  revision: selected,
4997
5326
  current: current ?? null,
4998
- diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot)),
5327
+ diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, snapshot),
4999
5328
  metadataDiff: configurationDiff(
5000
5329
  this.metadataFromProvenance(current?.presetProvenance),
5001
5330
  this.metadataFromProvenance(selected.presetProvenance)
5002
- )
5331
+ ),
5332
+ // The same checks `rollback` runs, so the editor can refuse the rollback before publishing.
5333
+ errors: [...validateEditorSnapshot(snapshot), ...runtimeSupportErrors(snapshot, controller.capabilities)]
5003
5334
  };
5004
5335
  });
5005
5336
  }
@@ -5007,9 +5338,9 @@ var WagoRevisions = class extends WagoDrafts {
5007
5338
  return this.withConfigurationLock(controllerId, async () => {
5008
5339
  await this.claimedController(controllerId);
5009
5340
  const rejected = await this.revisions.findOneBy({ controllerId, revision: revision2 });
5010
- if (!rejected) throw new import_common7.NotFoundException(`WAGO configuration revision ${revision2} not found`);
5341
+ if (!rejected) throw new import_common8.NotFoundException(`WAGO configuration revision ${revision2} not found`);
5011
5342
  if (rejected.state !== "rejected" || !rejected.reportedAt || expected.contentHash !== rejected.contentHash || expected.reportedAt !== rejected.reportedAt)
5012
- throw new import_common7.ConflictException("rejection changed; refresh and review it before acknowledging");
5343
+ throw new import_common8.ConflictException("rejection changed; refresh and review it before acknowledging");
5013
5344
  if (rejected.rejectionAcknowledgedAt) return rejected;
5014
5345
  return new WagoAudit(this.context).run(
5015
5346
  principal,
@@ -5031,12 +5362,13 @@ var WagoRevisions = class extends WagoDrafts {
5031
5362
  const controller = await this.claimedController(controllerId);
5032
5363
  this.requireConfigurationCompatibility(controller);
5033
5364
  const source = await this.revisions.findOneBy({ controllerId, revision: revision2 });
5034
- if (!source) throw new import_common7.NotFoundException(`WAGO configuration revision ${revision2} not found`);
5365
+ if (!source) throw new import_common8.NotFoundException(`WAGO configuration revision ${revision2} not found`);
5035
5366
  const current = await this.latestRevision(controllerId);
5036
5367
  const draft = await this.drafts.findOneBy({ controllerId });
5037
5368
  const snapshot = JSON.parse(source.snapshot);
5038
5369
  const errors = validateEditorSnapshot(snapshot);
5039
- if (errors.length) throw new import_common7.ConflictException({ message: "rollback configuration is invalid", errors });
5370
+ if (errors.length) throw new import_common8.ConflictException({ message: "rollback configuration is invalid", errors });
5371
+ requireRuntimeSupport(snapshot, controller.capabilities);
5040
5372
  const approvedImpacts = await configurationFlowImpacts(
5041
5373
  this.context,
5042
5374
  controllerId,
@@ -5045,11 +5377,11 @@ var WagoRevisions = class extends WagoDrafts {
5045
5377
  );
5046
5378
  const assertPreview = (impacts) => {
5047
5379
  if (draftHash !== this.rollbackIdentity(draft, current, source, impacts) || sourceHash !== source.contentHash || currentHash !== (current?.contentHash ?? null))
5048
- throw new import_common7.ConflictException("configuration changed since rollback preview; preview and confirm again");
5380
+ throw new import_common8.ConflictException("configuration changed since rollback preview; preview and confirm again");
5049
5381
  };
5050
5382
  assertPreview(approvedImpacts);
5051
5383
  if (approvedImpacts.length && !force)
5052
- throw new import_common7.ConflictException({
5384
+ throw new import_common8.ConflictException({
5053
5385
  message: "acknowledge potential flow impacts before publishing",
5054
5386
  impacts: approvedImpacts
5055
5387
  });
@@ -5109,7 +5441,7 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5109
5441
  }, assertOwned = async () => void 0) {
5110
5442
  await assertOwned();
5111
5443
  const manual = await this.context.getMqttCredentialProvisioning().revoke({ mqttServerId, identity, username: identity, vhost: "/" });
5112
- if (manual) throw new import_common8.ConflictException("Manual permanent credential revocation is required.");
5444
+ if (manual) throw new import_common9.ConflictException("Manual permanent credential revocation is required.");
5113
5445
  await assertOwned();
5114
5446
  controller.credentialMqttServerId = null;
5115
5447
  controller.credentialEpoch = null;
@@ -5141,32 +5473,32 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5141
5473
  }
5142
5474
  async prepareClaim(id, name, verifier, mqttServerId, assertOwned = async () => void 0, manualCredentials) {
5143
5475
  const controller = await this.controllers.findOneBy({ id });
5144
- if (!controller) throw new import_common8.NotFoundException(`WAGO controller ${id} not found`);
5145
- if (controller.trustState === "claimed") throw new import_common8.ConflictException("controller has already been claimed");
5146
- if (!name.trim()) throw new import_common8.ConflictException("a controller name is required");
5476
+ if (!controller) throw new import_common9.NotFoundException(`WAGO controller ${id} not found`);
5477
+ if (controller.trustState === "claimed") throw new import_common9.ConflictException("controller has already been claimed");
5478
+ if (!name.trim()) throw new import_common9.ConflictException("a controller name is required");
5147
5479
  if (!this.matchesVerifier(controller, verifier))
5148
- throw new import_common8.ConflictException("physical pairing code or fingerprint does not match the controller");
5149
- if (controller.compatibilityError) throw new import_common8.ConflictException(controller.compatibilityError);
5480
+ throw new import_common9.ConflictException("physical pairing code or fingerprint does not match the controller");
5481
+ if (controller.compatibilityError) throw new import_common9.ConflictException(controller.compatibilityError);
5150
5482
  const selectedServerId = mqttServerId ?? controller.mqttServerId;
5151
- if (!selectedServerId) throw new import_common8.ConflictException("select an MQTT server before claiming this controller");
5483
+ if (!selectedServerId) throw new import_common9.ConflictException("select an MQTT server before claiming this controller");
5152
5484
  if (selectedServerId !== controller.mqttServerId)
5153
- throw new import_common8.ConflictException("claim the controller on the MQTT server used for its enrollment package");
5485
+ throw new import_common9.ConflictException("claim the controller on the MQTT server used for its enrollment package");
5154
5486
  if (!await this.context.getMqttServerConfig(selectedServerId))
5155
- throw new import_common8.NotFoundException(`MQTT server ${selectedServerId} not found`);
5487
+ throw new import_common9.NotFoundException(`MQTT server ${selectedServerId} not found`);
5156
5488
  const enrollment = await this.activeEnrollment(controller.enrollmentId);
5157
5489
  if (!enrollment)
5158
- throw new import_common8.ConflictException(
5490
+ throw new import_common9.ConflictException(
5159
5491
  "the controller enrollment package has expired or was already consumed; create a new one"
5160
5492
  );
5161
5493
  const identity = `wago-controller-${controller.hardwareId}`;
5162
5494
  const settings = await this.getSettings();
5163
5495
  const namespace = normalizeOperationalPrefix(settings.operationalPrefix);
5164
5496
  if (manualCredentials && (await this.context.getMqttCredentialProvisioning().availableProviders(selectedServerId)).length)
5165
- throw new import_common8.ConflictException(
5497
+ throw new import_common9.ConflictException(
5166
5498
  "This broker uses automatic credential provisioning; use the standard claim operation"
5167
5499
  );
5168
5500
  if (controller.credentialMqttServerId && (!manualCredentials || controller.credentialMqttServerId !== selectedServerId))
5169
- throw new import_common8.ConflictException(
5501
+ throw new import_common9.ConflictException(
5170
5502
  "Remove the controller registration to revoke its unfinished permanent credential before claiming again."
5171
5503
  );
5172
5504
  await assertOwned();
@@ -5190,7 +5522,7 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5190
5522
  });
5191
5523
  await assertOwned();
5192
5524
  const credential = "password" in provisioned ? provisioned : manualCredentials;
5193
- if (!credential) throw new import_common8.ConflictException("Manual credential provisioning is required");
5525
+ if (!credential) throw new import_common9.ConflictException("Manual credential provisioning is required");
5194
5526
  const previousController = {
5195
5527
  trustState: controller.trustState,
5196
5528
  name: controller.name,
@@ -5285,19 +5617,19 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5285
5617
  }
5286
5618
  async completeManualCredentials(id, input, principal, assertOwned = async () => void 0) {
5287
5619
  const controller = await this.controllers.findOneBy({ id });
5288
- if (!controller) throw new import_common8.NotFoundException("controller not found");
5620
+ if (!controller) throw new import_common9.NotFoundException("controller not found");
5289
5621
  if (input.username !== `wago-controller-${controller.hardwareId}` || !input.password || input.password.length > 4096)
5290
- throw new import_common8.BadRequestException("Supply the controller identity and provisioned password");
5622
+ throw new import_common9.BadRequestException("Supply the controller identity and provisioned password");
5291
5623
  if (!JSON.parse(controller.capabilities).includes("claim-expiry-v1"))
5292
- throw new import_common8.ConflictException("Install a runtime supporting expiring claims before manual credential fallback");
5624
+ throw new import_common9.ConflictException("Install a runtime supporting expiring claims before manual credential fallback");
5293
5625
  return new WagoAudit(this.context).run(principal, id, "manual_credential_fallback", {}, async () => {
5294
5626
  let active2 = true;
5295
5627
  let dispatched = false;
5296
5628
  const expiresAt = new Date(Date.now() + 3e4).toISOString();
5297
5629
  const assertActive = async () => {
5298
- if (!active2) throw new import_common8.ConflictException("Controller credential operation ended");
5630
+ if (!active2) throw new import_common9.ConflictException("Controller credential operation ended");
5299
5631
  await assertOwned();
5300
- if (!active2) throw new import_common8.ConflictException("Controller credential operation ended");
5632
+ if (!active2) throw new import_common9.ConflictException("Controller credential operation ended");
5301
5633
  };
5302
5634
  let acknowledged;
5303
5635
  const receipt = new Promise((resolve2) => {
@@ -5308,7 +5640,7 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5308
5640
  timer = setTimeout(() => {
5309
5641
  active2 = false;
5310
5642
  reject(
5311
- dispatched ? new WagoCredentialOperationUncertainError("Controller credential acknowledgement timed out") : new import_common8.ConflictException("Controller credential acknowledgement timed out")
5643
+ dispatched ? new WagoCredentialOperationUncertainError("Controller credential acknowledgement timed out") : new import_common9.ConflictException("Controller credential acknowledgement timed out")
5312
5644
  );
5313
5645
  }, 3e4);
5314
5646
  });
@@ -5348,7 +5680,7 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5348
5680
  id,
5349
5681
  () => this.withClaimConfigurationLock(async () => {
5350
5682
  const controller = await this.controllers.findOneBy({ id });
5351
- if (!controller) throw new import_common8.NotFoundException(`WAGO controller ${id} not found`);
5683
+ if (!controller) throw new import_common9.NotFoundException(`WAGO controller ${id} not found`);
5352
5684
  const credentialServerId = controller.credentialMqttServerId ?? (controller.trustState === "claimed" ? controller.mqttServerId : null);
5353
5685
  if (credentialServerId) {
5354
5686
  const identity = `wago-controller-${controller.hardwareId}`;
@@ -5360,7 +5692,7 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5360
5692
  vhost: "/"
5361
5693
  });
5362
5694
  if (manual)
5363
- throw new import_common8.ConflictException(`Manual credential revocation is required: ${manual.instructions.join(" ")}`);
5695
+ throw new import_common9.ConflictException(`Manual credential revocation is required: ${manual.instructions.join(" ")}`);
5364
5696
  }
5365
5697
  if (controller.enrollmentId) await this.revokeEnrollmentById(controller.enrollmentId, assertOwned);
5366
5698
  await assertOwned();
@@ -5390,11 +5722,11 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5390
5722
  async createEnrollment(hardwareId, mqttServerId, manualCredentials, assertOwned = async () => void 0) {
5391
5723
  const normalizedHardwareId = hardwareId.trim();
5392
5724
  if (!isValidHardwareId(normalizedHardwareId))
5393
- throw new import_common8.ConflictException("a valid hardware ID without MQTT separators or wildcards is required");
5725
+ throw new import_common9.ConflictException("a valid hardware ID without MQTT separators or wildcards is required");
5394
5726
  const selectedServerId = mqttServerId ?? (await this.getSettings()).defaultMqttServerId;
5395
- if (!selectedServerId) throw new import_common8.ConflictException("select an MQTT server before creating an enrollment package");
5727
+ if (!selectedServerId) throw new import_common9.ConflictException("select an MQTT server before creating an enrollment package");
5396
5728
  const server = await this.context.getMqttServerConfig(selectedServerId);
5397
- if (!server) throw new import_common8.NotFoundException(`MQTT server ${selectedServerId} not found`);
5729
+ if (!server) throw new import_common9.NotFoundException(`MQTT server ${selectedServerId} not found`);
5398
5730
  const claimSecret = (0, import_node_crypto7.randomBytes)(24).toString("base64url");
5399
5731
  const identity = `wago-enrollment-${(0, import_node_crypto7.randomBytes)(8).toString("hex")}`;
5400
5732
  const expiresAt = new Date(Date.now() + 15 * 6e4).toISOString();
@@ -5427,13 +5759,13 @@ var WagoControllerEnrollment = class extends WagoRevisions {
5427
5759
  if (timer) clearTimeout(timer);
5428
5760
  this.enrollmentExpiryTimers.delete(enrollment.id);
5429
5761
  await this.enrollments.delete(enrollment.id);
5430
- throw new import_common8.ConflictException(
5762
+ throw new import_common9.ConflictException(
5431
5763
  `Manual discovery credentials are required: ${provisionedCredential.instructions.join(" ")}`
5432
5764
  );
5433
5765
  }
5434
5766
  const credential = "password" in provisionedCredential ? provisionedCredential : manualCredentials;
5435
5767
  if (!credential?.username.trim() || !credential.password)
5436
- throw new import_common8.ConflictException("a manual discovery username and password are required");
5768
+ throw new import_common9.ConflictException("a manual discovery username and password are required");
5437
5769
  await assertOwned();
5438
5770
  if (!("password" in provisionedCredential)) {
5439
5771
  enrollment.identity = credential.username;
@@ -5471,7 +5803,7 @@ var WagoSubscriptions = class extends WagoControllerEnrollment {
5471
5803
  const canTrackDiagnostics = this.diagnostics.canTrack(controller.id);
5472
5804
  const admitted = this.diagnostics.ingest(controller.id, "heartbeat", payload);
5473
5805
  if (canTrackDiagnostics && !admitted) return;
5474
- const heartbeatAt = admitted ? this.diagnostics.read(controller.id).heartbeatAt : typeof rawHeartbeat.timestamp === "string" ? rawHeartbeat.timestamp : void 0;
5806
+ const heartbeatAt = admitted ? this.diagnostics.heartbeatAt(controller.id) : typeof rawHeartbeat.timestamp === "string" ? rawHeartbeat.timestamp : void 0;
5475
5807
  if (canonical && !acceptedCanonicalHeartbeat(rawHeartbeat, heartbeatAt, controller.lastHeartbeatAt)) return;
5476
5808
  if (canonical && (admitted || !canTrackDiagnostics))
5477
5809
  this.reportRuntimeStatus(controller.id, heartbeat, rawHeartbeat);
@@ -5481,9 +5813,10 @@ var WagoSubscriptions = class extends WagoControllerEnrollment {
5481
5813
  }
5482
5814
  /** Parse a heartbeat for the expected controller; malformed packets are ignored. */
5483
5815
  parseHeartbeatPayload(hardwareId, payload) {
5484
- let heartbeat;
5816
+ let heartbeat, rawHeartbeat;
5485
5817
  try {
5486
- heartbeat = parseHeartbeat(payload);
5818
+ rawHeartbeat = parseObject(payload, "announcement");
5819
+ heartbeat = heartbeatFromObject(rawHeartbeat);
5487
5820
  } catch (error) {
5488
5821
  this.context.logger.warn(
5489
5822
  `Ignoring invalid WAGO heartbeat: ${error instanceof Error ? error.message : String(error)}`
@@ -5491,11 +5824,7 @@ var WagoSubscriptions = class extends WagoControllerEnrollment {
5491
5824
  return void 0;
5492
5825
  }
5493
5826
  if (heartbeat.hardwareId !== hardwareId) return void 0;
5494
- try {
5495
- return { heartbeat, rawHeartbeat: JSON.parse(payload.toString("utf8")) };
5496
- } catch {
5497
- return void 0;
5498
- }
5827
+ return { heartbeat, rawHeartbeat };
5499
5828
  }
5500
5829
  reportRuntimeStatus(controllerId, heartbeat, rawHeartbeat) {
5501
5830
  if (typeof rawHeartbeat.streamId !== "string" || typeof rawHeartbeat.timestamp !== "string") return;
@@ -5790,8 +6119,8 @@ var WagoService = class extends WagoSubscriptions {
5790
6119
  async claimedController(id) {
5791
6120
  const controller = await this.controllers.findOneBy({ id });
5792
6121
  if (!controller || controller.trustState !== "claimed")
5793
- throw new import_common9.NotFoundException(`claimed WAGO controller ${id} not found`);
5794
- if (!controller.mqttServerId) throw new import_common9.ConflictException(`WAGO controller ${id} has no MQTT server`);
6122
+ throw new import_common10.NotFoundException(`claimed WAGO controller ${id} not found`);
6123
+ if (!controller.mqttServerId) throw new import_common10.ConflictException(`WAGO controller ${id} has no MQTT server`);
5795
6124
  return controller;
5796
6125
  }
5797
6126
  async withClaimConfigurationLock(operation) {
@@ -5838,7 +6167,7 @@ var WagoService = class extends WagoSubscriptions {
5838
6167
  vhost: "/"
5839
6168
  });
5840
6169
  if (manual)
5841
- throw new import_common9.ConflictException(`Manual credential revocation is required: ${manual.instructions.join(" ")}`);
6170
+ throw new import_common10.ConflictException(`Manual credential revocation is required: ${manual.instructions.join(" ")}`);
5842
6171
  await assertOwned();
5843
6172
  enrollment.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
5844
6173
  await this.enrollments.save(enrollment);
@@ -5904,7 +6233,7 @@ var WagoService = class extends WagoSubscriptions {
5904
6233
  }
5905
6234
  connectivity(controller) {
5906
6235
  if (controller.trustState === "untrusted") return "untrusted";
5907
- const heartbeatAt = this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt;
6236
+ const heartbeatAt = this.diagnostics.heartbeatAt(controller.id) ?? controller.lastHeartbeatAt;
5908
6237
  if (freshness(heartbeatAt, Date.now(), STALE_AFTER_MS) !== "fresh") return "stale";
5909
6238
  if (this.isRuntimeUpdateRequired(controller.id)) {
5910
6239
  const policy = this.runtimePolicies.get(controller.id);
@@ -5913,12 +6242,13 @@ var WagoService = class extends WagoSubscriptions {
5913
6242
  return "online";
5914
6243
  }
5915
6244
  async publishRevision(controller, revision2) {
5916
- if (!controller.mqttServerId) throw new import_common9.ConflictException(`WAGO controller ${controller.id} has no MQTT server`);
6245
+ if (!controller.mqttServerId) throw new import_common10.ConflictException(`WAGO controller ${controller.id} has no MQTT server`);
5917
6246
  const incompatibility = compatibilityError({
5918
6247
  protocolVersion: controller.protocolVersion,
5919
6248
  capabilities: JSON.parse(controller.capabilities)
5920
6249
  });
5921
- if (incompatibility) throw new import_common9.ConflictException(`Cannot publish configuration: ${incompatibility}`);
6250
+ if (incompatibility) throw new import_common10.ConflictException(`Cannot publish configuration: ${incompatibility}`);
6251
+ requireRuntimeSupport(JSON.parse(revision2.snapshot), controller.capabilities);
5922
6252
  const settings = await this.getSettings();
5923
6253
  const runtimePolicy = this.runtimePolicies.get(controller.id);
5924
6254
  await this.context.mqtt.publish(
@@ -5939,13 +6269,11 @@ var WagoService = class extends WagoSubscriptions {
5939
6269
  revision2.state = "published";
5940
6270
  return this.revisions.save(revision2);
5941
6271
  }
6272
+ /** Revision a queued report acknowledges; throws for a report outside the controller payload limits. */
5942
6273
  configurationReportRevision(payload) {
5943
- try {
5944
- const report = JSON.parse(payload.toString("utf8"));
5945
- return Number.isSafeInteger(report.revision) && report.revision >= 1 ? report.revision : null;
5946
- } catch {
5947
- return null;
5948
- }
6274
+ const report = parseControllerJson(payload, "configuration report");
6275
+ const revision2 = report && typeof report === "object" ? report.revision : void 0;
6276
+ return Number.isSafeInteger(revision2) && revision2 >= 1 ? revision2 : null;
5949
6277
  }
5950
6278
  async processConfigurationReports(controllerId, payload, queue) {
5951
6279
  let next = payload;
@@ -5970,7 +6298,15 @@ var WagoService = class extends WagoSubscriptions {
5970
6298
  const queue = this.configurationReportQueues.get(controllerId) ?? { pending: /* @__PURE__ */ new Map(), processing: false };
5971
6299
  this.configurationReportQueues.set(controllerId, queue);
5972
6300
  if (queue.processing) {
5973
- const revision2 = this.configurationReportRevision(payload);
6301
+ let revision2;
6302
+ try {
6303
+ revision2 = this.configurationReportRevision(payload);
6304
+ } catch (error) {
6305
+ this.context.logger.warn(
6306
+ `Dropping invalid WAGO configuration report for controller ${controllerId}: ${error.message}`
6307
+ );
6308
+ return;
6309
+ }
5974
6310
  const key = revision2 ?? Number.NaN;
5975
6311
  if (queue.pending.has(key) || queue.pending.size < MAX_PENDING_CONFIGURATION_REPORTS)
5976
6312
  queue.pending.set(key, payload);
@@ -5986,7 +6322,7 @@ var WagoService = class extends WagoSubscriptions {
5986
6322
  async onConfigurationReported(controllerId, payload) {
5987
6323
  let report;
5988
6324
  try {
5989
- report = parseConfigurationReport(JSON.parse(payload.toString("utf8")));
6325
+ report = parseConfigurationReport(parseControllerJson(payload, "configuration report"));
5990
6326
  } catch {
5991
6327
  this.context.logger.warn(`Ignoring invalid WAGO configuration report for controller ${controllerId}`);
5992
6328
  return;
@@ -6040,32 +6376,32 @@ var WagoService = class extends WagoSubscriptions {
6040
6376
  }
6041
6377
  };
6042
6378
  WagoService = __decorateClass([
6043
- (0, import_common9.Injectable)(),
6044
- __decorateParam(0, (0, import_common9.Inject)(PLUGIN_CONTEXT))
6379
+ (0, import_common10.Injectable)(),
6380
+ __decorateParam(0, (0, import_common10.Inject)(PLUGIN_CONTEXT))
6045
6381
  ], WagoService);
6046
6382
 
6047
6383
  // backend/commissioning/delivery/preflight.ts
6048
- var import_common10 = require("@nestjs/common");
6384
+ var import_common11 = require("@nestjs/common");
6049
6385
  var import_node_crypto8 = require("node:crypto");
6050
6386
  function assertCommissioningBroker(config, now = Date.now()) {
6051
6387
  if (!/^[a-zA-Z0-9.-]+$/.test(config.host) || !Number.isInteger(config.port) || config.port < 1 || config.port > 65535)
6052
- throw new import_common10.ConflictException("Configure a valid MQTT hostname and port before commissioning.");
6388
+ throw new import_common11.ConflictException("Configure a valid MQTT hostname and port before commissioning.");
6053
6389
  if (!config.useTls || config.tlsInsecure) return;
6054
6390
  if (config.caCert == null) return;
6055
6391
  const certificates = config.caCert.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
6056
6392
  if (!certificates?.length || config.caCert.replace(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g, "").trim())
6057
- throw new import_common10.ConflictException("The MQTT private CA must contain PEM certificates only.");
6393
+ throw new import_common11.ConflictException("The MQTT private CA must contain PEM certificates only.");
6058
6394
  for (const pem of certificates) {
6059
6395
  let certificate;
6060
6396
  try {
6061
6397
  certificate = new import_node_crypto8.X509Certificate(pem);
6062
6398
  } catch {
6063
- throw new import_common10.ConflictException("The MQTT private CA certificate is invalid. Import a valid PEM CA certificate.");
6399
+ throw new import_common11.ConflictException("The MQTT private CA certificate is invalid. Import a valid PEM CA certificate.");
6064
6400
  }
6065
6401
  if (!certificate.ca)
6066
- throw new import_common10.ConflictException("The MQTT trust bundle must contain CA certificates, not server certificates.");
6402
+ throw new import_common11.ConflictException("The MQTT trust bundle must contain CA certificates, not server certificates.");
6067
6403
  if (now < Date.parse(certificate.validFrom) || now > Date.parse(certificate.validTo))
6068
- throw new import_common10.ConflictException(
6404
+ throw new import_common11.ConflictException(
6069
6405
  "The MQTT CA certificate is expired or not yet valid. Check the server clock and renew the CA."
6070
6406
  );
6071
6407
  }
@@ -6075,7 +6411,7 @@ function assertCommissioningBroker(config, now = Date.now()) {
6075
6411
  init_catalog();
6076
6412
 
6077
6413
  // backend/commissioning/readiness/readiness.ts
6078
- var import_common13 = require("@nestjs/common");
6414
+ var import_common14 = require("@nestjs/common");
6079
6415
  var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
6080
6416
  function isReadiness(value) {
6081
6417
  return !!value && typeof value === "object" && "configurationAccepted" in value && typeof value.configurationAccepted === "boolean" && "hardwareAvailable" in value && typeof value.hardwareAvailable === "boolean" && "ready" in value && typeof value.ready === "boolean";
@@ -6107,10 +6443,6 @@ var WagoCommissioningReadiness = class {
6107
6443
  const current = entry;
6108
6444
  void this.context.mqtt.subscribe(serverId, topic, (message) => {
6109
6445
  if (message.serverId !== serverId || message.topic !== topic) return;
6110
- if (message.payload.length > 65536) {
6111
- current.state = void 0;
6112
- return;
6113
- }
6114
6446
  try {
6115
6447
  const parsed = parseOperationalMessage(prefix, topic, message.payload);
6116
6448
  if (!parsed || parsed.hardwareId !== hardwareId || parsed.message.category !== "state") {
@@ -6120,7 +6452,7 @@ var WagoCommissioningReadiness = class {
6120
6452
  const event = parsed.message;
6121
6453
  const timestamp = Date.parse(event.timestamp);
6122
6454
  const now = Date.now();
6123
- const value = JSON.parse(message.payload.toString("utf8"));
6455
+ const value = parseObject(message.payload, "readiness state");
6124
6456
  const readiness = value.readiness;
6125
6457
  if (timestamp > now + CONTROLLER_CLOCK_TOLERANCE_MS || event.contentHash !== null && !/^[a-f0-9]{64}$/i.test(event.contentHash) || !isReadiness(readiness)) {
6126
6458
  current.state = void 0;
@@ -6163,12 +6495,12 @@ var WagoCommissioningReadiness = class {
6163
6495
  }
6164
6496
  };
6165
6497
  WagoCommissioningReadiness = __decorateClass([
6166
- (0, import_common13.Injectable)(),
6167
- __decorateParam(0, (0, import_common13.Inject)(import_plugins_backend_sdk8.PLUGIN_CONTEXT))
6498
+ (0, import_common14.Injectable)(),
6499
+ __decorateParam(0, (0, import_common14.Inject)(import_plugins_backend_sdk8.PLUGIN_CONTEXT))
6168
6500
  ], WagoCommissioningReadiness);
6169
6501
 
6170
6502
  // backend/runtime/managed/service.ts
6171
- var import_common18 = require("@nestjs/common");
6503
+ var import_common19 = require("@nestjs/common");
6172
6504
  var import_node_crypto17 = require("node:crypto");
6173
6505
 
6174
6506
  // backend/host/shell/stat.ts
@@ -9672,7 +10004,7 @@ function managedSetupFailure(stage, error, timedOut) {
9672
10004
  init_catalog();
9673
10005
 
9674
10006
  // backend/runtime/managed/reconciliation.ts
9675
- var import_common17 = require("@nestjs/common");
10007
+ var import_common18 = require("@nestjs/common");
9676
10008
 
9677
10009
  // backend/commissioning/sessions/verification.ts
9678
10010
  async function commissioningVerification(context, session, runtime) {
@@ -9927,11 +10259,11 @@ async function managedSsh(access, privateKey, header, signal, file) {
9927
10259
  }
9928
10260
 
9929
10261
  // backend/runtime/managed/access.ts
9930
- var import_common16 = require("@nestjs/common");
10262
+ var import_common17 = require("@nestjs/common");
9931
10263
 
9932
10264
  // backend/runtime/managed/enrollment.ts
9933
10265
  var import_node_crypto15 = require("node:crypto");
9934
- var import_common15 = require("@nestjs/common");
10266
+ var import_common16 = require("@nestjs/common");
9935
10267
 
9936
10268
  // backend/network/entity.ts
9937
10269
  var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
@@ -10230,7 +10562,7 @@ var WagoDeviceOperations = class {
10230
10562
  };
10231
10563
 
10232
10564
  // backend/runtime/managed/state.ts
10233
- var import_common14 = require("@nestjs/common");
10565
+ var import_common15 = require("@nestjs/common");
10234
10566
  init_catalog();
10235
10567
  var WagoManagedRuntimeServiceState = class {
10236
10568
  constructor(context, wago, artifacts, readiness) {
@@ -10262,10 +10594,10 @@ var WagoManagedRuntimeServiceState = class {
10262
10594
  }
10263
10595
  };
10264
10596
  WagoManagedRuntimeServiceState = __decorateClass([
10265
- __decorateParam(0, (0, import_common14.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
10266
- __decorateParam(1, (0, import_common14.Inject)(WagoService)),
10267
- __decorateParam(2, (0, import_common14.Inject)(WagoRuntimeArtifactsService)),
10268
- __decorateParam(3, (0, import_common14.Inject)(WagoCommissioningReadiness))
10597
+ __decorateParam(0, (0, import_common15.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
10598
+ __decorateParam(1, (0, import_common15.Inject)(WagoService)),
10599
+ __decorateParam(2, (0, import_common15.Inject)(WagoRuntimeArtifactsService)),
10600
+ __decorateParam(3, (0, import_common15.Inject)(WagoCommissioningReadiness))
10269
10601
  ], WagoManagedRuntimeServiceState);
10270
10602
 
10271
10603
  // backend/runtime/managed/enrollment.ts
@@ -10328,21 +10660,21 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10328
10660
  if (controllerId === null && !fingerprint2) return;
10329
10661
  const change = await this.context.getRepository(WagoNetworkChange).findOneBy(controllerId === null ? { fingerprint: fingerprint2 } : { controllerId });
10330
10662
  if (change && change.phase !== "completed")
10331
- throw new import_common15.ConflictException("Finish the pending MQTT/address change before another controller operation.");
10663
+ throw new import_common16.ConflictException("Finish the pending MQTT/address change before another controller operation.");
10332
10664
  }
10333
10665
  async assertUpdateSettled(controllerId) {
10334
10666
  await this.assertNetworkSettled(controllerId);
10335
10667
  const row = await this.updates.findOneBy({ controllerId });
10336
10668
  const update = row?.metadata ? JSON.parse(row.metadata) : null;
10337
- if (update?.token) throw new import_common15.ConflictException("Finish runtime update recovery before removing this controller");
10669
+ if (update?.token) throw new import_common16.ConflictException("Finish runtime update recovery before removing this controller");
10338
10670
  }
10339
10671
  async assertRemovable(controllerId) {
10340
10672
  await this.assertUpdateSettled(controllerId);
10341
10673
  if (await this.context.getRepository(WagoMqttCredentialRetirement).countBy({ controllerId }))
10342
- throw new import_common15.ConflictException("Retire previous broker credentials before removing this controller.");
10674
+ throw new import_common16.ConflictException("Retire previous broker credentials before removing this controller.");
10343
10675
  const access = await this.access.findOne({ where: { controllerId }, order: { sessionId: "DESC" } });
10344
10676
  if (access && access.state !== "retired")
10345
- throw new import_common15.ConflictException("Restore bootstrap SSH and retire managed access before removing this controller");
10677
+ throw new import_common16.ConflictException("Restore bootstrap SSH and retire managed access before removing this controller");
10346
10678
  }
10347
10679
  async retire(controllerId) {
10348
10680
  await this.assertRemovable(controllerId);
@@ -10351,7 +10683,7 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10351
10683
  }
10352
10684
  async recoverPassword(sessionId, principal) {
10353
10685
  const access = await this.loadSession(sessionId);
10354
- if (!access) throw new import_common15.NotFoundException("Managed recovery credential not found");
10686
+ if (!access) throw new import_common16.NotFoundException("Managed recovery credential not found");
10355
10687
  const operationId = (0, import_node_crypto15.randomUUID)();
10356
10688
  await this.securityAudit(sessionId, "root_recovery", principal, operationId, "attempted");
10357
10689
  try {
@@ -10438,7 +10770,7 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10438
10770
  const desired = await this.desired();
10439
10771
  let access = await this.loadSession(session.id);
10440
10772
  if (access && (access.fingerprint !== session.hostKeyFingerprint || access.host !== session.targetHost || ["retiring", "retired"].includes(access.state)))
10441
- throw new import_common15.ConflictException("Managed identity changed; create a fresh enrolment session.");
10773
+ throw new import_common16.ConflictException("Managed identity changed; create a fresh enrolment session.");
10442
10774
  if (!access) {
10443
10775
  const key = generateManagementKey();
10444
10776
  const credentials2 = {
@@ -10454,7 +10786,7 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10454
10786
  const plaintext = JSON.stringify(credentials2);
10455
10787
  const encryptedCredentials = this.context.secrets.encrypt(plaintext);
10456
10788
  if (!encryptedCredentials || encryptedCredentials === plaintext || this.context.secrets.decrypt(encryptedCredentials) !== plaintext)
10457
- throw new import_common15.ConflictException("Managed credential encryption failed.");
10789
+ throw new import_common16.ConflictException("Managed credential encryption failed.");
10458
10790
  access = await this.access.save(
10459
10791
  this.access.create({
10460
10792
  sessionId: session.id,
@@ -10469,11 +10801,11 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10469
10801
  );
10470
10802
  key.privateKey = "";
10471
10803
  access = await this.loadSession(session.id);
10472
- if (!access) throw new import_common15.ConflictException("Managed credential storage verification failed.");
10804
+ if (!access) throw new import_common16.ConflictException("Managed credential storage verification failed.");
10473
10805
  }
10474
10806
  const credentials = this.credentials(access);
10475
10807
  if (credentials.hardwareId !== session.hardwareId)
10476
- throw new import_common15.ConflictException("Managed credentials belong to another controller identity");
10808
+ throw new import_common16.ConflictException("Managed credentials belong to another controller identity");
10477
10809
  let stage = "filesystem";
10478
10810
  try {
10479
10811
  if (access.state !== "verified" && access.state !== "managed") {
@@ -10508,7 +10840,7 @@ var WagoManagedRuntimeEnrollment = class extends WagoManagedRuntimeServiceState
10508
10840
  const access = await this.loadSession(sessionId);
10509
10841
  if (!access) return;
10510
10842
  if (access.state !== "verified" || access.controllerId !== null && access.controllerId !== controllerId)
10511
- throw new import_common15.ConflictException("Managed enrolment is not verified for this controller");
10843
+ throw new import_common16.ConflictException("Managed enrolment is not verified for this controller");
10512
10844
  await this.access.update(sessionId, { controllerId });
10513
10845
  }
10514
10846
  registerRetirementProbe(probe) {
@@ -10612,7 +10944,7 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10612
10944
  installerPublicKey(value.installerPrivateKey);
10613
10945
  return value;
10614
10946
  } catch {
10615
- throw new import_common16.ConflictException("Managed credential envelope is unavailable or does not match this device");
10947
+ throw new import_common17.ConflictException("Managed credential envelope is unavailable or does not match this device");
10616
10948
  }
10617
10949
  }
10618
10950
  async required(controllerId) {
@@ -10635,29 +10967,29 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10635
10967
  async retryRuntime(controllerId) {
10636
10968
  await this.required(controllerId);
10637
10969
  if (await this.coordinator.reconcile(controllerId, true) === "busy")
10638
- throw new import_common16.ConflictException("Controller is busy; retry after its current operation finishes");
10970
+ throw new import_common17.ConflictException("Controller is busy; retry after its current operation finishes");
10639
10971
  }
10640
10972
  async setActiveState(sessionId, state) {
10641
10973
  const result = await this.access.createQueryBuilder().update().set({ state }).where("session_id = :sessionId AND state NOT IN (:...retired)", {
10642
10974
  sessionId,
10643
10975
  retired: ["retiring", "retired"]
10644
10976
  }).execute();
10645
- if (result.affected !== 1) throw new import_common16.ConflictException("Managed access is being retired");
10977
+ if (result.affected !== 1) throw new import_common17.ConflictException("Managed access is being retired");
10646
10978
  }
10647
10979
  async retryAccess(sessionId) {
10648
10980
  let access = await this.loadSession(sessionId);
10649
10981
  if (!access || ["retiring", "retired"].includes(access.state))
10650
- throw new import_common16.ConflictException("Managed access cannot be retried");
10982
+ throw new import_common17.ConflictException("Managed access cannot be retried");
10651
10983
  const fingerprint2 = access.fingerprint;
10652
10984
  const owner = (0, import_node_crypto16.randomBytes)(16).toString("hex");
10653
10985
  if (!await this.operations.acquire(access.fingerprint, owner, Date.now(), Date.now() + 6e4))
10654
- throw new import_common16.ConflictException("Controller is busy");
10986
+ throw new import_common17.ConflictException("Controller is busy");
10655
10987
  const operation = new AbortController();
10656
10988
  const timer = setTimeout(() => operation.abort(), 5e4).unref();
10657
10989
  try {
10658
10990
  access = await this.loadSession(sessionId);
10659
10991
  if (!access || ["retiring", "retired"].includes(access.state))
10660
- throw new import_common16.ConflictException("Managed access cannot be retried");
10992
+ throw new import_common17.ConflictException("Managed access cannot be retried");
10661
10993
  if (access.controllerId) await this.assertNetworkSettled(access.controllerId);
10662
10994
  await this.prove(access, operation.signal);
10663
10995
  const status = await this.connection(access, `access-status ${access.token}`, operation.signal);
@@ -10670,7 +11002,7 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10670
11002
  await this.setActiveState(sessionId, "verified");
10671
11003
  await this.connection(access, `access-key-commit ${access.token}`, operation.signal);
10672
11004
  await this.prove(access, operation.signal);
10673
- } else throw new import_common16.ConflictException("Wait for the SSH-policy watchdog to restore access before retrying");
11005
+ } else throw new import_common17.ConflictException("Wait for the SSH-policy watchdog to restore access before retrying");
10674
11006
  } finally {
10675
11007
  clearTimeout(timer);
10676
11008
  operation.abort();
@@ -10681,11 +11013,11 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10681
11013
  async restoreAccess(sessionId, principal) {
10682
11014
  const access = await this.loadSession(sessionId);
10683
11015
  if (!access || !this.rootProbe || !this.retirementProbe)
10684
- throw new import_common16.NotFoundException("Managed recovery access not found");
11016
+ throw new import_common17.NotFoundException("Managed recovery access not found");
10685
11017
  if (access.controllerId) await this.assertUpdateSettled(access.controllerId);
10686
11018
  const owner = (0, import_node_crypto16.randomBytes)(16).toString("hex");
10687
11019
  if (!await this.operations.acquire(access.fingerprint, owner, Date.now(), Date.now() + 6e4))
10688
- throw new import_common16.ConflictException("Controller is busy");
11020
+ throw new import_common17.ConflictException("Controller is busy");
10689
11021
  const operation = new AbortController();
10690
11022
  const timer = setTimeout(() => operation.abort(), 5e4).unref();
10691
11023
  const operationId = (0, import_node_crypto16.randomUUID)();
@@ -10709,12 +11041,12 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10709
11041
  await new Promise((resolve2) => setTimeout(resolve2, 1e3));
10710
11042
  }
10711
11043
  if (!restored)
10712
- throw new import_common16.ConflictException("Bootstrap SSH restoration is unverified; retry administrator recovery");
11044
+ throw new import_common17.ConflictException("Bootstrap SSH restoration is unverified; retry administrator recovery");
10713
11045
  await this.operations.assertOwned(access.fingerprint, owner);
10714
11046
  await this.connection(access, `access-retire ${access.token}`, operation.signal).catch(() => void 0);
10715
11047
  }
10716
11048
  if (!await this.retirementProbe(access.host, access.fingerprint, password))
10717
- throw new import_common16.ConflictException("Managed key retirement is unverified; retry administrator recovery");
11049
+ throw new import_common17.ConflictException("Managed key retirement is unverified; retry administrator recovery");
10718
11050
  await this.operations.assertOwned(access.fingerprint, owner);
10719
11051
  await this.access.update(sessionId, { state: "retired" });
10720
11052
  await this.securityAudit(sessionId, "security_recover", principal, operationId, "succeeded");
@@ -10739,7 +11071,7 @@ var WagoManagedRuntimeAccess = class extends WagoManagedRuntimeEnrollment {
10739
11071
  if (!access || !this.rootProbe || !["pending", "verified", "recovery_required"].includes(access.state)) return null;
10740
11072
  const credentials = this.credentials(access);
10741
11073
  if (access.host !== session.targetHost || access.fingerprint !== session.hostKeyFingerprint || credentials.hardwareId !== session.hardwareId)
10742
- throw new import_common16.ConflictException("Managed recovery identity changed; check the saved controller identity.");
11074
+ throw new import_common17.ConflictException("Managed recovery identity changed; check the saved controller identity.");
10743
11075
  return await this.rootProbe(access.host, access.fingerprint, credentials.recoveryPassword) ? credentials.recoveryPassword : null;
10744
11076
  }
10745
11077
  async hasAccess(sessionId) {
@@ -10759,7 +11091,7 @@ var WagoManagedRuntimeReconciliation = class extends WagoManagedRuntimeAccess {
10759
11091
  details: {}
10760
11092
  });
10761
11093
  if (receipt.status !== "recorded")
10762
- throw new import_common17.ConflictException("Durable audit is required for managed SSH recovery and transitions");
11094
+ throw new import_common18.ConflictException("Durable audit is required for managed SSH recovery and transitions");
10763
11095
  }
10764
11096
  async finishSession(id) {
10765
11097
  await this.sessions.update(id, {
@@ -11252,21 +11584,21 @@ var WagoManagedRuntimeService = class extends WagoManagedRuntimeReconciliation {
11252
11584
  }
11253
11585
  };
11254
11586
  WagoManagedRuntimeService = __decorateClass([
11255
- (0, import_common18.Injectable)(),
11256
- __decorateParam(0, (0, import_common18.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
11257
- __decorateParam(1, (0, import_common18.Inject)(WagoService)),
11258
- __decorateParam(2, (0, import_common18.Inject)(WagoRuntimeArtifactsService)),
11259
- __decorateParam(3, (0, import_common18.Inject)(WagoCommissioningReadiness))
11587
+ (0, import_common19.Injectable)(),
11588
+ __decorateParam(0, (0, import_common19.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
11589
+ __decorateParam(1, (0, import_common19.Inject)(WagoService)),
11590
+ __decorateParam(2, (0, import_common19.Inject)(WagoRuntimeArtifactsService)),
11591
+ __decorateParam(3, (0, import_common19.Inject)(WagoCommissioningReadiness))
11260
11592
  ], WagoManagedRuntimeService);
11261
11593
 
11262
11594
  // backend/commissioning/model.ts
11263
- var import_common19 = require("@nestjs/common");
11595
+ var import_common20 = require("@nestjs/common");
11264
11596
  function requireDeliveryCredentials(input) {
11265
11597
  if (input?.confirmInstall !== true)
11266
- throw new import_common19.ConflictException("explicit installation confirmation is required for every delivery attempt");
11598
+ throw new import_common20.ConflictException("explicit installation confirmation is required for every delivery attempt");
11267
11599
  const credential = input.temporarySsh;
11268
11600
  if (!credential || typeof credential.username !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$/.test(credential.username) || typeof credential.password !== "string" || !credential.password.trim() || /[\r\n\0]/.test(credential.password))
11269
- throw new import_common19.ConflictException("explicit valid SSH username and password are required for every delivery attempt");
11601
+ throw new import_common20.ConflictException("explicit valid SSH username and password are required for every delivery attempt");
11270
11602
  return credential;
11271
11603
  }
11272
11604
  var WagoStorageCapacityError = class extends Error {
@@ -11279,7 +11611,7 @@ var WagoControllerLockError = class extends Error {
11279
11611
  super("The CC100 is busy with a runtime operation. Retry installation shortly; no preparation was started.");
11280
11612
  }
11281
11613
  };
11282
- var RuntimeReleaseChangedError = class extends import_common19.ConflictException {
11614
+ var RuntimeReleaseChangedError = class extends import_common20.ConflictException {
11283
11615
  constructor() {
11284
11616
  super(
11285
11617
  "The runtime release changed during delivery. Recover any retained installation and retry with the current release."
@@ -11556,7 +11888,7 @@ timeout -s KILL 10 /etc/config-tools/config_clock type=utc "time=$time" "date=$d
11556
11888
  }
11557
11889
 
11558
11890
  // backend/commissioning/sessions/lifecycle.ts
11559
- var import_common27 = require("@nestjs/common");
11891
+ var import_common28 = require("@nestjs/common");
11560
11892
 
11561
11893
  // backend/commissioning/delivery/transport.ts
11562
11894
  var import_promises8 = require("node:fs/promises");
@@ -11564,7 +11896,7 @@ var import_node_os5 = require("node:os");
11564
11896
  var import_node_path6 = require("node:path");
11565
11897
 
11566
11898
  // backend/commissioning/delivery/host-identity.ts
11567
- var import_common20 = require("@nestjs/common");
11899
+ var import_common21 = require("@nestjs/common");
11568
11900
  var import_promises4 = require("node:fs/promises");
11569
11901
  var import_node_os3 = require("node:os");
11570
11902
  var import_node_path4 = require("node:path");
@@ -11660,14 +11992,14 @@ function scanHostKeys(host) {
11660
11992
  }
11661
11993
  async function pinnedHostKey(host, expectedFingerprint) {
11662
11994
  const key = (await scanHostKeys(host)).split("\n").find((line) => line.includes("ssh-ed25519"));
11663
- if (!key) throw new import_common20.ConflictException("the controller did not provide an Ed25519 SSH host key");
11995
+ if (!key) throw new import_common21.ConflictException("the controller did not provide an Ed25519 SSH host key");
11664
11996
  const dir = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "attraccess-cc100-"));
11665
11997
  const knownHosts = (0, import_node_path4.join)(dir, "known_hosts");
11666
11998
  try {
11667
11999
  await (0, import_promises4.writeFile)(knownHosts, `${key}
11668
12000
  `, { mode: 384 });
11669
12001
  if (await fingerprintFor(knownHosts) !== expectedFingerprint)
11670
- throw new import_common20.ConflictException("controller SSH host key changed after automatic identity verification");
12002
+ throw new import_common21.ConflictException("controller SSH host key changed after automatic identity verification");
11671
12003
  return `${key}
11672
12004
  `;
11673
12005
  } finally {
@@ -11679,7 +12011,7 @@ async function fingerprintFor(knownHosts) {
11679
12011
  timeoutMs: 5e3,
11680
12012
  maxOutputBytes: 1024
11681
12013
  })).match(/(SHA256:[A-Za-z0-9+/=]+)/)?.[1];
11682
- if (!result) throw new import_common20.ConflictException("the controller did not provide a supported SSH host key");
12014
+ if (!result) throw new import_common21.ConflictException("the controller did not provide a supported SSH host key");
11683
12015
  return result;
11684
12016
  }
11685
12017
 
@@ -11783,13 +12115,13 @@ var ManagementPeerVersion = class {
11783
12115
  };
11784
12116
 
11785
12117
  // backend/commissioning/delivery/transport.ts
11786
- var import_common26 = require("@nestjs/common");
12118
+ var import_common27 = require("@nestjs/common");
11787
12119
 
11788
12120
  // backend/commissioning/sessions/ownership.ts
11789
- var import_common25 = require("@nestjs/common");
12121
+ var import_common26 = require("@nestjs/common");
11790
12122
 
11791
12123
  // backend/commissioning/delivery/recovery.ts
11792
- var import_common24 = require("@nestjs/common");
12124
+ var import_common25 = require("@nestjs/common");
11793
12125
  var import_node_crypto22 = require("node:crypto");
11794
12126
  init_hardware_profile();
11795
12127
  var import_promises7 = require("node:fs/promises");
@@ -11798,7 +12130,7 @@ var import_promises7 = require("node:fs/promises");
11798
12130
  var import_promises6 = require("node:fs/promises");
11799
12131
  var import_node_os4 = require("node:os");
11800
12132
  var import_node_path5 = require("node:path");
11801
- var import_common23 = require("@nestjs/common");
12133
+ var import_common24 = require("@nestjs/common");
11802
12134
 
11803
12135
  // backend/management/model.ts
11804
12136
  var import_node_crypto19 = require("node:crypto");
@@ -12129,7 +12461,7 @@ init_hardware_profile();
12129
12461
  var import_node_crypto21 = require("node:crypto");
12130
12462
 
12131
12463
  // backend/commissioning/sessions/sessions.ts
12132
- var import_common22 = require("@nestjs/common");
12464
+ var import_common23 = require("@nestjs/common");
12133
12465
  var import_node_crypto20 = require("node:crypto");
12134
12466
 
12135
12467
  // backend/management/entity.ts
@@ -12465,7 +12797,7 @@ function createWagoManagementService(context, ssh) {
12465
12797
  }
12466
12798
 
12467
12799
  // backend/commissioning/state.ts
12468
- var import_common21 = require("@nestjs/common");
12800
+ var import_common22 = require("@nestjs/common");
12469
12801
  var import_node_async_hooks = require("node:async_hooks");
12470
12802
  var import_plugins_backend_sdk13 = require("@attraccess/plugins-backend-sdk");
12471
12803
  init_catalog();
@@ -12486,21 +12818,21 @@ var WagoCommissioningServiceState = class {
12486
12818
  }
12487
12819
  };
12488
12820
  WagoCommissioningServiceState = __decorateClass([
12489
- __decorateParam(0, (0, import_common21.Inject)(import_plugins_backend_sdk13.PLUGIN_CONTEXT)),
12490
- __decorateParam(1, (0, import_common21.Inject)(WagoService)),
12491
- __decorateParam(2, (0, import_common21.Optional)()),
12492
- __decorateParam(2, (0, import_common21.Inject)(WagoRuntimeArtifactsService)),
12493
- __decorateParam(3, (0, import_common21.Optional)()),
12494
- __decorateParam(3, (0, import_common21.Inject)(WagoCommissioningReadiness)),
12495
- __decorateParam(4, (0, import_common21.Optional)()),
12496
- __decorateParam(4, (0, import_common21.Inject)(WagoManagedRuntimeService))
12821
+ __decorateParam(0, (0, import_common22.Inject)(import_plugins_backend_sdk13.PLUGIN_CONTEXT)),
12822
+ __decorateParam(1, (0, import_common22.Inject)(WagoService)),
12823
+ __decorateParam(2, (0, import_common22.Optional)()),
12824
+ __decorateParam(2, (0, import_common22.Inject)(WagoRuntimeArtifactsService)),
12825
+ __decorateParam(3, (0, import_common22.Optional)()),
12826
+ __decorateParam(3, (0, import_common22.Inject)(WagoCommissioningReadiness)),
12827
+ __decorateParam(4, (0, import_common22.Optional)()),
12828
+ __decorateParam(4, (0, import_common22.Inject)(WagoManagedRuntimeService))
12497
12829
  ], WagoCommissioningServiceState);
12498
12830
 
12499
12831
  // backend/commissioning/sessions/sessions.ts
12500
12832
  var WagoCommissioningSessions = class extends WagoCommissioningServiceState {
12501
12833
  async verification(id) {
12502
12834
  const session = await this.sessions.findOneBy({ id });
12503
- if (!session) throw new import_common22.NotFoundException("commissioning session not found");
12835
+ if (!session) throw new import_common23.NotFoundException("commissioning session not found");
12504
12836
  const settings = this.readiness ? await this.wago.getSettings() : null;
12505
12837
  const runtime = this.readiness?.observe(session.mqttServerId, session.hardwareId, settings.operationalPrefix);
12506
12838
  const verification = await commissioningVerification(this.context, session, runtime);
@@ -12519,14 +12851,14 @@ var WagoCommissioningSessions = class extends WagoCommissioningServiceState {
12519
12851
  }
12520
12852
  async confirmHostKey(id, hostKeyFingerprint, trustMethod = "trusted_inventory", physicalIdentityConfirmed = false) {
12521
12853
  if (!["trusted_inventory", "isolated_service_connection"].includes(trustMethod) || trustMethod === "isolated_service_connection" && physicalIdentityConfirmed !== true)
12522
- throw new import_common22.ConflictException("Confirm the isolated service connection and physical controller identity.");
12854
+ throw new import_common23.ConflictException("Confirm the isolated service connection and physical controller identity.");
12523
12855
  return this.withDeliveryLock(id, async () => {
12524
12856
  const session = await this.sessions.findOneBy({ id });
12525
- if (!session) throw new import_common22.NotFoundException("commissioning session not found");
12857
+ if (!session) throw new import_common23.NotFoundException("commissioning session not found");
12526
12858
  if (session.state !== "awaiting_identity_confirmation")
12527
- throw new import_common22.ConflictException("commissioning session identity cannot be confirmed in its current state");
12859
+ throw new import_common23.ConflictException("commissioning session identity cannot be confirmed in its current state");
12528
12860
  if (hostKeyFingerprint !== session.hostKeyFingerprint)
12529
- throw new import_common22.ConflictException(
12861
+ throw new import_common23.ConflictException(
12530
12862
  "the supplied SSH host-key fingerprint does not match the scanned controller identity"
12531
12863
  );
12532
12864
  session.state = "awaiting_delivery";
@@ -12538,24 +12870,24 @@ var WagoCommissioningSessions = class extends WagoCommissioningServiceState {
12538
12870
  }
12539
12871
  async create(input, principal = null) {
12540
12872
  if (!isPrivateAddress(input.targetHost))
12541
- throw new import_common22.ConflictException("commissioning is limited to a protected controller address");
12542
- if (!input.name.trim()) throw new import_common22.ConflictException("a controller name is required");
12873
+ throw new import_common23.ConflictException("commissioning is limited to a protected controller address");
12874
+ if (!input.name.trim()) throw new import_common23.ConflictException("a controller name is required");
12543
12875
  if (!configuredFirmwareBaseline)
12544
- throw new import_common22.ConflictException(
12876
+ throw new import_common23.ConflictException(
12545
12877
  "no CC100 firmware baseline is configured; commissioning is disabled until an exact supported baseline is set"
12546
12878
  );
12547
12879
  let brokerAvailable;
12548
12880
  try {
12549
12881
  brokerAvailable = Boolean(await this.context.getMqttServerConfig(input.mqttServerId));
12550
12882
  } catch {
12551
- throw new import_common22.ConflictException("Commissioning MQTT configuration could not be resolved.");
12883
+ throw new import_common23.ConflictException("Commissioning MQTT configuration could not be resolved.");
12552
12884
  }
12553
- if (!brokerAvailable) throw new import_common22.NotFoundException("MQTT server not found");
12885
+ if (!brokerAvailable) throw new import_common23.NotFoundException("MQTT server not found");
12554
12886
  let hostKeyFingerprint;
12555
12887
  try {
12556
12888
  hostKeyFingerprint = await scanHostKey(input.targetHost);
12557
12889
  } catch {
12558
- throw new import_common22.ConflictException("Commissioning SSH host-key scan failed.");
12890
+ throw new import_common23.ConflictException("Commissioning SSH host-key scan failed.");
12559
12891
  }
12560
12892
  const hardwareId = `cc100-${(0, import_node_crypto20.createHash)("sha256").update(hostKeyFingerprint).digest("hex").slice(0, 16)}`;
12561
12893
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -12617,7 +12949,7 @@ var WagoCommissioningSessions = class extends WagoCommissioningServiceState {
12617
12949
  Buffer.from(script).toString("base64") + "\n",
12618
12950
  { timeoutMs: 12 * 6e4, maxOutputBytes: 1024, recoveryDiagnostic: true }
12619
12951
  );
12620
- if (result !== "OK\n") throw new import_common22.ConflictException("Commissioning acceptance could not be confirmed.");
12952
+ if (result !== "OK\n") throw new import_common23.ConflictException("Commissioning acceptance could not be confirmed.");
12621
12953
  await guard.assertOwned();
12622
12954
  });
12623
12955
  }
@@ -12741,12 +13073,12 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12741
13073
  id,
12742
13074
  () => this.withDeliveryLock(id, async () => {
12743
13075
  const session = await this.sessions.findOneBy({ id });
12744
- if (!session) throw new import_common23.NotFoundException("commissioning session not found");
13076
+ if (!session) throw new import_common24.NotFoundException("commissioning session not found");
12745
13077
  if (["awaiting_identity_confirmation", "delivering"].includes(session.state) || session.state === "revoked" && action !== "recover")
12746
- throw new import_common23.ConflictException("Confirm controller identity and finish delivery before management changes.");
13078
+ throw new import_common24.ConflictException("Confirm controller identity and finish delivery before management changes.");
12747
13079
  if (!session.managementControllerId) {
12748
13080
  const controller = await this.context.getRepository(WagoController).findOneBy({ hardwareId: session.hardwareId, mqttServerId: session.mqttServerId });
12749
- if (!controller) throw new import_common23.ConflictException("Wait for controller enrollment before management inspection.");
13081
+ if (!controller) throw new import_common24.ConflictException("Wait for controller enrollment before management inspection.");
12750
13082
  session.managementControllerId = controller.id;
12751
13083
  await this.save(session, "management_subject_bound");
12752
13084
  }
@@ -12783,7 +13115,7 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12783
13115
  this.operationContext.getStore()?.assertOwned
12784
13116
  );
12785
13117
  } catch (error) {
12786
- throw new import_common23.ConflictException(
13118
+ throw new import_common24.ConflictException(
12787
13119
  error instanceof ManagementError ? `Management security: ${error.code}` : "Management security request failed."
12788
13120
  );
12789
13121
  }
@@ -12802,15 +13134,15 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12802
13134
  }
12803
13135
  async managementStatus(id) {
12804
13136
  const session = await this.sessions.findOneBy({ id });
12805
- if (!session) throw new import_common23.NotFoundException("commissioning session not found");
13137
+ if (!session) throw new import_common24.NotFoundException("commissioning session not found");
12806
13138
  return session.managementControllerId ? this.management.status(session.managementControllerId) : null;
12807
13139
  }
12808
13140
  /** Persist ownership before destructive host changes so interruption is recoverable. */
12809
13141
  async prepareController(session, credential, verifiedBundleBytes, hardwareProfile = CC100_DIGITAL_PROFILE_ID) {
12810
13142
  if (session.deliveryToken)
12811
- throw new import_common23.ConflictException("Clean up the retained runtime installation before preparing the controller.");
13143
+ throw new import_common24.ConflictException("Clean up the retained runtime installation before preparing the controller.");
12812
13144
  if (session.dockerProvisionToken && session.dockerProvisionState !== "started")
12813
- throw new import_common23.ConflictException("Clean up the retained controller preparation before retrying.");
13145
+ throw new import_common24.ConflictException("Clean up the retained controller preparation before retrying.");
12814
13146
  await this.sudoRunScript(
12815
13147
  session.targetHost,
12816
13148
  session.hostKeyFingerprint,
@@ -12880,7 +13212,7 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12880
13212
  async platform(id, action, input, principal = null) {
12881
13213
  const credential = requireDeliveryCredentials({ temporarySsh: input.temporarySsh, confirmInstall: true });
12882
13214
  if (action !== "inspect" && input.reviewedDockerActivation !== true)
12883
- throw new import_common23.ConflictException("Explicit Docker activation or recovery approval is required.");
13215
+ throw new import_common24.ConflictException("Explicit Docker activation or recovery approval is required.");
12884
13216
  return auditCommissioning(
12885
13217
  this.context,
12886
13218
  principal,
@@ -12890,9 +13222,9 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12890
13222
  id,
12891
13223
  () => this.withDeliveryLock(id, async () => {
12892
13224
  const session = await this.sessions.findOneBy({ id });
12893
- if (!session) throw new import_common23.NotFoundException("commissioning session not found");
13225
+ if (!session) throw new import_common24.NotFoundException("commissioning session not found");
12894
13226
  if (["awaiting_identity_confirmation", "delivering"].includes(session.state) || session.state === "revoked" && action !== "recover")
12895
- throw new import_common23.ConflictException("Finish identity confirmation and any active delivery first.");
13227
+ throw new import_common24.ConflictException("Finish identity confirmation and any active delivery first.");
12896
13228
  try {
12897
13229
  if (action === "inspect") {
12898
13230
  session.platformReport = null;
@@ -12919,13 +13251,13 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12919
13251
  }
12920
13252
  } else {
12921
13253
  if (!session.dockerProvisionToken)
12922
- throw new import_common23.ConflictException("No Docker provisioning attempt to recover.");
13254
+ throw new import_common24.ConflictException("No Docker provisioning attempt to recover.");
12923
13255
  await this.cleanupControllerPreparation(session, credential);
12924
13256
  }
12925
13257
  session.failureReason = null;
12926
13258
  return this.toResponse(await this.save(session, `platform_${action}_succeeded`));
12927
13259
  } catch (error) {
12928
- if (error instanceof import_common23.ConflictException) throw error;
13260
+ if (error instanceof import_common24.ConflictException) throw error;
12929
13261
  if (action !== "inspect" && session.dockerProvisionToken)
12930
13262
  session.dockerProvisionState = "recovery_required";
12931
13263
  session.failureReason = error instanceof WagoStorageCapacityError ? error.message : error instanceof WagoControllerLockError ? error.message : error instanceof WagoRecoveryError ? error.message : action === "inspect" ? "Controller preflight could not be read. Check the explicit SSH credential and supported firmware tools." : action === "activate" ? "Controller preparation failed. Check the runtime release, staging storage and required tools. CODESYS must be stopped and permanently disabled before IO or runtime startup. Clean up any retained preparation attempt before retrying." : "Controller preparation cleanup remains unverified. Clean up any runtime transaction first, then retry preparation cleanup. The recovery token is retained; previous workloads are not restored.";
@@ -12957,7 +13289,7 @@ var WagoCommissioningPlatform = class extends WagoCommissioningSessions {
12957
13289
  var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
12958
13290
  async withControllerLock(id, operation) {
12959
13291
  const session = await this.sessions.findOneBy({ id });
12960
- if (!session) throw new import_common24.NotFoundException("commissioning session not found");
13292
+ if (!session) throw new import_common25.NotFoundException("commissioning session not found");
12961
13293
  const key = session.hostKeyFingerprint || session.targetHost || session.hardwareId;
12962
13294
  const previous = this.controllerLocks.get(key) ?? Promise.resolve();
12963
13295
  let release;
@@ -12973,7 +13305,7 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
12973
13305
  deadline,
12974
13306
  assertOwned: async () => {
12975
13307
  if (finished || controller.signal.aborted || Date.now() >= deadline)
12976
- throw new import_common24.ConflictException("Controller operation has stopped. Retry the request.");
13308
+ throw new import_common25.ConflictException("Controller operation has stopped. Retry the request.");
12977
13309
  }
12978
13310
  };
12979
13311
  try {
@@ -12982,7 +13314,7 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
12982
13314
  if (this.managedRuntime) {
12983
13315
  deviceOperations = new WagoDeviceOperations(this.context.getRepository(WagoDeviceOperation));
12984
13316
  if (!await deviceOperations.acquire(key, owner, Date.now(), deadline + 6e4))
12985
- throw new import_common24.ConflictException("Another controller operation is active. Retry after it finishes.");
13317
+ throw new import_common25.ConflictException("Another controller operation is active. Retry after it finishes.");
12986
13318
  }
12987
13319
  const originalAssert = guard.assertOwned;
12988
13320
  guard.assertOwned = async () => {
@@ -13011,7 +13343,7 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
13011
13343
  id,
13012
13344
  () => this.withDeliveryLock(id, async () => {
13013
13345
  const session = await this.sessions.findOneBy({ id });
13014
- if (!session) throw new import_common24.NotFoundException("commissioning session not found");
13346
+ if (!session) throw new import_common25.NotFoundException("commissioning session not found");
13015
13347
  if (![
13016
13348
  "delivery_failed",
13017
13349
  "awaiting_discovery",
@@ -13022,10 +13354,10 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
13022
13354
  "delivering",
13023
13355
  "awaiting_codesys_confirmation"
13024
13356
  ].includes(session.state))
13025
- throw new import_common24.ConflictException("commissioning session cannot be recovered in its current state");
13357
+ throw new import_common25.ConflictException("commissioning session cannot be recovered in its current state");
13026
13358
  const requiresNewSession = !session.pairingCode || ["claim_interrupted", "awaiting_verification"].includes(session.state);
13027
13359
  if (!session.deliveryToken)
13028
- throw new import_common24.ConflictException("commissioning session has no runtime recovery ownership token");
13360
+ throw new import_common25.ConflictException("commissioning session has no runtime recovery ownership token");
13029
13361
  let restored = session.state === "recovery_revocation_pending";
13030
13362
  try {
13031
13363
  const recoveryPassword = await this.managedRuntime?.commissioningRecoveryPassword?.(session);
@@ -13085,7 +13417,7 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
13085
13417
  }
13086
13418
  async acquireRuntimeBundle(session) {
13087
13419
  await this.requireRuntimeArtifact();
13088
- if (!this.artifacts) throw new import_common24.ConflictException("Bundled CC100 runtime is unavailable.");
13420
+ if (!this.artifacts) throw new import_common25.ConflictException("Bundled CC100 runtime is unavailable.");
13089
13421
  const acquired = await this.artifacts.acquire();
13090
13422
  const bundle = {
13091
13423
  ...acquired,
@@ -13104,13 +13436,13 @@ var WagoCommissioningRecovery = class extends WagoCommissioningPlatform {
13104
13436
  }
13105
13437
  async requireRuntimeArtifact() {
13106
13438
  if (!await this.artifacts?.has())
13107
- throw new import_common24.ConflictException("Build and install the bundled CC100 runtime before installation.");
13439
+ throw new import_common25.ConflictException("Build and install the bundled CC100 runtime before installation.");
13108
13440
  }
13109
13441
  async loadDeliverableSession(id) {
13110
13442
  const session = await this.sessions.findOneBy({ id });
13111
- if (!session) throw new import_common24.NotFoundException("commissioning session not found");
13443
+ if (!session) throw new import_common25.NotFoundException("commissioning session not found");
13112
13444
  if (!["awaiting_delivery", "delivering", "awaiting_codesys_confirmation", "delivery_failed"].includes(session.state))
13113
- throw new import_common24.ConflictException("commissioning session cannot be delivered in its current state");
13445
+ throw new import_common25.ConflictException("commissioning session cannot be delivered in its current state");
13114
13446
  return session;
13115
13447
  }
13116
13448
  async deliver(id, input = {}, principal = null) {
@@ -13207,7 +13539,7 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13207
13539
  /** The HTTP audit wrapper belongs inside remove(), so removal is audited once by ATT-983. */
13208
13540
  async removeControllerSafely(id, remove) {
13209
13541
  const controller = await this.context.getRepository(WagoController).findOneBy({ id });
13210
- if (!controller) throw new import_common25.NotFoundException("controller not found");
13542
+ if (!controller) throw new import_common26.NotFoundException("controller not found");
13211
13543
  const sessions = await this.sessions.find({ where: { hardwareId: controller.hardwareId }, order: { id: "DESC" } });
13212
13544
  if (!sessions.length) {
13213
13545
  await remove(async () => void 0);
@@ -13215,7 +13547,7 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13215
13547
  }
13216
13548
  await this.withControllerLock(sessions[0].id, async () => {
13217
13549
  const guard = this.operationContext.getStore();
13218
- if (!guard) throw new import_common25.ConflictException("Controller operation ownership is unavailable.");
13550
+ if (!guard) throw new import_common26.ConflictException("Controller operation ownership is unavailable.");
13219
13551
  const assertOwned = guard.assertOwned;
13220
13552
  await assertOwned();
13221
13553
  await this.managedRuntime?.assertRemovable(id);
@@ -13247,15 +13579,15 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13247
13579
  }
13248
13580
  async operateControllerSafely(id, operation, requireCommissioningSession = false) {
13249
13581
  const controller = await this.context.getRepository(WagoController).findOneBy({ id });
13250
- if (!controller) throw new import_common25.NotFoundException("controller not found");
13582
+ if (!controller) throw new import_common26.NotFoundException("controller not found");
13251
13583
  const sessions = await this.sessions.find({ where: { hardwareId: controller.hardwareId }, order: { id: "DESC" } });
13252
13584
  if (!sessions.length) {
13253
13585
  if (requireCommissioningSession)
13254
- throw new import_common25.ConflictException("A commissioning session must remain pinned while rotating credentials.");
13586
+ throw new import_common26.ConflictException("A commissioning session must remain pinned while rotating credentials.");
13255
13587
  const controller2 = new AbortController();
13256
13588
  const guard = {
13257
13589
  assertOwned: async () => {
13258
- if (controller2.signal.aborted) throw new import_common25.ConflictException("Controller operation ended");
13590
+ if (controller2.signal.aborted) throw new import_common26.ConflictException("Controller operation ended");
13259
13591
  },
13260
13592
  signal: controller2.signal,
13261
13593
  deadline: Date.now() + 6e4
@@ -13268,7 +13600,7 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13268
13600
  }
13269
13601
  return this.withControllerLock(sessions[0].id, async () => {
13270
13602
  const guard = this.operationContext.getStore();
13271
- if (!guard) throw new import_common25.ConflictException("Controller operation ownership is unavailable.");
13603
+ if (!guard) throw new import_common26.ConflictException("Controller operation ownership is unavailable.");
13272
13604
  await guard.assertOwned();
13273
13605
  return operation(guard.assertOwned, guard);
13274
13606
  });
@@ -13282,26 +13614,26 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13282
13614
  id,
13283
13615
  () => this.withDeliveryLock(id, async () => {
13284
13616
  const session = await this.sessions.findOneBy({ id });
13285
- if (!session) throw new import_common25.NotFoundException("commissioning session not found");
13617
+ if (!session) throw new import_common26.NotFoundException("commissioning session not found");
13286
13618
  if (session.deliveryToken)
13287
- throw new import_common25.ConflictException("Clean up the retained runtime installation before deleting this session.");
13619
+ throw new import_common26.ConflictException("Clean up the retained runtime installation before deleting this session.");
13288
13620
  if (session.dockerProvisionToken)
13289
- throw new import_common25.ConflictException("Clean up the retained controller preparation before deleting this session.");
13621
+ throw new import_common26.ConflictException("Clean up the retained controller preparation before deleting this session.");
13290
13622
  if (await this.managedRuntime?.hasAccess(id))
13291
- throw new import_common25.ConflictException(
13623
+ throw new import_common26.ConflictException(
13292
13624
  "Retain this session for audited managed SSH recovery. Re-enrolment creates a fresh management identity."
13293
13625
  );
13294
13626
  if (session.managementControllerId) {
13295
13627
  const security = await this.management.status(session.managementControllerId);
13296
13628
  if (security && (security.recoveryRequired || ["key_enrolled", "hardened"].includes(security.state)))
13297
- throw new import_common25.ConflictException("Recover the saved management access before deleting this session.");
13629
+ throw new import_common26.ConflictException("Recover the saved management access before deleting this session.");
13298
13630
  }
13299
13631
  if (session.enrollmentId !== null) {
13300
13632
  try {
13301
13633
  await this.wago.revokeEnrollmentById(session.enrollmentId, this.operationContext.getStore()?.assertOwned);
13302
13634
  await this.wago.deleteEnrollmentById(session.enrollmentId, this.operationContext.getStore()?.assertOwned);
13303
13635
  } catch {
13304
- throw new import_common25.ConflictException("Commissioning enrollment removal failed.");
13636
+ throw new import_common26.ConflictException("Commissioning enrollment removal failed.");
13305
13637
  }
13306
13638
  }
13307
13639
  await this.operationContext.getStore()?.assertOwned();
@@ -13314,7 +13646,7 @@ var WagoCommissioningOwnership = class extends WagoCommissioningRecovery {
13314
13646
  id,
13315
13647
  () => this.withDeliveryLock(id, async () => {
13316
13648
  const session = await this.sessions.findOneBy({ id });
13317
- if (!session) throw new import_common25.NotFoundException("commissioning session not found");
13649
+ if (!session) throw new import_common26.NotFoundException("commissioning session not found");
13318
13650
  await this.revokeSessionEnrollment(session);
13319
13651
  session.state = "revoked";
13320
13652
  session.pairingCode = null;
@@ -13392,7 +13724,7 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
13392
13724
  pinnedKey = await pinnedHostKey(host, fingerprint2);
13393
13725
  } catch (error) {
13394
13726
  if (limits2?.recoveryDiagnostic)
13395
- throw new WagoRecoveryError(error instanceof import_common26.ConflictException ? "identity" : "transport");
13727
+ throw new WagoRecoveryError(error instanceof import_common27.ConflictException ? "identity" : "transport");
13396
13728
  throw error;
13397
13729
  }
13398
13730
  await (0, import_promises8.writeFile)(knownHosts, pinnedKey, { mode: 384 });
@@ -13619,7 +13951,7 @@ var WagoCommissioningProgress = class extends WagoCommissioningTransport {
13619
13951
  }
13620
13952
  if (page.length < 100) break;
13621
13953
  }
13622
- if (recoveryFailed) throw new import_common27.ConflictException("Commissioning recovery requires credential revocation.");
13954
+ if (recoveryFailed) throw new import_common28.ConflictException("Commissioning recovery requires credential revocation.");
13623
13955
  }
13624
13956
  async invalidateVerifier(session) {
13625
13957
  session.state = "revoked";
@@ -13638,7 +13970,7 @@ var WagoCommissioningProgress = class extends WagoCommissioningTransport {
13638
13970
  try {
13639
13971
  await this.wago.revokeEnrollmentById(session.enrollmentId, this.operationContext.getStore()?.assertOwned);
13640
13972
  } catch {
13641
- throw new import_common27.ConflictException("Commissioning credential revocation requires attention.");
13973
+ throw new import_common28.ConflictException("Commissioning credential revocation requires attention.");
13642
13974
  }
13643
13975
  session.enrollmentId = null;
13644
13976
  session.enrollmentExpiresAt = null;
@@ -13651,14 +13983,14 @@ var WagoCommissioningProgress = class extends WagoCommissioningTransport {
13651
13983
  if (!/^[A-Za-z0-9_-]{43}$/.test(plaintext)) throw new Error();
13652
13984
  return plaintext;
13653
13985
  } catch {
13654
- throw new import_common27.ConflictException("Commissioning verifier is unavailable; create a new session.");
13986
+ throw new import_common28.ConflictException("Commissioning verifier is unavailable; create a new session.");
13655
13987
  }
13656
13988
  }
13657
13989
  encryptVerifier(plaintext) {
13658
13990
  try {
13659
13991
  return VERIFIER_PREFIX + this.context.secrets.encrypt(plaintext);
13660
13992
  } catch {
13661
- throw new import_common27.ConflictException("Commissioning verifier encryption failed.");
13993
+ throw new import_common28.ConflictException("Commissioning verifier encryption failed.");
13662
13994
  }
13663
13995
  }
13664
13996
  reportPreparationProgress(session, checkpoint) {
@@ -13748,11 +14080,11 @@ var WagoCommissioningService = class extends WagoCommissioningProgress {
13748
14080
  };
13749
14081
  try {
13750
14082
  const broker = await this.context.getMqttServerConfig(session.mqttServerId);
13751
- if (!broker) throw new import_common28.ConflictException("MQTT server not found");
14083
+ if (!broker) throw new import_common29.ConflictException("MQTT server not found");
13752
14084
  try {
13753
14085
  assertCommissioningBroker(broker);
13754
14086
  } catch (error) {
13755
- if (error instanceof import_common28.ConflictException) attempt.safeFailure = error.message;
14087
+ if (error instanceof import_common29.ConflictException) attempt.safeFailure = error.message;
13756
14088
  throw error;
13757
14089
  }
13758
14090
  const providers = await this.context.getMqttCredentialProvisioning().availableProviders(session.mqttServerId);
@@ -13969,22 +14301,22 @@ var WagoCommissioningService = class extends WagoCommissioningProgress {
13969
14301
  }
13970
14302
  };
13971
14303
  WagoCommissioningService = __decorateClass([
13972
- (0, import_common28.Injectable)(),
13973
- __decorateParam(0, (0, import_common28.Inject)(import_plugins_backend_sdk14.PLUGIN_CONTEXT)),
13974
- __decorateParam(1, (0, import_common28.Inject)(WagoService)),
13975
- __decorateParam(2, (0, import_common28.Optional)()),
13976
- __decorateParam(2, (0, import_common28.Inject)(WagoRuntimeArtifactsService)),
13977
- __decorateParam(3, (0, import_common28.Optional)()),
13978
- __decorateParam(3, (0, import_common28.Inject)(WagoCommissioningReadiness)),
13979
- __decorateParam(4, (0, import_common28.Optional)()),
13980
- __decorateParam(4, (0, import_common28.Inject)(WagoManagedRuntimeService))
14304
+ (0, import_common29.Injectable)(),
14305
+ __decorateParam(0, (0, import_common29.Inject)(import_plugins_backend_sdk14.PLUGIN_CONTEXT)),
14306
+ __decorateParam(1, (0, import_common29.Inject)(WagoService)),
14307
+ __decorateParam(2, (0, import_common29.Optional)()),
14308
+ __decorateParam(2, (0, import_common29.Inject)(WagoRuntimeArtifactsService)),
14309
+ __decorateParam(3, (0, import_common29.Optional)()),
14310
+ __decorateParam(3, (0, import_common29.Inject)(WagoCommissioningReadiness)),
14311
+ __decorateParam(4, (0, import_common29.Optional)()),
14312
+ __decorateParam(4, (0, import_common29.Inject)(WagoManagedRuntimeService))
13981
14313
  ], WagoCommissioningService);
13982
14314
 
13983
14315
  // backend/credentials/service.ts
13984
14316
  var import_plugins_backend_sdk15 = require("@attraccess/plugins-backend-sdk");
13985
- var import_common29 = require("@nestjs/common");
14317
+ var import_common30 = require("@nestjs/common");
13986
14318
  var import_node_crypto24 = require("node:crypto");
13987
- var WagoCredentialRotationUncertainError = class extends import_common29.ConflictException {
14319
+ var WagoCredentialRotationUncertainError = class extends import_common30.ConflictException {
13988
14320
  constructor() {
13989
14321
  super("Credential rotation is incomplete. Inspect its recovery state and retry the pending handoff.");
13990
14322
  }
@@ -14032,9 +14364,9 @@ var CAPABILITY = "credential-rotation-v1";
14032
14364
  var WAIT_MS = 3e4;
14033
14365
  function assertRotationController(controller, retry) {
14034
14366
  if (controller.trustState !== "claimed" || !controller.mqttServerId || controller.compatibilityError)
14035
- throw new import_common29.ConflictException("A compatible claimed controller is required.");
14367
+ throw new import_common30.ConflictException("A compatible claimed controller is required.");
14036
14368
  if ("credentialMqttServerId" in controller && controller.credentialMqttServerId != null && controller.credentialMqttServerId !== controller.mqttServerId)
14037
- throw new import_common29.ConflictException("Use the original credential broker before rotating credentials.");
14369
+ throw new import_common30.ConflictException("Use the original credential broker before rotating credentials.");
14038
14370
  let capabilities;
14039
14371
  try {
14040
14372
  capabilities = JSON.parse(controller.capabilities);
@@ -14042,22 +14374,22 @@ function assertRotationController(controller, retry) {
14042
14374
  capabilities = [];
14043
14375
  }
14044
14376
  if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY))
14045
- throw new import_common29.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
14377
+ throw new import_common30.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
14046
14378
  const heartbeatAt = sourceTime(controller.lastHeartbeatAt);
14047
14379
  const now = Date.now();
14048
14380
  if (!retry && (heartbeatAt === null || heartbeatAt > now + CONTROLLER_CLOCK_TOLERANCE_MS || now - heartbeatAt >= 9e4))
14049
- throw new import_common29.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
14381
+ throw new import_common30.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
14050
14382
  }
14051
14383
  function checkPendingRotation(previous, credentialEpoch, retry, mqttServerId, prefix) {
14052
14384
  if (previous && previous.credentialEpoch !== credentialEpoch)
14053
- throw new import_common29.ConflictException("Recover the previous credential registration before rotating again.");
14385
+ throw new import_common30.ConflictException("Recover the previous credential registration before rotating again.");
14054
14386
  if (previous && previous.phase !== "completed" && !retry)
14055
- throw new import_common29.ConflictException("Retry the pending credential handoff instead of rotating again.");
14387
+ throw new import_common30.ConflictException("Retry the pending credential handoff instead of rotating again.");
14056
14388
  if (retry && (!previous || previous.phase === "provisioning"))
14057
- throw new import_common29.ConflictException("Broker rotation completion is uncertain. Recover broker credentials before retrying.");
14389
+ throw new import_common30.ConflictException("Broker rotation completion is uncertain. Recover broker credentials before retrying.");
14058
14390
  if (retry && previous?.phase === "completed") return { state: "completed", revision: previous.revision };
14059
14391
  if (previous && (previous.mqttServerId !== mqttServerId || previous.prefix !== prefix))
14060
- throw new import_common29.ConflictException("Credential rotation must use its original broker and namespace.");
14392
+ throw new import_common30.ConflictException("Credential rotation must use its original broker and namespace.");
14061
14393
  return void 0;
14062
14394
  }
14063
14395
  var WagoCredentialRotationService = class {
@@ -14085,10 +14417,10 @@ var WagoCredentialRotationService = class {
14085
14417
  (async () => {
14086
14418
  await guard.assertOwned();
14087
14419
  const pending = await this.context.mqtt.subscribe(row.mqttServerId, `${topic}/ack`, (message) => {
14088
- if (closed || guard.signal.aborted || message.serverId !== row.mqttServerId || message.topic !== `${topic}/ack` || message.payload.length > 1024)
14420
+ if (closed || guard.signal.aborted || message.serverId !== row.mqttServerId || message.topic !== `${topic}/ack`)
14089
14421
  return;
14090
14422
  try {
14091
- const ack = JSON.parse(message.payload.toString("utf8"));
14423
+ const ack = parseControllerJson(message.payload, "rotation acknowledgement");
14092
14424
  if (ack?.credentialEpoch === row.credentialEpoch && ack?.revision === row.revision && ack?.token === row.token && ack?.status === "reconnected")
14093
14425
  acknowledge();
14094
14426
  } catch {
@@ -14127,7 +14459,7 @@ var WagoCredentialRotationService = class {
14127
14459
  async rotate(controllerId, prefix, principal, guard, retry = false) {
14128
14460
  await guard.assertOwned();
14129
14461
  const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
14130
- if (!controller) throw new import_common29.NotFoundException("WAGO controller not found");
14462
+ if (!controller) throw new import_common30.NotFoundException("WAGO controller not found");
14131
14463
  assertRotationController(controller, retry);
14132
14464
  const [epochRow] = await this.repository.query(
14133
14465
  "SELECT credential_epoch AS epoch FROM plugin_wago_controllers WHERE id = ?",
@@ -14135,7 +14467,7 @@ var WagoCredentialRotationService = class {
14135
14467
  );
14136
14468
  const credentialEpoch = epochRow?.epoch;
14137
14469
  if (typeof credentialEpoch !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(credentialEpoch))
14138
- throw new import_common29.ConflictException("Re-enroll this controller with a credential epoch before rotating credentials.");
14470
+ throw new import_common30.ConflictException("Re-enroll this controller with a credential epoch before rotating credentials.");
14139
14471
  prefix = normalizeOperationalPrefix(prefix);
14140
14472
  const identity = `wago-controller-${controller.hardwareId}`;
14141
14473
  const previous = await this.repository.createQueryBuilder("rotation").addSelect("rotation.encryptedCredentials").where("rotation.controllerId = :controllerId", { controllerId }).getOne();
@@ -14148,7 +14480,7 @@ var WagoCredentialRotationService = class {
14148
14480
  let row = previous;
14149
14481
  if (!retry) {
14150
14482
  const revision2 = (previous?.revision ?? 0) + 1;
14151
- if (!Number.isSafeInteger(revision2)) throw new import_common29.ConflictException("Credential rotation revision exhausted.");
14483
+ if (!Number.isSafeInteger(revision2)) throw new import_common30.ConflictException("Credential rotation revision exhausted.");
14152
14484
  row = this.repository.create({
14153
14485
  controllerId,
14154
14486
  revision: revision2,
@@ -14179,23 +14511,23 @@ var WagoCredentialRotationService = class {
14179
14511
  uncertain = false;
14180
14512
  if (previous) await this.repository.save(previous);
14181
14513
  else await this.repository.delete(controllerId);
14182
- throw new import_common29.ConflictException("Automatic broker credential rotation is unavailable.");
14514
+ throw new import_common30.ConflictException("Automatic broker credential rotation is unavailable.");
14183
14515
  }
14184
14516
  if (credential2.username !== identity || !credential2.password || credential2.password.length > 4096)
14185
- throw new import_common29.ConflictException("Broker credential rotation needs recovery.");
14517
+ throw new import_common30.ConflictException("Broker credential rotation needs recovery.");
14186
14518
  const plaintext = JSON.stringify({ username: identity, password: credential2.password });
14187
14519
  const encrypted = this.context.secrets.encrypt(plaintext);
14188
14520
  if (!encrypted || encrypted === plaintext || this.context.secrets.decrypt(encrypted) !== plaintext)
14189
- throw new import_common29.ConflictException("Credential recovery storage is unavailable.");
14521
+ throw new import_common30.ConflictException("Credential recovery storage is unavailable.");
14190
14522
  row.encryptedCredentials = encrypted;
14191
14523
  row.phase = "pending";
14192
14524
  await guard.assertOwned();
14193
14525
  await this.repository.save(row);
14194
14526
  }
14195
- if (!row?.encryptedCredentials) throw new import_common29.ConflictException("Credential recovery storage is unavailable.");
14527
+ if (!row?.encryptedCredentials) throw new import_common30.ConflictException("Credential recovery storage is unavailable.");
14196
14528
  const credential = JSON.parse(this.context.secrets.decrypt(row.encryptedCredentials));
14197
14529
  if (credential.username !== identity || typeof credential.password !== "string" || !credential.password || credential.password.length > 4096)
14198
- throw new import_common29.ConflictException("Credential recovery storage is unavailable.");
14530
+ throw new import_common30.ConflictException("Credential recovery storage is unavailable.");
14199
14531
  await this.handoff(controller.hardwareId, row, credential, guard);
14200
14532
  await guard.assertOwned();
14201
14533
  row.phase = "completed";
@@ -14207,7 +14539,7 @@ var WagoCredentialRotationService = class {
14207
14539
  } catch {
14208
14540
  await lifecycle.finish("failed");
14209
14541
  if (uncertain) throw new WagoCredentialRotationUncertainError();
14210
- throw new import_common29.ConflictException(
14542
+ throw new import_common30.ConflictException(
14211
14543
  "Credential rotation is incomplete. Inspect its recovery state and retry the pending handoff."
14212
14544
  );
14213
14545
  }
@@ -14216,7 +14548,7 @@ var WagoCredentialRotationService = class {
14216
14548
  async assertRemovalBroker(controllerId, mqttServerId) {
14217
14549
  const row = await this.repository.findOneBy({ controllerId });
14218
14550
  if (row && row.mqttServerId !== mqttServerId)
14219
- throw new import_common29.ConflictException("Revoke the original rotation broker identity before removing this controller.");
14551
+ throw new import_common30.ConflictException("Revoke the original rotation broker identity before removing this controller.");
14220
14552
  }
14221
14553
  async status(controllerId) {
14222
14554
  const row = await this.repository.findOne({ where: { controllerId }, select: { phase: true, revision: true } });
@@ -14227,15 +14559,15 @@ var WagoCredentialRotationService = class {
14227
14559
  }
14228
14560
  };
14229
14561
  WagoCredentialRotationService = __decorateClass([
14230
- (0, import_common29.Injectable)(),
14231
- __decorateParam(0, (0, import_common29.Inject)(import_plugins_backend_sdk15.PLUGIN_CONTEXT))
14562
+ (0, import_common30.Injectable)(),
14563
+ __decorateParam(0, (0, import_common30.Inject)(import_plugins_backend_sdk15.PLUGIN_CONTEXT))
14232
14564
  ], WagoCredentialRotationService);
14233
14565
 
14234
14566
  // backend/controllers/controller.ts
14235
14567
  function validateCommissioningAttempt(body, intent) {
14236
- if (body?.confirmInstall !== true) throw new import_common30.BadRequestException(`Explicit ${intent} consent is required`);
14568
+ if (body?.confirmInstall !== true) throw new import_common31.BadRequestException(`Explicit ${intent} consent is required`);
14237
14569
  if (typeof body.temporarySsh?.username !== "string" || !body.temporarySsh.username.trim() || typeof body.temporarySsh.password !== "string" || !body.temporarySsh.password) {
14238
- throw new import_common30.BadRequestException("Temporary SSH username and password are required");
14570
+ throw new import_common31.BadRequestException("Temporary SSH username and password are required");
14239
14571
  }
14240
14572
  return {
14241
14573
  confirmInstall: true,
@@ -14283,7 +14615,7 @@ var WagoControllerApi = class {
14283
14615
  return this.wago.saveDraft(id, body?.snapshot, body?.metadata, wagoAuditPrincipal(request), body?.expectedDraft);
14284
14616
  }
14285
14617
  applyPreset(id, body, request) {
14286
- if (!body?.application) throw new import_common30.BadRequestException("application is required");
14618
+ if (!body?.application) throw new import_common31.BadRequestException("application is required");
14287
14619
  return this.wago.applyPreset(
14288
14620
  id,
14289
14621
  body.application,
@@ -14294,7 +14626,7 @@ var WagoControllerApi = class {
14294
14626
  );
14295
14627
  }
14296
14628
  previewPreset(id, body) {
14297
- if (!body?.application) throw new import_common30.BadRequestException("application is required");
14629
+ if (!body?.application) throw new import_common31.BadRequestException("application is required");
14298
14630
  return this.wago.previewPreset(id, body.application, body.snapshot);
14299
14631
  }
14300
14632
  presets() {
@@ -14317,9 +14649,9 @@ var WagoControllerApi = class {
14317
14649
  }
14318
14650
  async rotateCredentials(id, body, request) {
14319
14651
  if (!body || Object.keys(body).some((key) => key !== "confirm" && key !== "retry") || body.confirm !== true)
14320
- throw new import_common30.BadRequestException("Explicit credential rotation consent is required");
14652
+ throw new import_common31.BadRequestException("Explicit credential rotation consent is required");
14321
14653
  if (body.retry !== void 0 && typeof body.retry !== "boolean")
14322
- throw new import_common30.BadRequestException("Invalid rotation retry flag");
14654
+ throw new import_common31.BadRequestException("Invalid rotation retry flag");
14323
14655
  const settings = await this.wago.getSettings();
14324
14656
  return this.commissioning.operateControllerSafely(
14325
14657
  id,
@@ -14339,7 +14671,7 @@ var WagoControllerApi = class {
14339
14671
  completeManualCredentials(id, body, request) {
14340
14672
  const principal = wagoAuditPrincipal(request);
14341
14673
  if (!body || Object.keys(body).some((key) => !["name", "verifier", "username", "password"].includes(key)) || typeof body.name !== "string" || !body.name || typeof body.verifier !== "string" || !body.verifier || typeof body.username !== "string" || !body.username || typeof body.password !== "string" || !body.password)
14342
- throw new import_common30.BadRequestException("Controller name, physical verifier and provisioned credentials are required");
14674
+ throw new import_common31.BadRequestException("Controller name, physical verifier and provisioned credentials are required");
14343
14675
  const input = { name: body.name, verifier: body.verifier, username: body.username, password: body.password };
14344
14676
  return this.commissioning.operateControllerSafely(
14345
14677
  id,
@@ -14363,7 +14695,7 @@ var WagoControllerApi = class {
14363
14695
  }
14364
14696
  manageSecurity(id, action, body, request) {
14365
14697
  if (!["inspect", "review", "apply", "recover"].includes(action))
14366
- throw new import_common30.BadRequestException("Unknown management action");
14698
+ throw new import_common31.BadRequestException("Unknown management action");
14367
14699
  return this.commissioning.manageSecurity(
14368
14700
  id,
14369
14701
  action,
@@ -14372,7 +14704,7 @@ var WagoControllerApi = class {
14372
14704
  );
14373
14705
  }
14374
14706
  platformAction(id, action, body, request) {
14375
- if (!["inspect", "activate", "recover"].includes(action)) throw new import_common30.BadRequestException("Unknown platform action");
14707
+ if (!["inspect", "activate", "recover"].includes(action)) throw new import_common31.BadRequestException("Unknown platform action");
14376
14708
  return this.commissioning.platform(
14377
14709
  id,
14378
14710
  action,
@@ -14401,7 +14733,7 @@ var WagoControllerApi = class {
14401
14733
  );
14402
14734
  }
14403
14735
  confirmCommissioningHostKey(id, body) {
14404
- if (!body?.hostKeyFingerprint) throw new import_common30.BadRequestException("SSH host-key fingerprint is required");
14736
+ if (!body?.hostKeyFingerprint) throw new import_common31.BadRequestException("SSH host-key fingerprint is required");
14405
14737
  return this.commissioning.confirmHostKey(
14406
14738
  id,
14407
14739
  body.hostKeyFingerprint,
@@ -14410,8 +14742,8 @@ var WagoControllerApi = class {
14410
14742
  );
14411
14743
  }
14412
14744
  createCommissioningSession(body, request) {
14413
- if (!body?.mqttServerId) throw new import_common30.BadRequestException("MQTT server is required");
14414
- if (!body.name?.trim()) throw new import_common30.BadRequestException("controller name is required");
14745
+ if (!body?.mqttServerId) throw new import_common31.BadRequestException("MQTT server is required");
14746
+ if (!body.name?.trim()) throw new import_common31.BadRequestException("controller name is required");
14415
14747
  return this.commissioning.create(
14416
14748
  {
14417
14749
  mqttServerId: body.mqttServerId,
@@ -14438,208 +14770,208 @@ var WagoControllerApi = class {
14438
14770
  }
14439
14771
  };
14440
14772
  __decorateClass([
14441
- (0, import_common30.Get)("controllers/:id/configuration/revisions/:revision/preview"),
14442
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14443
- __decorateParam(1, (0, import_common30.Param)("revision", import_common30.ParseIntPipe))
14773
+ (0, import_common31.Get)("controllers/:id/configuration/revisions/:revision/preview"),
14774
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14775
+ __decorateParam(1, (0, import_common31.Param)("revision", import_common31.ParseIntPipe))
14444
14776
  ], WagoControllerApi.prototype, "previewRevision", 1);
14445
14777
  __decorateClass([
14446
- (0, import_common30.Post)("controllers/:id/configuration/revisions/:revision/acknowledge-rejection"),
14447
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14448
- __decorateParam(1, (0, import_common30.Param)("revision", import_common30.ParseIntPipe)),
14449
- __decorateParam(2, (0, import_common30.Req)()),
14450
- __decorateParam(3, (0, import_common30.Body)())
14778
+ (0, import_common31.Post)("controllers/:id/configuration/revisions/:revision/acknowledge-rejection"),
14779
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14780
+ __decorateParam(1, (0, import_common31.Param)("revision", import_common31.ParseIntPipe)),
14781
+ __decorateParam(2, (0, import_common31.Req)()),
14782
+ __decorateParam(3, (0, import_common31.Body)())
14451
14783
  ], WagoControllerApi.prototype, "acknowledgeRejection", 1);
14452
14784
  __decorateClass([
14453
- (0, import_common30.Post)("controllers/:id/configuration/rollback/:revision"),
14454
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14455
- __decorateParam(1, (0, import_common30.Param)("revision", import_common30.ParseIntPipe)),
14456
- __decorateParam(2, (0, import_common30.Req)()),
14457
- __decorateParam(3, (0, import_common30.Body)())
14785
+ (0, import_common31.Post)("controllers/:id/configuration/rollback/:revision"),
14786
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14787
+ __decorateParam(1, (0, import_common31.Param)("revision", import_common31.ParseIntPipe)),
14788
+ __decorateParam(2, (0, import_common31.Req)()),
14789
+ __decorateParam(3, (0, import_common31.Body)())
14458
14790
  ], WagoControllerApi.prototype, "rollback", 1);
14459
14791
  __decorateClass([
14460
- (0, import_common30.Post)("controllers/:id/configuration/publish"),
14461
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14462
- __decorateParam(1, (0, import_common30.Req)()),
14463
- __decorateParam(2, (0, import_common30.Body)())
14792
+ (0, import_common31.Post)("controllers/:id/configuration/publish"),
14793
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14794
+ __decorateParam(1, (0, import_common31.Req)()),
14795
+ __decorateParam(2, (0, import_common31.Body)())
14464
14796
  ], WagoControllerApi.prototype, "publishDraft", 1);
14465
14797
  __decorateClass([
14466
- (0, import_common30.Get)("controllers/:id/configuration/revisions"),
14467
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14468
- __decorateParam(1, (0, import_common30.Query)("offset")),
14469
- __decorateParam(2, (0, import_common30.Query)("limit"))
14798
+ (0, import_common31.Get)("controllers/:id/configuration/revisions"),
14799
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14800
+ __decorateParam(1, (0, import_common31.Query)("offset")),
14801
+ __decorateParam(2, (0, import_common31.Query)("limit"))
14470
14802
  ], WagoControllerApi.prototype, "revisions", 1);
14471
14803
  __decorateClass([
14472
- (0, import_common30.Post)("controllers/:id/configuration/review"),
14473
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14804
+ (0, import_common31.Post)("controllers/:id/configuration/review"),
14805
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14474
14806
  ], WagoControllerApi.prototype, "reviewDraft", 1);
14475
14807
  __decorateClass([
14476
- (0, import_common30.Post)("controllers/:id/configuration/validate"),
14477
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14478
- __decorateParam(1, (0, import_common30.Body)())
14808
+ (0, import_common31.Post)("controllers/:id/configuration/validate"),
14809
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14810
+ __decorateParam(1, (0, import_common31.Body)())
14479
14811
  ], WagoControllerApi.prototype, "validateDraft", 1);
14480
14812
  __decorateClass([
14481
- (0, import_common30.Post)("controllers/:id/configuration/draft"),
14482
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14483
- __decorateParam(1, (0, import_common30.Body)()),
14484
- __decorateParam(2, (0, import_common30.Req)())
14813
+ (0, import_common31.Post)("controllers/:id/configuration/draft"),
14814
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14815
+ __decorateParam(1, (0, import_common31.Body)()),
14816
+ __decorateParam(2, (0, import_common31.Req)())
14485
14817
  ], WagoControllerApi.prototype, "saveDraft", 1);
14486
14818
  __decorateClass([
14487
- (0, import_common30.Post)("controllers/:id/configuration/presets/apply"),
14488
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14489
- __decorateParam(1, (0, import_common30.Body)()),
14490
- __decorateParam(2, (0, import_common30.Req)())
14819
+ (0, import_common31.Post)("controllers/:id/configuration/presets/apply"),
14820
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14821
+ __decorateParam(1, (0, import_common31.Body)()),
14822
+ __decorateParam(2, (0, import_common31.Req)())
14491
14823
  ], WagoControllerApi.prototype, "applyPreset", 1);
14492
14824
  __decorateClass([
14493
- (0, import_common30.Post)("controllers/:id/configuration/presets/preview"),
14494
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14495
- __decorateParam(1, (0, import_common30.Body)())
14825
+ (0, import_common31.Post)("controllers/:id/configuration/presets/preview"),
14826
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14827
+ __decorateParam(1, (0, import_common31.Body)())
14496
14828
  ], WagoControllerApi.prototype, "previewPreset", 1);
14497
14829
  __decorateClass([
14498
- (0, import_common30.Get)("configuration/presets")
14830
+ (0, import_common31.Get)("configuration/presets")
14499
14831
  ], WagoControllerApi.prototype, "presets", 1);
14500
14832
  __decorateClass([
14501
- (0, import_common30.Get)("controllers/:id/configuration/baseline"),
14502
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14833
+ (0, import_common31.Get)("controllers/:id/configuration/baseline"),
14834
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14503
14835
  ], WagoControllerApi.prototype, "baseline", 1);
14504
14836
  __decorateClass([
14505
- (0, import_common30.Get)("controllers/:id/configuration/draft"),
14506
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14837
+ (0, import_common31.Get)("controllers/:id/configuration/draft"),
14838
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14507
14839
  ], WagoControllerApi.prototype, "draft", 1);
14508
14840
  __decorateClass([
14509
- (0, import_common30.Delete)("controllers/:id"),
14510
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14511
- __decorateParam(1, (0, import_common30.Req)())
14841
+ (0, import_common31.Delete)("controllers/:id"),
14842
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14843
+ __decorateParam(1, (0, import_common31.Req)())
14512
14844
  ], WagoControllerApi.prototype, "removeController", 1);
14513
14845
  __decorateClass([
14514
14846
  (0, import_plugins_backend_sdk16.Auth)("resources.update"),
14515
- (0, import_common30.Post)("controllers/:id/commands"),
14516
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14517
- __decorateParam(1, (0, import_common30.Body)()),
14518
- __decorateParam(2, (0, import_common30.Req)())
14847
+ (0, import_common31.Post)("controllers/:id/commands"),
14848
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14849
+ __decorateParam(1, (0, import_common31.Body)()),
14850
+ __decorateParam(2, (0, import_common31.Req)())
14519
14851
  ], WagoControllerApi.prototype, "manualCommand", 1);
14520
14852
  __decorateClass([
14521
14853
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14522
- (0, import_common30.Post)("controllers/:id/credentials/rotate"),
14523
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14524
- __decorateParam(1, (0, import_common30.Body)()),
14525
- __decorateParam(2, (0, import_common30.Req)())
14854
+ (0, import_common31.Post)("controllers/:id/credentials/rotate"),
14855
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14856
+ __decorateParam(1, (0, import_common31.Body)()),
14857
+ __decorateParam(2, (0, import_common31.Req)())
14526
14858
  ], WagoControllerApi.prototype, "rotateCredentials", 1);
14527
14859
  __decorateClass([
14528
14860
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14529
- (0, import_common30.Get)("controllers/:id/credentials/rotation"),
14530
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14861
+ (0, import_common31.Get)("controllers/:id/credentials/rotation"),
14862
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14531
14863
  ], WagoControllerApi.prototype, "credentialRotationStatus", 1);
14532
14864
  __decorateClass([
14533
14865
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14534
- (0, import_common30.Post)("controllers/:id/credentials/manual/complete"),
14535
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14536
- __decorateParam(1, (0, import_common30.Body)()),
14537
- __decorateParam(2, (0, import_common30.Req)())
14866
+ (0, import_common31.Post)("controllers/:id/credentials/manual/complete"),
14867
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14868
+ __decorateParam(1, (0, import_common31.Body)()),
14869
+ __decorateParam(2, (0, import_common31.Req)())
14538
14870
  ], WagoControllerApi.prototype, "completeManualCredentials", 1);
14539
14871
  __decorateClass([
14540
- (0, import_common30.Post)("controllers/:id/claim"),
14541
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14542
- __decorateParam(1, (0, import_common30.Body)()),
14543
- __decorateParam(2, (0, import_common30.Req)())
14872
+ (0, import_common31.Post)("controllers/:id/claim"),
14873
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14874
+ __decorateParam(1, (0, import_common31.Body)()),
14875
+ __decorateParam(2, (0, import_common31.Req)())
14544
14876
  ], WagoControllerApi.prototype, "claim", 1);
14545
14877
  __decorateClass([
14546
14878
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14547
- (0, import_common30.Delete)("commissioning/sessions/:id"),
14548
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14879
+ (0, import_common31.Delete)("commissioning/sessions/:id"),
14880
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14549
14881
  ], WagoControllerApi.prototype, "removeCommissioningSession", 1);
14550
14882
  __decorateClass([
14551
14883
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14552
- (0, import_common30.Post)("commissioning/sessions/:id/revoke"),
14553
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14884
+ (0, import_common31.Post)("commissioning/sessions/:id/revoke"),
14885
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14554
14886
  ], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
14555
14887
  __decorateClass([
14556
14888
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14557
- (0, import_common30.Post)("commissioning/sessions/:id/management/:action"),
14558
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14559
- __decorateParam(1, (0, import_common30.Param)("action")),
14560
- __decorateParam(2, (0, import_common30.Body)()),
14561
- __decorateParam(3, (0, import_common30.Req)())
14889
+ (0, import_common31.Post)("commissioning/sessions/:id/management/:action"),
14890
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14891
+ __decorateParam(1, (0, import_common31.Param)("action")),
14892
+ __decorateParam(2, (0, import_common31.Body)()),
14893
+ __decorateParam(3, (0, import_common31.Req)())
14562
14894
  ], WagoControllerApi.prototype, "manageSecurity", 1);
14563
14895
  __decorateClass([
14564
14896
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14565
- (0, import_common30.Post)("commissioning/sessions/:id/platform/:action"),
14566
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14567
- __decorateParam(1, (0, import_common30.Param)("action")),
14568
- __decorateParam(2, (0, import_common30.Body)()),
14569
- __decorateParam(3, (0, import_common30.Req)())
14897
+ (0, import_common31.Post)("commissioning/sessions/:id/platform/:action"),
14898
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14899
+ __decorateParam(1, (0, import_common31.Param)("action")),
14900
+ __decorateParam(2, (0, import_common31.Body)()),
14901
+ __decorateParam(3, (0, import_common31.Req)())
14570
14902
  ], WagoControllerApi.prototype, "platformAction", 1);
14571
14903
  __decorateClass([
14572
14904
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14573
- (0, import_common30.Get)("commissioning/sessions/:id/management"),
14574
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14905
+ (0, import_common31.Get)("commissioning/sessions/:id/management"),
14906
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14575
14907
  ], WagoControllerApi.prototype, "managementStatus", 1);
14576
14908
  __decorateClass([
14577
14909
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14578
- (0, import_common30.Get)("commissioning/sessions/:id/verification"),
14579
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe))
14910
+ (0, import_common31.Get)("commissioning/sessions/:id/verification"),
14911
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe))
14580
14912
  ], WagoControllerApi.prototype, "commissioningVerification", 1);
14581
14913
  __decorateClass([
14582
14914
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14583
- (0, import_common30.Post)("commissioning/sessions/:id/recover"),
14584
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14585
- __decorateParam(1, (0, import_common30.Body)()),
14586
- __decorateParam(2, (0, import_common30.Req)())
14915
+ (0, import_common31.Post)("commissioning/sessions/:id/recover"),
14916
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14917
+ __decorateParam(1, (0, import_common31.Body)()),
14918
+ __decorateParam(2, (0, import_common31.Req)())
14587
14919
  ], WagoControllerApi.prototype, "recoverCommissioningSession", 1);
14588
14920
  __decorateClass([
14589
14921
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14590
- (0, import_common30.Post)("commissioning/sessions/:id/deliver"),
14591
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14592
- __decorateParam(1, (0, import_common30.Body)()),
14593
- __decorateParam(2, (0, import_common30.Req)())
14922
+ (0, import_common31.Post)("commissioning/sessions/:id/deliver"),
14923
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14924
+ __decorateParam(1, (0, import_common31.Body)()),
14925
+ __decorateParam(2, (0, import_common31.Req)())
14594
14926
  ], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
14595
14927
  __decorateClass([
14596
14928
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14597
- (0, import_common30.Post)("commissioning/sessions/:id/confirm-host-key"),
14598
- __decorateParam(0, (0, import_common30.Param)("id", import_common30.ParseIntPipe)),
14599
- __decorateParam(1, (0, import_common30.Body)())
14929
+ (0, import_common31.Post)("commissioning/sessions/:id/confirm-host-key"),
14930
+ __decorateParam(0, (0, import_common31.Param)("id", import_common31.ParseIntPipe)),
14931
+ __decorateParam(1, (0, import_common31.Body)())
14600
14932
  ], WagoControllerApi.prototype, "confirmCommissioningHostKey", 1);
14601
14933
  __decorateClass([
14602
14934
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14603
- (0, import_common30.Post)("commissioning/sessions"),
14604
- __decorateParam(0, (0, import_common30.Body)()),
14605
- __decorateParam(1, (0, import_common30.Req)())
14935
+ (0, import_common31.Post)("commissioning/sessions"),
14936
+ __decorateParam(0, (0, import_common31.Body)()),
14937
+ __decorateParam(1, (0, import_common31.Req)())
14606
14938
  ], WagoControllerApi.prototype, "createCommissioningSession", 1);
14607
14939
  __decorateClass([
14608
14940
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14609
- (0, import_common30.Get)("commissioning/sessions"),
14610
- __decorateParam(0, (0, import_common30.Query)("limit")),
14611
- __decorateParam(1, (0, import_common30.Query)("offset"))
14941
+ (0, import_common31.Get)("commissioning/sessions"),
14942
+ __decorateParam(0, (0, import_common31.Query)("limit")),
14943
+ __decorateParam(1, (0, import_common31.Query)("offset"))
14612
14944
  ], WagoControllerApi.prototype, "commissioningSessions", 1);
14613
14945
  __decorateClass([
14614
14946
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14615
- (0, import_common30.Get)("commissioning/support")
14947
+ (0, import_common31.Get)("commissioning/support")
14616
14948
  ], WagoControllerApi.prototype, "commissioningSupport", 1);
14617
14949
  __decorateClass([
14618
14950
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14619
- (0, import_common30.Post)("settings"),
14620
- __decorateParam(0, (0, import_common30.Body)())
14951
+ (0, import_common31.Post)("settings"),
14952
+ __decorateParam(0, (0, import_common31.Body)())
14621
14953
  ], WagoControllerApi.prototype, "setSettings", 1);
14622
14954
  __decorateClass([
14623
14955
  (0, import_plugins_backend_sdk16.Auth)("system.settings.manage"),
14624
- (0, import_common30.Get)("settings")
14956
+ (0, import_common31.Get)("settings")
14625
14957
  ], WagoControllerApi.prototype, "settings", 1);
14626
14958
  __decorateClass([
14627
- (0, import_common30.Get)("controllers")
14959
+ (0, import_common31.Get)("controllers")
14628
14960
  ], WagoControllerApi.prototype, "list", 1);
14629
14961
  WagoControllerApi = __decorateClass([
14630
14962
  (0, import_plugins_backend_sdk16.Auth)("resources.update"),
14631
- (0, import_common30.Controller)("wago"),
14632
- __decorateParam(0, (0, import_common30.Inject)(WagoService)),
14633
- __decorateParam(1, (0, import_common30.Inject)(WagoCommissioningService)),
14634
- __decorateParam(2, (0, import_common30.Inject)(WagoCredentialRotationService)),
14635
- __decorateParam(3, (0, import_common30.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context")))
14963
+ (0, import_common31.Controller)("wago"),
14964
+ __decorateParam(0, (0, import_common31.Inject)(WagoService)),
14965
+ __decorateParam(1, (0, import_common31.Inject)(WagoCommissioningService)),
14966
+ __decorateParam(2, (0, import_common31.Inject)(WagoCredentialRotationService)),
14967
+ __decorateParam(3, (0, import_common31.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context")))
14636
14968
  ], WagoControllerApi);
14637
14969
 
14638
14970
  // backend/plugin.ts
14639
14971
  init_catalog();
14640
14972
 
14641
14973
  // backend/runtime/artifacts/controller.ts
14642
- var import_common31 = require("@nestjs/common");
14974
+ var import_common32 = require("@nestjs/common");
14643
14975
  var import_plugins_backend_sdk17 = require("@attraccess/plugins-backend-sdk");
14644
14976
  init_catalog();
14645
14977
  var WagoArtifactsController = class {
@@ -14650,32 +14982,32 @@ var WagoArtifactsController = class {
14650
14982
  try {
14651
14983
  return await this.artifacts.list();
14652
14984
  } catch {
14653
- throw new import_common31.ServiceUnavailableException("Runtime releases could not be loaded. Retry shortly.");
14985
+ throw new import_common32.ServiceUnavailableException("Runtime releases could not be loaded. Retry shortly.");
14654
14986
  }
14655
14987
  }
14656
14988
  async current() {
14657
14989
  try {
14658
14990
  return await this.artifacts.current();
14659
14991
  } catch {
14660
- throw new import_common31.ServiceUnavailableException("The selected runtime release could not be loaded. Retry shortly.");
14992
+ throw new import_common32.ServiceUnavailableException("The selected runtime release could not be loaded. Retry shortly.");
14661
14993
  }
14662
14994
  }
14663
14995
  };
14664
14996
  __decorateClass([
14665
- (0, import_common31.Get)()
14997
+ (0, import_common32.Get)()
14666
14998
  ], WagoArtifactsController.prototype, "list", 1);
14667
14999
  __decorateClass([
14668
- (0, import_common31.Get)("current")
15000
+ (0, import_common32.Get)("current")
14669
15001
  ], WagoArtifactsController.prototype, "current", 1);
14670
15002
  WagoArtifactsController = __decorateClass([
14671
15003
  (0, import_plugins_backend_sdk17.Auth)("system.settings.manage"),
14672
- (0, import_common31.Controller)("wago/runtime-artifacts"),
14673
- __decorateParam(0, (0, import_common31.Inject)(WagoRuntimeArtifactsService))
15004
+ (0, import_common32.Controller)("wago/runtime-artifacts"),
15005
+ __decorateParam(0, (0, import_common32.Inject)(WagoRuntimeArtifactsService))
14674
15006
  ], WagoArtifactsController);
14675
15007
 
14676
15008
  // backend/diagnostics/service.ts
14677
15009
  var import_plugins_backend_sdk18 = require("@attraccess/plugins-backend-sdk");
14678
- var import_common32 = require("@nestjs/common");
15010
+ var import_common33 = require("@nestjs/common");
14679
15011
  function configurationRejections(latest, runtime) {
14680
15012
  return latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [];
14681
15013
  }
@@ -14729,6 +15061,7 @@ function diagnosticReferences(nodes, channelIds, revision2, capabilities) {
14729
15061
  function runtimeStreamSummary(runtime) {
14730
15062
  return {
14731
15063
  sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
15064
+ dropCounts: runtime.dropCounts ?? null,
14732
15065
  activeStream: runtime.activeStream ?? null,
14733
15066
  trackingExhausted: runtime.trackingExhausted,
14734
15067
  stateConnected: runtime.connected ?? null,
@@ -14744,7 +15077,7 @@ var WagoDiagnosticsService = class {
14744
15077
  }
14745
15078
  async get(controllerId) {
14746
15079
  const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
14747
- if (!controller) throw new import_common32.NotFoundException("WAGO controller not found");
15080
+ if (!controller) throw new import_common33.NotFoundException("WAGO controller not found");
14748
15081
  const [draft, latest, applied, nodes] = await Promise.all([
14749
15082
  this.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId }),
14750
15083
  this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId }, order: { revision: "DESC" } }),
@@ -14760,6 +15093,14 @@ var WagoDiagnosticsService = class {
14760
15093
  const expected = latest?.state === "rejected" ? applied : latest;
14761
15094
  const revisionMismatch = !!expected && (applied?.revision !== expected.revision || runtime.revision !== expected.revision || runtime.activeStream !== void 0 && runtime.contentHash !== expected.contentHash);
14762
15095
  const connected = runtime.connected === false ? false : heartbeatFreshness === "fresh" || freshness(runtime.stateSourceAt) === "fresh";
15096
+ const configuredChannels = (appliedSnapshot ?? snapshot)?.logicalChannels ?? [];
15097
+ const enabledChannels = configuredChannels.filter(channelEnabled);
15098
+ const disabledChannels = configuredChannels.filter((channel) => !channelEnabled(channel)).slice(0, MAX_CONFIGURATION_ITEMS).map((channel) => ({
15099
+ id: channel.id,
15100
+ profile: channel.profile,
15101
+ capabilities: channel.capabilities,
15102
+ availability: "disabled"
15103
+ }));
14763
15104
  const references = diagnosticReferences(
14764
15105
  nodes.slice(0, 1e3),
14765
15106
  appliedSnapshot?.logicalChannels.map((channel) => channel.id) ?? [],
@@ -14786,7 +15127,7 @@ var WagoDiagnosticsService = class {
14786
15127
  ) : [],
14787
15128
  hardwareReadiness: "unknown",
14788
15129
  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.",
14789
- channels: ((appliedSnapshot ?? snapshot)?.logicalChannels ?? []).slice(0, 256).map((channel) => {
15130
+ channels: enabledChannels.slice(0, MAX_CHANNELS).map((channel) => {
14790
15131
  const values = [
14791
15132
  channel.capabilities.includes("input") ? own(runtime.inputs, channel.id) : void 0,
14792
15133
  channel.capabilities.includes("output") ? own(runtime.outputs, channel.id) : void 0,
@@ -14810,13 +15151,14 @@ var WagoDiagnosticsService = class {
14810
15151
  acknowledgement: own(runtime.acknowledgements, channel.id) ?? null
14811
15152
  };
14812
15153
  }),
15154
+ disabledChannels,
14813
15155
  faults: Object.entries(runtime.faults).map(([channelId2, fault]) => ({ channelId: channelId2, ...fault })),
14814
15156
  references,
14815
15157
  referencesTruncated: nodes.length > 1e3,
14816
15158
  events: runtime.events,
14817
15159
  limitations: [
14818
15160
  "Hardware readiness remains unknown. Legacy samples have no source time and are never current.",
14819
- "Diagnostics retain at most 256 channels, 50 recent events and 15 minutes in this process.",
15161
+ "Diagnostics retain at most 256 enabled channels, 50 recent events and 15 minutes in this process. Disabled channels are listed without samples.",
14820
15162
  "Flow links select the affected node. Diagnostics warnings do not block resource usage; required flow nodes determine gating.",
14821
15163
  "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.",
14822
15164
  "Stream tracking keeps at most 16 retired boot stream IDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
@@ -14921,9 +15263,9 @@ var WagoDiagnosticsService = class {
14921
15263
  }
14922
15264
  };
14923
15265
  WagoDiagnosticsService = __decorateClass([
14924
- (0, import_common32.Injectable)(),
14925
- __decorateParam(0, (0, import_common32.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
14926
- __decorateParam(1, (0, import_common32.Inject)(WagoService))
15266
+ (0, import_common33.Injectable)(),
15267
+ __decorateParam(0, (0, import_common33.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
15268
+ __decorateParam(1, (0, import_common33.Inject)(WagoService))
14927
15269
  ], WagoDiagnosticsService);
14928
15270
  var WagoDiagnosticsController = class {
14929
15271
  constructor(diagnostics) {
@@ -14937,17 +15279,17 @@ var WagoDiagnosticsController = class {
14937
15279
  }
14938
15280
  };
14939
15281
  __decorateClass([
14940
- (0, import_common32.Get)("controllers/:id/diagnostics"),
14941
- __decorateParam(0, (0, import_common32.Param)("id", import_common32.ParseIntPipe))
15282
+ (0, import_common33.Get)("controllers/:id/diagnostics"),
15283
+ __decorateParam(0, (0, import_common33.Param)("id", import_common33.ParseIntPipe))
14942
15284
  ], WagoDiagnosticsController.prototype, "get", 1);
14943
15285
  __decorateClass([
14944
- (0, import_common32.Get)("resources/:id/diagnostics"),
14945
- __decorateParam(0, (0, import_common32.Param)("id", import_common32.ParseIntPipe))
15286
+ (0, import_common33.Get)("resources/:id/diagnostics"),
15287
+ __decorateParam(0, (0, import_common33.Param)("id", import_common33.ParseIntPipe))
14946
15288
  ], WagoDiagnosticsController.prototype, "getResource", 1);
14947
15289
  WagoDiagnosticsController = __decorateClass([
14948
15290
  (0, import_plugins_backend_sdk18.Auth)("resources.update"),
14949
- (0, import_common32.Controller)("wago"),
14950
- __decorateParam(0, (0, import_common32.Inject)(WagoDiagnosticsService))
15291
+ (0, import_common33.Controller)("wago"),
15292
+ __decorateParam(0, (0, import_common33.Inject)(WagoDiagnosticsService))
14951
15293
  ], WagoDiagnosticsController);
14952
15294
 
14953
15295
  // backend/flow/command-node.ts
@@ -14973,7 +15315,7 @@ function createWagoCommandNode(service) {
14973
15315
  }
14974
15316
 
14975
15317
  // backend/flow/service.ts
14976
- var import_common34 = require("@nestjs/common");
15318
+ var import_common35 = require("@nestjs/common");
14977
15319
 
14978
15320
  // backend/flow/model.ts
14979
15321
  var MAX_CACHE_ENTRIES = 2e3;
@@ -15040,7 +15382,7 @@ function createWagoStateNodes(service) {
15040
15382
  }
15041
15383
 
15042
15384
  // backend/flow/state.ts
15043
- var import_common33 = require("@nestjs/common");
15385
+ var import_common34 = require("@nestjs/common");
15044
15386
  var WagoFlowServiceState = class {
15045
15387
  constructor(context, wago) {
15046
15388
  __publicField(this, "context", context);
@@ -15053,6 +15395,8 @@ var WagoFlowServiceState = class {
15053
15395
  __publicField(this, "unavailableConfiguration", /* @__PURE__ */ new Set());
15054
15396
  __publicField(this, "appliedConfigurations", /* @__PURE__ */ new Map());
15055
15397
  __publicField(this, "channelCache", /* @__PURE__ */ new Map());
15398
+ /** IDs of the disabled channels in `channelCache`, kept in step with it so reads need no scan. */
15399
+ __publicField(this, "disabledChannelCache", /* @__PURE__ */ new Map());
15056
15400
  __publicField(this, "waiters", /* @__PURE__ */ new Set());
15057
15401
  __publicField(this, "waitersByKey", /* @__PURE__ */ new Map());
15058
15402
  __publicField(this, "subscriptions", []);
@@ -15074,14 +15418,14 @@ var WagoFlowServiceState = class {
15074
15418
  }
15075
15419
  };
15076
15420
  WagoFlowServiceState = __decorateClass([
15077
- __decorateParam(0, (0, import_common33.Inject)(PLUGIN_CONTEXT6)),
15078
- __decorateParam(1, (0, import_common33.Inject)(WagoService))
15421
+ __decorateParam(0, (0, import_common34.Inject)(PLUGIN_CONTEXT6)),
15422
+ __decorateParam(1, (0, import_common34.Inject)(WagoService))
15079
15423
  ], WagoFlowServiceState);
15080
15424
 
15081
15425
  // backend/flow/queries.ts
15082
15426
  var WagoFlowQueries = class extends WagoFlowServiceState {
15083
15427
  async wait(config) {
15084
- if (typeof config.controllerId !== "number" || typeof config.channelId !== "string" || typeof config.category !== "string")
15428
+ if (typeof config.controllerId !== "number" || typeof config.channelId !== "string" || typeof config.category !== "string" || this.disabled(config.controllerId, config.channelId))
15085
15429
  return null;
15086
15430
  const current = this.read(config);
15087
15431
  if (current && this.matchesCondition(current, config)) return current;
@@ -15110,8 +15454,13 @@ var WagoFlowQueries = class extends WagoFlowServiceState {
15110
15454
  this.waitersByKey.set(waiterKey, waiters);
15111
15455
  });
15112
15456
  }
15457
+ /** True when the applied configuration disables this channel; its readings are then unavailable. */
15458
+ disabled(controllerId, channelId2) {
15459
+ return this.disabledChannelCache.get(controllerId)?.has(channelId2) ?? false;
15460
+ }
15113
15461
  read(config) {
15114
- if (typeof config.controllerId !== "number" || typeof config.channelId !== "string") return null;
15462
+ if (typeof config.controllerId !== "number" || typeof config.channelId !== "string" || this.disabled(config.controllerId, config.channelId))
15463
+ return null;
15115
15464
  if (typeof config.category === "string")
15116
15465
  return this.cache.get(this.cacheKey(config.controllerId, config.channelId, config.category)) ?? null;
15117
15466
  const entries = [...this.cache.values()].filter(
@@ -15196,10 +15545,10 @@ var WagoFlowQueries = class extends WagoFlowServiceState {
15196
15545
  type: "string",
15197
15546
  title: "Logical Channel",
15198
15547
  refreshesSchema: true,
15199
- oneOf: channels.map((channel2) => ({
15200
- const: channel2.id,
15201
- title: typeof names[channel2.id] === "string" ? names[channel2.id] : channel2.id
15202
- }))
15548
+ oneOf: channels.map((channel2) => {
15549
+ const title = typeof names[channel2.id] === "string" ? names[channel2.id] : channel2.id;
15550
+ return { const: channel2.id, title: channel2.enabled === false ? `${title} (disabled)` : title };
15551
+ })
15203
15552
  }
15204
15553
  };
15205
15554
  if (kind === "event") {
@@ -15253,6 +15602,7 @@ var WagoFlowQueries = class extends WagoFlowServiceState {
15253
15602
  if (!controllerIds.has(id)) this.appliedConfigurations.delete(id);
15254
15603
  for (const id of this.streams.keys()) if (!controllerIds.has(id)) this.streams.delete(id);
15255
15604
  this.channelCache.clear();
15605
+ this.disabledChannelCache.clear();
15256
15606
  const revisions = await this.loadLatestAppliedRevisions(controllers.map((controller) => controller.id));
15257
15607
  for (const revision2 of revisions) this.cacheChannels(revision2);
15258
15608
  for (const controller of controllers)
@@ -15380,12 +15730,14 @@ var WagoFlowService = class extends WagoFlowQueries {
15380
15730
  return true;
15381
15731
  }
15382
15732
  readCategories(channel) {
15733
+ if (channel?.enabled === false) return [];
15383
15734
  return [
15384
15735
  ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
15385
15736
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : []
15386
15737
  ];
15387
15738
  }
15388
15739
  categories(channel) {
15740
+ if (channel?.enabled === false) return [];
15389
15741
  return [
15390
15742
  ...channel?.capabilities.some((capability) => capability === "input" || capability === "output") ? ["state"] : [],
15391
15743
  ...channel?.capabilities.includes("measurement") ? ["measurement"] : [],
@@ -15409,9 +15761,14 @@ var WagoFlowService = class extends WagoFlowQueries {
15409
15761
  contentHash: revision2.contentHash
15410
15762
  });
15411
15763
  this.channelCache.set(revision2.controllerId, channels);
15764
+ const disabled = new Set(channels.filter((channel) => channel.enabled === false).map((channel) => channel.id));
15765
+ this.disabledChannelCache.set(revision2.controllerId, disabled);
15766
+ for (const [key, state] of this.cache)
15767
+ if (state.controllerId === revision2.controllerId && disabled.has(state.channelId)) this.cache.delete(key);
15412
15768
  return channels;
15413
15769
  } catch {
15414
15770
  this.channelCache.set(revision2.controllerId, []);
15771
+ this.disabledChannelCache.delete(revision2.controllerId);
15415
15772
  return [];
15416
15773
  }
15417
15774
  }
@@ -15433,6 +15790,7 @@ var WagoFlowService = class extends WagoFlowQueries {
15433
15790
  });
15434
15791
  if (!revision2) {
15435
15792
  this.channelCache.set(controllerId, []);
15793
+ this.disabledChannelCache.delete(controllerId);
15436
15794
  return [];
15437
15795
  }
15438
15796
  return this.cacheChannels(revision2);
@@ -15486,6 +15844,7 @@ var WagoFlowService = class extends WagoFlowQueries {
15486
15844
  if (invalidateSamples || state.category === "state") state.invalidated = true;
15487
15845
  }
15488
15846
  for (const channel of channels) {
15847
+ if (channel.enabled === false) continue;
15489
15848
  const value = channel.capabilities.includes("input") && Object.hasOwn(event.inputs ?? {}, channel.id) ? event.inputs[channel.id] : channel.capabilities.includes("output") && Object.hasOwn(event.outputs, channel.id) ? event.outputs[channel.id] : void 0;
15490
15849
  if (typeof value === "boolean") this.store(controller, channel.id, event, value);
15491
15850
  }
@@ -15556,7 +15915,8 @@ var WagoFlowService = class extends WagoFlowQueries {
15556
15915
  this.applyState(controller, event, eventTime, stream, channels);
15557
15916
  } else if ("channelId" in event) {
15558
15917
  const channel = channels.find((channel2) => channel2.id === event.channelId);
15559
- if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
15918
+ if (!channel || channel.enabled === false || event.category === "measurement" && !channel.capabilities.includes("measurement"))
15919
+ return;
15560
15920
  this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
15561
15921
  }
15562
15922
  }
@@ -15567,9 +15927,9 @@ var WagoFlowService = class extends WagoFlowQueries {
15567
15927
  }
15568
15928
  };
15569
15929
  WagoFlowService = __decorateClass([
15570
- (0, import_common34.Injectable)(),
15571
- __decorateParam(0, (0, import_common34.Inject)(PLUGIN_CONTEXT6)),
15572
- __decorateParam(1, (0, import_common34.Inject)(WagoService))
15930
+ (0, import_common35.Injectable)(),
15931
+ __decorateParam(0, (0, import_common35.Inject)(PLUGIN_CONTEXT6)),
15932
+ __decorateParam(1, (0, import_common35.Inject)(WagoService))
15573
15933
  ], WagoFlowService);
15574
15934
 
15575
15935
  // backend/audit/policy.ts
@@ -15708,14 +16068,14 @@ var WAGO_AUDIT_DOMAIN = {
15708
16068
 
15709
16069
  // backend/runtime/update/controller.ts
15710
16070
  var import_plugins_backend_sdk19 = require("@attraccess/plugins-backend-sdk");
15711
- var import_common37 = require("@nestjs/common");
16071
+ var import_common38 = require("@nestjs/common");
15712
16072
 
15713
16073
  // backend/network/service.ts
15714
- var import_common36 = require("@nestjs/common");
16074
+ var import_common37 = require("@nestjs/common");
15715
16075
  var import_node_crypto25 = require("node:crypto");
15716
16076
 
15717
16077
  // backend/network/model.ts
15718
- var import_common35 = require("@nestjs/common");
16078
+ var import_common36 = require("@nestjs/common");
15719
16079
  var NetworkChangeError = class extends Error {
15720
16080
  constructor(failure) {
15721
16081
  super(failure);
@@ -15726,24 +16086,24 @@ var EVIDENCE_MS = 12e4;
15726
16086
  var OPERATION_MS2 = 20 * 6e4;
15727
16087
  function networkChangeInput(body) {
15728
16088
  if (!body || typeof body !== "object" || Array.isArray(body))
15729
- throw new import_common35.BadRequestException("Invalid MQTT/address change");
16089
+ throw new import_common36.BadRequestException("Invalid MQTT/address change");
15730
16090
  const input = body;
15731
16091
  if (Object.keys(input).some((k) => !["targetHost", "mqttServerId"].includes(k)) || typeof input.targetHost !== "string" || !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(input.targetHost))
15732
- throw new import_common35.BadRequestException("Enter the CC100 private IPv4 address.");
16092
+ throw new import_common36.BadRequestException("Enter the CC100 private IPv4 address.");
15733
16093
  const octets = input.targetHost.split(".").map(Number);
15734
16094
  if (octets.some((n) => n > 255) || octets.join(".") !== input.targetHost || !(octets[0] === 10 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31 || octets[0] === 192 && octets[1] === 168))
15735
- throw new import_common35.BadRequestException("Enter the CC100 private IPv4 address.");
16095
+ throw new import_common36.BadRequestException("Enter the CC100 private IPv4 address.");
15736
16096
  if (input.mqttServerId !== null && (!Number.isSafeInteger(input.mqttServerId) || input.mqttServerId <= 0))
15737
- throw new import_common35.BadRequestException("Select a configured MQTT server or update only the address.");
16097
+ throw new import_common36.BadRequestException("Select a configured MQTT server or update only the address.");
15738
16098
  return { targetHost: input.targetHost, mqttServerId: input.mqttServerId };
15739
16099
  }
15740
16100
  function assertNoUnfinishedChange(row) {
15741
16101
  if (row && row.phase !== "completed" && !(row.phase === "connecting" && row.failure))
15742
- throw new import_common35.ConflictException("Retry the pending MQTT/address change before starting another.");
16102
+ throw new import_common36.ConflictException("Retry the pending MQTT/address change before starting another.");
15743
16103
  }
15744
16104
  function assertSameManagedIdentity(row, access) {
15745
16105
  if (!row || row.sessionId !== access.sessionId || row.fingerprint !== access.fingerprint)
15746
- throw new import_common35.ConflictException("Pending change belongs to another managed identity.");
16106
+ throw new import_common36.ConflictException("Pending change belongs to another managed identity.");
15747
16107
  }
15748
16108
 
15749
16109
  // backend/network/service.ts
@@ -15757,14 +16117,14 @@ var WagoNetworkChangeService = class {
15757
16117
  __publicField(this, "destroyed", false);
15758
16118
  }
15759
16119
  async apply(controllerId, body, principal, retry = false) {
15760
- if (this.destroyed) throw new import_common36.ConflictException("Controller operation has stopped. Retry the request.");
16120
+ if (this.destroyed) throw new import_common37.ConflictException("Controller operation has stopped. Retry the request.");
15761
16121
  const input = retry ? null : networkChangeInput(body);
15762
16122
  const access = await this.context.getRepository(WagoManagedAccess).findOne({ where: { controllerId, state: "managed" }, order: { sessionId: "DESC" } });
15763
- if (!access) throw new import_common36.ConflictException("Automatic SSH management is required for this operation.");
16123
+ if (!access) throw new import_common37.ConflictException("Automatic SSH management is required for this operation.");
15764
16124
  const operations = new WagoDeviceOperations(this.context.getRepository(WagoDeviceOperation));
15765
16125
  const owner = (0, import_node_crypto25.randomBytes)(16).toString("hex"), now = Date.now();
15766
16126
  if (!await operations.acquire(access.fingerprint, owner, now, now + OPERATION_MS2 + 6e4))
15767
- throw new import_common36.ConflictException("Another controller operation is active. Retry after it finishes.");
16127
+ throw new import_common37.ConflictException("Another controller operation is active. Retry after it finishes.");
15768
16128
  const abort = new AbortController();
15769
16129
  this.active.add(abort);
15770
16130
  const timer = setTimeout(() => abort.abort(), OPERATION_MS2).unref();
@@ -15778,7 +16138,7 @@ var WagoNetworkChangeService = class {
15778
16138
  try {
15779
16139
  const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId, trustState: "claimed" });
15780
16140
  if (!controller?.mqttServerId || !controller.credentialEpoch)
15781
- throw new import_common36.ConflictException("Managed enrolled controller required.");
16141
+ throw new import_common37.ConflictException("Managed enrolled controller required.");
15782
16142
  row = await this.repository.createQueryBuilder("change").addSelect("change.encryptedPayload").where("change.controllerId = :controllerId", { controllerId }).getOne();
15783
16143
  if (retry && (!row || row.phase === "completed")) return this.status(controllerId);
15784
16144
  if (!retry) assertNoUnfinishedChange(row);
@@ -15807,7 +16167,7 @@ var WagoNetworkChangeService = class {
15807
16167
  } catch (error) {
15808
16168
  if (row && row.phase !== "completed") await this.recordFailure(row, error, assertOwned);
15809
16169
  if (audited) await lifecycle.finish("failed");
15810
- throw new import_common36.ConflictException("MQTT/address change is incomplete. Check its status and retry the saved operation.");
16170
+ throw new import_common37.ConflictException("MQTT/address change is incomplete. Check its status and retry the saved operation.");
15811
16171
  } finally {
15812
16172
  clearTimeout(timer);
15813
16173
  abort.abort();
@@ -15820,13 +16180,13 @@ var WagoNetworkChangeService = class {
15820
16180
  const rotation = await this.context.getRepository(WagoCredentialRotationEntity).findOneBy({ controllerId });
15821
16181
  const update = await this.context.getRepository(WagoRuntimeUpdateEntity).findOneBy({ controllerId });
15822
16182
  if (rotation && rotation.phase !== "completed" || update?.metadata && JSON.parse(update.metadata).token)
15823
- throw new import_common36.ConflictException(
16183
+ throw new import_common37.ConflictException(
15824
16184
  "Finish credential rotation or runtime update recovery before changing MQTT/address settings."
15825
16185
  );
15826
16186
  }
15827
16187
  /** Durably record a new change intent before contacting the controller. */
15828
16188
  async savePendingChange(controllerId, access, input, assertOwned) {
15829
- if (!input) throw new import_common36.BadRequestException("Invalid MQTT/address change");
16189
+ if (!input) throw new import_common37.BadRequestException("Invalid MQTT/address change");
15830
16190
  const row = this.repository.create({
15831
16191
  controllerId,
15832
16192
  sessionId: access.sessionId,
@@ -16048,7 +16408,7 @@ var WagoNetworkChangeService = class {
16048
16408
  }
16049
16409
  async status(controllerId) {
16050
16410
  const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
16051
- if (!controller) throw new import_common36.NotFoundException("WAGO controller not found");
16411
+ if (!controller) throw new import_common37.NotFoundException("WAGO controller not found");
16052
16412
  const access = await this.context.getRepository(WagoManagedAccess).findOne({ where: { controllerId }, order: { sessionId: "DESC" } });
16053
16413
  const row = await this.repository.findOneBy({ controllerId });
16054
16414
  const lease = row && await this.context.getRepository(WagoDeviceOperation).findOneBy({ fingerprint: row.fingerprint });
@@ -16068,10 +16428,10 @@ var WagoNetworkChangeService = class {
16068
16428
  }
16069
16429
  async retirePreviousCredentials(controllerId, principal) {
16070
16430
  const access = await this.context.getRepository(WagoManagedAccess).findOne({ where: { controllerId }, order: { sessionId: "DESC" } });
16071
- if (!access) throw new import_common36.ConflictException("Controller management identity is unavailable.");
16431
+ if (!access) throw new import_common37.ConflictException("Controller management identity is unavailable.");
16072
16432
  const operations = new WagoDeviceOperations(this.context.getRepository(WagoDeviceOperation)), owner = (0, import_node_crypto25.randomBytes)(16).toString("hex");
16073
16433
  if (!await operations.acquire(access.fingerprint, owner, Date.now(), Date.now() + 12e4))
16074
- throw new import_common36.ConflictException("Controller is busy");
16434
+ throw new import_common37.ConflictException("Controller is busy");
16075
16435
  const lifecycle = new WagoAudit(this.context).begin(principal, controllerId, "network_credential_retirement");
16076
16436
  let audited = false;
16077
16437
  try {
@@ -16119,7 +16479,7 @@ var WagoNetworkChangeService = class {
16119
16479
  return this.status(controllerId);
16120
16480
  } catch {
16121
16481
  if (audited) await lifecycle.finish("failed").catch(() => void 0);
16122
- throw new import_common36.ConflictException(
16482
+ throw new import_common37.ConflictException(
16123
16483
  "Previous broker credentials could not be retired. Check the previous broker connection and retry."
16124
16484
  );
16125
16485
  } finally {
@@ -16135,10 +16495,10 @@ var WagoNetworkChangeService = class {
16135
16495
  }
16136
16496
  };
16137
16497
  WagoNetworkChangeService = __decorateClass([
16138
- (0, import_common36.Injectable)(),
16139
- __decorateParam(0, (0, import_common36.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
16140
- __decorateParam(1, (0, import_common36.Inject)(WagoManagedRuntimeService)),
16141
- __decorateParam(2, (0, import_common36.Inject)(WagoService))
16498
+ (0, import_common37.Injectable)(),
16499
+ __decorateParam(0, (0, import_common37.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
16500
+ __decorateParam(1, (0, import_common37.Inject)(WagoManagedRuntimeService)),
16501
+ __decorateParam(2, (0, import_common37.Inject)(WagoService))
16142
16502
  ], WagoNetworkChangeService);
16143
16503
 
16144
16504
  // backend/runtime/update/controller.ts
@@ -16170,7 +16530,7 @@ var WagoUpdatesController = class {
16170
16530
  }
16171
16531
  recovery(id, body, request) {
16172
16532
  if (body?.confirm !== true || Object.keys(body).length !== 1)
16173
- throw new import_common37.BadRequestException("Confirm administrator recovery disclosure");
16533
+ throw new import_common38.BadRequestException("Confirm administrator recovery disclosure");
16174
16534
  return this.managed.recoverPassword(id, wagoAuditPrincipal(request));
16175
16535
  }
16176
16536
  retry(id) {
@@ -16178,70 +16538,70 @@ var WagoUpdatesController = class {
16178
16538
  }
16179
16539
  restore(id, body, request) {
16180
16540
  if (body?.confirm !== true || Object.keys(body).length !== 1)
16181
- throw new import_common37.BadRequestException("Confirm bootstrap SSH restoration");
16541
+ throw new import_common38.BadRequestException("Confirm bootstrap SSH restoration");
16182
16542
  return this.managed.restoreAccess(id, wagoAuditPrincipal(request));
16183
16543
  }
16184
16544
  };
16185
16545
  __decorateClass([
16186
- (0, import_common37.Get)("controllers/:id/network-change"),
16187
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe))
16546
+ (0, import_common38.Get)("controllers/:id/network-change"),
16547
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe))
16188
16548
  ], WagoUpdatesController.prototype, "networkStatus", 1);
16189
16549
  __decorateClass([
16190
- (0, import_common37.Post)("controllers/:id/network-change"),
16191
- (0, import_common37.Header)("Cache-Control", "no-store"),
16192
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe)),
16193
- __decorateParam(1, (0, import_common37.Body)()),
16194
- __decorateParam(2, (0, import_common37.Req)())
16550
+ (0, import_common38.Post)("controllers/:id/network-change"),
16551
+ (0, import_common38.Header)("Cache-Control", "no-store"),
16552
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe)),
16553
+ __decorateParam(1, (0, import_common38.Body)()),
16554
+ __decorateParam(2, (0, import_common38.Req)())
16195
16555
  ], WagoUpdatesController.prototype, "changeNetwork", 1);
16196
16556
  __decorateClass([
16197
- (0, import_common37.Post)("controllers/:id/network-change/retry"),
16198
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe)),
16199
- __decorateParam(1, (0, import_common37.Req)())
16557
+ (0, import_common38.Post)("controllers/:id/network-change/retry"),
16558
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe)),
16559
+ __decorateParam(1, (0, import_common38.Req)())
16200
16560
  ], WagoUpdatesController.prototype, "retryNetwork", 1);
16201
16561
  __decorateClass([
16202
- (0, import_common37.Post)("controllers/:id/network-change/retire-credentials"),
16203
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe)),
16204
- __decorateParam(1, (0, import_common37.Req)())
16562
+ (0, import_common38.Post)("controllers/:id/network-change/retire-credentials"),
16563
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe)),
16564
+ __decorateParam(1, (0, import_common38.Req)())
16205
16565
  ], WagoUpdatesController.prototype, "retirePreviousCredentials", 1);
16206
16566
  __decorateClass([
16207
- (0, import_common37.Get)("controllers/:id/runtime-update"),
16208
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe))
16567
+ (0, import_common38.Get)("controllers/:id/runtime-update"),
16568
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe))
16209
16569
  ], WagoUpdatesController.prototype, "status", 1);
16210
16570
  __decorateClass([
16211
- (0, import_common37.Get)("commissioning/sessions/:id/managed-access"),
16212
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe))
16571
+ (0, import_common38.Get)("commissioning/sessions/:id/managed-access"),
16572
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe))
16213
16573
  ], WagoUpdatesController.prototype, "sessionStatus", 1);
16214
16574
  __decorateClass([
16215
- (0, import_common37.Post)("controllers/:id/runtime-update/retry"),
16216
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe))
16575
+ (0, import_common38.Post)("controllers/:id/runtime-update/retry"),
16576
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe))
16217
16577
  ], WagoUpdatesController.prototype, "retryRuntime", 1);
16218
16578
  __decorateClass([
16219
- (0, import_common37.Post)("commissioning/sessions/:id/root-recovery"),
16220
- (0, import_common37.Header)("Cache-Control", "no-store"),
16221
- (0, import_common37.Header)("Pragma", "no-cache"),
16222
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe)),
16223
- __decorateParam(1, (0, import_common37.Body)()),
16224
- __decorateParam(2, (0, import_common37.Req)())
16579
+ (0, import_common38.Post)("commissioning/sessions/:id/root-recovery"),
16580
+ (0, import_common38.Header)("Cache-Control", "no-store"),
16581
+ (0, import_common38.Header)("Pragma", "no-cache"),
16582
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe)),
16583
+ __decorateParam(1, (0, import_common38.Body)()),
16584
+ __decorateParam(2, (0, import_common38.Req)())
16225
16585
  ], WagoUpdatesController.prototype, "recovery", 1);
16226
16586
  __decorateClass([
16227
- (0, import_common37.Post)("commissioning/sessions/:id/managed-access/retry"),
16228
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe))
16587
+ (0, import_common38.Post)("commissioning/sessions/:id/managed-access/retry"),
16588
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe))
16229
16589
  ], WagoUpdatesController.prototype, "retry", 1);
16230
16590
  __decorateClass([
16231
- (0, import_common37.Post)("commissioning/sessions/:id/managed-access/restore"),
16232
- __decorateParam(0, (0, import_common37.Param)("id", import_common37.ParseIntPipe)),
16233
- __decorateParam(1, (0, import_common37.Body)()),
16234
- __decorateParam(2, (0, import_common37.Req)())
16591
+ (0, import_common38.Post)("commissioning/sessions/:id/managed-access/restore"),
16592
+ __decorateParam(0, (0, import_common38.Param)("id", import_common38.ParseIntPipe)),
16593
+ __decorateParam(1, (0, import_common38.Body)()),
16594
+ __decorateParam(2, (0, import_common38.Req)())
16235
16595
  ], WagoUpdatesController.prototype, "restore", 1);
16236
16596
  WagoUpdatesController = __decorateClass([
16237
- (0, import_common37.Controller)("wago"),
16597
+ (0, import_common38.Controller)("wago"),
16238
16598
  (0, import_plugins_backend_sdk19.Auth)("system.settings.manage"),
16239
- __decorateParam(0, (0, import_common37.Inject)(WagoManagedRuntimeService)),
16240
- __decorateParam(1, (0, import_common37.Inject)(WagoNetworkChangeService))
16599
+ __decorateParam(0, (0, import_common38.Inject)(WagoManagedRuntimeService)),
16600
+ __decorateParam(1, (0, import_common38.Inject)(WagoNetworkChangeService))
16241
16601
  ], WagoUpdatesController);
16242
16602
 
16243
16603
  // backend/wago-live-updates.service.ts
16244
- var import_common38 = require("@nestjs/common");
16604
+ var import_common39 = require("@nestjs/common");
16245
16605
  var import_plugins_backend_sdk20 = require("@attraccess/plugins-backend-sdk");
16246
16606
  var WagoLiveUpdatesService = class {
16247
16607
  constructor(context, wago, commissioning, diagnostics, managed, network) {
@@ -16261,14 +16621,14 @@ var WagoLiveUpdatesService = class {
16261
16621
  topic,
16262
16622
  identifier: kind === "none" ? "none" : "required",
16263
16623
  authorize: async ({ identifier: identifier3 }, user) => {
16264
- if (!user.effectivePermissions?.has(permission)) throw new import_common38.ForbiddenException();
16624
+ if (!user.effectivePermissions?.has(permission)) throw new import_common39.ForbiddenException();
16265
16625
  if (kind === "none") return;
16266
16626
  const pattern = kind === "revisions" ? /^[1-9]\d*:(0|[1-9]\d*)$/ : /^[1-9]\d*$/;
16267
- if (!pattern.test(identifier3 ?? "")) throw new import_common38.NotFoundException();
16627
+ if (!pattern.test(identifier3 ?? "")) throw new import_common39.NotFoundException();
16268
16628
  const [id, offset = 0] = identifier3.split(":").map(Number);
16269
- if (!Number.isSafeInteger(id) || !Number.isSafeInteger(offset)) throw new import_common38.NotFoundException();
16629
+ if (!Number.isSafeInteger(id) || !Number.isSafeInteger(offset)) throw new import_common39.NotFoundException();
16270
16630
  const exists = kind === "session" ? await this.context.getRepository(WagoCommissioningSession).existsBy({ id }) : await this.context.getRepository(WagoController).existsBy({ id });
16271
- if (!exists) throw new import_common38.NotFoundException();
16631
+ if (!exists) throw new import_common39.NotFoundException();
16272
16632
  },
16273
16633
  source: ({ identifier: identifier3 }) => this.sample(`${topic}:${identifier3 ?? ""}`, interval, () => {
16274
16634
  const [id = 0, offset = 0] = (identifier3 ?? "").split(":").map(Number);
@@ -16306,13 +16666,13 @@ var WagoLiveUpdatesService = class {
16306
16666
  }
16307
16667
  };
16308
16668
  WagoLiveUpdatesService = __decorateClass([
16309
- (0, import_common38.Injectable)(),
16310
- __decorateParam(0, (0, import_common38.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
16311
- __decorateParam(1, (0, import_common38.Inject)(WagoService)),
16312
- __decorateParam(2, (0, import_common38.Inject)(WagoCommissioningService)),
16313
- __decorateParam(3, (0, import_common38.Inject)(WagoDiagnosticsService)),
16314
- __decorateParam(4, (0, import_common38.Inject)(WagoManagedRuntimeService)),
16315
- __decorateParam(5, (0, import_common38.Inject)(WagoNetworkChangeService))
16669
+ (0, import_common39.Injectable)(),
16670
+ __decorateParam(0, (0, import_common39.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
16671
+ __decorateParam(1, (0, import_common39.Inject)(WagoService)),
16672
+ __decorateParam(2, (0, import_common39.Inject)(WagoCommissioningService)),
16673
+ __decorateParam(3, (0, import_common39.Inject)(WagoDiagnosticsService)),
16674
+ __decorateParam(4, (0, import_common39.Inject)(WagoManagedRuntimeService)),
16675
+ __decorateParam(5, (0, import_common39.Inject)(WagoNetworkChangeService))
16316
16676
  ], WagoLiveUpdatesService);
16317
16677
 
16318
16678
  // backend/plugin.ts