@attraccess/plugin-wago 0.1.0-nightly.929.1 → 0.1.0-nightly.935.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,8 +35,8 @@ __export(plugin_exports, {
35
35
  module.exports = __toCommonJS(plugin_exports);
36
36
 
37
37
  // backend/wago.controller.ts
38
- var import_common2 = require("@nestjs/common");
39
- var import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
38
+ var import_common3 = require("@nestjs/common");
39
+ var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
40
40
 
41
41
  // backend/wago.service.ts
42
42
  var import_node_crypto2 = require("node:crypto");
@@ -277,7 +277,36 @@ var CHANNEL_PROFILES = [
277
277
  "generic-digital-output",
278
278
  "generic-monitored-input"
279
279
  ];
280
- var CAPABILITIES = ["output", "input", "measurement", "pulse", "guard"];
280
+ var CAPABILITIES = ["output", "input", "measurement", "pulse", "guard", "feedback"];
281
+ var WAGO_PRESETS = [
282
+ {
283
+ id: "metered-switched-load",
284
+ name: "Metered switched load",
285
+ description: "Switches an output off immediately when disconnected and reports a transformed measurement."
286
+ },
287
+ {
288
+ id: "pulsed-lock-bank",
289
+ name: "Pulsed lock bank",
290
+ description: "Pulses an output briefly and returns it to off immediately when disconnected."
291
+ },
292
+ {
293
+ id: "guarded-enable-request",
294
+ name: "Guarded enable request",
295
+ description: "Makes a non-safety enable request only while an operational guard is satisfied."
296
+ },
297
+ { id: "generic-digital-output", name: "Generic digital output", description: "A conservative output foundation." },
298
+ { id: "generic-monitored-input", name: "Generic monitored input", description: "A monitored digital input foundation." }
299
+ ];
300
+ function applyPreset(snapshot, application) {
301
+ const preset = application && WAGO_PRESETS.find((item) => item.id === application.presetId);
302
+ if (!preset) throw new Error("unknown WAGO preset");
303
+ const channel = presetChannel(application);
304
+ const existingIndex = snapshot.logicalChannels.findIndex((item) => item.id === application.channelId);
305
+ const logicalChannels = [...snapshot.logicalChannels];
306
+ if (existingIndex === -1) logicalChannels.push(channel);
307
+ else logicalChannels[existingIndex] = channel;
308
+ return { ...snapshot, logicalChannels };
309
+ }
281
310
  function canonicalSnapshot(snapshot) {
282
311
  if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
283
312
  throw new Error("configuration snapshot must be an object");
@@ -321,6 +350,7 @@ function validateSnapshot(snapshot) {
321
350
  const channels = collection(value.logicalChannels, "logicalChannels", errors);
322
351
  const pointIds = /* @__PURE__ */ new Set();
323
352
  const channelIds = /* @__PURE__ */ new Set();
353
+ const channelsById = /* @__PURE__ */ new Map();
324
354
  points.forEach((point, index) => {
325
355
  const path = `physicalPoints[${index}]`;
326
356
  if (!record(point, path, errors)) return;
@@ -335,8 +365,11 @@ function validateSnapshot(snapshot) {
335
365
  });
336
366
  });
337
367
  channels.forEach((channel, index) => {
338
- if (channel && typeof channel === "object" && !Array.isArray(channel))
339
- addId(channel.id, `logicalChannels[${index}].id`, channelIds, errors);
368
+ if (channel && typeof channel === "object" && !Array.isArray(channel)) {
369
+ const item = channel;
370
+ addId(item.id, `logicalChannels[${index}].id`, channelIds, errors);
371
+ if (typeof item.id === "string") channelsById.set(item.id, item);
372
+ }
340
373
  });
341
374
  channels.forEach((channel, index) => {
342
375
  const path = `logicalChannels[${index}]`;
@@ -353,6 +386,7 @@ function validateSnapshot(snapshot) {
353
386
  "range",
354
387
  "pulse",
355
388
  "guard",
389
+ "feedback",
356
390
  "measurement"
357
391
  ],
358
392
  errors
@@ -365,6 +399,7 @@ function validateSnapshot(snapshot) {
365
399
  validateRange(channel.range, `${path}.range`, capabilities, errors);
366
400
  validatePulse(channel.pulse, `${path}.pulse`, capabilities, errors);
367
401
  validateGuard(channel.guard, `${path}.guard`, capabilities, channelIds, errors);
402
+ validateFeedback(channel.feedback, `${path}.feedback`, capabilities, channel.id, channelsById, errors);
368
403
  validateMeasurement(channel.measurement, `${path}.measurement`, capabilities, errors);
369
404
  validateProfile(channel.profile, capabilities, path, errors);
370
405
  });
@@ -411,7 +446,7 @@ function exactKeys(value, path, allowed, errors, optional = []) {
411
446
  message: "field is not supported by configuration version 1"
412
447
  })
413
448
  );
414
- allowed.filter((key) => !["range", "pulse", "guard", "measurement", ...optional].includes(key) && !(key in value)).forEach(
449
+ allowed.filter((key) => !["range", "pulse", "guard", "feedback", "measurement", ...optional].includes(key) && !(key in value)).forEach(
415
450
  (key) => errors.push({
416
451
  path: path === "$" ? key : `${path}.${key}`,
417
452
  code: "required_field",
@@ -519,6 +554,22 @@ function validateMeasurement(value, path, capabilities, errors) {
519
554
  if (!capabilities.has("measurement"))
520
555
  errors.push({ path, code: "unsupported_field", message: "measurement requires measurement capability" });
521
556
  }
557
+ function validateFeedback(value, path, capabilities, currentChannelId, channelsById, errors) {
558
+ if (value === void 0) return;
559
+ if (!record(value, path, errors)) return;
560
+ exactKeys(value, path, ["channelId", "expected", "timeoutMs"], errors);
561
+ const feedbackChannel = typeof value.channelId === "string" ? channelsById.get(value.channelId) : void 0;
562
+ if (!feedbackChannel)
563
+ errors.push(referenceError(`${path}.channelId`, "logical channel", value.channelId));
564
+ else if (value.channelId === currentChannelId || !Array.isArray(feedbackChannel.capabilities) || !feedbackChannel.capabilities.includes("input"))
565
+ errors.push({ path: `${path}.channelId`, code: "invalid_feedback_channel", message: "feedback must reference a distinct input channel" });
566
+ if (!["match", "inverse"].includes(value.expected))
567
+ errors.push({ path: `${path}.expected`, code: "unsupported_value", message: "expected must be match or inverse" });
568
+ if (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0)
569
+ errors.push({ path: `${path}.timeoutMs`, code: "invalid_timeout", message: "timeoutMs must be a positive integer" });
570
+ if (!capabilities.has("feedback"))
571
+ errors.push({ path, code: "unsupported_field", message: "feedback requires feedback capability" });
572
+ }
522
573
  function validateProfile(value, capabilities, path, errors) {
523
574
  const required = {
524
575
  "metered-switched-load": ["output", "measurement"],
@@ -535,6 +586,35 @@ function validateProfile(value, capabilities, path, errors) {
535
586
  })
536
587
  );
537
588
  }
589
+ function presetChannel(application) {
590
+ const base = {
591
+ id: application.channelId,
592
+ physicalPointId: application.physicalPointId,
593
+ profile: application.presetId,
594
+ disconnectPolicy: { mode: application.presetId === "generic-monitored-input" ? "hold" : "immediate" }
595
+ };
596
+ switch (application.presetId) {
597
+ case "metered-switched-load":
598
+ return { ...base, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
599
+ case "pulsed-lock-bank":
600
+ return { ...base, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
601
+ case "guarded-enable-request":
602
+ if (!application.guardChannelId) throw new Error("guarded enable requests require a guard channel");
603
+ return {
604
+ ...base,
605
+ capabilities: ["output", "guard"],
606
+ guard: { channelId: application.guardChannelId, when: "on" }
607
+ };
608
+ case "generic-monitored-input":
609
+ return { ...base, capabilities: ["input"] };
610
+ case "generic-digital-output":
611
+ return {
612
+ ...base,
613
+ capabilities: application.feedbackChannelId ? ["output", "feedback"] : ["output"],
614
+ ...application.feedbackChannelId ? { feedback: { channelId: application.feedbackChannelId, expected: "match", timeoutMs: 1e3 } } : {}
615
+ };
616
+ }
617
+ }
538
618
  function referenceError(path, type, id) {
539
619
  return { path, code: "missing_reference", message: `${type} ${String(id)} does not exist in this snapshot` };
540
620
  }
@@ -554,6 +634,7 @@ var WagoConfigurationDraft = class {
554
634
  __publicField(this, "controllerId");
555
635
  __publicField(this, "snapshot");
556
636
  __publicField(this, "reviewedHash");
637
+ __publicField(this, "presetProvenance");
557
638
  __publicField(this, "updatedAt");
558
639
  }
559
640
  };
@@ -569,6 +650,9 @@ __decorateClass([
569
650
  __decorateClass([
570
651
  (0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "reviewed_hash", nullable: true })
571
652
  ], WagoConfigurationDraft.prototype, "reviewedHash", 2);
653
+ __decorateClass([
654
+ (0, import_plugins_backend_sdk4.Column)({ type: "text", name: "preset_provenance", nullable: true })
655
+ ], WagoConfigurationDraft.prototype, "presetProvenance", 2);
572
656
  __decorateClass([
573
657
  (0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "updated_at" })
574
658
  ], WagoConfigurationDraft.prototype, "updatedAt", 2);
@@ -627,6 +711,12 @@ var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
627
711
  var STALE_AFTER_MS = 9e4;
628
712
  var MAX_PENDING_CONFIGURATION_REPORTS = 100;
629
713
  var ENROLLMENT_RETRY_MS = 6e4;
714
+ var MqttSubscriptionError = class extends Error {
715
+ constructor(mqttError) {
716
+ super(String(mqttError));
717
+ __publicField(this, "mqttError", mqttError);
718
+ }
719
+ };
630
720
  var WagoService = class {
631
721
  constructor(context) {
632
722
  __publicField(this, "context", context);
@@ -645,16 +735,22 @@ var WagoService = class {
645
735
  __publicField(this, "subscriptionRetryTimer", null);
646
736
  __publicField(this, "activeSubscriptionGeneration", 0);
647
737
  __publicField(this, "destroyed", false);
648
- this.controllers = context.getRepository(WagoController);
649
- this.settings = context.getRepository(WagoSettings);
650
- this.enrollments = context.getRepository(WagoEnrollment);
651
- this.drafts = context.getRepository(WagoConfigurationDraft);
652
- this.revisions = context.getRepository(WagoConfigurationRevision);
653
738
  }
654
739
  async onModuleInit() {
740
+ this.controllers = this.context.getRepository(WagoController);
741
+ this.settings = this.context.getRepository(WagoSettings);
742
+ this.enrollments = this.context.getRepository(WagoEnrollment);
743
+ this.drafts = this.context.getRepository(WagoConfigurationDraft);
744
+ this.revisions = this.context.getRepository(WagoConfigurationRevision);
655
745
  const enrollments = await this.enrollments.createQueryBuilder("enrollment").where("enrollment.consumedAt IS NULL").getMany();
656
746
  for (const enrollment of enrollments) this.scheduleEnrollmentExpiry(enrollment);
657
- await this.subscribeConfiguredServers();
747
+ try {
748
+ await this.subscribeConfiguredServers();
749
+ } catch (error) {
750
+ if (!(error instanceof MqttSubscriptionError)) throw error;
751
+ this.context.logger.warn(`Could not establish WAGO MQTT subscriptions during startup: ${String(error.mqttError)}`);
752
+ this.scheduleSubscriptionRetry();
753
+ }
658
754
  }
659
755
  onModuleDestroy() {
660
756
  this.destroyed = true;
@@ -715,6 +811,12 @@ var WagoService = class {
715
811
  async setDefaultMqttServer(serverId) {
716
812
  return this.setSettings(serverId);
717
813
  }
814
+ async enrollmentCredentialSupport(mqttServerId) {
815
+ if (!await this.context.getMqttServerConfig(mqttServerId))
816
+ throw new import_common.NotFoundException(`MQTT server ${mqttServerId} not found`);
817
+ const providers = await this.context.getMqttCredentialProvisioning().availableProviders(mqttServerId);
818
+ return { automatic: providers.length === 1 };
819
+ }
718
820
  async getDraft(controllerId) {
719
821
  await this.claimedController(controllerId);
720
822
  return this.drafts.findOneBy({ controllerId });
@@ -728,6 +830,35 @@ var WagoService = class {
728
830
  const errors = validateSnapshot(JSON.parse(draft.snapshot));
729
831
  return { valid: errors.length === 0, errors };
730
832
  }
833
+ presets() {
834
+ return WAGO_PRESETS;
835
+ }
836
+ async previewPreset(controllerId, application) {
837
+ const draft = await this.draftForPreset(controllerId);
838
+ const snapshot = JSON.parse(draft.snapshot);
839
+ return { draftHash: configurationHash(snapshot), diff: configurationDiff(snapshot, applyPreset(snapshot, application)) };
840
+ }
841
+ async applyPreset(controllerId, application, selectedPaths, previewedDraftHash) {
842
+ return this.withConfigurationLock(controllerId, async () => {
843
+ const draft = await this.draftForPreset(controllerId);
844
+ const snapshot = JSON.parse(draft.snapshot);
845
+ if (previewedDraftHash !== configurationHash(snapshot))
846
+ throw new import_common.ConflictException("selected preset changes no longer match the configuration draft");
847
+ const candidate = applyPreset(snapshot, application);
848
+ const diff = configurationDiff(snapshot, candidate);
849
+ const validPaths = new Set(diff.map((change) => change.path));
850
+ if (!Array.isArray(selectedPaths) || selectedPaths.some((path) => !validPaths.has(path)))
851
+ throw new import_common.ConflictException("selected preset changes no longer match the configuration draft");
852
+ draft.snapshot = canonicalSnapshot(applySelectedChanges(snapshot, diff, selectedPaths));
853
+ draft.reviewedHash = null;
854
+ draft.presetProvenance = JSON.stringify([
855
+ ...parsePresetProvenance(draft.presetProvenance).slice(-99),
856
+ { presetId: application.presetId, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), selectedPaths }
857
+ ]);
858
+ draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
859
+ return this.drafts.save(draft);
860
+ });
861
+ }
731
862
  async revisionsFor(controllerId, offset = 0, limit = 20) {
732
863
  await this.claimedController(controllerId);
733
864
  const pageOffset = Number.isSafeInteger(offset) && offset > 0 ? offset : 0;
@@ -780,7 +911,7 @@ var WagoService = class {
780
911
  await this.claimedController(controllerId);
781
912
  const serialized = canonicalSnapshot(snapshot);
782
913
  const existing = await this.drafts.findOneBy({ controllerId });
783
- const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
914
+ const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, presetProvenance: null, updatedAt: "" });
784
915
  draft.snapshot = serialized;
785
916
  draft.reviewedHash = null;
786
917
  draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -796,6 +927,12 @@ var WagoService = class {
796
927
  const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
797
928
  return { draft, previous, changed: diff.length > 0, diff };
798
929
  }
930
+ async draftForPreset(controllerId) {
931
+ await this.claimedController(controllerId);
932
+ const draft = await this.drafts.findOneBy({ controllerId });
933
+ if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
934
+ return draft;
935
+ }
799
936
  async publishDraftWhileLocked(controllerId) {
800
937
  const controller = await this.claimedController(controllerId);
801
938
  const draft = await this.drafts.findOneBy({ controllerId });
@@ -879,6 +1016,11 @@ var WagoService = class {
879
1016
  ) : void 0
880
1017
  };
881
1018
  }
1019
+ /** Server-side commissioning calls this to revoke a bootstrap identity without exposing it to a browser. */
1020
+ async revokeEnrollmentForHardwareId(hardwareId) {
1021
+ const enrollment = (await this.enrollments.find({ where: { hardwareId } })).find((item) => this.isActiveEnrollment(item));
1022
+ if (enrollment) await this.revokeEnrollment(enrollment);
1023
+ }
882
1024
  async claim(id, name, verifier, mqttServerId) {
883
1025
  return this.withClaimLock(id, async () => {
884
1026
  const prepared = await this.withClaimConfigurationLock(() => this.prepareClaim(id, name, verifier, mqttServerId));
@@ -1040,7 +1182,7 @@ var WagoService = class {
1040
1182
  try {
1041
1183
  for (const serverId of serverIds) {
1042
1184
  replacements.push(
1043
- await this.context.mqtt.subscribe(serverId, `${DISCOVERY_ROOT}/+`, async (message) => {
1185
+ await this.subscribeMqtt(serverId, `${DISCOVERY_ROOT}/+`, async (message) => {
1044
1186
  if (!this.isActiveSubscriptionGeneration(generation)) return;
1045
1187
  await this.onDiscovery(serverId, message.topic, message.payload);
1046
1188
  })
@@ -1056,7 +1198,7 @@ var WagoService = class {
1056
1198
  claimedControllers.map((controller) => [controller.hardwareId, controller])
1057
1199
  );
1058
1200
  replacements.push(
1059
- await this.context.mqtt.subscribe(
1201
+ await this.subscribeMqtt(
1060
1202
  serverId,
1061
1203
  configurationReportedWildcardTopic(settings.operationalPrefix),
1062
1204
  (message) => {
@@ -1073,7 +1215,7 @@ var WagoService = class {
1073
1215
  }
1074
1216
  for (const controller of claimedControllers) {
1075
1217
  replacements.push(
1076
- await this.context.mqtt.subscribe(
1218
+ await this.subscribeMqtt(
1077
1219
  serverId,
1078
1220
  heartbeatTopic(settings.operationalPrefix, controller.hardwareId),
1079
1221
  async (message) => {
@@ -1103,6 +1245,13 @@ var WagoService = class {
1103
1245
  unsubscribe() {
1104
1246
  this.subscriptions.splice(0).forEach((subscription) => subscription.unsubscribe());
1105
1247
  }
1248
+ async subscribeMqtt(...args) {
1249
+ try {
1250
+ return await this.context.mqtt.subscribe(...args);
1251
+ } catch (error) {
1252
+ throw new MqttSubscriptionError(error);
1253
+ }
1254
+ }
1106
1255
  isActiveSubscriptionGeneration(generation) {
1107
1256
  return !this.destroyed && generation === this.activeSubscriptionGeneration;
1108
1257
  }
@@ -1414,11 +1563,462 @@ function safeEqual(left, right) {
1414
1563
  function isValidHardwareId(hardwareId) {
1415
1564
  return Boolean(hardwareId) && !/[/+#]/.test(hardwareId);
1416
1565
  }
1566
+ function parsePresetProvenance(value) {
1567
+ if (!value) return [];
1568
+ try {
1569
+ const parsed = JSON.parse(value);
1570
+ return Array.isArray(parsed) ? parsed : [];
1571
+ } catch {
1572
+ return [];
1573
+ }
1574
+ }
1575
+ function applySelectedChanges(snapshot, diff, selectedPaths) {
1576
+ let merged = JSON.parse(JSON.stringify(snapshot));
1577
+ const changes = new Map(diff.map((change) => [change.path, change]));
1578
+ for (const path of selectedPaths) {
1579
+ const change = changes.get(path);
1580
+ if (!change) continue;
1581
+ const segments = [...path.matchAll(/\.([^.[\]]+)|\[(\d+)\]/g)].map((match) => match[1] ?? Number(match[2]));
1582
+ if (!segments.length || segments.some((segment) => typeof segment === "string" && unsafePathSegment(segment))) continue;
1583
+ merged = replacePath(merged, segments, change.current);
1584
+ }
1585
+ return merged;
1586
+ }
1587
+ function replacePath(value, [segment, ...remaining], replacement) {
1588
+ if (segment === void 0) return replacement;
1589
+ if (typeof segment === "number") {
1590
+ const next = Array.isArray(value) ? [...value] : [];
1591
+ if (remaining.length) next[segment] = replacePath(next[segment], remaining, replacement);
1592
+ else if (replacement === void 0) delete next[segment];
1593
+ else next[segment] = replacement;
1594
+ return next;
1595
+ }
1596
+ const entries = Object.entries(value ?? {}).filter(([key]) => key !== segment);
1597
+ if (remaining.length) entries.push([segment, replacePath(value?.[segment], remaining, replacement)]);
1598
+ else if (replacement !== void 0) entries.push([segment, replacement]);
1599
+ return Object.fromEntries(entries);
1600
+ }
1601
+ function unsafePathSegment(segment) {
1602
+ return segment === "__proto__" || segment === "constructor" || segment === "prototype";
1603
+ }
1604
+
1605
+ // backend/wago-commissioning.service.ts
1606
+ var import_common2 = require("@nestjs/common");
1607
+ var import_node_child_process = require("node:child_process");
1608
+ var import_node_crypto3 = require("node:crypto");
1609
+ var import_promises = require("node:fs/promises");
1610
+ var import_node_os = require("node:os");
1611
+ var import_node_path = require("node:path");
1612
+ var import_plugins_backend_sdk7 = require("@attraccess/plugins-backend-sdk");
1613
+
1614
+ // backend/wago-commissioning-session.entity.ts
1615
+ var import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
1616
+ var WagoCommissioningSession = class {
1617
+ constructor() {
1618
+ __publicField(this, "id");
1619
+ __publicField(this, "hardwareId");
1620
+ __publicField(this, "mqttServerId");
1621
+ __publicField(this, "targetHost");
1622
+ __publicField(this, "hostKeyFingerprint");
1623
+ __publicField(this, "firmwareBaseline");
1624
+ __publicField(this, "state");
1625
+ __publicField(this, "enrollmentExpiresAt");
1626
+ __publicField(this, "codesysState");
1627
+ __publicField(this, "auditLog");
1628
+ __publicField(this, "failureReason");
1629
+ __publicField(this, "createdAt");
1630
+ __publicField(this, "updatedAt");
1631
+ }
1632
+ };
1633
+ __decorateClass([
1634
+ (0, import_plugins_backend_sdk6.PrimaryGeneratedColumn)({ type: "integer" })
1635
+ ], WagoCommissioningSession.prototype, "id", 2);
1636
+ __decorateClass([
1637
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "hardware_id" })
1638
+ ], WagoCommissioningSession.prototype, "hardwareId", 2);
1639
+ __decorateClass([
1640
+ (0, import_plugins_backend_sdk6.Column)({ type: "integer", name: "mqtt_server_id" })
1641
+ ], WagoCommissioningSession.prototype, "mqttServerId", 2);
1642
+ __decorateClass([
1643
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "target_host" })
1644
+ ], WagoCommissioningSession.prototype, "targetHost", 2);
1645
+ __decorateClass([
1646
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "host_key_fingerprint" })
1647
+ ], WagoCommissioningSession.prototype, "hostKeyFingerprint", 2);
1648
+ __decorateClass([
1649
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "firmware_baseline" })
1650
+ ], WagoCommissioningSession.prototype, "firmwareBaseline", 2);
1651
+ __decorateClass([
1652
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar" })
1653
+ ], WagoCommissioningSession.prototype, "state", 2);
1654
+ __decorateClass([
1655
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "enrollment_expires_at", nullable: true })
1656
+ ], WagoCommissioningSession.prototype, "enrollmentExpiresAt", 2);
1657
+ __decorateClass([
1658
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "codesys_state", nullable: true })
1659
+ ], WagoCommissioningSession.prototype, "codesysState", 2);
1660
+ __decorateClass([
1661
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "audit_log" })
1662
+ ], WagoCommissioningSession.prototype, "auditLog", 2);
1663
+ __decorateClass([
1664
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "failure_reason", nullable: true })
1665
+ ], WagoCommissioningSession.prototype, "failureReason", 2);
1666
+ __decorateClass([
1667
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "created_at" })
1668
+ ], WagoCommissioningSession.prototype, "createdAt", 2);
1669
+ __decorateClass([
1670
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "updated_at" })
1671
+ ], WagoCommissioningSession.prototype, "updatedAt", 2);
1672
+ WagoCommissioningSession = __decorateClass([
1673
+ (0, import_plugins_backend_sdk6.Entity)({ name: "plugin_wago_commissioning_sessions" })
1674
+ ], WagoCommissioningSession);
1675
+
1676
+ // backend/wago-commissioning.service.ts
1677
+ var SSH_TIMEOUT_MS = 2e4;
1678
+ var configuredFirmwareBaseline = process.env.WAGO_CC100_FIRMWARE_BASELINE?.trim() || "31";
1679
+ var configuredRuntimeImage = process.env.WAGO_CC100_RUNTIME_IMAGE?.trim() ?? "";
1680
+ var configuredRuntimeBundle = process.env.WAGO_CC100_RUNTIME_BUNDLE_PATH?.trim() ?? "";
1681
+ var configuredRuntimeBundleChecksum = process.env.WAGO_CC100_RUNTIME_BUNDLE_SHA256_PATH?.trim() ?? "";
1682
+ var configuredRuntimeBundleSignature = process.env.WAGO_CC100_RUNTIME_BUNDLE_SIGNATURE_PATH?.trim() ?? "";
1683
+ var SIGNING_NAMESPACE = "attraccess-wago-runtime";
1684
+ var SIGNING_IDENTITY = "attraccess-wago-runtime";
1685
+ var WagoCommissioningService = class {
1686
+ constructor(context, wago) {
1687
+ __publicField(this, "context", context);
1688
+ __publicField(this, "wago", wago);
1689
+ __publicField(this, "sessions");
1690
+ this.sessions = context.getRepository(WagoCommissioningSession);
1691
+ }
1692
+ support() {
1693
+ return {
1694
+ firmwareBaseline: configuredFirmwareBaseline || null,
1695
+ ready: Boolean(
1696
+ configuredFirmwareBaseline && isImmutableImage(configuredRuntimeImage) && configuredRuntimeBundle && configuredRuntimeBundleChecksum && configuredRuntimeBundleSignature
1697
+ )
1698
+ };
1699
+ }
1700
+ async create(input) {
1701
+ const hardwareId = input.hardwareId.trim();
1702
+ if (!/^[A-Za-z0-9._-]+$/.test(hardwareId)) throw new import_common2.ConflictException("a valid controller hardware ID is required");
1703
+ if (!isPrivateAddress(input.targetHost)) throw new import_common2.ConflictException("commissioning is limited to a private controller address");
1704
+ if (!configuredFirmwareBaseline)
1705
+ throw new import_common2.ConflictException("no CC100 firmware baseline is configured; commissioning is disabled until an exact supported baseline is set");
1706
+ if (!await this.context.getMqttServerConfig(input.mqttServerId)) throw new import_common2.NotFoundException("MQTT server not found");
1707
+ const hostKeyFingerprint = await scanHostKey(input.targetHost);
1708
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1709
+ return this.sessions.save(
1710
+ this.sessions.create({
1711
+ hardwareId,
1712
+ mqttServerId: input.mqttServerId,
1713
+ targetHost: input.targetHost,
1714
+ hostKeyFingerprint,
1715
+ firmwareBaseline: configuredFirmwareBaseline,
1716
+ state: "awaiting_identity_confirmation",
1717
+ enrollmentExpiresAt: null,
1718
+ codesysState: null,
1719
+ auditLog: JSON.stringify([{ at: now, event: "host_key_scanned" }]),
1720
+ failureReason: null,
1721
+ createdAt: now,
1722
+ updatedAt: now
1723
+ })
1724
+ );
1725
+ }
1726
+ async list() {
1727
+ return this.sessions.find({ order: { updatedAt: "DESC" } });
1728
+ }
1729
+ async deliver(id, input) {
1730
+ const session = await this.sessions.findOneBy({ id });
1731
+ if (!session) throw new import_common2.NotFoundException("commissioning session not found");
1732
+ if (session.state === "completed" || session.state === "revoked") throw new import_common2.ConflictException("commissioning session is no longer active");
1733
+ if (!input.physicalIdentityConfirmed) throw new import_common2.ConflictException("physical controller identity confirmation is required");
1734
+ if (input.hostKeyFingerprint !== session.hostKeyFingerprint) throw new import_common2.ConflictException("controller SSH host key does not match the scanned key");
1735
+ if (!input.temporarySsh.username.trim() || !input.temporarySsh.password)
1736
+ throw new import_common2.ConflictException("temporary SSH credentials are required for this session only");
1737
+ if (!isRuntimeArtifactConfigured())
1738
+ throw new import_common2.ConflictException("no immutable, signed CC100 runtime bundle is configured for commissioning");
1739
+ try {
1740
+ const inspection = await this.inspect(session.targetHost, session.hostKeyFingerprint, input.temporarySsh);
1741
+ session.codesysState = inspection.codesys;
1742
+ if (!isSupportedController(inspection.firmware, session.firmwareBaseline)) {
1743
+ throw new import_common2.ConflictException(`unsupported CC100 model or firmware; expected baseline ${session.firmwareBaseline}`);
1744
+ }
1745
+ if (inspection.codesys === "active" && !input.codesysStopConfirmed) {
1746
+ session.state = "awaiting_codesys_confirmation";
1747
+ session.failureReason = "CODESYS is active; explicit administrator confirmation is required before it is stopped.";
1748
+ return this.save(session, "codesys_confirmation_required");
1749
+ }
1750
+ const bundle = await verifyRuntimeBundle();
1751
+ const enrollment = await this.wago.createEnrollment(session.hardwareId, session.mqttServerId);
1752
+ if (!enrollment.password) throw new import_common2.ConflictException("automatic restricted MQTT credential provisioning is required for secure commissioning");
1753
+ const runtimeEnv = [
1754
+ `WAGO_HARDWARE_ID=${session.hardwareId}`,
1755
+ `WAGO_MQTT_URL=${enrollment.broker.useTls ? "mqtts" : "mqtt"}://${enrollment.broker.host}:${enrollment.broker.port}`,
1756
+ `WAGO_MQTT_USERNAME=${enrollment.username}`,
1757
+ `WAGO_MQTT_PASSWORD=${enrollment.password}`,
1758
+ `WAGO_ENROLLMENT_SECRET=${enrollment.claimSecret}`
1759
+ ].join("\n");
1760
+ if (inspection.codesys === "active")
1761
+ await this.sudoRun(session.targetHost, session.hostKeyFingerprint, input.temporarySsh, "/etc/init.d/pp_codesys3 stop");
1762
+ await this.sudoRun(
1763
+ session.targetHost,
1764
+ session.hostKeyFingerprint,
1765
+ input.temporarySsh,
1766
+ "docker info >/dev/null 2>&1 || /etc/init.d/dockerd start; docker info >/dev/null"
1767
+ );
1768
+ await this.writeRuntimeEnvironment(session.targetHost, session.hostKeyFingerprint, input.temporarySsh, runtimeEnv);
1769
+ try {
1770
+ await this.copyTo(session.targetHost, session.hostKeyFingerprint, input.temporarySsh, bundle.path, "/tmp/attraccess-wago-runtime.tar");
1771
+ await this.sudoRun(
1772
+ session.targetHost,
1773
+ session.hostKeyFingerprint,
1774
+ input.temporarySsh,
1775
+ `docker load -i /tmp/attraccess-wago-runtime.tar && rm -f /tmp/attraccess-wago-runtime.tar && docker image inspect ${runtimeImageTag()} >/dev/null && (docker rm -f attraccess-wago >/dev/null 2>&1 || true) && docker run -d --name attraccess-wago --restart unless-stopped --env-file /etc/attraccess-wago/runtime.env -v /var/lib/attraccess-wago:/var/lib/attraccess-wago ${runtimeImageTag()}`
1776
+ );
1777
+ } finally {
1778
+ await (0, import_promises.rm)(bundle.directory, { recursive: true, force: true });
1779
+ }
1780
+ session.state = "awaiting_discovery";
1781
+ session.enrollmentExpiresAt = enrollment.expiresAt;
1782
+ session.failureReason = null;
1783
+ return this.save(session, "bootstrap_delivered");
1784
+ } catch (error) {
1785
+ session.state = "delivery_failed";
1786
+ session.failureReason = error instanceof Error ? redact(error.message) : "Secure delivery failed";
1787
+ return this.save(session, "delivery_failed");
1788
+ }
1789
+ }
1790
+ async revoke(id) {
1791
+ const session = await this.sessions.findOneBy({ id });
1792
+ if (!session) throw new import_common2.NotFoundException("commissioning session not found");
1793
+ await this.wago.revokeEnrollmentForHardwareId(session.hardwareId);
1794
+ session.state = "revoked";
1795
+ session.failureReason = null;
1796
+ return this.save(session, "revoked");
1797
+ }
1798
+ async inspect(host, fingerprint, credential) {
1799
+ const output = await this.run(host, fingerprint, credential, "cat /etc/os-release; printf '\\nCODESYS='; ps");
1800
+ const [, codesys = "unknown"] = output.match(/CODESYS=([^\r\n]*)/) ?? [];
1801
+ return { firmware: output, codesys: /codesys/i.test(codesys) ? "active" : "inactive" };
1802
+ }
1803
+ async writeRuntimeEnvironment(host, fingerprint, credential, content) {
1804
+ await this.sudoRun(
1805
+ host,
1806
+ fingerprint,
1807
+ credential,
1808
+ "install -d -m 0700 /etc/attraccess-wago && umask 077 && base64 -d > /etc/attraccess-wago/runtime.env && chmod 0600 /etc/attraccess-wago/runtime.env",
1809
+ Buffer.from(content).toString("base64")
1810
+ );
1811
+ }
1812
+ async sudoRun(host, fingerprint, credential, command, input) {
1813
+ return this.run(host, fingerprint, credential, `sudo -S sh -c ${shellQuote(command)}`, `${credential.password}
1814
+ ${input ?? ""}`);
1815
+ }
1816
+ async run(host, fingerprint, credential, command, input) {
1817
+ const dir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "attraccess-cc100-"));
1818
+ const knownHosts = (0, import_node_path.join)(dir, "known_hosts");
1819
+ const askPass = (0, import_node_path.join)(dir, "askpass");
1820
+ try {
1821
+ await (0, import_promises.writeFile)(knownHosts, await pinnedHostKey(host, fingerprint), { mode: 384 });
1822
+ await (0, import_promises.writeFile)(askPass, `#!/bin/sh
1823
+ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
1824
+ `, { mode: 448 });
1825
+ return await runProcess(
1826
+ "ssh",
1827
+ [
1828
+ "-o",
1829
+ "BatchMode=no",
1830
+ "-o",
1831
+ "NumberOfPasswordPrompts=1",
1832
+ "-o",
1833
+ "HostKeyAlgorithms=ssh-ed25519",
1834
+ "-o",
1835
+ "StrictHostKeyChecking=yes",
1836
+ "-o",
1837
+ `UserKnownHostsFile=${knownHosts}`,
1838
+ "-o",
1839
+ "ConnectTimeout=15",
1840
+ `${credential.username}@${host}`,
1841
+ "sh",
1842
+ "-c",
1843
+ command
1844
+ ],
1845
+ input,
1846
+ { SSH_ASKPASS: askPass, SSH_ASKPASS_REQUIRE: "force", DISPLAY: "attraccess", ATTRACCESS_SSH_PASSWORD: credential.password }
1847
+ );
1848
+ } finally {
1849
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
1850
+ }
1851
+ }
1852
+ async copyTo(host, fingerprint, credential, source, destination) {
1853
+ const dir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "attraccess-cc100-"));
1854
+ const knownHosts = (0, import_node_path.join)(dir, "known_hosts");
1855
+ const askPass = (0, import_node_path.join)(dir, "askpass");
1856
+ try {
1857
+ await (0, import_promises.writeFile)(knownHosts, await pinnedHostKey(host, fingerprint), { mode: 384 });
1858
+ await (0, import_promises.writeFile)(askPass, `#!/bin/sh
1859
+ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
1860
+ `, { mode: 448 });
1861
+ await runProcess(
1862
+ "scp",
1863
+ [
1864
+ "-o",
1865
+ "BatchMode=no",
1866
+ "-o",
1867
+ "NumberOfPasswordPrompts=1",
1868
+ "-o",
1869
+ "HostKeyAlgorithms=ssh-ed25519",
1870
+ "-o",
1871
+ "StrictHostKeyChecking=yes",
1872
+ "-o",
1873
+ `UserKnownHostsFile=${knownHosts}`,
1874
+ "-o",
1875
+ "ConnectTimeout=15",
1876
+ source,
1877
+ `${credential.username}@${host}:${destination}`
1878
+ ],
1879
+ void 0,
1880
+ { SSH_ASKPASS: askPass, SSH_ASKPASS_REQUIRE: "force", DISPLAY: "attraccess", ATTRACCESS_SSH_PASSWORD: credential.password }
1881
+ );
1882
+ } finally {
1883
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
1884
+ }
1885
+ }
1886
+ save(session, event) {
1887
+ const audit = JSON.parse(session.auditLog);
1888
+ audit.push({ at: (/* @__PURE__ */ new Date()).toISOString(), event });
1889
+ session.auditLog = JSON.stringify(audit.slice(-50));
1890
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1891
+ return this.sessions.save(session);
1892
+ }
1893
+ };
1894
+ WagoCommissioningService = __decorateClass([
1895
+ (0, import_common2.Injectable)(),
1896
+ __decorateParam(0, (0, import_common2.Inject)(import_plugins_backend_sdk7.PLUGIN_CONTEXT))
1897
+ ], WagoCommissioningService);
1898
+ async function scanHostKey(host) {
1899
+ const dir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "attraccess-cc100-"));
1900
+ const knownHosts = (0, import_node_path.join)(dir, "known_hosts");
1901
+ try {
1902
+ await (0, import_promises.writeFile)(knownHosts, await scanHostKeys(host), { mode: 384 });
1903
+ return fingerprintFor(knownHosts);
1904
+ } finally {
1905
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
1906
+ }
1907
+ }
1908
+ function scanHostKeys(host) {
1909
+ return runProcess("ssh-keyscan", ["-T", "15", "-t", "ed25519", host]);
1910
+ }
1911
+ async function pinnedHostKey(host, expectedFingerprint) {
1912
+ const key = (await scanHostKeys(host)).split("\n").find((line) => line.includes("ssh-ed25519"));
1913
+ if (!key) throw new import_common2.ConflictException("the controller did not provide an Ed25519 SSH host key");
1914
+ const dir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "attraccess-cc100-"));
1915
+ const knownHosts = (0, import_node_path.join)(dir, "known_hosts");
1916
+ try {
1917
+ await (0, import_promises.writeFile)(knownHosts, `${key}
1918
+ `, { mode: 384 });
1919
+ if (await fingerprintFor(knownHosts) !== expectedFingerprint) throw new import_common2.ConflictException("controller SSH host key changed after identity confirmation");
1920
+ return `${key}
1921
+ `;
1922
+ } finally {
1923
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
1924
+ }
1925
+ }
1926
+ async function fingerprintFor(knownHosts) {
1927
+ const result = (await runProcess("ssh-keygen", ["-lf", knownHosts, "-E", "sha256"])).match(/(SHA256:[A-Za-z0-9+/=]+)/)?.[1];
1928
+ if (!result) throw new import_common2.ConflictException("the controller did not provide a supported SSH host key");
1929
+ return result;
1930
+ }
1931
+ function runProcess(command, args, input, environment) {
1932
+ return new Promise((resolve, reject) => {
1933
+ const child = (0, import_node_child_process.spawn)(command, args, { env: { ...process.env, ...environment }, stdio: ["pipe", "pipe", "pipe"] });
1934
+ let stdout = "";
1935
+ let stderr = "";
1936
+ const timer = setTimeout(() => child.kill(), SSH_TIMEOUT_MS);
1937
+ child.stdout.on("data", (chunk) => stdout += chunk);
1938
+ child.stderr.on("data", (chunk) => stderr += chunk);
1939
+ child.on("error", reject);
1940
+ child.on("close", (code) => {
1941
+ clearTimeout(timer);
1942
+ if (code === 0) resolve(stdout);
1943
+ else reject(new Error(stderr || `${command} exited with status ${code}`));
1944
+ });
1945
+ child.stdin.end(input);
1946
+ });
1947
+ }
1948
+ function isPrivateAddress(host) {
1949
+ const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1950
+ if (!match) return false;
1951
+ const octets = match.slice(1).map(Number);
1952
+ if (octets.some((octet) => octet > 255)) return false;
1953
+ return octets[0] === 10 || octets[0] === 192 && octets[1] === 168 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31;
1954
+ }
1955
+ function redact(value) {
1956
+ return value.replace(/(?:password|secret|token)=?[^\s]*/gi, "[redacted]");
1957
+ }
1958
+ function isImmutableImage(image) {
1959
+ return /@sha256:[a-f0-9]{64}$/i.test(image);
1960
+ }
1961
+ function isRuntimeArtifactConfigured() {
1962
+ return Boolean(
1963
+ isImmutableImage(configuredRuntimeImage) && configuredRuntimeBundle && configuredRuntimeBundleChecksum && configuredRuntimeBundleSignature
1964
+ );
1965
+ }
1966
+ async function verifyRuntimeBundle() {
1967
+ const directory = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "attraccess-cc100-runtime-"));
1968
+ const bundlePath = (0, import_node_path.join)(directory, "runtime.tar");
1969
+ const checksumPath = (0, import_node_path.join)(directory, "runtime.tar.sha256");
1970
+ const signaturePath = (0, import_node_path.join)(directory, "runtime.tar.sig");
1971
+ try {
1972
+ await Promise.all([
1973
+ (0, import_promises.copyFile)(configuredRuntimeBundle, bundlePath),
1974
+ (0, import_promises.copyFile)(configuredRuntimeBundleChecksum, checksumPath),
1975
+ (0, import_promises.copyFile)(configuredRuntimeBundleSignature, signaturePath)
1976
+ ]);
1977
+ } catch (error) {
1978
+ await (0, import_promises.rm)(directory, { recursive: true, force: true });
1979
+ throw error;
1980
+ }
1981
+ const [bundle, checksum, signature] = await Promise.all([(0, import_promises.readFile)(bundlePath), (0, import_promises.readFile)(checksumPath, "utf8"), (0, import_promises.stat)(signaturePath)]);
1982
+ if (!signature.isFile()) throw new import_common2.ConflictException("CC100 runtime signature is not a file");
1983
+ const digest = (0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex");
1984
+ if (!new RegExp(`^${digest}\\s+\\*?${escapeRegExp(configuredRuntimeBundle.split("/").pop() ?? "")}\\s*$`, "m").test(checksum)) {
1985
+ throw new import_common2.ConflictException("CC100 runtime bundle checksum does not match");
1986
+ }
1987
+ try {
1988
+ const allowedSigners = (0, import_node_path.join)(directory, "allowed_signers");
1989
+ const publicKey = await (0, import_promises.readFile)((0, import_node_path.join)(__dirname, "signing-public-key.pub"), "utf8");
1990
+ await (0, import_promises.writeFile)(allowedSigners, `${SIGNING_IDENTITY} ${publicKey.trim()}
1991
+ `, { mode: 384 });
1992
+ await runProcess(
1993
+ "ssh-keygen",
1994
+ ["-Y", "verify", "-f", allowedSigners, "-I", SIGNING_IDENTITY, "-n", SIGNING_NAMESPACE, "-s", signaturePath],
1995
+ bundle
1996
+ );
1997
+ } finally {
1998
+ await (0, import_promises.rm)((0, import_node_path.join)(directory, "allowed_signers"), { force: true });
1999
+ }
2000
+ return { directory, path: bundlePath };
2001
+ }
2002
+ function escapeRegExp(value) {
2003
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2004
+ }
2005
+ function runtimeImageTag() {
2006
+ return configuredRuntimeImage.split("@", 1)[0];
2007
+ }
2008
+ function isSupportedController(inspection, firmwareBaseline) {
2009
+ if (!inspection.includes('PTXDIST_PLATFORM_NAME="cc100"')) return false;
2010
+ const reportedFirmware = inspection.match(/firmware[^\r\n]*?(\d+(?:\.\d+)+|\d+)/i)?.[1];
2011
+ return !reportedFirmware || reportedFirmware.includes(firmwareBaseline);
2012
+ }
2013
+ function shellQuote(value) {
2014
+ return `'${value.replace(/'/g, `'\\"'\\"'`)}'`;
2015
+ }
1417
2016
 
1418
2017
  // backend/wago.controller.ts
1419
2018
  var WagoControllerApi = class {
1420
- constructor(wago) {
2019
+ constructor(wago, commissioning) {
1421
2020
  __publicField(this, "wago", wago);
2021
+ __publicField(this, "commissioning", commissioning);
1422
2022
  }
1423
2023
  list() {
1424
2024
  return this.wago.list();
@@ -1429,9 +2029,30 @@ var WagoControllerApi = class {
1429
2029
  setSettings(body) {
1430
2030
  return this.wago.setSettings(body?.defaultMqttServerId, body?.operationalPrefix);
1431
2031
  }
1432
- enrollment(body) {
1433
- const manualCredentials = body?.manualUsername || body?.manualPassword ? { username: body.manualUsername ?? "", password: body.manualPassword ?? "" } : void 0;
1434
- return this.wago.createEnrollment(body?.hardwareId ?? "", body?.mqttServerId, manualCredentials);
2032
+ commissioningSupport() {
2033
+ return this.commissioning.support();
2034
+ }
2035
+ commissioningSessions() {
2036
+ return this.commissioning.list();
2037
+ }
2038
+ createCommissioningSession(body) {
2039
+ if (!body?.mqttServerId) throw new import_common3.BadRequestException("MQTT server is required");
2040
+ return this.commissioning.create({
2041
+ hardwareId: body.hardwareId ?? "",
2042
+ mqttServerId: body.mqttServerId,
2043
+ targetHost: body.targetHost ?? ""
2044
+ });
2045
+ }
2046
+ deliverCommissioningSession(id, body) {
2047
+ return this.commissioning.deliver(id, {
2048
+ hostKeyFingerprint: body?.hostKeyFingerprint ?? "",
2049
+ physicalIdentityConfirmed: body?.physicalIdentityConfirmed === true,
2050
+ codesysStopConfirmed: body?.codesysStopConfirmed === true,
2051
+ temporarySsh: { username: body?.temporarySsh?.username ?? "", password: body?.temporarySsh?.password ?? "" }
2052
+ });
2053
+ }
2054
+ revokeCommissioningSession(id) {
2055
+ return this.commissioning.revoke(id);
1435
2056
  }
1436
2057
  claim(id, body) {
1437
2058
  return this.wago.claim(id, body?.name ?? "", body?.verifier ?? "", body?.mqttServerId);
@@ -1439,6 +2060,17 @@ var WagoControllerApi = class {
1439
2060
  draft(id) {
1440
2061
  return this.wago.getDraft(id);
1441
2062
  }
2063
+ presets() {
2064
+ return this.wago.presets();
2065
+ }
2066
+ previewPreset(id, body) {
2067
+ if (!body?.application) throw new import_common3.BadRequestException("application is required");
2068
+ return this.wago.previewPreset(id, body.application);
2069
+ }
2070
+ applyPreset(id, body) {
2071
+ if (!body?.application) throw new import_common3.BadRequestException("application is required");
2072
+ return this.wago.applyPreset(id, body.application, body.selectedPaths ?? [], body.previewedDraftHash ?? "");
2073
+ }
1442
2074
  saveDraft(id, body) {
1443
2075
  return this.wago.saveDraft(id, body?.snapshot);
1444
2076
  }
@@ -1462,78 +2094,114 @@ var WagoControllerApi = class {
1462
2094
  }
1463
2095
  };
1464
2096
  __decorateClass([
1465
- (0, import_common2.Get)("controllers")
2097
+ (0, import_common3.Get)("controllers")
1466
2098
  ], WagoControllerApi.prototype, "list", 1);
1467
2099
  __decorateClass([
1468
- (0, import_common2.Get)("settings")
2100
+ (0, import_common3.Get)("settings")
1469
2101
  ], WagoControllerApi.prototype, "settings", 1);
1470
2102
  __decorateClass([
1471
- (0, import_common2.Post)("settings"),
1472
- __decorateParam(0, (0, import_common2.Body)())
2103
+ (0, import_common3.Post)("settings"),
2104
+ __decorateParam(0, (0, import_common3.Body)())
1473
2105
  ], WagoControllerApi.prototype, "setSettings", 1);
1474
2106
  __decorateClass([
1475
- (0, import_common2.Post)("enrollments"),
1476
- __decorateParam(0, (0, import_common2.Body)())
1477
- ], WagoControllerApi.prototype, "enrollment", 1);
2107
+ (0, import_common3.Get)("commissioning/support")
2108
+ ], WagoControllerApi.prototype, "commissioningSupport", 1);
2109
+ __decorateClass([
2110
+ (0, import_common3.Get)("commissioning/sessions")
2111
+ ], WagoControllerApi.prototype, "commissioningSessions", 1);
2112
+ __decorateClass([
2113
+ (0, import_common3.Post)("commissioning/sessions"),
2114
+ __decorateParam(0, (0, import_common3.Body)())
2115
+ ], WagoControllerApi.prototype, "createCommissioningSession", 1);
2116
+ __decorateClass([
2117
+ (0, import_common3.Post)("commissioning/sessions/:id/deliver"),
2118
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2119
+ __decorateParam(1, (0, import_common3.Body)())
2120
+ ], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
1478
2121
  __decorateClass([
1479
- (0, import_common2.Post)("controllers/:id/claim"),
1480
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1481
- __decorateParam(1, (0, import_common2.Body)())
2122
+ (0, import_common3.Post)("commissioning/sessions/:id/revoke"),
2123
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe))
2124
+ ], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
2125
+ __decorateClass([
2126
+ (0, import_common3.Post)("controllers/:id/claim"),
2127
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2128
+ __decorateParam(1, (0, import_common3.Body)())
1482
2129
  ], WagoControllerApi.prototype, "claim", 1);
1483
2130
  __decorateClass([
1484
- (0, import_common2.Get)("controllers/:id/configuration/draft"),
1485
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
2131
+ (0, import_common3.Get)("controllers/:id/configuration/draft"),
2132
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe))
1486
2133
  ], WagoControllerApi.prototype, "draft", 1);
1487
2134
  __decorateClass([
1488
- (0, import_common2.Post)("controllers/:id/configuration/draft"),
1489
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1490
- __decorateParam(1, (0, import_common2.Body)())
2135
+ (0, import_common3.Get)("configuration/presets")
2136
+ ], WagoControllerApi.prototype, "presets", 1);
2137
+ __decorateClass([
2138
+ (0, import_common3.Post)("controllers/:id/configuration/presets/preview"),
2139
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2140
+ __decorateParam(1, (0, import_common3.Body)())
2141
+ ], WagoControllerApi.prototype, "previewPreset", 1);
2142
+ __decorateClass([
2143
+ (0, import_common3.Post)("controllers/:id/configuration/presets/apply"),
2144
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2145
+ __decorateParam(1, (0, import_common3.Body)())
2146
+ ], WagoControllerApi.prototype, "applyPreset", 1);
2147
+ __decorateClass([
2148
+ (0, import_common3.Post)("controllers/:id/configuration/draft"),
2149
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2150
+ __decorateParam(1, (0, import_common3.Body)())
1491
2151
  ], WagoControllerApi.prototype, "saveDraft", 1);
1492
2152
  __decorateClass([
1493
- (0, import_common2.Post)("controllers/:id/configuration/validate"),
1494
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
2153
+ (0, import_common3.Post)("controllers/:id/configuration/validate"),
2154
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe))
1495
2155
  ], WagoControllerApi.prototype, "validateDraft", 1);
1496
2156
  __decorateClass([
1497
- (0, import_common2.Post)("controllers/:id/configuration/review"),
1498
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
2157
+ (0, import_common3.Post)("controllers/:id/configuration/review"),
2158
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe))
1499
2159
  ], WagoControllerApi.prototype, "reviewDraft", 1);
