@project-chip/matter.js 0.11.0-alpha.0-20240928-08865c2ce → 0.11.0-alpha.0-20241002-e7b377c34

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.
Files changed (46) hide show
  1. package/dist/cjs/CommissioningController.d.ts +7 -35
  2. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  3. package/dist/cjs/CommissioningController.js +2 -1
  4. package/dist/cjs/CommissioningController.js.map +1 -1
  5. package/dist/cjs/CommissioningServer.d.ts.map +1 -1
  6. package/dist/cjs/CommissioningServer.js +2 -2
  7. package/dist/cjs/CommissioningServer.js.map +1 -1
  8. package/dist/cjs/MatterController.d.ts +21 -49
  9. package/dist/cjs/MatterController.d.ts.map +1 -1
  10. package/dist/cjs/MatterController.js +102 -502
  11. package/dist/cjs/MatterController.js.map +2 -2
  12. package/dist/cjs/compat/protocol.d.ts +1 -1
  13. package/dist/cjs/compat/protocol.d.ts.map +1 -1
  14. package/dist/cjs/compat/protocol.js +1 -2
  15. package/dist/cjs/compat/protocol.js.map +1 -1
  16. package/dist/cjs/device/LegacyInteractionServer.d.ts.map +1 -1
  17. package/dist/cjs/device/PairedNode.d.ts +1 -2
  18. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  19. package/dist/cjs/device/PairedNode.js +2 -3
  20. package/dist/cjs/device/PairedNode.js.map +1 -1
  21. package/dist/esm/CommissioningController.d.ts +7 -35
  22. package/dist/esm/CommissioningController.d.ts.map +1 -1
  23. package/dist/esm/CommissioningController.js +2 -1
  24. package/dist/esm/CommissioningController.js.map +1 -1
  25. package/dist/esm/CommissioningServer.d.ts.map +1 -1
  26. package/dist/esm/CommissioningServer.js +2 -2
  27. package/dist/esm/CommissioningServer.js.map +1 -1
  28. package/dist/esm/MatterController.d.ts +21 -49
  29. package/dist/esm/MatterController.d.ts.map +1 -1
  30. package/dist/esm/MatterController.js +105 -519
  31. package/dist/esm/MatterController.js.map +2 -2
  32. package/dist/esm/compat/protocol.d.ts +1 -1
  33. package/dist/esm/compat/protocol.d.ts.map +1 -1
  34. package/dist/esm/compat/protocol.js +2 -4
  35. package/dist/esm/compat/protocol.js.map +1 -1
  36. package/dist/esm/device/LegacyInteractionServer.d.ts.map +1 -1
  37. package/dist/esm/device/PairedNode.d.ts +1 -2
  38. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  39. package/dist/esm/device/PairedNode.js +1 -1
  40. package/dist/esm/device/PairedNode.js.map +1 -1
  41. package/package.json +8 -8
  42. package/src/CommissioningController.ts +7 -42
  43. package/src/CommissioningServer.ts +2 -2
  44. package/src/MatterController.ts +126 -713
  45. package/src/compat/protocol.ts +2 -3
  46. package/src/device/PairedNode.ts +1 -1
@@ -11,53 +11,39 @@
11
11
  */
12
12
 
13
13
  import { GeneralCommissioning } from "#clusters";
14
+ import { NodeCommissioningOptions } from "#CommissioningController.js";
14
15
  import {
15
16
  CRYPTO_SYMMETRIC_KEY_LENGTH,
16
- Channel,
17
17
  ChannelType,
18
18
  Construction,
19
19
  Crypto,
20
20
  ImplementationError,
21
21
  Logger,
22
22
  NetInterfaceSet,
23
- NoResponseTimeoutError,
24
- ServerAddress,
25
23
  ServerAddressIp,
26
24
  StorageBackendMemory,
27
25
  StorageContext,
28
26
  StorageManager,
29
27
  SupportedStorageTypes,
30
- Time,
31
- Timer,
32
- anyPromise,
33
- createPromise,
34
- isIPv6,
35
28
  serverAddressToString,
36
29
  } from "#general";
