@project-chip/matter.js 0.13.0-alpha.0-20250311-3eb0af5f2 → 0.13.0-alpha.0-20250322-f085fa576

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.
@@ -22,6 +22,7 @@ import {
22
22
  CRYPTO_SYMMETRIC_KEY_LENGTH,
23
23
  ImplementationError,
24
24
  Logger,
25
+ MatterError,
25
26
  NetInterfaceSet,
26
27
  ServerAddressIp,
27
28
  serverAddressToString,
@@ -59,7 +60,6 @@ import {
59
60
  SecureChannelProtocol,
60
61
  SessionManager,
61
62
  SubscriptionClient,
62
- UnknownNodeError,
63
63
  } from "#protocol";
64
64
  import {
65
65
  CaseAuthenticatedTag,
@@ -72,7 +72,9 @@ import {
72
72
  TypeFromPartialBitSchema,
73
73
  VendorId,
74
74
  } from "#types";
75
+ import { ClassExtends } from "@matter/general";
75
76
  import { ControllerStoreInterface } from "@matter/node";
77
+ import { ControllerCommissioningFlow, MessageChannel } from "@matter/protocol";
76
78
 
77
79
  export type CommissionedNodeDetails = {
78
80
  operationalServerAddress?: ServerAddressIp;
@@ -104,6 +106,9 @@ export class MatterController {
104
106
  adminFabricIndex?: FabricIndex;
105
107
  caseAuthenticatedTags?: CaseAuthenticatedTag[];
106
108
  adminFabricLabel: string;
109
+ rootNodeId?: NodeId;
110
+ rootCertificateAuthority?: CertificateAuthority;
111
+ rootFabric?: Fabric;
107
112
  }): Promise<MatterController> {
108
113
  const {
109
114
  controllerStore,
@@ -115,17 +120,20 @@ export class MatterController {
115
120
  adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
116
121
  caseAuthenticatedTags,
117
122
  adminFabricLabel,
123
+ rootNodeId,
124
+ rootCertificateAuthority,
125
+ rootFabric,
118
126
  } = options;
119
127
 
120
- const ca = await CertificateAuthority.create(controllerStore.caStorage);
128
+ const ca = rootCertificateAuthority ?? (await CertificateAuthority.create(controllerStore.caStorage));
121
129
  const fabricStorage = controllerStore.fabricStorage;
122
130
 
123
131
  let controller: MatterController | undefined = undefined;
124
132
  // Check if we have a fabric stored in the storage, if yes initialize this one, else build a new one
125
- if (await fabricStorage.has("fabric")) {
126
- const fabric = new Fabric(await fabricStorage.get<Fabric.Config>("fabric"));
133
+ if (rootFabric !== undefined || (await fabricStorage.has("fabric"))) {
134
+ const fabric = rootFabric ?? new Fabric(await fabricStorage.get<Fabric.Config>("fabric"));
127
135
  if (Bytes.areEqual(fabric.rootCert, ca.rootCert)) {
128
- logger.info("Loaded existing fabric from storage");
136
+ logger.info("Used existing fabric");
129
137
  controller = new MatterController({
130
138
  controllerStore,
131
139
  scanners,
@@ -136,9 +144,12 @@ export class MatterController {
136
144
  sessionClosedCallback,
137
145
  });
138
146
  } else {
147
+ if (rootFabric !== undefined) {
148
+ throw new MatterError("Fabric CA certificate is not in sync with CA.");
149
+ }
139
150
  logger.info("Fabric CA certificate changed ...");
140
151
  if (await controllerStore.nodesStorage.has("commissionedNodes")) {
141
- throw new Error(
152
+ throw new MatterError(
142
153
  "Fabric certificate changed, but commissioned nodes are still present. Please clear the storage.",
143
154
  );
144
155
  }
@@ -146,16 +157,16 @@ export class MatterController {
146
157
  }
147
158
  if (controller === undefined) {
148
159
  logger.info("Creating new fabric");
149
- const rootNodeId = NodeId.randomOperationalNodeId();
160
+ const controllerNodeId = rootNodeId ?? NodeId.randomOperationalNodeId();
150
161
  const ipkValue = Crypto.getRandomData(CRYPTO_SYMMETRIC_KEY_LENGTH);
151
162
  const fabricBuilder = new FabricBuilder()
152
163
  .setRootCert(ca.rootCert)
153
- .setRootNodeId(rootNodeId)
164
+ .setRootNodeId(controllerNodeId)
154
165
  .setIdentityProtectionKey(ipkValue)
155
166
  .setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID)
156
167
  .setLabel(adminFabricLabel);
157
168
  fabricBuilder.setOperationalCert(
158
- ca.generateNoc(fabricBuilder.publicKey, adminFabricId, rootNodeId, caseAuthenticatedTags),
169
+ ca.generateNoc(fabricBuilder.publicKey, adminFabricId, controllerNodeId, caseAuthenticatedTags),
159
170
  );
160
171
  const fabric = await fabricBuilder.build(adminFabricIndex);
161
172
 
@@ -174,7 +185,8 @@ export class MatterController {
174
185
  }
175
186
 
176
187
  public static async createAsPaseCommissioner(options: {
177
- certificateAuthorityConfig: CertificateAuthority.Configuration;
188
+ certificateAuthorityConfig?: CertificateAuthority.Configuration;
189
+ rootCertificateAuthority?: CertificateAuthority;
178
190
  fabricConfig: Fabric.Config;
179
191
  scanners: ScannerSet;
180
192
  netInterfaces: NetInterfaceSet;
@@ -183,6 +195,7 @@ export class MatterController {
183
195
  }): Promise<MatterController> {
184
196
  const {
185
197
  certificateAuthorityConfig,
198
+ rootCertificateAuthority,
186
199
  fabricConfig,
187
200
  adminFabricLabel,
188
201
  scanners,
@@ -200,7 +213,12 @@ export class MatterController {
200
213
  logger.info("BLE is not enabled. Using only IP network for commissioning.");
201
214
  }
202
215
 
203
- const certificateManager = await CertificateAuthority.create(certificateAuthorityConfig);
216
+ if (rootCertificateAuthority === undefined && certificateAuthorityConfig === undefined) {
217
+ throw new ImplementationError("Either rootCertificateAuthority or certificateAuthorityConfig must be set.");
218
+ }
219
+
220
+ const certificateManager =
221
+ rootCertificateAuthority ?? (await CertificateAuthority.create(certificateAuthorityConfig!));
204
222
 
205
223
  // Stored data are temporary anyway and no node will be connected, so just use an in-memory storage
206
224
  const storageManager = new StorageManager(new StorageBackendMemory());
@@ -388,7 +406,10 @@ export class MatterController {
388
406
  */
389
407
  async commission(
390
408
  options: NodeCommissioningOptions,
391
- completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
409
+ customizations?: {
410
+ completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>;
411
+ commissioningFlowImpl?: ClassExtends<ControllerCommissioningFlow>;
412
+ },
392
413
  ): Promise<NodeId> {
393
414
  const commissioningOptions: DiscoveryAndCommissioningOptions = {
394
415
  ...options.commissioning,
@@ -397,6 +418,8 @@ export class MatterController {
397
418
  passcode: options.passcode,
398
419
  };
399
420
 
421
+ const { completeCommissioningCallback, commissioningFlowImpl } = customizations ?? {};
422
+
400
423
  if (completeCommissioningCallback) {
401
424
  commissioningOptions.finalizeCommissioning = async (peerAddress, discoveryData) => {
402
425
  const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData);
@@ -405,6 +428,7 @@ export class MatterController {
405
428
  }
406
429
  };
407
430
  }
431
+ commissioningOptions.commissioningFlowImpl = commissioningFlowImpl;
408
432
 
409
433
  const address = await this.commissioner.commissionWithDiscovery(commissioningOptions);
410
434
 
@@ -417,6 +441,17 @@ export class MatterController {
417
441
  return this.peers.disconnect(this.fabric.addressOf(nodeId));
418
442
  }
419
443
 
444
+ async connectPaseChannel(options: NodeCommissioningOptions) {
445
+ const { paseSecureChannel } = await this.commissioner.discoverAndEstablishPase({
446
+ ...options.commissioning,
447
+ fabric: this.fabric,
448
+ discovery: options.discovery,
449
+ passcode: options.passcode,
450
+ });
451
+ logger.warn("PASE channel established", paseSecureChannel.session.name, paseSecureChannel.session.isSecure);
452
+ return paseSecureChannel;
453
+ }
454
+
420
455
  async removeNode(nodeId: NodeId) {
421
456
  return this.peers.delete(this.fabric.addressOf(nodeId));
422
457
  }
@@ -504,8 +539,11 @@ export class MatterController {
504
539
  return this.clients.connect(this.fabric.addressOf(peerNodeId), { discoveryOptions, allowUnknownPeer });
505
540
  }
506
541
 
507
- createInteractionClient(peerNodeId: NodeId, discoveryOptions: DiscoveryOptions) {
508
- return this.clients.getInteractionClient(this.fabric.addressOf(peerNodeId), discoveryOptions);
542
+ createInteractionClient(peerNodeIdOrChannel: NodeId | MessageChannel, discoveryOptions: DiscoveryOptions) {
543
+ if (peerNodeIdOrChannel instanceof MessageChannel) {
544
+ return this.clients.getInteractionClientForChannel(peerNodeIdOrChannel);
545
+ }
546
+ return this.clients.getInteractionClient(this.fabric.addressOf(peerNodeIdOrChannel), discoveryOptions);
509
547
  }
510
548
 
511
549
  async getNextAvailableSessionId() {
@@ -548,11 +586,8 @@ export class MatterController {
548
586
  filterClusterId?: ClusterId,
549
587
  ): Promise<{ endpointId: EndpointNumber; clusterId: ClusterId; dataVersion: number }[]> {
550
588
  const peer = this.peers.get(this.fabric.addressOf(nodeId));
551
- if (peer === undefined) {
552
- throw new UnknownNodeError(`Node ${nodeId} is not commissioned.`);
553
- }
554
- if (peer.dataStore === undefined) {
555
- return []; // We have no store, also also no data
589
+ if (peer === undefined || peer.dataStore === undefined) {
590
+ return []; // We have no store, also no data
556
591
  }
557
592
  await peer.dataStore.construction;
558
593
  return peer.dataStore.getClusterDataVersions(filterEndpointId, filterClusterId);
@@ -564,11 +599,8 @@ export class MatterController {
564
599
  clusterId: ClusterId,
565
600
  ): Promise<DecodedAttributeReportValue<any>[]> {
566
601
  const peer = this.peers.get(this.fabric.addressOf(nodeId));
567
- if (peer === undefined) {
568
- throw new UnknownNodeError(`Node ${nodeId} is not commissioned.`);
569
- }
570
- if (peer.dataStore === undefined) {
571
- return []; // We have no store, also also no data
602
+ if (peer === undefined || peer.dataStore === undefined) {
603
+ return []; // We have no store, also no data
572
604
  }
573
605
  await peer.dataStore.construction;
574
606
  return peer.dataStore.retrieveAttributes(endpointId, clusterId);
@@ -119,7 +119,7 @@ export class PaseCommissioner {
119
119
  ) {
120
120
  const controller = this.assertControllerIsStarted();
121
121
 
122
- return await controller.commission(nodeOptions, completeCommissioningCallback);
122
+ return await controller.commission(nodeOptions, { completeCommissioningCallback });
123
123
  }
124
124
 
125
125
  /** Disconnects all connected nodes and Closes the network connections and other resources of the controller. */
@@ -346,20 +346,26 @@ export class PairedNode {
346
346
  this.#reconnectFunc = reconnectFunc;
347
347
 
348
348
  this.#interactionClient = interactionClient;
349
- this.#interactionClient.channelUpdated.on(() => {
350
- // When we had planned a reconnect because of a disconnect we can stop the timer now
351
- if (
352
- this.#reconnectDelayTimer?.isRunning &&
353
- !this.#clientReconnectInProgress &&
354
- !this.#reconnectionInProgress &&
355
- this.#connectionState === NodeStates.Reconnecting
356
- ) {
357
- logger.info(`Node ${this.nodeId}: Got a reconnect, so reconnection not needed anymore ...`);
358
- this.#reconnectDelayTimer?.stop();
359
- this.#reconnectDelayTimer = undefined;
360
- this.#setConnectionState(NodeStates.Connected);
361
- }
362
- });
349
+ if (this.#interactionClient.isReconnectable) {
350
+ this.#interactionClient.channelUpdated.on(() => {
351
+ // When we had planned a reconnect because of a disconnect we can stop the timer now
352
+ if (
353
+ this.#reconnectDelayTimer?.isRunning &&
354
+ !this.#clientReconnectInProgress &&
355
+ !this.#reconnectionInProgress &&
356
+ this.#connectionState === NodeStates.Reconnecting
357
+ ) {
358
+ logger.info(`Node ${this.nodeId}: Got a reconnect, so reconnection not needed anymore ...`);
359
+ this.#reconnectDelayTimer?.stop();
360
+ this.#reconnectDelayTimer = undefined;
361
+ this.#setConnectionState(NodeStates.Connected);
362
+ }
363
+ });
364
+ } else {
365
+ logger.warn(
366
+ `Node ${this.nodeId}: InteractionClient is not reconnectable, no automatic reconnection will happen in case of errors.`,
367
+ );
368
+ }
363
369
  this.#nodeDetails = new DeviceInformation(nodeId, knownNodeDetails);
364
370
  logger.info(`Node ${this.nodeId}: Created paired node with device data`, this.#nodeDetails.meta);
365
371