1500
2160
  __decorateClass([
1501
- (0, import_common2.Get)("controllers/:id/configuration/revisions"),
1502
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1503
- __decorateParam(1, (0, import_common2.Query)("offset")),
1504
- __decorateParam(2, (0, import_common2.Query)("limit"))
2161
+ (0, import_common3.Get)("controllers/:id/configuration/revisions"),
2162
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2163
+ __decorateParam(1, (0, import_common3.Query)("offset")),
2164
+ __decorateParam(2, (0, import_common3.Query)("limit"))
1505
2165
  ], WagoControllerApi.prototype, "revisions", 1);
1506
2166
  __decorateClass([
1507
- (0, import_common2.Post)("controllers/:id/configuration/publish"),
1508
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
2167
+ (0, import_common3.Post)("controllers/:id/configuration/publish"),
2168
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe))
1509
2169
  ], WagoControllerApi.prototype, "publishDraft", 1);
1510
2170
  __decorateClass([
1511
- (0, import_common2.Post)("controllers/:id/configuration/rollback/:revision"),
1512
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1513
- __decorateParam(1, (0, import_common2.Param)("revision", import_common2.ParseIntPipe))
2171
+ (0, import_common3.Post)("controllers/:id/configuration/rollback/:revision"),
2172
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2173
+ __decorateParam(1, (0, import_common3.Param)("revision", import_common3.ParseIntPipe))
1514
2174
  ], WagoControllerApi.prototype, "rollback", 1);
