@matter/nodejs-shell 0.16.0-alpha.0-20251112-dba1973d5 → 0.16.0-alpha.0-20251125-16883ca92

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/src/MatterNode.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  // Include this first to auto-register Crypto, Network and Time Node.js implementations
8
8
  import { Environment, Logger, StorageContext, StorageService } from "#general";
9
+ import { DclCertificateService, DclOtaUpdateService, DclVendorInfoService } from "#protocol";
9
10
  import { NodeId } from "#types";
10
11
  import { CommissioningController, ControllerStore } from "@project-chip/matter.js";
11
12
  import { CommissioningControllerNodeOptions, Endpoint, PairedNode } from "@project-chip/matter.js/device";
@@ -16,11 +17,12 @@ const logger = Logger.get("Node");
16
17
  export class MatterNode {
17
18
  #storageLocation?: string;
18
19
  #storageContext?: StorageContext;
19
- readonly #environment?: Environment;
20
+ readonly #environment: Environment;
20
21
  commissioningController?: CommissioningController;
21
22
  #started = false;
22
23
  readonly #nodeNum: number;
23
24
  readonly #netInterface?: string;
25
+ #dclFetchTestCertificates = false;
24
26
 
25
27
  constructor(nodeNum: number, netInterface?: string) {
26
28
  this.#environment = Environment.default;
@@ -33,6 +35,30 @@ export class MatterNode {
33
35
  return this.#storageLocation;
34
36
  }
35
37
 
38
+ get environment() {
39
+ return this.#environment;
40
+ }
41
+
42
+ get otaService() {
43
+ return this.environment.has(DclOtaUpdateService)
44
+ ? this.environment.get(DclOtaUpdateService)
45
+ : new DclOtaUpdateService(this.environment); // Will add itself on initial instantiation
46
+ }
47
+
48
+ get certificateService() {
49
+ if (this.environment.has(DclCertificateService)) {
50
+ return this.environment.get(DclCertificateService);
51
+ }
52
+
53
+ return new DclCertificateService(this.environment, { fetchTestCertificates: this.#dclFetchTestCertificates });
54
+ }
55
+
56
+ get vendorInfoService() {
57
+ return this.environment.has(DclVendorInfoService)
58
+ ? this.environment.get(DclVendorInfoService)
59
+ : new DclVendorInfoService(this.environment); // Will add itself on initial instantiation
60
+ }
61
+
36
62
  async initialize(resetStorage: boolean) {
37
63
  /**
38
64
  * Initialize the storage system.
@@ -63,6 +89,9 @@ export class MatterNode {
63
89
  }
64
90
  this.#storageContext = controllerStore.storage.createContext("Node");
65
91
 
92
+ // Read DCL test certificates setting
93
+ this.#dclFetchTestCertificates = await this.#storageContext.get<boolean>("DclFetchTestCertificates", false);
94
+
66
95
  const storageService = this.commissioningController.env.get(StorageService);
67
96
  const baseLocation = storageService.location;
68
97
  if (baseLocation !== undefined) {
@@ -13,17 +13,21 @@ import yargs from "yargs/yargs";
13
13
  import { MatterNode } from "../MatterNode.js";
14
14
  import { exit } from "../app";
15
15
  import { commandlineParser } from "../util/CommandlineParser.js";
16
+ import cmdCert from "./cmd_cert.js";
16
17
  import cmdAttributes from "./cmd_cluster-attributes";
17
18
  import cmdCommands from "./cmd_cluster-commands";
18
19
  import cmdEvents from "./cmd_cluster-events";
19
20
  import cmdCommission from "./cmd_commission.js";
20
21
  import cmdConfig from "./cmd_config.js";
22
+ import cmdDcl from "./cmd_dcl.js";
21
23
  import cmdDiscover from "./cmd_discover.js";
22
24
  import cmdIdentify from "./cmd_identify.js";
23
25
  import cmdNodes from "./cmd_nodes.js";
26
+ import cmdOta from "./cmd_ota.js";
24
27
  import cmdSession from "./cmd_session.js";
25
28
  import cmdSubscribe from "./cmd_subscribe.js";
26
29
  import cmdTlv from "./cmd_tlv";
30
+ import cmdVendor from "./cmd_vendor.js";
27
31
 
28
32
  const MAX_HISTORY_SIZE = 1000;
29
33
 
@@ -148,7 +152,11 @@ export class Shell {
148
152
  cmdAttributes(this.theNode),
149
153
  cmdEvents(this.theNode),
150
154
  cmdCommands(this.theNode),
155
+ cmdOta(this.theNode),
156
+ cmdCert(this.theNode),
157
+ cmdVendor(this.theNode),
151
158
  cmdTlv(),
159
+ cmdDcl(),
152
160
  exitCommand(),
153
161
  ])
154
162
  .command({
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Diagnostic } from "#general";
8
+ import type { Argv } from "yargs";
9
+ import { MatterNode } from "../MatterNode.js";
10
+
11
+ export default function commands(theNode: MatterNode) {
12
+ return {
13
+ command: "cert",
14
+ describe: "Certificate management operations",
15
+ builder: (yargs: Argv) =>
16
+ yargs
17
+ .command(
18
+ ["*", "list [vendor-id]"],
19
+ "List all stored certificates",
20
+ yargs => {
21
+ return yargs.positional("vendor-id", {
22
+ describe: "Filter by vendor ID (hex format like 0xFFF1 or decimal)",
23
+ type: "string",
24
+ });
25
+ },
26
+ async argv => {
27
+ const { vendorId: vendorIdStr } = argv;
28
+
29
+ await theNode.start();
30
+ let certificates = theNode.certificateService.certificates;
31
+
32
+ // Filter by vendor ID if provided
33
+ if (vendorIdStr) {
34
+ let vendorId: number;
35
+ if (vendorIdStr.startsWith("0x")) {
36
+ const hexStr = vendorIdStr.replace(/^0x/i, "");
37
+ vendorId = parseInt(hexStr, 16);
38
+ } else {
39
+ vendorId = parseInt(vendorIdStr, 10);
40
+ }
41
+
42
+ if (!isFinite(vendorId)) {
43
+ console.error(`Error: Invalid vendor ID "${vendorIdStr}"`);
44
+ return;
45
+ }
46
+ certificates = certificates.filter(cert => cert.vid === vendorId);
47
+ }
48
+
49
+ if (certificates.length === 0) {
50
+ console.log(
51
+ vendorIdStr
52
+ ? `No certificates found for vendor ID ${vendorIdStr}.`
53
+ : "No certificates found in storage.",
54
+ );
55
+ return;
56
+ }
57
+
58
+ console.log(`\nFound ${certificates.length} certificate(s):\n`);
59
+
60
+ certificates.forEach(cert => {
61
+ console.log(`Subject Key ID: ${cert.subjectKeyId}`);
62
+ console.log(` Subject: ${cert.subjectAsText || cert.subject || "N/A"}`);
63
+ console.log("");
64
+ });
65
+ },
66
+ )
67
+ .command(
68
+ "details <subject-key-id>",
69
+ "Display detailed information about a certificate",
70
+ yargs => {
71
+ return yargs.positional("subject-key-id", {
72
+ describe: "Subject Key ID of the certificate",
73
+ type: "string",
74
+ demandOption: true,
75
+ });
76
+ },
77
+ async argv => {
78
+ const { subjectKeyId } = argv;
79
+
80
+ await theNode.start();
81
+ const cert = theNode.certificateService.getCertificate(subjectKeyId);
82
+ if (!cert) {
83
+ console.error(`Certificate with subject key ID ${subjectKeyId} not found`);
84
+ return;
85
+ }
86
+
87
+ console.log("\nCertificate Details:");
88
+ console.log(Diagnostic.json(cert));
89
+ },
90
+ )
91
+ .command(
92
+ "as-pem <subject-key-id>",
93
+ "Get certificate in PEM format",
94
+ yargs => {
95
+ return yargs.positional("subject-key-id", {
96
+ describe: "Subject Key ID of the certificate",
97
+ type: "string",
98
+ demandOption: true,
99
+ });
100
+ },
101
+ async argv => {
102
+ const { subjectKeyId } = argv;
103
+ // Normalize subject key ID by removing colons
104
+ const normalizedId = subjectKeyId.replace(/:/g, "").toUpperCase();
105
+
106
+ await theNode.start();
107
+ const pemCert = await theNode.certificateService.getCertificateAsPem(normalizedId);
108
+ console.log(pemCert);
109
+ },
110
+ )
111
+ .command(
112
+ "delete <subject-key-id>",
113
+ "Deletes a certificate from the storage",
114
+ yargs => {
115
+ return yargs.positional("subject-key-id", {
116
+ describe: "Subject Key ID of the certificate to delete",
117
+ type: "string",
118
+ demandOption: true,
119
+ });
120
+ },
121
+ async argv => {
122
+ const { subjectKeyId } = argv;
123
+ // Normalize subject key ID by removing colons
124
+ const normalizedId = subjectKeyId.replace(/:/g, "").toUpperCase();
125
+
126
+ await theNode.start();
127
+ await theNode.certificateService.deleteCertificate(normalizedId);
128
+ console.log(`Certificate ${subjectKeyId} deleted successfully`);
129
+ },
130
+ )
131
+ .command(
132
+ "update",
133
+ "Update certificates from DCL",
134
+ yargs =>
135
+ yargs.option("force", {
136
+ describe: "Force re-download and overwrite existing certificates",
137
+ type: "boolean",
138
+ default: false,
139
+ }),
140
+ async argv => {
141
+ const { force } = argv;
142
+ await theNode.start();
143
+
144
+ console.log(`Updating certificates from DCL${force ? " (force mode)" : ""}...`);
145
+ await theNode.certificateService.update(force);
146
+ console.log("Certificate update completed successfully");
147
+
148
+ const count = theNode.certificateService.certificates.length;
149
+ console.log(`Total certificates in storage: ${count}`);
150
+ },
151
+ ),
152
+ handler: async (argv: any) => {
153
+ argv.unhandled = true;
154
+ },
155
+ };
156
+ }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Logger } from "#general";
8
+ import { DclCertificateService } from "@matter/protocol";
8
9
  import { Argv } from "yargs";
9
10
  import { MatterNode } from "../MatterNode";
10
11
  import { setLogLevel } from "../app";
@@ -220,7 +221,42 @@ export default function commands(theNode: MatterNode) {
220
221
  },
221
222
  argv => doThreadCredentials(theNode, { action: "set", ...argv }),
222
223
  );
223
- }),
224
+ })
225
+
226
+ // DCL Test Certificates
227
+ .command(
228
+ "dcl-test-certificates",
229
+ "Manage DCL test certificate fetching (production only vs. include test)",
230
+ yargs => {
231
+ return yargs
232
+ .command(
233
+ "* [action]",
234
+ "get/delete DCL test certificate setting",
235
+ yargs => {
236
+ return yargs.positional("action", {
237
+ describe: "get/delete",
238
+ choices: ["get", "delete"] as const,
239
+ default: "get",
240
+ type: "string",
241
+ });
242
+ },
243
+ async argv => doDclTestCertificates(theNode, argv),
244
+ )
245
+ .command(
246
+ "set <value>",
247
+ "Enable or disable test certificate fetching from DCL",
248
+ yargs => {
249
+ return yargs.positional("value", {
250
+ describe: "Enable test certificates (true/false)",
251
+ type: "string",
252
+ choices: ["true", "false"] as const,
253
+ demandOption: true,
254
+ });
255
+ },
256
+ async argv => doDclTestCertificates(theNode, { action: "set", ...argv }),
257
+ );
258
+ },
259
+ ),
224
260
  handler: async (argv: any) => {
225
261
  argv.unhandled = true;
226
262
  },
@@ -424,3 +460,46 @@ async function doThreadCredentials(
424
460
  break;
425
461
  }
426
462
  }
463
+
464
+ async function doDclTestCertificates(
465
+ theNode: MatterNode,
466
+ args: {
467
+ action: string;
468
+ value?: string;
469
+ },
470
+ ) {
471
+ const { action, value } = args;
472
+ switch (action) {
473
+ case "get":
474
+ const enabled = await theNode.Store.get<boolean>("DclFetchTestCertificates", false);
475
+ console.log(
476
+ `DCL test certificates: ${enabled ? "enabled (production + test + GitHub)" : "disabled (production only)"}`,
477
+ );
478
+ break;
479
+ case "set":
480
+ if (value === undefined) {
481
+ console.log(`Cannot change DCL test certificates setting: New value not provided`);
482
+ return;
483
+ }
484
+ const newValue = value === "true";
485
+ await theNode.Store.set("DclFetchTestCertificates", newValue);
486
+
487
+ // Close existing certificate service so it gets re-initialized with new setting
488
+ await theNode.environment.close(DclCertificateService);
489
+
490
+ console.log(
491
+ `DCL test certificates: ${newValue ? "enabled (production + test + GitHub)" : "disabled (production only)"}. Please restart the shell for the changes to take effect.`,
492
+ );
493
+ break;
494
+ case "delete":
495
+ await theNode.Store.delete("DclFetchTestCertificates");
496
+
497
+ // Close existing certificate service so it gets re-initialized with new setting
498
+ await theNode.environment.close(DclCertificateService);
499
+
500
+ console.log(
501
+ `DCL test certificates setting reset to default (disabled). Please restart the shell for the changes to take effect.`,
502
+ );
503
+ break;
504
+ }
505
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Diagnostic } from "#general";
8
+ import { DclClient } from "@matter/protocol";
9
+ import type { Argv } from "yargs";
10
+
11
+ /**
12
+ * Parse a VID or PID from string (supports decimal or hex with 0x prefix)
13
+ */
14
+ function parseVidPid(value: string, fieldName: string): number {
15
+ const num = value.startsWith("0x") ? parseInt(value, 16) : parseInt(value, 10);
16
+ if (isNaN(num) || num < 0 || num > 0xffff) {
17
+ throw new Error(`${fieldName} must be a valid 16-bit number`);
18
+ }
19
+ return num;
20
+ }
21
+
22
+ /**
23
+ * Get DCL environment description
24
+ */
25
+ function getDclEnv(isTest: boolean): string {
26
+ return isTest ? "test" : "production";
27
+ }
28
+
29
+ /**
30
+ * Create yargs positional argument configuration for VID
31
+ */
32
+ function vidPositional() {
33
+ return {
34
+ describe: "Vendor ID (decimal or hex with 0x prefix)",
35
+ type: "string" as const,
36
+ demandOption: true,
37
+ coerce: (value: string) => parseVidPid(value, "VID"),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Create yargs positional argument configuration for PID
43
+ */
44
+ function pidPositional() {
45
+ return {
46
+ describe: "Product ID (decimal or hex with 0x prefix)",
47
+ type: "string" as const,
48
+ demandOption: true,
49
+ coerce: (value: string) => parseVidPid(value, "PID"),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * DCL (Distributed Compliance Ledger) query commands
55
+ *
56
+ * These commands allow querying the CSA DCL for Matter compliance information,
57
+ * including root certificates, device models, and firmware versions.
58
+ */
59
+ export default function commands() {
60
+ return {
61
+ command: "dcl",
62
+ describe: "Query the CSA Distributed Compliance Ledger (DCL) for Matter compliance information",
63
+ builder: (yargs: Argv) =>
64
+ yargs
65
+ .option("test", {
66
+ alias: "t",
67
+ describe: "Use test DCL network instead of production",
68
+ type: "boolean",
69
+ default: false,
70
+ global: true,
71
+ })
72
+ .command(
73
+ "fetch-root-certificates",
74
+ "Fetch list of all approved Product Attestation Authority (PAA) root certificates",
75
+ yargs => yargs,
76
+ async argv => {
77
+ const { test } = argv;
78
+ const client = new DclClient(!test);
79
+
80
+ try {
81
+ console.log(`Fetching root certificate list from ${getDclEnv(test)} DCL...`);
82
+ const certificates = await client.fetchRootCertificateList();
83
+ console.log(`Found ${certificates.length} root certificates:`);
84
+ console.log(JSON.stringify(certificates, null, 2));
85
+ } catch (error) {
86
+ console.error("Error fetching root certificates:", error);
87
+ }
88
+ },
89
+ )
90
+ .command(
91
+ "fetch-root-certificate <subject> <subjectKeyId>",
92
+ "Fetch detailed information for a specific root certificate",
93
+ yargs => {
94
+ return yargs
95
+ .positional("subject", {
96
+ describe: "Certificate subject (DN)",
97
+ type: "string",
98
+ demandOption: true,
99
+ })
100
+ .positional("subjectKeyId", {
101
+ describe: "Subject key identifier",
102
+ type: "string",
103
+ demandOption: true,
104
+ });
105
+ },
106
+ async argv => {
107
+ const { subject, subjectKeyId, test } = argv;
108
+ const client = new DclClient(!test);
109
+
110
+ try {
111
+ console.log(`Fetching certificate details from ${getDclEnv(test)} DCL...`);
112
+ console.log(`Subject: ${subject}`);
113
+ console.log(`Subject Key ID: ${subjectKeyId}`);
114
+
115
+ const certificate = await client.fetchRootCertificateBySubject({
116
+ subject,
117
+ subjectKeyId,
118
+ });
119
+ console.log("Certificate details:");
120
+ console.log(JSON.stringify(certificate, null, 2));
121
+ } catch (error) {
122
+ console.error("Error fetching certificate:", error);
123
+ }
124
+ },
125
+ )
126
+ .command(
127
+ "fetch-model <vid> <pid>",
128
+ "Fetch device model information by Vendor ID and Product ID",
129
+ yargs => {
130
+ return yargs.positional("vid", vidPositional()).positional("pid", pidPositional());
131
+ },
132
+ async argv => {
133
+ const { vid, pid, test } = argv;
134
+ if (vid === undefined || pid === undefined) {
135
+ throw new Error("VID and PID are required");
136
+ }
137
+ const client = new DclClient(!test);
138
+
139
+ try {
140
+ console.log(`Fetching model information from ${getDclEnv(test)} DCL...`);
141
+ console.log(`VID: ${Diagnostic.hex(vid, 4)}`);
142
+ console.log(`PID: ${Diagnostic.hex(pid, 4)}`);
143
+
144
+ const model = await client.fetchModelByVidPid(vid, pid);
145
+ console.log("Device model information:");
146
+ console.log(JSON.stringify(model, null, 2));
147
+ } catch (error) {
148
+ console.error("Error fetching model:", error);
149
+ }
150
+ },
151
+ )
152
+ .command(
153
+ "fetch-model-versions <vid> <pid>",
154
+ "Fetch available software versions for a device model",
155
+ yargs => {
156
+ return yargs.positional("vid", vidPositional()).positional("pid", pidPositional());
157
+ },
158
+ async argv => {
159
+ const { vid, pid, test } = argv;
160
+ if (vid === undefined || pid === undefined) {
161
+ throw new Error("VID and PID are required");
162
+ }
163
+ const client = new DclClient(!test);
164
+
165
+ try {
166
+ console.log(`Fetching available versions from ${getDclEnv(test)} DCL...`);
167
+ console.log(`VID: ${Diagnostic.hex(vid, 4)}`);
168
+ console.log(`PID: ${Diagnostic.hex(pid, 4)}`);
169
+
170
+ const versions = await client.fetchModelVersionsByVidPid(vid, pid);
171
+ console.log(`Found ${versions.length} software version(s):`);
172
+ console.log(JSON.stringify(versions, null, 2));
173
+ } catch (error) {
174
+ console.error("Error fetching versions:", error);
175
+ }
176
+ },
177
+ )
178
+ .command(
179
+ "fetch-model-version <vid> <pid> <softwareVersion>",
180
+ "Fetch detailed information for a specific software version",
181
+ yargs => {
182
+ return yargs
183
+ .positional("vid", vidPositional())
184
+ .positional("pid", pidPositional())
185
+ .positional("softwareVersion", {
186
+ describe: "Software version number",
187
+ type: "number",
188
+ demandOption: true,
189
+ });
190
+ },
191
+ async argv => {
192
+ const { vid, pid, softwareVersion, test } = argv;
193
+ if (vid === undefined || pid === undefined || softwareVersion === undefined) {
194
+ throw new Error("VID, PID, and software version are required");
195
+ }
196
+ const client = new DclClient(!test);
197
+
198
+ try {
199
+ console.log(`Fetching version details from ${getDclEnv(test)} DCL...`);
200
+ console.log(`VID: ${Diagnostic.hex(vid, 4)}`);
201
+ console.log(`PID: ${Diagnostic.hex(pid, 4)}`);
202
+ console.log(`Software Version: ${softwareVersion}`);
203
+
204
+ const versionInfo = await client.fetchModelVersionByVidPidSoftwareVersion(
205
+ vid,
206
+ pid,
207
+ softwareVersion,
208
+ );
209
+ console.log("Software version information:");
210
+ console.log(JSON.stringify(versionInfo, null, 2));
211
+ } catch (error) {
212
+ console.error("Error fetching version details:", error);
213
+ }
214
+ },
215
+ )
216
+ .demandCommand(1, "You must specify a DCL query command"),
217
+ handler: async (argv: any) => {
218
+ argv.unhandled = true;
219
+ },
220
+ };
221
+ }