37
30
  import {
38
- CaseClient,
39
31
  ChannelManager,
40
32
  ClusterClient,
41
- CommissionableDevice,
42
33
  CommissioningError,
43
- CommissioningSuccessfullyFinished,
44
34
  ControllerCommissioner,
45
- ControllerCommissioningOptions,
46
- ControllerDiscovery,
47
35
  DiscoveryData,
48
- DiscoveryError,
36
+ DiscoveryOptions,
49
37
  ExchangeManager,
50
- ExchangeProvider,
51
38
  Fabric,
52
39
  FabricBuilder,
53
40
  FabricJsonObject,
54
41
  FabricManager,
55
- InteractionClient,
56
- MdnsScanner,
57
- MessageChannel,
58
- NoChannelError,
59
- PairRetransmissionLimitReachedError,
60
- PaseClient,
42
+ NodeDiscoveryType,
43
+ OperationalPeer,
44
+ PeerCommissioningOptions,
45
+ PeerSet,
46
+ PeerStore,
61
47
  ResumptionRecord,
62
48
  RetransmissionLimitReachedError,
63
49
  RootCertificateManager,
@@ -72,7 +58,6 @@ import {
72
58
  FabricId,
73
59
  FabricIndex,
74
60
  NodeId,
75
- SECURE_CHANNEL_PROTOCOL_ID,
76
61
  TlvEnum,
77
62
  TlvField,
78
63
  TlvObject,
@@ -81,7 +66,6 @@ import {
81
66
  TypeFromSchema,
82
67
  VendorId,
83
68
  } from "#types";
84
- import { NodeCommissioningOptions } from "./CommissioningController.js";
85
69
 
86
70
  const TlvCommissioningSuccessFailureResponse = TlvObject({
87
71
  /** Contain the result of the operation. */
@@ -98,37 +82,20 @@ export type CommissionedNodeDetails = {
98
82
  basicInformationData?: Record<string, SupportedStorageTypes>;
99
83
  };
100
84
 
101
- type DiscoveryOptions = {
102
- discoveryType?: NodeDiscoveryType;
103
- timeoutSeconds?: number;
104
- discoveryData?: DiscoveryData;
105
- };
106
-
85
+ const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
107
86
  const DEFAULT_FABRIC_INDEX = FabricIndex(1);
108
87
  const DEFAULT_FABRIC_ID = FabricId(1);
109
- const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
110
-
111
- const RECONNECTION_POLLING_INTERVAL_MS = 600_000; // 10 minutes
112
- const RETRANSMISSION_DISCOVERY_TIMEOUT_MS = 5_000;
113
88
 
114
89
  const CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE = 3;
115
90
  const CONTROLLER_MAX_PATHS_PER_INVOKE = 10;
116
91
 
117
92
  const logger = Logger.get("MatterController");
118
93
 
119
- export enum NodeDiscoveryType {
120
- /** No discovery is done, in calls means that only known addresses are tried. */
121
- None = 0,
122
-
123
- /** Retransmission discovery means that we ignore known addresses and start a query for 5s. */
124
- RetransmissionDiscovery = 1,
94
+ // Operational peer extended with basic information as required for conversion to CommissionedNodeDetails
95
+ type CommissionedPeer = OperationalPeer & { basicInformationData?: Record<string, SupportedStorageTypes> };
125
96
 
126
- /** Timed discovery means that the device is discovered for a defined timeframe, including known addresses. */
127
- TimedDiscovery = 2,
128
-
129
- /** Full discovery means that the device is discovered until it is found, excluding known addresses. */
130
- FullDiscovery = 3,
131
- }
97
+ // Backward-compatible persistence record for nodes
98
+ type StoredOperationalPeer = [NodeId, CommissionedNodeDetails];
132
99
 
133
100
  export class MatterController {
134
101
  public static async create(options: {
@@ -152,7 +119,7 @@ export class MatterController {
152
119
  scanners,
153
120
  netInterfaces,
154
121
  sessionClosedCallback,
155
- adminVendorId = VendorId(DEFAULT_ADMIN_VENDOR_ID),
122
+ adminVendorId,
156
123
  adminFabricId = FabricId(DEFAULT_FABRIC_ID),
157
124
  adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
158
125
  caseAuthenticatedTags,
@@ -172,7 +139,6 @@ export class MatterController {
172
139
  netInterfaces,
173
140
  certificateManager,
174
141
  fabric,
175
- adminVendorId: fabric.rootVendorId,
176
142
  sessionClosedCallback,
177
143
  });
178
144
  } else {
@@ -182,7 +148,7 @@ export class MatterController {
182
148
  .setRootCert(certificateManager.rootCert)
183
149
  .setRootNodeId(rootNodeId)
184
150
  .setIdentityProtectionKey(ipkValue)
185
- .setRootVendorId(adminVendorId);
151
+ .setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID);
186
152
  fabricBuilder.setOperationalCert(
187
153
  certificateManager.generateNoc(
188
154
  fabricBuilder.publicKey,
@@ -201,7 +167,6 @@ export class MatterController {
201
167
  netInterfaces,
202
168
  certificateManager,
203
169
  fabric,
204
- adminVendorId,
205
170
  sessionClosedCallback,
206
171
  });
207
172
  }
@@ -245,7 +210,6 @@ export class MatterController {
245
210
  netInterfaces,
246
211
  certificateManager,
247
212
  fabric,
248
- adminVendorId: fabric.rootVendorId,
249
213
  sessionClosedCallback,
250
214
  });
251
215
  await controller.construction;
@@ -256,27 +220,17 @@ export class MatterController {
256
220
  private readonly netInterfaces = new NetInterfaceSet();
257
221
  private readonly channelManager = new ChannelManager(CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE);
258
222
  private readonly exchangeManager: ExchangeManager;
259
- private readonly paseClient;
260
- private readonly caseClient;
261
- private readonly commissionedNodes = new Map<NodeId, CommissionedNodeDetails>();
223
+ private readonly peers: PeerSet;
224
+ private readonly commissioner: ControllerCommissioner;
262
225
  #construction: Construction<MatterController>;
263
226
 
264
227
  readonly sessionStorage: StorageContext;
265
228
  readonly fabricStorage?: StorageContext;
266
- readonly nodesStorage: StorageContext;
229
+ readonly nodesStore: CommissionedNodeStore;
267
230
  private readonly scanners: ScannerSet;
268
231
  private readonly certificateManager: RootCertificateManager;
269
232
  private readonly fabric: Fabric;
270
- private readonly adminVendorId: VendorId;
271
233
  private readonly sessionClosedCallback?: (peerNodeId: NodeId) => void;
272
- readonly #runningNodeDiscoveries = new Map<
273
- NodeId,
274
- {
275
- type: NodeDiscoveryType;
276
- promises?: (() => Promise<MessageChannel>)[];
277
- timer?: Timer;
278
- }
279
- >();
280
234
 
281
235
  get construction() {
282
236
  return this.#construction;
@@ -290,7 +244,6 @@ export class MatterController {
290
244
  netInterfaces: NetInterfaceSet;
291
245
  certificateManager: RootCertificateManager;
292
246
  fabric: Fabric;
293
- adminVendorId: VendorId;
294
247
  sessionClosedCallback?: (peerNodeId: NodeId) => void;
295
248
  }) {
296
249
  const {
@@ -302,17 +255,14 @@ export class MatterController {
302
255
  certificateManager,
303
256
  fabric,
304
257
  sessionClosedCallback,
305
- adminVendorId,
306
258
  } = options;
307
259
  this.sessionStorage = sessionStorage;
308
260
  this.fabricStorage = fabricStorage;
309
- this.nodesStorage = nodesStorage;
310
261
  this.scanners = scanners;
311
262
  this.netInterfaces = netInterfaces;
312
263
  this.certificateManager = certificateManager;
313
264
  this.fabric = fabric;
314
265
  this.sessionClosedCallback = sessionClosedCallback;
315
- this.adminVendorId = adminVendorId;
316
266
 
317
267
  const fabricManager = new FabricManager();
318
268
  fabricManager.addFabric(fabric);
@@ -324,12 +274,9 @@ export class MatterController {
324
274
  maxPathsPerInvoke: CONTROLLER_MAX_PATHS_PER_INVOKE,
325
275
  },
326
276
  });
327
- this.paseClient = new PaseClient(this.sessionManager);
328
- this.caseClient = new CaseClient(this.sessionManager);
329
277
  this.sessionManager.sessions.deleted.on(async session => {
330
278
  this.sessionClosedCallback?.(session.peerNodeId);
331
279
  });
332
- this.sessionManager.resubmissionStarted.on(this.#handleResubmissionStarted.bind(this));
333
280
 
334
281
  this.exchangeManager = new ExchangeManager({
335
282
  sessionManager: this.sessionManager,
@@ -338,17 +285,29 @@ export class MatterController {
338
285
  });
339
286
  this.exchangeManager.addProtocolHandler(new StatusReportOnlySecureChannelProtocol());
340
287
 
341
- this.#construction = Construction(this, async () => {
342
- // If controller has a stored operational server address, use it, irrelevant what was passed in the constructor
343
- if (await this.nodesStorage.has("commissionedNodes")) {
344
- const commissionedNodes =
345
- await this.nodesStorage.get<[NodeId, CommissionedNodeDetails][]>("commissionedNodes");
346
- this.commissionedNodes.clear();
347
- for (const [nodeId, details] of commissionedNodes) {
348
- this.commissionedNodes.set(nodeId, details);
349
- }
350
- }
288
+ // Adapts the historical storage format for MatterController to OperationalPeer objects
289
+ this.nodesStore = new CommissionedNodeStore(nodesStorage, fabric);
290
+
291
+ this.nodesStore.peers = this.peers = new PeerSet({
292
+ sessions: this.sessionManager,
293
+ channels: this.channelManager,
294
+ exchanges: this.exchangeManager,
295
+ scanners: this.scanners,
296
+ netInterfaces: this.netInterfaces,
297
+ store: this.nodesStore,
298
+ });
299
+
300
+ this.commissioner = new ControllerCommissioner({
301
+ peers: this.peers,
302
+ scanners: this.scanners,
303
+ netInterfaces: this.netInterfaces,
304
+ exchanges: this.exchangeManager,
305
+ sessions: this.sessionManager,
306
+ certificates: this.certificateManager,
307
+ });
351
308
 
309
+ this.#construction = Construction(this, async () => {
310
+ await this.peers.construction.ready;
352
311
  await this.sessionManager.construction.ready;
353
312
  });
354
313
  }
@@ -394,267 +353,35 @@ export class MatterController {
394
353
  options: NodeCommissioningOptions,
395
354
  completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
396
355
  ): Promise<NodeId> {
397
- const {
398
- commissioning: commissioningOptions = {
399
- regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor, // Set to the most restrictive if relevant
400
- regulatoryCountryCode: "XX",
401
- },
402
- discovery: { timeoutSeconds = 30 },
403
- passcode,
404
- } = options;
405
- const commissionableDevice =
406
- "commissionableDevice" in options.discovery ? options.discovery.commissionableDevice : undefined;
407
- let {
408
- discovery: { discoveryCapabilities = {}, knownAddress },
409
- } = options;
410
- let identifierData = "identifierData" in options.discovery ? options.discovery.identifierData : {};
411
-
412
- if (
413
- this.scanners.hasScannerFor(ChannelType.UDP) &&
414
- this.netInterfaces.hasInterfaceFor(ChannelType.UDP, "::") !== undefined
415
- ) {
416
- discoveryCapabilities.onIpNetwork = true; // We always discover on network as defined by specs
417
- }
418
- if (commissionableDevice !== undefined) {
419
- let { addresses } = commissionableDevice;
420
- if (discoveryCapabilities.ble === true) {
421
- discoveryCapabilities = { onIpNetwork: true, ble: addresses.some(address => address.type === "ble") };
422
- } else if (discoveryCapabilities.onIpNetwork === true) {
423
- // do not use BLE if not specified, even if existing
424
- addresses = addresses.filter(address => address.type !== "ble");
425
- }
426
- addresses.sort(a => (a.type === "udp" ? -1 : 1)); // Sort addresses to use UDP first
427
- knownAddress = addresses[0];
428
- if ("instanceId" in commissionableDevice && commissionableDevice.instanceId !== undefined) {
429
- // it is an UDP discovery
430
- identifierData = { instanceId: commissionableDevice.instanceId as string };
431
- } else {
432
- identifierData = { longDiscriminator: commissionableDevice.D };
433
- }
434
- }
435
-
436
- const scannersToUse = this.collectScanners(discoveryCapabilities);
437
-
438
- logger.info(
439
- `Commissioning device with identifier ${Logger.toJSON(identifierData)} and ${
440
- scannersToUse.length
441
- } scanners and knownAddress ${Logger.toJSON(knownAddress)}`,
442
- );
443
-
444
- // If we have a known address we try this first before we discover the device
445
- let paseSecureChannel: MessageChannel | undefined;
446
- let discoveryData: DiscoveryData | undefined;
356
+ const commissioningOptions: PeerCommissioningOptions = {
357
+ ...options.commissioning,
358
+ fabric: this.fabric,
359
+ discovery: options.discovery,
360
+ passcode: options.passcode,
361
+ };
447
362
 
448
- // If we have a last known address, try this first
449
- if (knownAddress !== undefined) {
450
- try {
451
- paseSecureChannel = await this.initializePaseSecureChannel(knownAddress, passcode);
452
- } catch (error) {
453
- NoResponseTimeoutError.accept(error);
454
- }
363
+ if (completeCommissioningCallback) {
364
+ commissioningOptions.performCaseCommissioning = async (peerAddress, discoveryData) => {
365
+ const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData);
366
+ if (!result) {
367
+ throw new RetransmissionLimitReachedError("Device could not be discovered");
368
+ }
369
+ };
455
370
  }
456
- if (paseSecureChannel === undefined) {
457
- const discoveredDevices = await ControllerDiscovery.discoverDeviceAddressesByIdentifier(
458
- scannersToUse,
459
- identifierData,
460
- timeoutSeconds,
461
- );
462
371
 
463
- const { result } = await ControllerDiscovery.iterateServerAddresses(
464
- discoveredDevices,
465
- NoResponseTimeoutError,
466
- async () =>
467
- scannersToUse.flatMap(scanner => scanner.getDiscoveredCommissionableDevices(identifierData)),
468
- async (address, device) => {
469
- const channel = await this.initializePaseSecureChannel(address, passcode, device);
470
- discoveryData = device;
471
- return channel;
472
- },
473
- );
372
+ const address = await this.commissioner.commission(commissioningOptions);
474
373
 
475
- // Pairing was successful, so store the address and assign the established secure channel
476
- paseSecureChannel = result;
477
- }
374
+ await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
478
375
 
479
- return await this.commissionDevice(
480
- paseSecureChannel,
481
- commissioningOptions,
482
- discoveryData,
483
- completeCommissioningCallback,
484
- );
376
+ return address.nodeId;
485
377
  }
486
378
 
487
379
  async disconnect(nodeId: NodeId) {
488
- await this.sessionManager.removeAllSessionsForNode(nodeId, true);
489
- await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
380
+ return this.peers.disconnect(this.fabric.addressOf(nodeId));
490
381
  }
491
382
 
492
383
  async removeNode(nodeId: NodeId) {
493
- logger.info(`Removing commissioned node ${nodeId} from controller.`);
494
- await this.sessionManager.removeAllSessionsForNode(nodeId);
495
- await this.sessionManager.removeResumptionRecord(nodeId);
496
- await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
497
- this.commissionedNodes.delete(nodeId);
498
- await this.storeCommissionedNodes();
499
- }
500
-
501
- /**
502
- * Method to start commission process with a PASE pairing.
503
- * If this not successful and throws an RetransmissionLimitReachedError the address is invalid or the passcode
504
- * is wrong.
505
- */
506
- private async initializePaseSecureChannel(
507
- address: ServerAddress,
508
- passcode: number,
509
- device?: CommissionableDevice,
510
- ): Promise<MessageChannel> {
511
- let paseChannel: Channel<Uint8Array>;
512
- if (device !== undefined) {
513
- logger.info(`Commissioning device`, MdnsScanner.discoveryDataDiagnostics(device));
514
- }
515
- if (address.type === "udp") {
516
- const { ip } = address;
517
-
518
- const isIpv6Address = isIPv6(ip);
519
- const paseInterface = this.netInterfaces.interfaceFor(ChannelType.UDP, isIpv6Address ? "::" : "0.0.0.0");
520
- if (paseInterface === undefined) {
521
- // mainly IPv6 address when IPv4 is disabled
522
- throw new PairRetransmissionLimitReachedError(
523
- `IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`,
524
- );
525
- }
526
- paseChannel = await paseInterface.openChannel(address);
527
- } else {
528
- const ble = this.netInterfaces.interfaceFor(ChannelType.BLE);
529
- if (!ble) {
530
- throw new PairRetransmissionLimitReachedError(
531
- `BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`,
532
- );
533
- }
534
- // TODO Have a Timeout mechanism here for connections
535
- paseChannel = await ble.openChannel(address);
536
- }
537
-
538
- // Do PASE paring
539
- const unsecureSession = this.sessionManager.createInsecureSession({
540
- // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
541
- sessionParameters: {
542
- idleIntervalMs: device?.SII,
543
- activeIntervalMs: device?.SAI,
544
- activeThresholdMs: device?.SAT,
545
- },
546
- isInitiator: true,
547
- });
548
- const paseUnsecureMessageChannel = new MessageChannel(paseChannel, unsecureSession);
549
- const paseExchange = this.exchangeManager.initiateExchangeWithChannel(
550
- paseUnsecureMessageChannel,
551
- SECURE_CHANNEL_PROTOCOL_ID,
552
- );
553
-
554
- let paseSecureSession;
555
- try {
556
- paseSecureSession = await this.paseClient.pair(
557
- this.sessionManager.sessionParameters,
558
- paseExchange,
559
- passcode,
560
- );
561
- } catch (e) {
562
- // Close the exchange and rethrow
563
- await paseExchange.close();
564
- throw e;
565
- }
566
-
567
- await unsecureSession.destroy();
568
- return new MessageChannel(paseChannel, paseSecureSession);
569
- }
570
-
571
- /**
572
- * Method to commission a device with a PASE secure channel. It returns the NodeId of the commissioned device on
573
- * success.
574
- */
575
- private async commissionDevice(
576
- paseSecureMessageChannel: MessageChannel,
577
- commissioningOptions: ControllerCommissioningOptions,
578
- discoveryData?: DiscoveryData,
579
- completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
580
- ): Promise<NodeId> {
581
- // TODO: Create the fabric only when needed before commissioning (to do when refactoring MatterController away)
582
- // TODO also move certificateManager and other parts into that class to get rid of them here
583
- // TODO Depending on the Error type during commissioning we can do a retry ...
584
- /*
585
- Whenever the Fail-Safe timer is armed, Commissioners and Administrators SHALL NOT consider any cluster
586
- operation to have timed-out before waiting at least 30 seconds for a valid response from the cluster server.
587
- Some commands and attributes with complex side-effects MAY require longer and have specific timing requirements
588
- stated in their respective cluster specification.
589
-
590
- In concurrent connection commissioning flow, the failure of any of the steps 2 through 10 SHALL result in the
591
- Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment) and
592
- repeating each step. The failure of any of the steps 11 through 15 in concurrent connection commissioning flow
593
- SHALL result in the Commissioner and Commissionee returning to step 11 (configuration of operational network
594
- information). In the case of failure of any of the steps 11 through 15 in concurrent connection commissioning
595
- flow, the Commissioner and Commissionee SHALL reuse the existing PASE-derived encryption keys over the
596
- commissioning channel and all steps up to and including step 10 are considered to have been successfully
597
- completed.
598
- In non-concurrent connection commissioning flow, the failure of any of the steps 2 through 15 SHALL result in
599
- the Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment)
600
- and repeating each step.
601
-
602
- Commissioners that need to restart from step 2 MAY immediately expire the fail-safe by invoking the ArmFailSafe
603
- command with an ExpiryLengthSeconds field set to 0. Otherwise, Commissioners will need to wait until the current
604
- fail-safe timer has expired for the Commissionee to begin accepting PASE again.
605
- In both concurrent connection commissioning flow and non-concurrent connection commissioning flow, the
606
- Commissionee SHALL exit Commissioning Mode after 20 failed attempts.
607
- */
608
-
609
- const peerNodeId = commissioningOptions.nodeId ?? NodeId.randomOperationalNodeId();
610
- const commissioningManager = new ControllerCommissioner(
611
- // Use the created secure session to do the commissioning
612
- new InteractionClient(new ExchangeProvider(this.exchangeManager, paseSecureMessageChannel), peerNodeId),
613
- this.certificateManager,
614
- this.fabric,
615
- commissioningOptions,
616
- peerNodeId,
617
- this.adminVendorId,
618
- async () => {
619
- // TODO Right now we always close after step 12 because we do not check for commissioning flow requirements
620
- /*
621
- In concurrent connection commissioning flow the commissioning channel SHALL terminate after
622
- successful step 15 (CommissioningComplete command invocation). In non-concurrent connection
623
- commissioning flow the commissioning channel SHALL terminate after successful step 12 (trigger
624
- joining of operational network at Commissionee). The PASE-derived encryption keys SHALL be deleted
625
- when commissioning channel terminates. The PASE session SHALL be terminated by both Commissioner and
626
- Commissionee once the CommissioningComplete command is received by the Commissionee.
627
- */
628
- await paseSecureMessageChannel.close(); // We reconnect using Case, so close PASE connection
629
-
630
- if (completeCommissioningCallback !== undefined) {
631
- if (!(await completeCommissioningCallback(peerNodeId, discoveryData))) {
632
- throw new RetransmissionLimitReachedError("Device could not be discovered");
633
- }
634
- throw new CommissioningSuccessfullyFinished();
635
- }
636
- // Look for the device broadcast over MDNS and do CASE pairing
637
- return await this.connect(peerNodeId, {
638
- discoveryType: NodeDiscoveryType.TimedDiscovery,
639
- timeoutSeconds: 120,
640
- discoveryData,
641
- }); // Wait maximum 120s to find the operational device for commissioning process
642
- },
643
- );
644
-
645
- try {
646
- await commissioningManager.executeCommissioning();
647
- } catch (error) {
648
- if (this.commissionedNodes.has(peerNodeId)) {
649
- // We might have added data for an operational address that we need to cleanup
650
- this.commissionedNodes.delete(peerNodeId);
651
- }
652
- throw error;
653
- }
654
-
655
- await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
656
-
657
- return peerNodeId;
384
+ return this.peers.delete(this.fabric.addressOf(nodeId));
658
385
  }
659
386
 
660
387
  /**
@@ -676,357 +403,45 @@ export class MatterController {
676
403
  useExtendedFailSafeMessageResponseTimeout: true,
677
404
  });
678
405
  if (errorCode !== GeneralCommissioning.CommissioningError.Ok) {
679
- if (this.commissionedNodes.has(peerNodeId)) {
680
- // We might have added data for an operational address that we need to cleanup
681
- this.commissionedNodes.delete(peerNodeId);
682
- }
406
+ // We might have added data for an operational address that we need to cleanup
407
+ await this.peers.delete(this.fabric.addressOf(peerNodeId));
683
408
  throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`);
684
409
  }
685
410
  await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
686
411
  }
687
412
 
688
- #handleResubmissionStarted(peerNodeId?: NodeId) {
689
- if (peerNodeId === undefined) {
690
- return;
691
- }
692
- if (this.#runningNodeDiscoveries.has(peerNodeId)) {
693
- // We already discover for this node, so we do not need to start a new discovery
694
- return;
695
- }
696
- this.#runningNodeDiscoveries.set(peerNodeId, { type: NodeDiscoveryType.RetransmissionDiscovery });
697
- this.scanners
698
- .scannerFor(ChannelType.UDP)
699
- ?.findOperationalDevice(this.fabric, peerNodeId, RETRANSMISSION_DISCOVERY_TIMEOUT_MS, true)
700
- .catch(error => {
701
- logger.error(`Failed to discover device ${peerNodeId} after resubmission started.`, error);
702
- })
703
- .finally(() => {
704
- if (this.#runningNodeDiscoveries.get(peerNodeId)?.type === NodeDiscoveryType.RetransmissionDiscovery) {
705
- this.#runningNodeDiscoveries.delete(peerNodeId);
706
- }
707
- });
708
- }
709
-
710
- private async reconnectKnownAddress(
711
- peerNodeId: NodeId,
712
- operationalAddress: ServerAddressIp,
713
- discoveryData?: DiscoveryData,
714
- expectedProcessingTimeMs?: number,
715
- ): Promise<MessageChannel | undefined> {
716
- const { ip, port } = operationalAddress;
717
- try {
718
- logger.debug(
719
- `Resume device connection to configured server at ${ip}:${port}${expectedProcessingTimeMs !== undefined ? ` with expected processing time of ${expectedProcessingTimeMs}ms` : ""} ...`,
720
- );
721
- const channel = await this.pair(peerNodeId, operationalAddress, discoveryData, expectedProcessingTimeMs);
722
- await this.setOperationalDeviceData(peerNodeId, operationalAddress);
723
- return channel;
724
- } catch (error) {
725
- if (error instanceof NoResponseTimeoutError) {
726
- logger.debug(
727
- `Failed to resume connection to node ${peerNodeId} connection with ${ip}:${port}, discover the device ...`,
728
- error,
729
- );
730
- // We remove all sessions, this also informs the PairedNode class
731
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
732
- return undefined;
733
- } else {
734
- throw error;
735
- }
736
- }
737
- }
738
-
739
- private async connectOrDiscoverNode(
740
- peerNodeId: NodeId,
741
- operationalAddress?: ServerAddressIp,
742
- discoveryOptions: DiscoveryOptions = {},
743
- ) {
744
- const {
745
- discoveryType: requestedDiscoveryType = NodeDiscoveryType.FullDiscovery,
746
- timeoutSeconds,
747
- discoveryData = this.commissionedNodes.get(peerNodeId)?.discoveryData,
748
- } = discoveryOptions;
749
- if (timeoutSeconds !== undefined && requestedDiscoveryType !== NodeDiscoveryType.TimedDiscovery) {
750
- throw new ImplementationError("Cannot set timeout without timed discovery.");
751
- }
752
- if (requestedDiscoveryType === NodeDiscoveryType.RetransmissionDiscovery) {
753
- throw new ImplementationError("Cannot set retransmission discovery type.");
754
- }
755
-
756
- const mdnsScanner = this.scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
757
- if (!mdnsScanner) {
758
- throw new ImplementationError("Cannot discover device without mDNS scanner.");
759
- }
760
-
761
- const existingDiscoveryDetails = this.#runningNodeDiscoveries.get(peerNodeId) ?? {
762
- type: NodeDiscoveryType.None,
763
- };
764
-
765
- // If we currently run another "lower" retransmission type we cancel it
766
- if (
767
- existingDiscoveryDetails.type !== NodeDiscoveryType.None &&
768
- existingDiscoveryDetails.type < requestedDiscoveryType
769
- ) {
770
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
771
- this.#runningNodeDiscoveries.delete(peerNodeId);
772
- existingDiscoveryDetails.type = NodeDiscoveryType.None;
773
- }
774
-
775
- const { type: runningDiscoveryType, promises } = existingDiscoveryDetails;
776
-
777
- // If we have a last known address try to reach the device directly when we are not already discovering
778
- // In worst case parallel cases we do this step twice, but that's ok
779
- if (
780
- operationalAddress !== undefined &&
781
- (runningDiscoveryType === NodeDiscoveryType.None || requestedDiscoveryType === NodeDiscoveryType.None)
782
- ) {
783
- const directReconnection = await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData);
784
- if (directReconnection !== undefined) {
785
- return directReconnection;
786
- }
787
- if (requestedDiscoveryType === NodeDiscoveryType.None) {
788
- throw new DiscoveryError(`Node ${peerNodeId} is not reachable right now.`);
789
- }
790
- }
791
-
792
- if (promises !== undefined) {
793
- if (runningDiscoveryType > requestedDiscoveryType) {
794
- // We already run a "longer" discovery, so we know it is unreachable for now
795
- throw new DiscoveryError(
796
- `Node ${peerNodeId} is not reachable right now and discovery already running.`,
797
- );
798
- } else {
799
- // If we are already discovering this node, so we reuse promises
800
- return await anyPromise(promises);
801
- }
802
- }
803
-
804
- const discoveryPromises = new Array<() => Promise<MessageChannel>>();
805
- let reconnectionPollingTimer: Timer | undefined;
806
-
807
- if (operationalAddress !== undefined) {
808
- // Additionally to general discovery we also try to poll the formerly known operational address
809
- if (requestedDiscoveryType === NodeDiscoveryType.FullDiscovery) {
810
- const { promise, resolver, rejecter } = createPromise<MessageChannel>();
811
-
812
- reconnectionPollingTimer = Time.getPeriodicTimer(
813
- "Controller reconnect",
814
- RECONNECTION_POLLING_INTERVAL_MS,
815
- async () => {
816
- try {
817
- logger.debug(`Polling for device at ${serverAddressToString(operationalAddress)} ...`);
818
- const result = await this.reconnectKnownAddress(
819
- peerNodeId,
820
- operationalAddress,
821
- discoveryData,
822
- );
823
- if (result !== undefined && reconnectionPollingTimer?.isRunning) {
824
- reconnectionPollingTimer?.stop();
825
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
826
- this.#runningNodeDiscoveries.delete(peerNodeId);
827
- resolver(result);
828
- }
829
- } catch (error) {
830
- if (reconnectionPollingTimer?.isRunning) {
831
- reconnectionPollingTimer?.stop();
832
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
833
- this.#runningNodeDiscoveries.delete(peerNodeId);
834
- rejecter(error);
835
- }
836
- }
837
- },
838
- ).start();
839
-
840
- discoveryPromises.push(() => promise);
841
- }
842
- }
843
-
844
- discoveryPromises.push(async () => {
845
- const scanResult = await ControllerDiscovery.discoverOperationalDevice(
846
- this.fabric,
847
- peerNodeId,
848
- mdnsScanner,
849
- timeoutSeconds,
850
- timeoutSeconds === undefined,
851
- );
852
- const { timer } = this.#runningNodeDiscoveries.get(peerNodeId) ?? {};
853
- timer?.stop();
854
- this.#runningNodeDiscoveries.delete(peerNodeId);
855
-
856
- const { result } = await ControllerDiscovery.iterateServerAddresses(
857
- [scanResult],
858
- NoResponseTimeoutError,
859
- async () => {
860
- const device = mdnsScanner.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
861
- return device !== undefined ? [device] : [];
862
- },
863
- async (address, device) => {
864
- const result = await this.pair(peerNodeId, address, device);
865
- await this.setOperationalDeviceData(peerNodeId, address, {
866
- ...discoveryData,
867
- ...device,
868
- });
869
- return result;
870
- },
871
- );
872
-
873
- return result;
874
- });
875
-
876
- this.#runningNodeDiscoveries.set(peerNodeId, {
877
- type: requestedDiscoveryType,
878
- promises: discoveryPromises,
879
- timer: reconnectionPollingTimer,
880
- });
881
-
882
- return await anyPromise(discoveryPromises).finally(() => this.#runningNodeDiscoveries.delete(peerNodeId));
883
- }
884
-
885
- /**
886
- * Resume a device connection and establish a CASE session that was previously paired with the controller. This
887
- * method will try to connect to the device using the previously used server address (if set). If that fails, the
888
- * device is discovered again using its operational instance details.
889
- * It returns the operational MessageChannel on success.
890
- */
891
- private async resume(peerNodeId: NodeId, discoveryOptions?: DiscoveryOptions) {
892
- const operationalAddress = this.getLastOperationalAddress(peerNodeId);
893
-
894
- try {
895
- return await this.connectOrDiscoverNode(peerNodeId, operationalAddress, discoveryOptions);
896
- } catch (error) {
897
- if (
898
- (error instanceof DiscoveryError || error instanceof NoResponseTimeoutError) &&
899
- this.commissionedNodes.has(peerNodeId)
900
- ) {
901
- logger.info(`Resume failed, remove all sessions for node ${peerNodeId}`);
902
- // We remove all sessions, this also informs the PairedNode class
903
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
904
- }
905
- throw error;
906
- }
907
- }
908
-
909
- /** Pair with an operational device (already commissioned) and establish a CASE session. */
910
- private async pair(
911
- peerNodeId: NodeId,
912
- operationalServerAddress: ServerAddressIp,
913
- discoveryData?: DiscoveryData,
914
- expectedProcessingTimeMs?: number,
915
- ) {
916
- const { ip, port } = operationalServerAddress;
917
- // Do CASE pairing
918
- const isIpv6Address = isIPv6(ip);
919
- const operationalInterface = this.netInterfaces.interfaceFor(ChannelType.UDP, isIpv6Address ? "::" : "0.0.0.0");
920
-
921
- if (operationalInterface === undefined) {
922
- throw new PairRetransmissionLimitReachedError(
923
- `IPv${
924
- isIpv6Address ? "6" : "4"
925
- } interface not initialized for port ${port}. Cannot use ${ip} for pairing.`,
926
- );
927
- }
928
-
929
- const operationalChannel = await operationalInterface.openChannel(operationalServerAddress);
930
- const { sessionParameters } = this.findResumptionRecordByNodeId(peerNodeId) ?? {};
931
- const unsecureSession = this.sessionManager.createInsecureSession({
932
- // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
933
- sessionParameters: {
934
- idleIntervalMs: discoveryData?.SII ?? sessionParameters?.idleIntervalMs,
935
- activeIntervalMs: discoveryData?.SAI ?? sessionParameters?.activeIntervalMs,
936
- activeThresholdMs: discoveryData?.SAT ?? sessionParameters?.activeThresholdMs,
937
- },
938
- isInitiator: true,
939
- });
940
- const operationalUnsecureMessageExchange = new MessageChannel(operationalChannel, unsecureSession);
941
- let operationalSecureSession;
942
- try {
943
- const exchange = this.exchangeManager.initiateExchangeWithChannel(
944
- operationalUnsecureMessageExchange,
945
- SECURE_CHANNEL_PROTOCOL_ID,
946
- );
947
-
948
- try {
949
- operationalSecureSession = await this.caseClient.pair(
950
- exchange,
951
- this.fabric,
952
- peerNodeId,
953
- expectedProcessingTimeMs,
954
- );
955
- } catch (e) {
956
- await exchange.close();
957
- throw e;
958
- }
959
- } catch (e) {
960
- NoResponseTimeoutError.accept(e);
961
-
962
- // Convert error
963
- throw new PairRetransmissionLimitReachedError(e.message);
964
- }
965
- await unsecureSession.destroy();
966
- const channel = new MessageChannel(operationalChannel, operationalSecureSession);
967
- await this.channelManager.setChannel(this.fabric, peerNodeId, channel);
968
- return channel;
969
- }
970
-
971
413
  isCommissioned() {
972
- return this.commissionedNodes.size > 0;
414
+ return this.peers.size > 0;
973
415
  }
974
416
 
975
417
  getCommissionedNodes() {
976
- return Array.from(this.commissionedNodes.keys());
418
+ return this.peers.map(peer => peer.address.nodeId);
977
419
  }
978
420
 
979
421
  getCommissionedNodesDetails() {
980
- return Array.from(this.commissionedNodes.entries()).map(
981
- ([nodeId, { operationalServerAddress, discoveryData, basicInformationData }]) => ({
982
- nodeId,
983
- operationalAddress: operationalServerAddress
984
- ? serverAddressToString(operationalServerAddress)
985
- : undefined,
422
+ return this.peers.map(peer => {
423
+ const { address, operationalAddress, discoveryData, basicInformationData } = peer as CommissionedPeer;
424
+ return {
425
+ nodeId: address.nodeId,
426
+ operationalAddress: operationalAddress ? serverAddressToString(operationalAddress) : undefined,
986
427
  advertisedName: discoveryData?.DN,
987
428
  discoveryData,
988
429
  basicInformationData,
989
- }),
990
- );
991
- }
992
-
993
- private async setOperationalDeviceData(
994
- nodeId: NodeId,
995
- operationalServerAddress: ServerAddressIp,
996
- discoveryData?: DiscoveryData,
997
- ) {
998
- const nodeDetails = this.commissionedNodes.get(nodeId) ?? {};
999
- nodeDetails.operationalServerAddress = operationalServerAddress;
1000
- if (discoveryData !== undefined) {
1001
- nodeDetails.discoveryData = {
1002
- ...nodeDetails.discoveryData,
1003
- ...discoveryData,
1004
430
  };
1005
- }
1006
- this.commissionedNodes.set(nodeId, nodeDetails);
1007
- await this.storeCommissionedNodes();
431
+ });
1008
432
  }
1009
433
 
1010
434
  async enhanceCommissionedNodeDetails(
1011
435
  nodeId: NodeId,
1012
436
  data: { basicInformationData: Record<string, SupportedStorageTypes> },
1013
437
  ) {
1014
- const nodeDetails = this.commissionedNodes.get(nodeId);
438
+ const nodeDetails = this.peers.get(this.fabric.addressOf(nodeId)) as CommissionedPeer;
1015
439
  if (nodeDetails === undefined) {
1016
440
  throw new Error(`Node ${nodeId} is not commissioned.`);
1017
441
  }
1018
442
  const { basicInformationData } = data;
1019
443
  nodeDetails.basicInformationData = basicInformationData;
1020
- this.commissionedNodes.set(nodeId, nodeDetails);
1021
- await this.storeCommissionedNodes();
1022
- }
1023
-
1024
- private getLastOperationalAddress(nodeId: NodeId) {
1025
- return this.commissionedNodes.get(nodeId)?.operationalServerAddress;
1026
- }
1027
-
1028
- private async storeCommissionedNodes() {
1029
- await this.nodesStorage.set("commissionedNodes", Array.from(this.commissionedNodes.entries()));
444
+ await this.nodesStore.save();
1030
445
  }
1031
446
 
1032
447
  /**
@@ -1034,59 +449,7 @@ export class MatterController {
1034
449
  * Returns a InteractionClient on success.
1035
450
  */
1036
451
  async connect(peerNodeId: NodeId, discoveryOptions: DiscoveryOptions) {
1037
- const { discoveryData } = discoveryOptions;
1038
-
1039
- let channel: MessageChannel;
1040
- try {
1041
- channel = this.channelManager.getChannel(this.fabric, peerNodeId);
1042
- } catch (error) {
1043
- NoChannelError.accept(error);
1044
-
1045
- channel = await this.resume(peerNodeId, discoveryOptions);
1046
- }
1047
- return new InteractionClient(
1048
- new ExchangeProvider(this.exchangeManager, channel, async () => {
1049
- if (!this.channelManager.hasChannel(this.fabric, peerNodeId)) {
1050
- throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is currently not reachable.`);
1051
- }
1052
- await this.channelManager.removeAllNodeChannels(this.fabric, peerNodeId);
1053
-
1054
- const mdnsScanner = this.scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
1055
- const discoveredAddresses = mdnsScanner?.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
1056
- const lastKnownAddress = this.getLastOperationalAddress(peerNodeId);
1057
-
1058
- if (
1059
- lastKnownAddress !== undefined &&
1060
- discoveredAddresses !== undefined &&
1061
- discoveredAddresses.addresses.some(
1062
- ({ ip, port }) => ip === lastKnownAddress.ip && port === lastKnownAddress.port,
1063
- )
1064
- ) {
1065
- // We found the same address, so assume somehow cached response because we just tried to connect,
1066
- // and it failed, so clear list
1067
- discoveredAddresses.addresses.length = 0;
1068
- }
1069
-
1070
- // Try to use first result for one last try before we need to reconnect
1071
- const operationalAddress = discoveredAddresses?.addresses[0];
1072
- if (operationalAddress === undefined) {
1073
- logger.info(
1074
- `Re-Discovering device failed (no address found), remove all sessions for node ${peerNodeId}`,
1075
- );
1076
- // We remove all sessions, this also informs the PairedNode class
1077
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
1078
- throw new RetransmissionLimitReachedError(`No operational address found for node ${peerNodeId}`);
1079
- }
1080
- if (
1081
- (await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData, 2_000)) ===
1082
- undefined
1083
- ) {
1084
- throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is not reachable.`);
1085
- }
1086
- return this.channelManager.getChannel(this.fabric, peerNodeId);
1087
- }),
1088
- peerNodeId,
1089
- );
452
+ return this.peers.connect(this.fabric.addressOf(peerNodeId), discoveryOptions);
1090
453
  }
1091
454
 
1092
455
  async getNextAvailableSessionId() {
@@ -1098,7 +461,7 @@ export class MatterController {
1098
461
  }
1099
462
 
1100
463
  findResumptionRecordByNodeId(nodeId: NodeId) {
1101
- return this.sessionManager.findResumptionRecordByNodeId(nodeId);
464
+ return this.sessionManager.findResumptionRecordByAddress(this.fabric.addressOf(nodeId));
1102
465
  }
1103
466
 
1104
467
  async saveResumptionRecord(resumptionRecord: ResumptionRecord) {
@@ -1110,11 +473,7 @@ export class MatterController {
1110
473
  }
1111
474
 
1112
475
  async close() {
1113
- const mdnsScanner = this.scanners.scannerFor(ChannelType.UDP) as MdnsScanner | undefined;
1114
- for (const [nodeId, { timer }] of this.#runningNodeDiscoveries.entries()) {
1115
- timer?.stop();
1116
- mdnsScanner?.cancelOperationalDeviceDiscovery(this.fabric, nodeId, false); // This ends discovery without triggering promises
1117
- }
476
+ await this.peers.close();
1118
477
  await this.exchangeManager.close();
1119
478
  await this.sessionManager.close();
1120
479
  await this.channelManager.close();
@@ -1125,3 +484,57 @@ export class MatterController {
1125
484
  return this.sessionManager.getActiveSessionInformation();
1126
485
  }
1127
486
  }
487
+
488
+ class CommissionedNodeStore extends PeerStore {
489
+ declare peers: PeerSet;
490
+
491
+ constructor(
492
+ private nodesStorage: StorageContext,
493
+ private fabric: Fabric,
494
+ ) {
495
+ super();
496
+ }
497
+
498
+ async loadPeers() {
499
+ if (!(await this.nodesStorage.has("commissionedNodes"))) {
500
+ return [];
501
+ }
502
+
503
+ const commissionedNodes = await this.nodesStorage.get<StoredOperationalPeer[]>("commissionedNodes");
504
+ return commissionedNodes.map(
505
+ ([nodeId, { operationalServerAddress, discoveryData, basicInformationData }]) =>
506
+ ({
507
+ address: this.fabric.addressOf(nodeId),
508
+ operationalAddress: operationalServerAddress,
509
+ discoveryData,
510
+ basicInformationData,
511
+ }) satisfies CommissionedPeer,
512
+ );
513
+ }
514
+
515
+ async updatePeer() {
516
+ return this.save();
517
+ }
518
+
519
+ async deletePeer() {
520
+ return this.save();
521
+ }
522
+
523
+ async save() {
524
+ await this.nodesStorage.set(
525
+ "commissionedNodes",
526
+ this.peers.map(peer => {
527
+ const {
528
+ address,
529
+ operationalAddress: operationalServerAddress,
530
+ basicInformationData,
531
+ discoveryData,
532
+ } = peer as CommissionedPeer;
533
+ return [
534
+ address.nodeId,
535
+ { operationalServerAddress, basicInformationData, discoveryData },
536
+ ] satisfies StoredOperationalPeer;
537
+ }),
538
+ );
539
+ }
540
+ }