1515
2175
  __decorateClass([
1516
- (0, import_common2.Get)("controllers/:id/configuration/revisions/:revision/preview"),
1517
- __decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
1518
- __decorateParam(1, (0, import_common2.Param)("revision", import_common2.ParseIntPipe))
2176
+ (0, import_common3.Get)("controllers/:id/configuration/revisions/:revision/preview"),
2177
+ __decorateParam(0, (0, import_common3.Param)("id", import_common3.ParseIntPipe)),
2178
+ __decorateParam(1, (0, import_common3.Param)("revision", import_common3.ParseIntPipe))
1519
2179
  ], WagoControllerApi.prototype, "previewRevision", 1);
1520
2180
  WagoControllerApi = __decorateClass([
1521
- (0, import_plugins_backend_sdk6.Auth)("resources.update"),
1522
- (0, import_common2.Controller)("wago"),
1523
- __decorateParam(0, (0, import_common2.Inject)(WagoService))
2181
+ (0, import_plugins_backend_sdk8.Auth)("system.settings.manage"),
2182
+ (0, import_common3.Controller)("wago"),
2183
+ __decorateParam(0, (0, import_common3.Inject)(WagoService)),
2184
+ __decorateParam(1, (0, import_common3.Inject)(WagoCommissioningService))
1524
2185
  ], WagoControllerApi);
1525
2186
 
1526
2187
  // backend/plugin.ts
1527
- var PLUGIN_CONTEXT2 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
2188
+ var PLUGIN_CONTEXT3 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
1528
2189
  var WagoPluginModule = class {
1529
2190
  };
1530
2191
  var plugin = {
1531
- entities: [WagoController, WagoSettings, WagoEnrollment, WagoConfigurationDraft, WagoConfigurationRevision],
2192
+ entities: [
2193
+ WagoController,
2194
+ WagoSettings,
2195
+ WagoEnrollment,
2196
+ WagoConfigurationDraft,
2197
+ WagoConfigurationRevision,
2198
+ WagoCommissioningSession
2199
+ ],
1532
2200
  register(context) {
1533
2201
  return {
1534
2202
  module: WagoPluginModule,
1535
2203
  controllers: [WagoControllerApi],
1536
- providers: [{ provide: PLUGIN_CONTEXT2, useValue: context }, WagoService]
2204
+ providers: [{ provide: PLUGIN_CONTEXT3, useValue: context }, WagoService, WagoCommissioningService]
1537
2205
  };
1538
2206
  }
1539
2207
  };