From 194eb5154fd42575c49729342af29ee14ff9b8f4 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Thu, 3 Oct 2024 14:25:32 -0700 Subject: [PATCH 01/11] Client node work Bifurcates node stores between the local (ServerNodeStore) and remote (ClientNodeStore) implementations. Fleshes out the "nodes" implementation with wiring for loading client nodes from storage. Adds a dynamic loading mechanism for loading dynamically loading behaviors and endpoint definitions by name. --- .../general/src/storage/StorageContext.ts | 6 +- .../general/src/storage/StorageManager.ts | 4 +- packages/general/src/util/Construction.ts | 2 +- packages/node/package.json | 10 ++ .../internal/ClientBehaviorBacking.ts | 12 +- .../internal/ServerBehaviorBacking.ts | 8 +- .../system/network/ServerNetworkRuntime.ts | 4 +- packages/node/src/build.config.ts | 13 ++ packages/node/src/endpoint/EndpointServer.ts | 12 +- packages/node/src/index.ts | 1 + packages/node/src/loader/index.ts | 7 + packages/node/src/loader/load.cjs | 11 ++ packages/node/src/loader/load.d.ts | 9 ++ packages/node/src/loader/load.mjs | 9 ++ packages/node/src/loader/loader.ts | 79 +++++++++++ packages/node/src/node/ClientNode.ts | 30 ++-- packages/node/src/node/Nodes.ts | 26 +++- packages/node/src/node/ServerNode.ts | 14 +- .../node/client/ClientEndpointInitializer.ts | 11 +- packages/node/src/node/paths/AttributePath.ts | 5 +- .../node/server/ServerEndpointInitializer.ts | 6 +- .../node/src/node/storage/ClientNodeStore.ts | 77 ++++++++++ .../src/node/storage/EndpointStoreService.ts | 15 +- packages/node/src/node/storage/NodeStore.ts | 112 +++------------ .../node/src/node/storage/ServerNodeStore.ts | 132 ++++++++++++++++++ packages/node/src/node/storage/index.ts | 2 + packages/node/src/tsconfig.json | 1 + packages/node/test/loader/loaderTest.ts | 29 ++++ packages/node/test/node/mock-node.ts | 8 +- packages/protocol/src/peer/OperationalPeer.ts | 1 - 30 files changed, 496 insertions(+), 160 deletions(-) create mode 100644 packages/node/src/build.config.ts create mode 100644 packages/node/src/loader/index.ts create mode 100644 packages/node/src/loader/load.cjs create mode 100644 packages/node/src/loader/load.d.ts create mode 100644 packages/node/src/loader/load.mjs create mode 100644 packages/node/src/loader/loader.ts create mode 100644 packages/node/src/node/storage/ClientNodeStore.ts create mode 100644 packages/node/src/node/storage/ServerNodeStore.ts create mode 100644 packages/node/test/loader/loaderTest.ts diff --git a/packages/general/src/storage/StorageContext.ts b/packages/general/src/storage/StorageContext.ts index df1664e322..e6af550748 100644 --- a/packages/general/src/storage/StorageContext.ts +++ b/packages/general/src/storage/StorageContext.ts @@ -8,7 +8,11 @@ import { MaybePromise } from "../util/Promises.js"; import { Storage, StorageError, StorageOperationResult } from "./Storage.js"; import { SupportedStorageTypes } from "./StringifyTools.js"; -export class StorageContext { +export interface StorageContextFactory { + createContext(context: string): StorageContext; +} + +export class StorageContext implements StorageContextFactory { constructor( private readonly storage: S, readonly thisContexts: string[], diff --git a/packages/general/src/storage/StorageManager.ts b/packages/general/src/storage/StorageManager.ts index 2e2903e6e5..c88bb38792 100644 --- a/packages/general/src/storage/StorageManager.ts +++ b/packages/general/src/storage/StorageManager.ts @@ -6,9 +6,9 @@ import { MaybePromise } from "../util/Promises.js"; import { Storage, StorageError } from "./Storage.js"; -import { StorageContext } from "./StorageContext.js"; +import { StorageContext, StorageContextFactory } from "./StorageContext.js"; -export class StorageManager { +export class StorageManager implements StorageContextFactory { private initialized = false; constructor(private storage: S) {} diff --git a/packages/general/src/util/Construction.ts b/packages/general/src/util/Construction.ts index 880dc3fc4b..b538f058ed 100644 --- a/packages/general/src/util/Construction.ts +++ b/packages/general/src/util/Construction.ts @@ -56,7 +56,7 @@ export interface Constructable { readonly construction: Construction; } -export module Constructable { +export namespace Constructable { /** * An {@link Constructable} that supports deferred construction. * diff --git a/packages/node/package.json b/packages/node/package.json index feb1c31c5c..174ba35205 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -71,6 +71,16 @@ "default": "./dist/cjs/index.js" } }, + "./load": { + "import": { + "types": "./dist/esm/loader/load.d.ts", + "default": "./dist/esm/loader/load.mjs" + }, + "require": { + "types": "./dist/cjs/loader/load.d.ts", + "default": "./dist/cjs/loader/load.cjs" + } + }, "./behaviors": { "import": { "types": "./dist/esm/behaviors/index.d.ts", diff --git a/packages/node/src/behavior/internal/ClientBehaviorBacking.ts b/packages/node/src/behavior/internal/ClientBehaviorBacking.ts index 8c8df69198..73b241f4e2 100644 --- a/packages/node/src/behavior/internal/ClientBehaviorBacking.ts +++ b/packages/node/src/behavior/internal/ClientBehaviorBacking.ts @@ -4,13 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Behavior } from "#behavior/Behavior.js"; import { Datasource } from "#behavior/state/managed/Datasource.js"; +import { Endpoint } from "#endpoint/Endpoint.js"; +import { EndpointStore } from "#endpoint/index.js"; import { BehaviorBacking } from "./BehaviorBacking.js"; /** * This class backs the client implementation of a behavior. */ export class ClientBehaviorBacking extends BehaviorBacking { - // TODO - protected override store?: Datasource.Store | undefined; + protected override store: Datasource.Store | undefined; + + constructor(endpoint: Endpoint, behavior: Behavior.Type, endpointStore: EndpointStore) { + super(endpoint, behavior); + + this.store = endpointStore.storeForBehavior(behavior.id); + } } diff --git a/packages/node/src/behavior/internal/ServerBehaviorBacking.ts b/packages/node/src/behavior/internal/ServerBehaviorBacking.ts index 8ca093adc7..47d67ee45a 100644 --- a/packages/node/src/behavior/internal/ServerBehaviorBacking.ts +++ b/packages/node/src/behavior/internal/ServerBehaviorBacking.ts @@ -6,7 +6,7 @@ import { camelize } from "#general"; import { FieldValue } from "#model"; -import { NodeStore } from "#node/storage/NodeStore.js"; +import { ServerNodeStore } from "#node/storage/ServerNodeStore.js"; import { EventHandler } from "#protocol"; import { Behavior } from "../Behavior.js"; import { Val } from "../state/Val.js"; @@ -22,7 +22,9 @@ export class ServerBehaviorBacking extends BehaviorBacking { override get store() { if (!this.#store) { - this.#store = this.#serverStore.endpointStores.storeForPart(this.endpoint).storeForBehavior(this.type.id); + this.#store = this.#serverStore.endpointStores + .storeForEndpoint(this.endpoint) + .storeForBehavior(this.type.id); } return this.#store; } @@ -53,7 +55,7 @@ export class ServerBehaviorBacking extends BehaviorBacking { } get #serverStore() { - return this.endpoint.env.get(NodeStore); + return this.endpoint.env.get(ServerNodeStore); } /** diff --git a/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts b/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts index febb48de97..244513fa21 100644 --- a/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts +++ b/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts @@ -15,8 +15,8 @@ import { UdpInterface, } from "#general"; import { ServerNode } from "#node/ServerNode.js"; +import { ServerNodeStore } from "#node/index.js"; import { TransactionalInteractionServer } from "#node/server/TransactionalInteractionServer.js"; -import { NodeStore } from "#node/storage/NodeStore.js"; import { Ble, DeviceAdvertiser, @@ -269,7 +269,7 @@ export class ServerNetworkRuntime extends NetworkRuntime { await this.owner.act("start-network", agent => agent.load(ProductDescriptionServer)); - const { sessionStorage, fabricStorage } = this.owner.env.get(NodeStore); + const { sessionStorage, fabricStorage } = this.owner.env.get(ServerNodeStore); // TODO - convert to using components directly, not via MatterDevice const matterDevice = await MatterDevice.create( diff --git a/packages/node/src/build.config.ts b/packages/node/src/build.config.ts new file mode 100644 index 0000000000..f2b00ce0f0 --- /dev/null +++ b/packages/node/src/build.config.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Project } from "@matter.js/tools"; + +export async function before({ project }: Project.Context) { + // We must load "load.cjs" or "load.mjs" via self reference to conditional exports, but typescript won't find the + // type definition this way unless it's in the dist location. So copy it there prior to build + project.copyToDist("src/loader/load.d.ts", "loader/load.d.ts"); +} diff --git a/packages/node/src/endpoint/EndpointServer.ts b/packages/node/src/endpoint/EndpointServer.ts index 2a7f78d585..56cac56334 100644 --- a/packages/node/src/endpoint/EndpointServer.ts +++ b/packages/node/src/endpoint/EndpointServer.ts @@ -15,7 +15,7 @@ import { ClusterId, ClusterType, EndpointNumber } from "#types"; import { Endpoint } from "./Endpoint.js"; const SERVER = Symbol("server"); -interface ServerPart extends Endpoint { +interface ServerEndpoint extends Endpoint { [SERVER]?: EndpointServer; } @@ -36,7 +36,7 @@ export class EndpointServer implements EndpointInterface { } constructor(endpoint: Endpoint) { - (endpoint as ServerPart)[SERVER] = this; + (endpoint as ServerEndpoint)[SERVER] = this; this.#endpoint = endpoint; this.#name = endpoint.type.name; } @@ -105,9 +105,9 @@ export class EndpointServer implements EndpointInterface { async [Symbol.asyncDispose]() { // I believe the cluster servers are effectively disposed when the structure is emptied this.#clusterServers.clear(); - delete (this.#endpoint as ServerPart)[SERVER]; + delete (this.#endpoint as ServerEndpoint)[SERVER]; for (const endpoint of this.#endpoint.parts) { - const server = (endpoint as ServerPart)[SERVER]; + const server = (endpoint as ServerEndpoint)[SERVER]; if (server) { await server[Symbol.asyncDispose](); } @@ -177,9 +177,9 @@ export class EndpointServer implements EndpointInterface { * Retrieve the server for an endpoint. */ static forEndpoint(endpoint: Endpoint) { - let server = (endpoint as ServerPart)[SERVER]; + let server = (endpoint as ServerEndpoint)[SERVER]; if (!server) { - server = (endpoint as ServerPart)[SERVER] = new EndpointServer(endpoint); + server = (endpoint as ServerEndpoint)[SERVER] = new EndpointServer(endpoint); } return server; } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index f5b6b8f609..6dd0cb8e84 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -9,5 +9,6 @@ export * from "./behavior/index.js"; export * from "./endpoint/index.js"; +export * from "./loader/index.js"; export * from "./node/index.js"; export * from "./tags/index.js"; diff --git a/packages/node/src/loader/index.ts b/packages/node/src/loader/index.ts new file mode 100644 index 0000000000..7647883a4d --- /dev/null +++ b/packages/node/src/loader/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./loader.js"; diff --git a/packages/node/src/loader/load.cjs b/packages/node/src/loader/load.cjs new file mode 100644 index 0000000000..2a115d0bf6 --- /dev/null +++ b/packages/node/src/loader/load.cjs @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = { + load(path) { + return require(path); + }, +}; diff --git a/packages/node/src/loader/load.d.ts b/packages/node/src/loader/load.d.ts new file mode 100644 index 0000000000..f4885a131f --- /dev/null +++ b/packages/node/src/loader/load.d.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { MaybePromise } from "#general"; + +export declare function load(path: string): MaybePromise<{}>; diff --git a/packages/node/src/loader/load.mjs b/packages/node/src/loader/load.mjs new file mode 100644 index 0000000000..c4273c0814 --- /dev/null +++ b/packages/node/src/loader/load.mjs @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function load(path) { + return import(path); +} diff --git a/packages/node/src/loader/loader.ts b/packages/node/src/loader/loader.ts new file mode 100644 index 0000000000..7d6a7c14ce --- /dev/null +++ b/packages/node/src/loader/loader.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Behavior } from "#behavior/Behavior.js"; +import { EndpointType } from "#endpoint/type/EndpointType.js"; +import { camelize, decamelize, ImplementationError, MaybePromise } from "#general"; + +// Must load from public export so node selects the correct format +import { load } from "@matter.js/node/load"; + +const cache = {} as Record; + +/** + * Dynamically load API components. + * + * We generate a huge amount of code, both statically and at runtime. You can use this namespace to load components + * from this package dynamically regardless of module format (CJS or ESM). + * + * If you are not targeting both formats then either require or a dynamic import may make more sense. + */ +export namespace loader { + export function behavior(name: string) { + const exportName = `${camelize(name, true)}Behavior`; + return doLoad( + `behavior ${name}`, + exportName, + `../behaviors/${decamelize(name)}/${exportName}.js`, + ) as MaybePromise; + } + + export function device(name: string) { + const exportName = `${camelize(name, true)}Device`; + return doLoad( + `device type ${name}`, + exportName, + `../devices/${decamelize(name)}.js`, + ) as MaybePromise; + } + + export function endpoint(name: string) { + const exportName = `${camelize(name, true)}Endpoint`; + return doLoad( + `endpoint type ${name}`, + exportName, + `../endpoints/${decamelize(name)}.js`, + ) as MaybePromise; + } +} + +function doLoad(description: string, exportName: string, path: string) { + const cachedResult = cache[path]; + if (cachedResult) { + return cachedResult; + } + + function extractExport(module: {}) { + if (exportName in module) { + return (module as Record)[exportName]; + } + failure(); + } + + try { + const module = load(path); + if (MaybePromise.is(module)) { + return module.then(extractExport, failure); + } + return extractExport(module); + } catch (e) { + failure(); + } + + function failure() { + throw new ImplementationError(`No implementation available for ${description}`); + } +} diff --git a/packages/node/src/node/ClientNode.ts b/packages/node/src/node/ClientNode.ts index c709bd0612..eff4ff5969 100644 --- a/packages/node/src/node/ClientNode.ts +++ b/packages/node/src/node/ClientNode.ts @@ -7,39 +7,34 @@ import { NetworkRuntime } from "#behavior/system/network/NetworkRuntime.js"; import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js"; import { ImplementationError, NotImplementedError } from "#general"; -import { FabricId, NodeId } from "@matter/types"; +import { PeerAddress } from "#protocol"; import { ClientEndpointInitializer } from "./client/ClientEndpointInitializer.js"; import { ClientNodeLifecycle } from "./ClientNodeLifecycle.js"; import { Node } from "./Node.js"; import { ServerNode } from "./ServerNode.js"; +import { ClientNodeStore } from "./storage/ClientNodeStore.js"; /** * A client-side Matter {@link Node}. */ export class ClientNode extends Node { - #fabricId: FabricId; - #nodeId: NodeId; + #address: PeerAddress; - constructor(options: ClientNode.Options) { + constructor({ owner, store }: ClientNode.Options) { + const { address } = store; super({ - id: `${options.fabricId}:${options.nodeId}`, + id: `${address.fabricIndex}:${address.nodeId.toString(16)}`, number: 0, type: Node.CommonRootEndpoint, - owner: options.owner, + owner, }); + this.#address = store.address; - this.#fabricId = options.fabricId; - this.#nodeId = options.nodeId; - - this.env.set(EndpointInitializer, new ClientEndpointInitializer()); - } - - get fabricId() { - return this.#fabricId; + this.env.set(EndpointInitializer, new ClientEndpointInitializer(store)); } - get nodeId() { - return this.#nodeId; + get address() { + return this.#address; } override get lifecycle() { @@ -73,7 +68,6 @@ export class ClientNode extends Node { export namespace ClientNode { export interface Options extends Node.Options { owner: ServerNode; - fabricId: FabricId; - nodeId: NodeId; + store: ClientNodeStore; } } diff --git a/packages/node/src/node/Nodes.ts b/packages/node/src/node/Nodes.ts index 7adae4b26a..e87dd47d1b 100644 --- a/packages/node/src/node/Nodes.ts +++ b/packages/node/src/node/Nodes.ts @@ -5,15 +5,37 @@ */ import { EndpointContainer } from "#endpoint/properties/EndpointContainer.js"; +import { Construction } from "#general"; +import { ClientNode } from "./ClientNode.js"; import { type Node } from "./Node.js"; import { ServerNode } from "./ServerNode.js"; +import { ServerNodeStore } from "./storage/ServerNodeStore.js"; /** * Manages the set of known endpoints that share a fabric with a {@link Node}. */ export class Nodes extends EndpointContainer { - constructor(endpoint: ServerNode) { - super(endpoint); + #construction: Construction; + + get construction() { + return this.#construction; + } + + constructor(owner: ServerNode) { + super(owner); + + this.#construction = Construction(this, async () => { + const stores = await this.endpoint.env.get(ServerNodeStore).allPeerStores(); + for (const store of stores) { + this.add( + // TODO - initialize endpoint correctly from the wire + new ClientNode({ + owner, + store, + }), + ); + } + }); } override get endpoint() { diff --git a/packages/node/src/node/ServerNode.ts b/packages/node/src/node/ServerNode.ts index 6551a7a7a9..15da3d168e 100644 --- a/packages/node/src/node/ServerNode.ts +++ b/packages/node/src/node/ServerNode.ts @@ -19,7 +19,7 @@ import { Node } from "./Node.js"; import { Nodes } from "./Nodes.js"; import { IdentityService } from "./server/IdentityService.js"; import { ServerEndpointInitializer } from "./server/ServerEndpointInitializer.js"; -import { NodeStore } from "./storage/NodeStore.js"; +import { ServerNodeStore } from "./storage/ServerNodeStore.js"; /** * Thrown when there is an error during factory reset. @@ -99,10 +99,10 @@ export class ServerNode(); + + for (const addrStr of addresses) { + const addrComponents = addrStr.match(/^([0-9]+)-([0-9a-f])/i); + if (!addrComponents) { + logger.warn(`Ignoring peer storage context "${addrStr}" due to invalid name format`); + continue; + } + + const fabricIndex = FabricIndex(Number.parseInt(addrComponents[1])); + const nodeId = NodeId(Number.parseInt(addrComponents[2], 16)); + const addr = PeerAddress({ fabricIndex, nodeId }); + + stores.push(new ClientNodeStore(addr, peerStorage.createContext(addrStr))); + } + + return stores; + } + + /** + * Add a store for a new (previously unknown) peer. + */ + static async create(address: PeerAddress, peerStorage: StorageContext) { + const addrStr = `${address.fabricIndex}-${address.nodeId.toString(16)}`; + const store = new ClientNodeStore(address, peerStorage.createContext(addrStr)); + + // Ensure we don't accidentally pick up stale information for the same address + await store.erase(); + } + + get discoveryStorage() { + if (!this.#discoveryStorage) { + this.#discoveryStorage = this.factory.createContext("discovery"); + } + return this.#discoveryStorage; + } + + override async erase() { + await this.discoveryStorage.clearAll(); + await super.erase(); + } +} diff --git a/packages/node/src/node/storage/EndpointStoreService.ts b/packages/node/src/node/storage/EndpointStoreService.ts index d57bd9f9bb..3803f6c0f2 100644 --- a/packages/node/src/node/storage/EndpointStoreService.ts +++ b/packages/node/src/node/storage/EndpointStoreService.ts @@ -39,11 +39,8 @@ export abstract class EndpointStoreService { * Obtain the store for a single {@link Endpoint}. * * These stores are cached internally by ID. - * - * TODO - when StorageContext becomes async we can keep this synchronous if we add "StorageContext.subcontexts" or - * somesuch */ - abstract storeForPart(endpoint: Endpoint): EndpointStore; + abstract storeForEndpoint(endpoint: Endpoint): EndpointStore; } export class EndpointStoreFactory extends EndpointStoreService { @@ -116,12 +113,12 @@ export class EndpointStoreFactory extends EndpointStoreService { this.#construction.assert(); - const store = this.storeForPart(endpoint); + const store = this.storeForEndpoint(endpoint); if (endpoint.lifecycle.hasNumber) { // Reserve number if (this.#allocatedNumbers.has(endpoint.number)) { - if (this.storeForPart(endpoint).number !== endpoint.number) { + if (this.storeForEndpoint(endpoint).number !== endpoint.number) { throw new IdentityConflictError( `Endpoint ${endpoint.id} number ${endpoint.number} is allocated to another endpoint`, ); @@ -160,14 +157,14 @@ export class EndpointStoreFactory extends EndpointStoreService { this.#persistNumber(endpoint); } - storeForPart(endpoint: Endpoint): EndpointStore { + storeForEndpoint(endpoint: Endpoint): EndpointStore { this.#construction.assert(); if (!endpoint.lifecycle.hasId) { throw new InternalError("Endpoint storage access without assigned ID"); } if (endpoint.owner) { - return this.storeForPart(endpoint.owner).childStoreFor(endpoint); + return this.storeForEndpoint(endpoint.owner).childStoreFor(endpoint); } if (endpoint.number !== 0) { throw new InternalError( @@ -203,7 +200,7 @@ export class EndpointStoreFactory extends EndpointStoreService { this.#numbersToPersist = undefined; for (const endpoint of numbersToPersist) { - const store = this.storeForPart(endpoint); + const store = this.storeForEndpoint(endpoint); await store.saveNumber(); } diff --git a/packages/node/src/node/storage/NodeStore.ts b/packages/node/src/node/storage/NodeStore.ts index 1d98f800fc..adc5943071 100644 --- a/packages/node/src/node/storage/NodeStore.ts +++ b/packages/node/src/node/storage/NodeStore.ts @@ -4,35 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - Construction, - Destructable, - Diagnostic, - Environment, - ImplementationError, - Logger, - StorageContext, - StorageManager, - StorageService, - asyncNew, -} from "#general"; -import { EventHandler } from "#protocol"; +import { asyncNew, Construction, ImplementationError, StorageContextFactory } from "#general"; import type { Node } from "../Node.js"; import { EndpointStoreFactory, EndpointStoreService } from "./EndpointStoreService.js"; -const logger = Logger.get("ServerStore"); - /** * Non-volatile state management for a {@link Node}. */ -export class NodeStore implements Destructable { - #location: string; - #nodeId: string; - #storageManager?: StorageManager; - #eventHandler?: EventHandler; - #sessionStorage?: StorageContext; - #fabricStorage?: StorageContext; - #eventStorage?: StorageContext; +export class NodeStore { + #factory: StorageContextFactory; #rootStore?: EndpointStoreFactory; #construction: Construction; @@ -40,80 +20,23 @@ export class NodeStore implements Destructable { return this.#construction; } - /** - * Create a new store. - */ - constructor(environment: Environment, nodeId?: string) { - if (nodeId === undefined) { - throw new ImplementationError("ServerStore must be created with a nodeId"); - } - - const storage = environment.get(StorageService); - this.#location = storage.location ?? "(unknown location)"; - this.#nodeId = nodeId; - - const initializeStorage = async () => { - this.#storageManager = await storage.open(nodeId); - - this.#rootStore = await asyncNew(EndpointStoreFactory, { - storage: this.#storageManager.createContext("root"), - }); - - this.#eventStorage = this.#storage.createContext("events"); - this.#eventHandler = await EventHandler.create(this.eventStorage); - - this.#logChange("Opened"); - }; + constructor(factory: StorageContextFactory) { + this.#factory = factory; + this.#construction = Construction(this); + } - this.#construction = Construction(this, initializeStorage); + toString() { + return "node store"; } - static async create(environment: Environment, nodeId: string) { - return await asyncNew(this, environment, nodeId); + [Construction.construct]() { + return this.initializeStorage(); } async erase() { - await this.#sessionStorage?.clearAll(); - await this.#fabricStorage?.clearAll(); - await this.#eventStorage?.clearAll(); await this.#rootStore?.erase(); } - async close() { - await this.#construction.close(async () => { - await this.#storageManager?.close(); - this.#logChange("Closed"); - }); - } - - get eventStorage() { - if (!this.#eventStorage) { - throw new ImplementationError("Event storage accessed prior to initialization"); - } - return this.#eventStorage; - } - - get eventHandler() { - if (!this.#eventHandler) { - throw new ImplementationError("Event handler accessed prior to initialization"); - } - return this.#eventHandler; - } - - get sessionStorage() { - if (!this.#sessionStorage) { - this.#sessionStorage = this.#storage.createContext("sessions"); - } - return this.#sessionStorage; - } - - get fabricStorage() { - if (!this.#fabricStorage) { - this.#fabricStorage = this.#storage.createContext("fabrics"); - } - return this.#fabricStorage; - } - get endpointStores(): EndpointStoreService { if (this.#rootStore === undefined) { throw new ImplementationError("Endpoint storage accessed prior to initialization"); @@ -121,14 +44,13 @@ export class NodeStore implements Destructable { return this.#rootStore; } - get #storage() { - if (this.#storageManager === undefined) { - throw new ImplementationError("Node storage accessed prior to initialization"); - } - return this.#storageManager; + protected async initializeStorage() { + this.#rootStore = await asyncNew(EndpointStoreFactory, { + storage: this.factory.createContext("root"), + }); } - #logChange(what: "Opened" | "Closed") { - logger.info(what, Diagnostic.strong(this.#nodeId ?? "node"), "storage at", `${this.#location}/${this.#nodeId}`); + protected get factory() { + return this.#factory; } } diff --git a/packages/node/src/node/storage/ServerNodeStore.ts b/packages/node/src/node/storage/ServerNodeStore.ts new file mode 100644 index 0000000000..88106b8792 --- /dev/null +++ b/packages/node/src/node/storage/ServerNodeStore.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + asyncNew, + Destructable, + Diagnostic, + Environment, + ImplementationError, + Logger, + StorageContext, + StorageManager, + StorageService, +} from "#general"; +import { EventHandler, PeerAddress } from "#protocol"; +import { ClientNodeStore } from "./ClientNodeStore.js"; +import { NodeStore } from "./NodeStore.js"; + +const logger = Logger.get("ServerNodeStore"); + +/** + * The "server" node store is a {@link NodeStore} with storage for components shared by the server node and client + * nodes. + */ +export class ServerNodeStore extends NodeStore implements Destructable { + #storageService: StorageService; + #nodeId: string; + #location: string; + #storageManager?: StorageManager; + #sessionStorage?: StorageContext; + #eventHandler?: EventHandler; + #fabricStorage?: StorageContext; + #eventStorage?: StorageContext; + #peerStorage?: StorageContext; + + constructor(environment: Environment, nodeId: string) { + super({ + createContext: (name: string) => { + if (!this.#storageManager) { + throw new ImplementationError( + `Cannot create storage context ${name} because store is not initialized`, + ); + } + return this.#storageManager.createContext(name); + }, + }); + + this.#storageService = environment.get(StorageService); + this.#nodeId = nodeId; + this.#location = this.#storageService.location ?? "(unknown location)"; + + this.construction.start(); + } + + static async create(environment: Environment, nodeId: string) { + return await asyncNew(this, environment, nodeId); + } + + get eventStorage() { + if (!this.#eventStorage) { + this.#eventStorage = this.factory.createContext("events"); + } + return this.#eventStorage; + } + + get eventHandler() { + if (!this.#eventHandler) { + throw new ImplementationError("Event handler accessed prior to initialization"); + } + return this.#eventHandler; + } + + get sessionStorage() { + if (!this.#sessionStorage) { + this.#sessionStorage = this.factory.createContext("sessions"); + } + return this.#sessionStorage; + } + + get fabricStorage() { + if (!this.#fabricStorage) { + this.#fabricStorage = this.factory.createContext("fabrics"); + } + return this.#fabricStorage; + } + + allPeerStores() { + return ClientNodeStore.load(this.#peers); + } + + addPeerStore(address: PeerAddress) { + return ClientNodeStore.create(address, this.#peers); + } + + async close() { + await this.construction.close(async () => { + await this.#storageManager?.close(); + this.#logChange("Closed"); + }); + } + + override async erase() { + await this.#sessionStorage?.clearAll(); + await this.#fabricStorage?.clearAll(); + await this.#eventStorage?.clearAll(); + await super.erase(); + } + + get #peers() { + if (!this.#peerStorage) { + this.#peerStorage = this.factory.createContext("peer"); + } + return this.#peerStorage; + } + + #logChange(what: "Opened" | "Closed") { + logger.info(what, Diagnostic.strong(this.#nodeId ?? "node"), "storage at", `${this.#location}/${this.#nodeId}`); + } + + override async initializeStorage() { + this.#storageManager = await this.#storageService.open(this.#nodeId); + + this.#eventHandler = await EventHandler.create(this.eventStorage); + + await super.initializeStorage(); + + this.#logChange("Opened"); + } +} diff --git a/packages/node/src/node/storage/index.ts b/packages/node/src/node/storage/index.ts index c151f09339..f8039d9d4e 100644 --- a/packages/node/src/node/storage/index.ts +++ b/packages/node/src/node/storage/index.ts @@ -4,5 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +export * from "./ClientNodeStore.js"; export * from "./EndpointStoreService.js"; export * from "./NodeStore.js"; +export * from "./ServerNodeStore.js"; diff --git a/packages/node/src/tsconfig.json b/packages/node/src/tsconfig.json index 93fd05e500..39746c74e0 100644 --- a/packages/node/src/tsconfig.json +++ b/packages/node/src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tools/tsc/tsconfig.lib.json", "compilerOptions": { + "allowJs": true, "types": [ "@matter/tools", "globals" diff --git a/packages/node/test/loader/loaderTest.ts b/packages/node/test/loader/loaderTest.ts new file mode 100644 index 0000000000..3e3f10c656 --- /dev/null +++ b/packages/node/test/loader/loaderTest.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { loader } from "@matter.js/node"; + +if (typeof window === "undefined") { + describe("loader", () => { + it("loads behaviors", async () => { + const OnOffBehavior = await loader.behavior("on-off"); + expect(typeof OnOffBehavior).equals("function"); + expect(OnOffBehavior.name).equals("OnOffBehavior"); + }); + + it("loads devices", async () => { + const OnOffLightDevice = await loader.device("on-off-light"); + expect(typeof OnOffLightDevice).equals("object"); + expect(OnOffLightDevice.name).equals("OnOffLight"); + }); + + it("loads endpoints", async () => { + const BridgedNodeEndpoint = await loader.endpoint("BridgedNode"); + expect(typeof BridgedNodeEndpoint).equals("object"); + expect(BridgedNodeEndpoint.name).equals("BridgedNode"); + }); + }); +} diff --git a/packages/node/test/node/mock-node.ts b/packages/node/test/node/mock-node.ts index 85f6263e17..33d9213f70 100644 --- a/packages/node/test/node/mock-node.ts +++ b/packages/node/test/node/mock-node.ts @@ -16,7 +16,7 @@ import { Node } from "#node/Node.js"; import { ServerNode } from "#node/ServerNode.js"; import { IdentityService } from "#node/server/IdentityService.js"; import { EndpointStoreService } from "#node/storage/EndpointStoreService.js"; -import { NodeStore } from "#node/storage/NodeStore.js"; +import { ServerNodeStore } from "#node/storage/ServerNodeStore.js"; import { EndpointNumber } from "#types"; export class MockPartInitializer extends EndpointInitializer { @@ -53,7 +53,7 @@ class MockEndpointStoreService extends EndpointStoreService { endpoint.number = this.#nextNumber++; } - override storeForPart(endpoint: Endpoint): EndpointStore { + override storeForEndpoint(endpoint: Endpoint): EndpointStore { if (this.#stores[endpoint.number]) { return this.#stores[endpoint.number]; } @@ -62,7 +62,7 @@ class MockEndpointStoreService extends EndpointStoreService { } } -export class MockServerStore extends NodeStore { +export class MockServerStore extends ServerNodeStore { #endpointStores?: MockEndpointStoreService; override get endpointStores() { @@ -93,7 +93,7 @@ export class MockNode new StorageBackendMemory())); this.env.set(EndpointInitializer, new MockPartInitializer()); - this.env.set(NodeStore, new MockServerStore(this.env, "test")); + this.env.set(ServerNodeStore, new MockServerStore(this.env, "test")); this.env.set(EndpointStoreService, new MockEndpointStoreService()); this.env.set(IdentityService, new IdentityService(this)); return super.initialize(); diff --git a/packages/protocol/src/peer/OperationalPeer.ts b/packages/protocol/src/peer/OperationalPeer.ts index 843e243eb5..a2ea699e59 100644 --- a/packages/protocol/src/peer/OperationalPeer.ts +++ b/packages/protocol/src/peer/OperationalPeer.ts @@ -13,7 +13,6 @@ import { PeerAddress } from "./PeerAddress.js"; * * For our purposes a "peer" is another node commissioned to a fabric to which we have access. */ - export interface OperationalPeer { /** * The logical address of the peer. From 26e197cb4ef658548efc6503098e956b1f582815 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Fri, 4 Oct 2024 00:28:27 -0700 Subject: [PATCH 02/11] Certificate related refactoring Rename "RootCertificateManager" to "CertificateAuthority" to better describe. Refactor Fabric a bit to bring inline with newer components. --- .../matter.js/src/CommissioningController.ts | 9 +- packages/matter.js/src/MatterController.ts | 41 +++-- packages/matter.js/src/PaseCommissioner.ts | 16 +- packages/matter.js/src/compat/certificate.ts | 2 +- packages/matter.js/src/compat/fabric.ts | 5 +- .../test/cluster/AttributeServerTest.ts | 36 ++-- packages/node/src/node/Nodes.ts | 3 +- packages/nodejs/test/IntegrationTest.ts | 4 +- .../test/cluster/ClusterServerTestingUtil.ts | 24 +-- .../nodejs/test/cluster/GroupsServerTest.ts | 26 +-- .../nodejs/test/fabric/FabricManagerTest.ts | 6 +- packages/nodejs/test/fabric/FabricTest.ts | 64 +++---- .../test/interaction/InteractionTestUtils.ts | 32 ++-- ...cateManager.ts => CertificateAuthority.ts} | 31 ++-- packages/protocol/src/certificate/index.ts | 2 +- packages/protocol/src/fabric/Fabric.ts | 168 +++++++++--------- packages/protocol/src/fabric/FabricManager.ts | 11 +- .../src/peer/ControllerCommissioner.ts | 11 +- .../src/peer/ControllerCommissioningFlow.ts | 6 +- 19 files changed, 247 insertions(+), 250 deletions(-) rename packages/protocol/src/certificate/{RootCertificateManager.ts => CertificateAuthority.ts} (87%) diff --git a/packages/matter.js/src/CommissioningController.ts b/packages/matter.js/src/CommissioningController.ts index bc8578d3ed..2f6b851956 100644 --- a/packages/matter.js/src/CommissioningController.ts +++ b/packages/matter.js/src/CommissioningController.ts @@ -149,13 +149,14 @@ export class CommissioningController extends MatterNode { return this.controllerInstance?.nodeId; } - get paseCommissionerData() { + get paseCommissionerConfig() { const controller = this.assertControllerIsStarted( "The CommissioningController needs to be started to get the PASE commissioner data.", ); + const { caConfig, fabricConfig: fabricData } = controller; return { - rootCertificateData: controller.rootCertificateData, - fabricData: controller.fabricData, + caConfig, + fabricData, }; } @@ -213,7 +214,7 @@ export class CommissioningController extends MatterNode { return await MatterController.create({ sessionStorage, - rootCertificateStorage, + caStorage: rootCertificateStorage, fabricStorage, nodesStorage, scanners, diff --git a/packages/matter.js/src/MatterController.ts b/packages/matter.js/src/MatterController.ts index f7b8b8e05d..6c391cfa93 100644 --- a/packages/matter.js/src/MatterController.ts +++ b/packages/matter.js/src/MatterController.ts @@ -28,6 +28,7 @@ import { StorageManager, } from "#general"; import { + CertificateAuthority, ChannelManager, ClusterClient, CommissioningError, @@ -37,7 +38,6 @@ import { ExchangeManager, Fabric, FabricBuilder, - FabricJsonObject, FabricManager, NodeDiscoveryType, OperationalPeer, @@ -46,7 +46,6 @@ import { PeerStore, ResumptionRecord, RetransmissionLimitReachedError, - RootCertificateManager, ScannerSet, SessionManager, StatusReportOnlySecureChannelProtocol, @@ -86,7 +85,7 @@ type StoredOperationalPeer = [NodeId, CommissionedNodeDetails]; export class MatterController { public static async create(options: { sessionStorage: StorageContext; - rootCertificateStorage: StorageContext; + caStorage: StorageContext; fabricStorage: StorageContext; nodesStorage: StorageContext; scanners: ScannerSet; @@ -99,7 +98,7 @@ export class MatterController { }): Promise { const { sessionStorage, - rootCertificateStorage, + caStorage, fabricStorage, nodesStorage, scanners, @@ -111,12 +110,12 @@ export class MatterController { caseAuthenticatedTags, } = options; - const certificateManager = await RootCertificateManager.create(rootCertificateStorage); + const certificateManager = await CertificateAuthority.create(caStorage); let controller: MatterController; // Check if we have a fabric stored in the storage, if yes initialize this one, else build a new one if (await fabricStorage.has("fabric")) { - const fabric = Fabric.createFromStorageObject(await fabricStorage.get("fabric")); + const fabric = new Fabric(await fabricStorage.get("fabric")); controller = new MatterController({ sessionStorage, fabricStorage, @@ -161,13 +160,13 @@ export class MatterController { } public static async createAsPaseCommissioner(options: { - rootCertificateData: RootCertificateManager.Data; - fabricData: FabricJsonObject; + certificateAuthorityConfig: CertificateAuthority.Configuration; + fabricConfig: Fabric.Config; scanners: ScannerSet; netInterfaces: NetInterfaceSet; sessionClosedCallback?: (peerNodeId: NodeId) => void; }): Promise { - const { rootCertificateData, fabricData, scanners, netInterfaces, sessionClosedCallback } = options; + const { certificateAuthorityConfig, fabricConfig, scanners, netInterfaces, sessionClosedCallback } = options; // Verify an appropriate network interface is available if (!netInterfaces.hasInterfaceFor(ChannelType.BLE)) { @@ -179,7 +178,7 @@ export class MatterController { logger.info("BLE is not enabled. Using only IP network for commissioning."); } - const certificateManager = await RootCertificateManager.create(rootCertificateData); + const certificateManager = await CertificateAuthority.create(certificateAuthorityConfig); // Stored data are temporary anyway and no node will be connected, so just use an in-memory storage const storageManager = new StorageManager(new StorageBackendMemory()); @@ -187,7 +186,7 @@ export class MatterController { const sessionStorage = storageManager.createContext("sessions"); const nodesStorage = storageManager.createContext("nodes"); - const fabric = Fabric.createFromStorageObject(fabricData); + const fabric = new Fabric(fabricConfig); // Check if we have a fabric stored in the storage, if yes initialize this one, else build a new one const controller = new MatterController({ sessionStorage, @@ -214,7 +213,7 @@ export class MatterController { readonly fabricStorage?: StorageContext; readonly nodesStore: CommissionedNodeStore; private readonly scanners: ScannerSet; - private readonly certificateManager: RootCertificateManager; + private readonly ca: CertificateAuthority; private readonly fabric: Fabric; private readonly sessionClosedCallback?: (peerNodeId: NodeId) => void; @@ -228,7 +227,7 @@ export class MatterController { nodesStorage: StorageContext; scanners: ScannerSet; netInterfaces: NetInterfaceSet; - certificateManager: RootCertificateManager; + certificateManager: CertificateAuthority; fabric: Fabric; sessionClosedCallback?: (peerNodeId: NodeId) => void; }) { @@ -246,7 +245,7 @@ export class MatterController { this.fabricStorage = fabricStorage; this.scanners = scanners; this.netInterfaces = netInterfaces; - this.certificateManager = certificateManager; + this.ca = certificateManager; this.fabric = fabric; this.sessionClosedCallback = sessionClosedCallback; @@ -289,7 +288,7 @@ export class MatterController { netInterfaces: this.netInterfaces, exchanges: this.exchangeManager, sessions: this.sessionManager, - certificates: this.certificateManager, + authority: this.ca, }); this.#construction = Construction(this, async () => { @@ -302,12 +301,12 @@ export class MatterController { return this.fabric.rootNodeId; } - get rootCertificateData() { - return this.certificateManager.data; + get caConfig() { + return this.ca.config; } - get fabricData() { - return this.fabric.toStorageObject(); + get fabricConfig() { + return this.fabric.config; } getFabrics() { @@ -357,7 +356,7 @@ export class MatterController { const address = await this.commissioner.commission(commissioningOptions); - await this.fabricStorage?.set("fabric", this.fabric.toStorageObject()); + await this.fabricStorage?.set("fabric", this.fabric.config); return address.nodeId; } @@ -393,7 +392,7 @@ export class MatterController { await this.peers.delete(this.fabric.addressOf(peerNodeId)); throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`); } - await this.fabricStorage?.set("fabric", this.fabric.toStorageObject()); + await this.fabricStorage?.set("fabric", this.fabric.config); } isCommissioned() { diff --git a/packages/matter.js/src/PaseCommissioner.ts b/packages/matter.js/src/PaseCommissioner.ts index 95e1e10525..bb1f16ecd5 100644 --- a/packages/matter.js/src/PaseCommissioner.ts +++ b/packages/matter.js/src/PaseCommissioner.ts @@ -5,14 +5,14 @@ */ import { Environment, ImplementationError, Logger } from "#general"; import { + CertificateAuthority, CommissionableDevice, CommissionableDeviceIdentifiers, ControllerDiscovery, DiscoveryData, - FabricJsonObject, + Fabric, MdnsScanner, MdnsService, - RootCertificateManager, Scanner, } from "#protocol"; import { DiscoveryCapabilitiesBitmap, NodeId, TypeFromPartialBitSchema } from "#types"; @@ -31,10 +31,10 @@ type PaseCommissionerOptions = Omit { let datasource: ClusterDatasource; function create(options: Partial> = {}) { - testFabric = new Fabric( - FabricIndex(1), - FabricId(BigInt(1)), - NodeId(BigInt(1)), - NodeId(BigInt(2)), - ZERO, - ZERO, - DUMMY_KEY, - VendorId(1), - ZERO, - ZERO, - ZERO, - ZERO, - ZERO, - "", - ); + const ZERO = new Uint8Array(1); + + testFabric = new Fabric({ + fabricIndex: FabricIndex(1), + fabricId: FabricId(1n), + nodeId: NodeId(1n), + rootNodeId: NodeId(2n), + operationalId: ZERO, + keyPair: DUMMY_KEY, + rootPublicKey: DUMMY_KEY.publicKey, + rootVendorId: VendorId(1), + rootCert: ZERO, + identityProtectionKey: ZERO, + operationalIdentityProtectionKey: ZERO, + intermediateCACert: ZERO, + operationalCert: ZERO, + label: "", + }); const config = withDefaults(options, testFabric); datasource = config.datasource; diff --git a/packages/node/src/node/Nodes.ts b/packages/node/src/node/Nodes.ts index e87dd47d1b..cc09032729 100644 --- a/packages/node/src/node/Nodes.ts +++ b/packages/node/src/node/Nodes.ts @@ -28,7 +28,6 @@ export class Nodes extends EndpointContainer { const stores = await this.endpoint.env.get(ServerNodeStore).allPeerStores(); for (const store of stores) { this.add( - // TODO - initialize endpoint correctly from the wire new ClientNode({ owner, store, @@ -41,4 +40,6 @@ export class Nodes extends EndpointContainer { override get endpoint() { return super.endpoint as ServerNode; } + + async commission() {} } diff --git a/packages/nodejs/test/IntegrationTest.ts b/packages/nodejs/test/IntegrationTest.ts index 8fca1cdeab..56e27de675 100644 --- a/packages/nodejs/test/IntegrationTest.ts +++ b/packages/nodejs/test/IntegrationTest.ts @@ -1185,10 +1185,10 @@ describe("Integration Test", () => { assert.equal(firstFabric.fabricIndex, 1); assert.equal(firstFabric.fabricId, 1); - const groupsClusterEndpointMap = firstFabric.scopedClusterData.get(Groups.Cluster.id); + const groupsClusterEndpointMap = firstFabric.scopedClusterData?.get(Groups.Cluster.id); assert.ok(groupsClusterEndpointMap); assert.equal(groupsClusterEndpointMap.size, 1); - const groupsClusterData = groupsClusterEndpointMap.get("1"); + const groupsClusterData = groupsClusterEndpointMap?.get("1"); assert.ok(groupsClusterData instanceof Map); assert.equal(groupsClusterData.get(1), "Group 1"); diff --git a/packages/nodejs/test/cluster/ClusterServerTestingUtil.ts b/packages/nodejs/test/cluster/ClusterServerTestingUtil.ts index a5fb5c5b72..09490429da 100644 --- a/packages/nodejs/test/cluster/ClusterServerTestingUtil.ts +++ b/packages/nodejs/test/cluster/ClusterServerTestingUtil.ts @@ -5,10 +5,11 @@ */ import { PrivateKey } from "#general"; -import { Fabric, Message, SecureSession } from "#protocol"; -import { FabricId, FabricIndex, NodeId, StatusCode, VendorId } from "#types"; +import { Message, SecureSession } from "#protocol"; +import { NodeId, StatusCode } from "#types"; import { asClusterServerInternal, ClusterServerObj, ClusterType } from "@project-chip/matter.js/cluster"; import { Endpoint } from "@project-chip/matter.js/device"; +import { createTestFabric } from "../interaction/InteractionTestUtils.js"; export const ZERO = new Uint8Array(1); const PRIVATE_KEY = new Uint8Array(32); @@ -36,22 +37,9 @@ export async function callCommandOnClusterServer( } export async function createTestSessionWithFabric() { - const testFabric = new Fabric( - FabricIndex(1), - FabricId(BigInt(1)), - NodeId(BigInt(1)), - NodeId(BigInt(2)), - ZERO, - ZERO, - KEY, - VendorId(1), - ZERO, - ZERO, - ZERO, - ZERO, - ZERO, - "", - ); + const ZERO = new Uint8Array(1); + + const testFabric = createTestFabric(); return await SecureSession.create({ id: 1, diff --git a/packages/nodejs/test/cluster/GroupsServerTest.ts b/packages/nodejs/test/cluster/GroupsServerTest.ts index 1b12abaf05..356488eb9f 100644 --- a/packages/nodejs/test/cluster/GroupsServerTest.ts +++ b/packages/nodejs/test/cluster/GroupsServerTest.ts @@ -5,7 +5,7 @@ */ import { createPromise } from "#general"; -import { Fabric, FabricJsonObject, Message, SecureSession, SessionType } from "#protocol"; +import { Fabric, Message, SecureSession, SessionType } from "#protocol"; import { EndpointNumber, GroupId, StatusCode, ValidationOutOfBoundsError } from "#types"; import { ClusterServer, @@ -57,8 +57,8 @@ describe("Groups Server test", () => { }); it("add new group and verify storage", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, @@ -84,8 +84,8 @@ describe("Groups Server test", () => { }); it("add another new group and verify storage", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, @@ -122,8 +122,8 @@ describe("Groups Server test", () => { }); it("add another new group on other endpoint and verify storage", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, @@ -246,8 +246,8 @@ describe("Groups Server test", () => { }); it("delete group and verify storage", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, @@ -279,8 +279,8 @@ describe("Groups Server test", () => { }); it("delete all groups and verify storage", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, @@ -361,8 +361,8 @@ describe("Groups Server test", () => { }); it("add group while identifying", async () => { - const { promise: firstPromise, resolver: firstResolver } = createPromise(); - testFabric!.persistCallback = () => firstResolver(testFabric!.toStorageObject()); + const { promise: firstPromise, resolver: firstResolver } = createPromise(); + testFabric!.persistCallback = () => firstResolver(testFabric!.config); const result = await callCommandOnClusterServer( groupsServer!, diff --git a/packages/nodejs/test/fabric/FabricManagerTest.ts b/packages/nodejs/test/fabric/FabricManagerTest.ts index 1e77a53bcc..04e5f9a2ad 100644 --- a/packages/nodejs/test/fabric/FabricManagerTest.ts +++ b/packages/nodejs/test/fabric/FabricManagerTest.ts @@ -56,17 +56,17 @@ describe("FabricManager", () => { assert.deepEqual(fabricManager.getFabrics(), [fabric]); - assert.deepEqual(storage.get(["Context"], "fabrics"), [fabric.toStorageObject()]); + assert.deepEqual(storage.get(["Context"], "fabrics"), [fabric.config]); }); it("restore a fabric from storage", async () => { const fabric = await buildFabric(); - storage.set(["Context"], "fabrics", [fabric.toStorageObject()]); + storage.set(["Context"], "fabrics", [fabric.config]); fabricManager = new FabricManager(storageManager.createContext("Context")); await fabricManager.construction.ready; assert.equal(fabricManager.getFabrics().length, 1); - assert.deepEqual(fabricManager.getFabrics()[0].toStorageObject(), fabric.toStorageObject()); + assert.deepEqual(fabricManager.getFabrics()[0].config, fabric.config); }); }); diff --git a/packages/nodejs/test/fabric/FabricTest.ts b/packages/nodejs/test/fabric/FabricTest.ts index 285cb94761..34aad618d9 100644 --- a/packages/nodejs/test/fabric/FabricTest.ts +++ b/packages/nodejs/test/fabric/FabricTest.ts @@ -55,22 +55,22 @@ describe("FabricBuilder", () => { describe("Fabric", () => { describe("getDestinationId", () => { it("generates the correct destination ID", () => { - const fabric = new Fabric( - TEST_FABRIC_INDEX, - TEST_FABRIC_ID, - TEST_NODE_ID, - TEST_ROOT_NODE, - Buffer.alloc(0), - TEST_ROOT_PUBLIC_KEY, - Crypto.createKeyPair(), - VendorId(0), - Buffer.alloc(0), - Buffer.alloc(0), - TEST_IDENTITY_PROTECTION_KEY, - undefined, - Buffer.alloc(0), - "", - ); + const fabric = new Fabric({ + fabricIndex: TEST_FABRIC_INDEX, + fabricId: TEST_FABRIC_ID, + nodeId: TEST_NODE_ID, + rootNodeId: TEST_ROOT_NODE, + operationalId: Buffer.alloc(0), + keyPair: Crypto.createKeyPair(), + rootPublicKey: TEST_ROOT_PUBLIC_KEY, + rootVendorId: VendorId(0), + rootCert: Buffer.alloc(0), + identityProtectionKey: Buffer.alloc(0), + operationalIdentityProtectionKey: TEST_IDENTITY_PROTECTION_KEY, + intermediateCACert: Buffer.alloc(0), + operationalCert: Buffer.alloc(0), + label: "", + }); const result = fabric.getDestinationId(TEST_NODE_ID, TEST_RANDOM); @@ -86,22 +86,22 @@ describe("Fabric", () => { }); it("generates the correct destination ID 3", () => { - const fabric = new Fabric( - TEST_FABRIC_INDEX, - TEST_FABRIC_ID_3, - TEST_NODE_ID_3, - TEST_ROOT_NODE, - Buffer.alloc(0), - TEST_ROOT_PUBLIC_KEY_3, - Crypto.createKeyPair(), - VendorId(0), - Buffer.alloc(0), - Buffer.alloc(0), - TEST_IDENTITY_PROTECTION_KEY_3, - undefined, - Buffer.alloc(0), - "", - ); + const fabric = new Fabric({ + fabricIndex: TEST_FABRIC_INDEX, + fabricId: TEST_FABRIC_ID_3, + nodeId: TEST_NODE_ID_3, + rootNodeId: TEST_ROOT_NODE, + operationalId: Buffer.alloc(0), + keyPair: Crypto.createKeyPair(), + rootPublicKey: TEST_ROOT_PUBLIC_KEY_3, + rootVendorId: VendorId(0), + rootCert: Buffer.alloc(0), + identityProtectionKey: Buffer.alloc(0), + operationalIdentityProtectionKey: TEST_IDENTITY_PROTECTION_KEY_3, + intermediateCACert: Buffer.alloc(0), + operationalCert: Buffer.alloc(0), + label: "", + }); const result = fabric.getDestinationId(TEST_NODE_ID_3, TEST_RANDOM_3); diff --git a/packages/nodejs/test/interaction/InteractionTestUtils.ts b/packages/nodejs/test/interaction/InteractionTestUtils.ts index c894354c30..7416eb9e7c 100644 --- a/packages/nodejs/test/interaction/InteractionTestUtils.ts +++ b/packages/nodejs/test/interaction/InteractionTestUtils.ts @@ -17,22 +17,22 @@ import { FabricId, FabricIndex, NodeId, VendorId } from "#types"; import { KEY } from "../cluster/ClusterServerTestingUtil.js"; export function createTestFabric() { - return new Fabric( - FabricIndex(1), - FabricId(1), - NodeId(BigInt(1)), - NodeId(1), - Bytes.fromHex("00"), - Bytes.fromHex("00"), - KEY, - VendorId(1), - Bytes.fromHex("00"), - Bytes.fromHex("00"), - Bytes.fromHex("00"), - Bytes.fromHex("00"), - Bytes.fromHex("00"), - "", - ); + return new Fabric({ + fabricIndex: FabricIndex(1), + fabricId: FabricId(1n), + nodeId: NodeId(1n), + rootNodeId: NodeId(2n), + operationalId: Bytes.fromHex("00"), + keyPair: KEY, + rootPublicKey: Bytes.fromHex("00"), + rootVendorId: VendorId(1), + rootCert: Bytes.fromHex("00"), + identityProtectionKey: Bytes.fromHex("00"), + operationalIdentityProtectionKey: Bytes.fromHex("00"), + intermediateCACert: Bytes.fromHex("00"), + operationalCert: Bytes.fromHex("00"), + label: "", + }); } class DummyMessageExchange { diff --git a/packages/protocol/src/certificate/RootCertificateManager.ts b/packages/protocol/src/certificate/CertificateAuthority.ts similarity index 87% rename from packages/protocol/src/certificate/RootCertificateManager.ts rename to packages/protocol/src/certificate/CertificateAuthority.ts index 453f618dc3..fe3a09667e 100644 --- a/packages/protocol/src/certificate/RootCertificateManager.ts +++ b/packages/protocol/src/certificate/CertificateAuthority.ts @@ -30,27 +30,30 @@ import { jsToMatterDate, } from "./CertificateManager.js"; -const logger = Logger.get("RootCertificateManager"); +const logger = Logger.get("CertificateAuthority"); -export class RootCertificateManager { +/** + * Manages the root key pair for a fabric owned by a local node. + */ +export class CertificateAuthority { private rootCertId = BigInt(0); private rootKeyPair = Crypto.createKeyPair(); private rootKeyIdentifier = Crypto.hash(this.rootKeyPair.publicKey).slice(0, 20); - private rootCertBytes = this.generateRootCert(); + private rootCertBytes = this.#generateRootCert(); private nextCertificateId = BigInt(1); - #construction: Construction; + #construction: Construction; get construction() { return this.#construction; } - static async create(options: StorageContext | RootCertificateManager.Data) { - return asyncNew(RootCertificateManager, options); + static async create(options: StorageContext | CertificateAuthority.Configuration) { + return asyncNew(CertificateAuthority, options); } - constructor(options: StorageContext | RootCertificateManager.Data) { + constructor(options: StorageContext | CertificateAuthority.Configuration) { this.#construction = Construction(this, async () => { - // Use provided root certificate data or read from storage if we have them stored, else store the just generated data + // Use provided CA config or read from storage, otherwise initialize and store const certValues = options instanceof StorageContext ? await options.values() : options; if ( @@ -85,8 +88,8 @@ export class RootCertificateManager { [Environmental.create](env: Environment) { const storage = env.get(StorageManager).createContext("certificates"); - const instance = new RootCertificateManager(storage); - env.set(RootCertificateManager, instance); + const instance = new CertificateAuthority(storage); + env.set(CertificateAuthority, instance); return instance; } @@ -94,7 +97,7 @@ export class RootCertificateManager { return this.rootCertBytes; } - get data(): RootCertificateManager.Data { + get config(): CertificateAuthority.Configuration { return { rootCertId: this.rootCertId, rootKeyPair: this.rootKeyPair.keyPair, @@ -104,7 +107,7 @@ export class RootCertificateManager { }; } - private generateRootCert() { + #generateRootCert() { const now = Time.get().now(); const unsignedCertificate: Unsigned = { serialNumber: Bytes.fromHex(toHex(this.rootCertId)), @@ -168,8 +171,8 @@ export class RootCertificateManager { } } -export namespace RootCertificateManager { - export type Data = { +export namespace CertificateAuthority { + export type Configuration = { rootCertId: bigint; rootKeyPair: BinaryKeyPair; rootKeyIdentifier: Uint8Array; diff --git a/packages/protocol/src/certificate/index.ts b/packages/protocol/src/certificate/index.ts index 11d05f7cca..4a3efd7ca2 100644 --- a/packages/protocol/src/certificate/index.ts +++ b/packages/protocol/src/certificate/index.ts @@ -5,8 +5,8 @@ */ export * from "./AttestationCertificateManager.js"; +export * from "./CertificateAuthority.js"; export * from "./CertificateManager.js"; export * from "./CertificationDeclarationManager.js"; export * from "./ChipPAAuthorities.js"; export * from "./DeviceCertification.js"; -export * from "./RootCertificateManager.js"; diff --git a/packages/protocol/src/fabric/Fabric.ts b/packages/protocol/src/fabric/Fabric.ts index 99311b7b04..30a303bbb3 100644 --- a/packages/protocol/src/fabric/Fabric.ts +++ b/packages/protocol/src/fabric/Fabric.ts @@ -37,24 +37,6 @@ const GROUP_SECURITY_INFO = Bytes.fromString("GroupKey v1.0"); export class PublicKeyError extends MatterError {} -export type FabricJsonObject = { - fabricIndex: FabricIndex; - fabricId: FabricId; - nodeId: NodeId; - rootNodeId: NodeId; - operationalId: Uint8Array; - rootPublicKey: Uint8Array; - keyPair: BinaryKeyPair; - rootVendorId: VendorId; - rootCert: Uint8Array; - identityProtectionKey: Uint8Array; - operationalIdentityProtectionKey: Uint8Array; - intermediateCACert: Uint8Array | undefined; - operationalCert: Uint8Array; - label: string; - scopedClusterData: Map>; -}; - type OperationalGroupKeySet = TypeFromSchema & { operationalEpochKey0: Uint8Array; groupSessionId0: number | null; @@ -103,36 +85,48 @@ export type ExposedFabricInformation = { }; export class Fabric { - readonly #sessions = new Set(); - readonly #scopedClusterData: Map; - + readonly fabricIndex: FabricIndex; + readonly fabricId: FabricId; + readonly nodeId: NodeId; + readonly rootNodeId: NodeId; + readonly operationalId: Uint8Array; + readonly rootPublicKey: Uint8Array; + readonly rootVendorId: VendorId; + readonly rootCert: Uint8Array; + readonly identityProtectionKey: Uint8Array; + readonly operationalIdentityProtectionKey: Uint8Array; + readonly intermediateCACert: Uint8Array | undefined; + readonly operationalCert: Uint8Array; + + readonly #scopedClusterData: Fabric.ScopedClusterData; readonly #keyPair: Key; + readonly #sessions = new Set(); + + label: string; #removeCallbacks = new Array<() => MaybePromise>(); #persistCallback: ((isUpdate?: boolean) => MaybePromise) | undefined; - constructor( - readonly fabricIndex: FabricIndex, - readonly fabricId: FabricId, - readonly nodeId: NodeId, - readonly rootNodeId: NodeId, - readonly operationalId: Uint8Array, - readonly rootPublicKey: Uint8Array, - keyPair: Key, - readonly rootVendorId: VendorId, - readonly rootCert: Uint8Array, - readonly identityProtectionKey: Uint8Array, - readonly operationalIdentityProtectionKey: Uint8Array, - readonly intermediateCACert: Uint8Array | undefined, - readonly operationalCert: Uint8Array, - public label: string, - scopedClusterData?: Map>, - ) { - this.#keyPair = keyPair; - this.#scopedClusterData = scopedClusterData ?? new Map>(); - } - - toStorageObject(): FabricJsonObject { + constructor(config: Fabric.Config) { + this.fabricIndex = config.fabricIndex; + this.fabricId = config.fabricId; + this.nodeId = config.nodeId; + this.rootNodeId = config.rootNodeId; + this.operationalId = config.operationalId; + this.rootPublicKey = config.rootPublicKey; + this.rootVendorId = config.rootVendorId; + this.rootCert = config.rootCert; + this.identityProtectionKey = config.identityProtectionKey; + this.operationalIdentityProtectionKey = config.operationalIdentityProtectionKey; + this.operationalCert = config.operationalCert; + this.label = config.label; + + this.#keyPair = PrivateKey(config.keyPair); + + this.#scopedClusterData = config.scopedClusterData ?? new Map(); + } + + get config(): Fabric.Config { return { fabricIndex: this.fabricIndex, fabricId: this.fabricId, @@ -152,26 +146,6 @@ export class Fabric { }; } - static createFromStorageObject(fabricObject: FabricJsonObject): Fabric { - return new Fabric( - fabricObject.fabricIndex, - fabricObject.fabricId, - fabricObject.nodeId, - fabricObject.rootNodeId, - fabricObject.operationalId, - fabricObject.rootPublicKey, - PrivateKey(fabricObject.keyPair), - fabricObject.rootVendorId, - fabricObject.rootCert, - fabricObject.identityProtectionKey, - fabricObject.operationalIdentityProtectionKey, - fabricObject.intermediateCACert, - fabricObject.operationalCert, - fabricObject.label, - fabricObject.scopedClusterData, - ); - } - async setLabel(label: string) { this.label = label; await this.persist(); @@ -270,14 +244,14 @@ export class Fabric { if (dataMap === undefined) { return undefined; } - return dataMap.get(clusterDataKey); + return dataMap.get(clusterDataKey) as T; } setScopedClusterDataValue(cluster: Cluster, clusterDataKey: string, value: T) { if (!this.#scopedClusterData.has(cluster.id)) { this.#scopedClusterData.set(cluster.id, new Map()); } - this.#scopedClusterData.get(cluster.id).set(clusterDataKey, value); + this.#scopedClusterData.get(cluster.id)!.set(clusterDataKey, value as SupportedStorageTypes); return this.persist(false); } @@ -285,12 +259,12 @@ export class Fabric { if (!this.#scopedClusterData.has(cluster.id)) { return; } - this.#scopedClusterData.get(cluster.id).delete(clusterDataKey); + this.#scopedClusterData.get(cluster.id)!.delete(clusterDataKey); return this.persist(false); } hasScopedClusterDataValue(cluster: Cluster, clusterDataKey: string) { - return this.#scopedClusterData.has(cluster.id) && this.#scopedClusterData.get(cluster.id).has(clusterDataKey); + return this.#scopedClusterData.has(cluster.id) && this.#scopedClusterData.get(cluster.id)!.has(clusterDataKey); } deleteScopedClusterData(cluster: Cluster) { @@ -302,7 +276,7 @@ export class Fabric { if (!this.#scopedClusterData.has(cluster.id)) { return []; } - return Array.from(this.#scopedClusterData.get(cluster.id).keys()); + return Array.from(this.#scopedClusterData.get(cluster.id)!.keys()); } getGroupKeySet(groupKeySetId: number) { @@ -508,21 +482,47 @@ export class FabricBuilder { 8, ); - return new Fabric( - this.#fabricIndex, - this.#fabricId, - this.#nodeId, - this.#rootNodeId, - operationalId, - this.#rootPublicKey, - this.#keyPair, - this.#rootVendorId, - this.#rootCert, - this.#identityProtectionKey, // Epoch Key - await Crypto.hkdf(this.#identityProtectionKey, operationalId, GROUP_SECURITY_INFO), - this.#intermediateCACert, - this.#operationalCert, - this.#label, - ); + return new Fabric({ + fabricIndex: this.#fabricIndex, + fabricId: this.#fabricId, + nodeId: this.#nodeId, + rootNodeId: this.#rootNodeId, + operationalId: operationalId, + rootPublicKey: this.#rootPublicKey, + keyPair: this.#keyPair, + rootVendorId: this.#rootVendorId, + rootCert: this.#rootCert, + identityProtectionKey: this.#identityProtectionKey, // Epoch Key + operationalIdentityProtectionKey: await Crypto.hkdf( + this.#identityProtectionKey, + operationalId, + GROUP_SECURITY_INFO, + ), + intermediateCACert: this.#intermediateCACert, + operationalCert: this.#operationalCert, + label: this.#label, + }); } } + +export namespace Fabric { + export interface ScopedClusterData extends Map> {} + + export type Config = { + fabricIndex: FabricIndex; + fabricId: FabricId; + nodeId: NodeId; + rootNodeId: NodeId; + operationalId: Uint8Array; + rootPublicKey: Uint8Array; + keyPair: BinaryKeyPair; + rootVendorId: VendorId; + rootCert: Uint8Array; + identityProtectionKey: Uint8Array; + operationalIdentityProtectionKey: Uint8Array; + intermediateCACert: Uint8Array | undefined; + operationalCert: Uint8Array; + label: string; + scopedClusterData?: ScopedClusterData; + }; +} diff --git a/packages/protocol/src/fabric/FabricManager.ts b/packages/protocol/src/fabric/FabricManager.ts index 8686765419..cca1b94128 100644 --- a/packages/protocol/src/fabric/FabricManager.ts +++ b/packages/protocol/src/fabric/FabricManager.ts @@ -21,7 +21,7 @@ import { } from "#general"; import { PeerAddress } from "#peer/PeerAddress.js"; import { FabricIndex } from "#types"; -import { Fabric, FabricJsonObject } from "./Fabric.js"; +import { Fabric } from "./Fabric.js"; /** Specific Error for when a fabric is not found. */ export class FabricNotFoundError extends MatterError {} @@ -32,7 +32,6 @@ export enum FabricAction { Removed, Updated, } - export class FabricManager { #nextFabricIndex = 1; readonly #fabrics = new Map(); @@ -59,9 +58,9 @@ export class FabricManager { return; } - const fabrics = await this.#storage.get("fabrics", []); - for (const fabric of fabrics) { - this.#addFabric(Fabric.createFromStorageObject(fabric)); + const fabrics = await this.#storage.get("fabrics", []); + for (const fabricConfig of fabrics) { + this.#addFabric(new Fabric(fabricConfig)); } this.#nextFabricIndex = await this.#storage.get("nextFabricIndex", this.#nextFabricIndex); @@ -126,7 +125,7 @@ export class FabricManager { const storeResult = this.#storage.set( "fabrics", - Array.from(this.#fabrics.values()).map(fabric => fabric.toStorageObject()), + Array.from(this.#fabrics.values()).map(fabric => fabric.config), ); if (MaybePromise.is(storeResult)) { return storeResult.then(() => this.#storage!.set("nextFabricIndex", this.#nextFabricIndex)); diff --git a/packages/protocol/src/peer/ControllerCommissioner.ts b/packages/protocol/src/peer/ControllerCommissioner.ts index db7a373ca6..49b1e09130 100644 --- a/packages/protocol/src/peer/ControllerCommissioner.ts +++ b/packages/protocol/src/peer/ControllerCommissioner.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RootCertificateManager } from "#certificate/RootCertificateManager.js"; +import { CertificateAuthority } from "#certificate/CertificateAuthority.js"; import { GeneralCommissioning } from "#clusters/general-commissioning"; import { CommissionableDevice, CommissionableDeviceIdentifiers, DiscoveryData, ScannerSet } from "#common/Scanner.js"; import { Fabric } from "#fabric/Fabric.js"; @@ -97,7 +97,7 @@ export interface ControllerCommissionerContext { netInterfaces: NetInterfaceSet; sessions: SessionManager; exchanges: ExchangeManager; - certificates: RootCertificateManager; + authority: CertificateAuthority; } /** @@ -119,12 +119,15 @@ export class ControllerCommissioner { netInterfaces: env.get(NetInterfaceSet), sessions: env.get(SessionManager), exchanges: env.get(ExchangeManager), - certificates: env.get(RootCertificateManager), + authority: env.get(CertificateAuthority), }); env.set(ControllerCommissioner, instance); return instance; } + /** + * Commission a node. + */ async commission(options: PeerCommissioningOptions): Promise { const { discovery: { timeoutSeconds = 30 }, @@ -330,7 +333,7 @@ export class ControllerCommissioner { const commissioningManager = new ControllerCommissioningFlow( // Use the created secure session to do the commissioning new InteractionClient(new ExchangeProvider(this.#context.exchanges, paseSecureMessageChannel), address), - this.#context.certificates, + this.#context.authority, fabric, commissioningOptions, async address => { diff --git a/packages/protocol/src/peer/ControllerCommissioningFlow.ts b/packages/protocol/src/peer/ControllerCommissioningFlow.ts index 791991ce86..f708621dc0 100644 --- a/packages/protocol/src/peer/ControllerCommissioningFlow.ts +++ b/packages/protocol/src/peer/ControllerCommissioningFlow.ts @@ -20,8 +20,8 @@ import { TypeFromSchema, VendorId, } from "#types"; +import { CertificateAuthority } from "../certificate/CertificateAuthority.js"; import { CertificateManager } from "../certificate/CertificateManager.js"; -import { RootCertificateManager } from "../certificate/RootCertificateManager.js"; import { ClusterClient } from "../cluster/client/ClusterClient.js"; import { ClusterClientObj } from "../cluster/client/ClusterClientTypes.js"; import { TlvCertSigningRequest } from "../common/OperationalCredentialsTypes.js"; @@ -138,7 +138,7 @@ const DEFAULT_FAILSAFE_TIME_MS = 60_000; // 60 seconds */ export class ControllerCommissioningFlow { #interactionClient: InteractionClient; - readonly #certificateManager: RootCertificateManager; + readonly #certificateManager: CertificateAuthority; readonly #fabric: Fabric; readonly #transitionToCase: (peerAddress: PeerAddress) => Promise; readonly #commissioningOptions: ControllingCommissioningFlowOptions; @@ -157,7 +157,7 @@ export class ControllerCommissioningFlow { interactionClient: InteractionClient, /** CertificateManager of the controller. */ - certificateManager: RootCertificateManager, + certificateManager: CertificateAuthority, /** Fabric of the controller. */ fabric: Fabric, From 028dab89347e15bd12fb5ade41649f38c5743737 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Fri, 4 Oct 2024 14:04:55 -0700 Subject: [PATCH 03/11] Remove MatterDevice from ServerNode Simplifies ServerNetworkRuntime and better supports sharing of components between ServerNode and ClientNode. --- .../general/src/environment/Environment.ts | 29 ++- packages/general/src/net/NetInterface.ts | 4 +- .../general/src/net/TransportInterface.ts | 8 + packages/matter.js/src/CommissioningServer.ts | 2 +- packages/matter.js/src/MatterController.ts | 2 +- .../src/MatterDevice.ts | 33 +-- .../src/cluster/server/AccessControlServer.ts | 2 +- .../AdministratorCommissioningServer.ts | 3 +- .../server/GeneralCommissioningServer.ts | 3 +- .../server/OperationalCredentialsServer.ts | 2 +- packages/matter.js/src/export.ts | 4 +- packages/matter.js/tsconfig.json | 2 +- .../behavior/internal/ClusterServerBacking.ts | 6 +- .../internal/ServerBehaviorBacking.ts | 9 - .../behavior/system/network/NetworkRuntime.ts | 2 - .../system/network/ServerNetworkRuntime.ts | 213 +++++++----------- .../AdministratorCommissioningServer.ts | 2 +- .../OperationalCredentialsServer.ts | 6 +- packages/node/src/build.config.ts | 2 +- packages/node/src/loader/loader.ts | 2 +- packages/node/src/node/ServerNode.ts | 22 +- .../node/src/node/storage/ServerNodeStore.ts | 52 +---- packages/node/test/loader/loaderTest.ts | 2 +- .../src/certificate/CertificateAuthority.ts | 6 +- packages/protocol/src/common/Scanner.ts | 2 +- packages/protocol/src/fabric/FabricManager.ts | 9 +- packages/protocol/src/index.ts | 1 - .../protocol/src/interaction/EventHandler.ts | 62 +++-- packages/protocol/src/mdns/MdnsService.ts | 4 +- .../src/peer/ControllerCommissioner.ts | 8 +- packages/protocol/src/peer/PeerSet.ts | 2 +- .../protocol/src/protocol/ChannelManager.ts | 4 +- .../protocol/src/protocol/DeviceAdvertiser.ts | 19 +- .../src/protocol/DeviceCommissioner.ts | 2 +- .../protocol/src/protocol/ExchangeManager.ts | 2 +- .../securechannel/SecureChannelProtocol.ts | 10 +- .../protocol/src/session/SessionManager.ts | 24 +- 37 files changed, 294 insertions(+), 273 deletions(-) rename packages/{protocol => matter.js}/src/MatterDevice.ts (91%) diff --git a/packages/general/src/environment/Environment.ts b/packages/general/src/environment/Environment.ts index 783034acfb..3c5225afbd 100644 --- a/packages/general/src/environment/Environment.ts +++ b/packages/general/src/environment/Environment.ts @@ -4,11 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { MaybePromise } from "#util/Promises.js"; import { DiagnosticSource } from "../log/DiagnosticSource.js"; import { Logger } from "../log/Logger.js"; import "../polyfills/disposable.js"; import { Time } from "../time/Time.js"; -import { UnsupportedDependencyError } from "../util/Lifecycle.js"; +import { Destructable, UnsupportedDependencyError } from "../util/Lifecycle.js"; import { Observable } from "../util/Observable.js"; import { Environmental } from "./Environmental.js"; import { RuntimeService } from "./RuntimeService.js"; @@ -53,7 +54,7 @@ export class Environment { * Access an environmental service. */ get(type: abstract new (...args: any[]) => T): T { - let instance = this.#services?.get(type) ?? this.#parent?.get(type); + let instance = this.#services?.get(type) ?? this.#parent?.maybeGet(type); if (instance) { return instance as T; @@ -67,6 +68,15 @@ export class Environment { throw new UnsupportedDependencyError(`Required dependency ${type.name}`, "is not available"); } + /** + * Access an environmental service that may not exist. + */ + maybeGet(type: abstract new (...args: any[]) => T): T | undefined { + if (this.has(type)) { + return this.get(type); + } + } + /** * Remove an environmental service. * @@ -88,6 +98,21 @@ export class Environment { } } + /** + * Remove and close an environmental service. + */ + close( + type: abstract new (...args: any[]) => T, + ): T extends { close: () => MaybePromise } ? MaybePromise : void { + const instance = this.maybeGet(type); + if (instance !== undefined) { + this.delete(type, instance); + return (instance as Partial).close?.() as T extends { close: () => MaybePromise } + ? MaybePromise + : void; + } + } + /** * Access an environmental service, waiting for any async initialization to complete. */ diff --git a/packages/general/src/net/NetInterface.ts b/packages/general/src/net/NetInterface.ts index e4f047e33c..a5b0026eae 100644 --- a/packages/general/src/net/NetInterface.ts +++ b/packages/general/src/net/NetInterface.ts @@ -25,9 +25,9 @@ export function isNetworkInterface(obj: TransportInterface | NetInterface): obj * A collection of {@link NetInterfaces} managed as a unit. */ export class NetInterfaceSet extends TransportInterfaceSet { - [Environmental.create](env: Environment) { + static override [Environmental.create](env: Environment) { const instance = new NetInterfaceSet(); - env.set(NetInterfaceSet, this); + env.set(NetInterfaceSet, instance); return instance; } } diff --git a/packages/general/src/net/TransportInterface.ts b/packages/general/src/net/TransportInterface.ts index 0a92543081..e90851b525 100644 --- a/packages/general/src/net/TransportInterface.ts +++ b/packages/general/src/net/TransportInterface.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Environment } from "#environment/Environment.js"; +import { Environmental } from "#environment/Environmental.js"; import { BasicSet } from "#util/Set.js"; import { Channel, ChannelType } from "./Channel.js"; @@ -31,6 +33,12 @@ export class TransportInterfaceSet { diff --git a/packages/protocol/src/MatterDevice.ts b/packages/matter.js/src/MatterDevice.ts similarity index 91% rename from packages/protocol/src/MatterDevice.ts rename to packages/matter.js/src/MatterDevice.ts index ae7c6a1054..bfc4913b95 100644 --- a/packages/protocol/src/MatterDevice.ts +++ b/packages/matter.js/src/MatterDevice.ts @@ -19,21 +19,26 @@ import { TransportInterfaceSet, asyncNew, } from "#general"; -import { PeerAddress } from "#peer/PeerAddress.js"; -import { DeviceAdvertiser } from "#protocol/DeviceAdvertiser.js"; -import { CommissioningConfigProvider, DeviceCommissioner } from "#protocol/DeviceCommissioner.js"; +import { + ChannelManager, + CommissioningConfigProvider, + DeviceAdvertiser, + DeviceCommissioner, + ExchangeManager, + Fabric, + FabricAction, + FabricManager, + FailsafeContext, + InstanceBroadcaster, + PaseServer, + PeerAddress, + ProtocolHandler, + SecureChannelProtocol, + Session, + SessionManager, + SessionParameters, +} from "#protocol"; import { CommissioningOptions, FabricIndex } from "#types"; -import { FailsafeContext } from "./common/FailsafeContext.js"; -import { InstanceBroadcaster } from "./common/InstanceBroadcaster.js"; -import { Fabric } from "./fabric/Fabric.js"; -import { FabricAction, FabricManager } from "./fabric/FabricManager.js"; -import { ChannelManager } from "./protocol/ChannelManager.js"; -import { ExchangeManager } from "./protocol/ExchangeManager.js"; -import { ProtocolHandler } from "./protocol/ProtocolHandler.js"; -import { SecureChannelProtocol } from "./securechannel/SecureChannelProtocol.js"; -import { Session, SessionParameters } from "./session/Session.js"; -import { SessionManager } from "./session/SessionManager.js"; -import { PaseServer } from "./session/pase/PaseServer.js"; export class MatterDevice { readonly #exchangeManager; diff --git a/packages/matter.js/src/cluster/server/AccessControlServer.ts b/packages/matter.js/src/cluster/server/AccessControlServer.ts index b0235b166e..b96df0fd35 100644 --- a/packages/matter.js/src/cluster/server/AccessControlServer.ts +++ b/packages/matter.js/src/cluster/server/AccessControlServer.ts @@ -6,6 +6,7 @@ import { AccessControl, AccessControlCluster } from "#clusters"; import { InternalError, isDeepEqual, Logger, SyncStorage } from "#general"; +import { MatterDevice } from "#MatterDevice.js"; import { AclExtensionEntry, assertSecureSession, @@ -13,7 +14,6 @@ import { genericFabricScopedAttributeGetter, genericFabricScopedAttributeGetterFromFabric, genericFabricScopedAttributeSetterForFabric, - MatterDevice, } from "#protocol"; import { CaseAuthenticatedTag, diff --git a/packages/matter.js/src/cluster/server/AdministratorCommissioningServer.ts b/packages/matter.js/src/cluster/server/AdministratorCommissioningServer.ts index 006777da23..a0f9a8ea12 100644 --- a/packages/matter.js/src/cluster/server/AdministratorCommissioningServer.ts +++ b/packages/matter.js/src/cluster/server/AdministratorCommissioningServer.ts @@ -6,8 +6,9 @@ import { AdministratorCommissioning } from "#clusters"; import { InternalError, Logger, Time, Timer } from "#general"; +import { MatterDevice } from "#MatterDevice.js"; import { AccessLevel } from "#model"; -import { AttributeServer, MatterDevice, PaseServer, Session } from "#protocol"; +import { AttributeServer, PaseServer, Session } from "#protocol"; import { Command, FabricIndex, diff --git a/packages/matter.js/src/cluster/server/GeneralCommissioningServer.ts b/packages/matter.js/src/cluster/server/GeneralCommissioningServer.ts index 4405574bdc..3d1f73747f 100644 --- a/packages/matter.js/src/cluster/server/GeneralCommissioningServer.ts +++ b/packages/matter.js/src/cluster/server/GeneralCommissioningServer.ts @@ -11,7 +11,8 @@ import { GeneralCommissioningCluster, } from "#clusters"; import { ImplementationError, Logger, MatterFlowError } from "#general"; -import { assertSecureSession, MatterDevice } from "#protocol"; +import { MatterDevice } from "#MatterDevice.js"; +import { assertSecureSession } from "#protocol"; import { ClusterServerHandlers } from "./ClusterServerTypes.js"; import { CommissioningServerFailsafeContext } from "./CommissioningServerFailsafeContext.js"; diff --git a/packages/matter.js/src/cluster/server/OperationalCredentialsServer.ts b/packages/matter.js/src/cluster/server/OperationalCredentialsServer.ts index ab1447112c..aba3908b0a 100644 --- a/packages/matter.js/src/cluster/server/OperationalCredentialsServer.ts +++ b/packages/matter.js/src/cluster/server/OperationalCredentialsServer.ts @@ -8,13 +8,13 @@ import { AccessControl, BasicInformation, OperationalCredentials } from "#clusters"; import { CryptoVerifyError, Logger, MatterFlowError, UnexpectedDataError } from "#general"; +import { MatterDevice } from "#MatterDevice.js"; import { AccessLevel } from "#model"; import { assertSecureSession, CertificateError, DeviceCertification, FabricTableFullError, - MatterDevice, MatterFabricConflictError, MatterFabricInvalidAdminSubjectError, PublicKeyError, diff --git a/packages/matter.js/src/export.ts b/packages/matter.js/src/export.ts index b0843cffd0..fe97e03688 100644 --- a/packages/matter.js/src/export.ts +++ b/packages/matter.js/src/export.ts @@ -6,9 +6,7 @@ export * from "./CommissioningController.js"; export * from "./CommissioningServer.js"; +export * from "./MatterDevice.js"; export * from "./MatterNode.js"; export * from "./MatterServer.js"; export * from "./PaseCommissioner.js"; - -// Compat -export { MatterDevice } from "#protocol"; diff --git a/packages/matter.js/tsconfig.json b/packages/matter.js/tsconfig.json index b3e7fa2976..1a833f6035 100644 --- a/packages/matter.js/tsconfig.json +++ b/packages/matter.js/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { "composite": true }, "files": [], - "references": [{ "path": "src" }, { "path": "test" }, { "path": "build/src" }] + "references": [{ "path": "./src" }, { "path": "./test" }, { "path": "./build/src" }] } diff --git a/packages/node/src/behavior/internal/ClusterServerBacking.ts b/packages/node/src/behavior/internal/ClusterServerBacking.ts index 6818a81fa9..1738e97334 100644 --- a/packages/node/src/behavior/internal/ClusterServerBacking.ts +++ b/packages/node/src/behavior/internal/ClusterServerBacking.ts @@ -13,6 +13,7 @@ import { ClusterServer, CommandServer, createAttributeServer as ConstructAttributeServer, + EventHandler, EventServer, FabricManager, Message, @@ -141,7 +142,6 @@ export class ClusterServerBacking extends ServerBehaviorBacking { * Create the {@link ClusterDatasource} that adapts the Behavior API to the AttributeServer API. */ #createClusterDatasource(): ClusterDatasource { - const eventHandler = this.eventHandler; const datasource = this.datasource; const env = this.endpoint.env; @@ -151,7 +151,7 @@ export class ClusterServerBacking extends ServerBehaviorBacking { }, get eventHandler() { - return eventHandler; + return env.get(EventHandler); }, get fabrics() { @@ -398,7 +398,7 @@ function createEventServer(name: string, definition: Event, backing: C }); server.assignToEndpoint(backing.server); - const promise = server.bindToEventHandler(backing.eventHandler); + const promise = server.bindToEventHandler(backing.endpoint.env.get(EventHandler)); if (MaybePromise.is(promise)) { // Current code structure means this should never happen. Refactor after removal of old API will resolve this throw new InternalError("Event handler binding returned a promise"); diff --git a/packages/node/src/behavior/internal/ServerBehaviorBacking.ts b/packages/node/src/behavior/internal/ServerBehaviorBacking.ts index 47d67ee45a..2f95ab0217 100644 --- a/packages/node/src/behavior/internal/ServerBehaviorBacking.ts +++ b/packages/node/src/behavior/internal/ServerBehaviorBacking.ts @@ -7,7 +7,6 @@ import { camelize } from "#general"; import { FieldValue } from "#model"; import { ServerNodeStore } from "#node/storage/ServerNodeStore.js"; -import { EventHandler } from "#protocol"; import { Behavior } from "../Behavior.js"; import { Val } from "../state/Val.js"; import { Datasource } from "../state/managed/Datasource.js"; @@ -18,7 +17,6 @@ import { BehaviorBacking } from "./BehaviorBacking.js"; */ export class ServerBehaviorBacking extends BehaviorBacking { #store?: Datasource.Store; - #eventHandler?: EventHandler; override get store() { if (!this.#store) { @@ -29,13 +27,6 @@ export class ServerBehaviorBacking extends BehaviorBacking { return this.#store; } - get eventHandler() { - if (!this.#eventHandler) { - this.#eventHandler = this.#serverStore.eventHandler; - } - return this.#eventHandler; - } - protected override invokeInitializer(behavior: Behavior, options?: Behavior.Options) { const finalizeState = () => { this.#applyTransitiveDefaults(behavior.state); diff --git a/packages/node/src/behavior/system/network/NetworkRuntime.ts b/packages/node/src/behavior/system/network/NetworkRuntime.ts index fa396a5202..4240ee43a6 100644 --- a/packages/node/src/behavior/system/network/NetworkRuntime.ts +++ b/packages/node/src/behavior/system/network/NetworkRuntime.ts @@ -56,8 +56,6 @@ export abstract class NetworkRuntime { await this.construction.close(); } - abstract operationalPort: number; - protected abstract start(): Promise; protected abstract stop(): Promise; diff --git a/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts b/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts index 244513fa21..ed84e4d9c1 100644 --- a/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts +++ b/packages/node/src/behavior/system/network/ServerNetworkRuntime.ts @@ -7,30 +7,29 @@ import { ImplementationError, InterfaceType, - InternalError, Network, NetworkInterface, NetworkInterfaceDetailed, + ObserverGroup, TransportInterface, + TransportInterfaceSet, UdpInterface, } from "#general"; import { ServerNode } from "#node/ServerNode.js"; -import { ServerNodeStore } from "#node/index.js"; import { TransactionalInteractionServer } from "#node/server/TransactionalInteractionServer.js"; import { Ble, + ChannelManager, + CommissioningConfigProvider, DeviceAdvertiser, DeviceCommissioner, ExchangeManager, - FabricAction, - FabricManager, InstanceBroadcaster, - MatterDevice, MdnsInstanceBroadcaster, MdnsService, + SecureChannelProtocol, SessionManager, } from "#protocol"; -import { FabricIndex } from "#types"; import { CommissioningBehavior } from "../commissioning/CommissioningBehavior.js"; import { ProductDescriptionServer } from "../product-description/ProductDescriptionServer.js"; import { SessionsBehavior } from "../sessions/SessionsBehavior.js"; @@ -50,12 +49,10 @@ function convertNetworkEnvironmentType(type: string | number) { */ export class ServerNetworkRuntime extends NetworkRuntime { #interactionServer?: TransactionalInteractionServer; - #matterDevice?: MatterDevice; #mdnsBroadcaster?: MdnsInstanceBroadcaster; - #primaryNetInterface?: UdpInterface; #bleBroadcaster?: InstanceBroadcaster; #bleTransport?: TransportInterface; - #commissionedListener?: () => void; + #observers = new ObserverGroup(this); override get owner() { return super.owner as ServerNode; @@ -100,39 +97,11 @@ export class ServerNetworkRuntime extends NetworkRuntime { } openAdvertisementWindow() { - if (!this.#matterDevice) { - throw new InternalError("Server runtime device instance is missing"); - } - - return this.#matterDevice.startAnnouncement(); + return this.owner.env.get(DeviceAdvertiser).startAdvertising(); } advertiseNow() { - if (!this.#matterDevice) { - throw new InternalError("Server runtime device instance is missing"); - } - - // TODO - see comment in startAdvertising - return this.#matterDevice.announce(true); - } - - /** - * The IPv6 {@link UdpInterface}. We create this interface independently of the server so the OS can select a port - * before we are fully online. - */ - protected async getPrimaryNetInterface() { - if (this.#primaryNetInterface === undefined) { - const port = this.owner.state.network.port; - this.#primaryNetInterface = await UdpInterface.create( - this.owner.env.get(Network), - "udp6", - port ? port : undefined, - this.owner.state.network.listeningAddressIpv6, - ); - - await this.owner.set({ network: { operationalPort: this.#primaryNetInterface.port } }); - } - return this.#primaryNetInterface; + return this.owner.env.get(DeviceAdvertiser).advertise(true); } /** @@ -157,15 +126,24 @@ export class ServerNetworkRuntime extends NetworkRuntime { } /** - * Add transports to the {@link MatterDevice}. + * Add transports to the {@link TransportInterfaceSet}. */ - protected async addTransports(device: MatterDevice) { - device.addTransportInterface(await this.getPrimaryNetInterface()); - + protected async addTransports(interfaces: TransportInterfaceSet) { const netconf = this.owner.state.network; + const port = this.owner.state.network.port; + const ipv6Intf = await UdpInterface.create( + this.owner.env.get(Network), + "udp6", + port ? port : undefined, + netconf.listeningAddressIpv6, + ); + interfaces.add(ipv6Intf); + + await this.owner.set({ network: { operationalPort: ipv6Intf.port } }); + if (netconf.ipv4) { - device.addTransportInterface( + interfaces.add( await UdpInterface.create( this.owner.env.get(Network), "udp4", @@ -176,14 +154,16 @@ export class ServerNetworkRuntime extends NetworkRuntime { } if (netconf.ble) { - device.addTransportInterface(this.bleTransport); + interfaces.add(this.bleTransport); } } /** - * Add broadcasters to the {@link MatterDevice}. + * Add broadcasters to the {@link DeviceAdvertiser}. */ - protected async addBroadcasters(device: MatterDevice) { + protected async addBroadcasters(advertiser: DeviceAdvertiser) { + await advertiser.clearBroadcasters(); + const isCommissioned = !!this.#commissionedFabrics; let discoveryCapabilities = this.owner.state.network.discoveryCapabilities; @@ -194,11 +174,11 @@ export class ServerNetworkRuntime extends NetworkRuntime { } if (discoveryCapabilities.onIpNetwork) { - device.addBroadcaster(this.mdnsBroadcaster); + advertiser.addBroadcaster(this.mdnsBroadcaster); } if (discoveryCapabilities.ble) { - device.addBroadcaster(this.bleBroadcaster); + advertiser.addBroadcaster(this.bleBroadcaster); } } @@ -206,9 +186,10 @@ export class ServerNetworkRuntime extends NetworkRuntime { * When the first Fabric gets added we need to enable MDNS broadcasting. */ enableMdnsBroadcasting() { + const advertiser = this.owner.env.get(DeviceAdvertiser); const mdnsBroadcaster = this.mdnsBroadcaster; - if (!this.#matterDevice?.hasBroadcaster(mdnsBroadcaster)) { - this.#matterDevice?.addBroadcaster(mdnsBroadcaster); + if (!advertiser.hasBroadcaster(mdnsBroadcaster)) { + advertiser.addBroadcaster(mdnsBroadcaster); } } @@ -218,6 +199,10 @@ export class ServerNetworkRuntime extends NetworkRuntime { * On decommission we're destroyed so don't need to handle that case. */ endUncommissionedMode() { + // Ensure MDNS broadcasting are active when the first fabric is added. It might not be active initially if the + // node was not on an IP network prior to commissioning + this.enableMdnsBroadcasting(); + if (this.#bleBroadcaster) { this.owner.env.runtime.add(this.#removeBleBroadcaster(this.#bleBroadcaster)); this.#bleBroadcaster = undefined; @@ -230,12 +215,14 @@ export class ServerNetworkRuntime extends NetworkRuntime { } async #removeBleBroadcaster(bleBroadcaster: InstanceBroadcaster) { - await this.#matterDevice?.deleteBroadcaster(bleBroadcaster); + const advertiser = this.owner.env.get(DeviceAdvertiser); + await advertiser.deleteBroadcaster(bleBroadcaster); await bleBroadcaster.close(); } async #removeBleTransport(bleTransport: TransportInterface) { - await this.#matterDevice?.deleteTransportInterface(bleTransport); + const transportInterfaces = this.owner.env.get(TransportInterfaceSet); + transportInterfaces.delete(bleTransport); await bleTransport.close(); } @@ -253,101 +240,73 @@ export class ServerNetworkRuntime extends NetworkRuntime { return this.owner.state.operationalCredentials.commissionedFabrics; } - override get operationalPort() { - return this.#primaryNetInterface?.port ?? 0; - } - endCommissioning() { - if (this.#matterDevice !== undefined) { - return this.#matterDevice.endCommissioning(); - } + return this.owner.env.get(DeviceCommissioner).endCommissioning(); } protected override async start() { + const { owner } = this; + const { env } = owner; + // Ensure MdnsService is fully constructed - await this.owner.env.load(MdnsService); - - await this.owner.act("start-network", agent => agent.load(ProductDescriptionServer)); - - const { sessionStorage, fabricStorage } = this.owner.env.get(ServerNodeStore); - - // TODO - convert to using components directly, not via MatterDevice - const matterDevice = await MatterDevice.create( - sessionStorage, - fabricStorage, - () => ({ - ...this.owner.state.commissioning, - productDescription: this.owner.state.productDescription, - ble: !!this.owner.state.network.ble, - }), - this.owner.state.basicInformation.capabilityMinima.caseSessionsPerFabric, // Internally it is "Session and Node", so we support even more - (_fabricIndex: FabricIndex, _fabricAction: FabricAction) => { - // We use events directly - }, - (_fabricIndex: FabricIndex) => { - // Wired differently using SessionBehavior - }, - { maxPathsPerInvoke: this.owner.state.basicInformation.maxPathsPerInvoke }, - ); - this.#matterDevice = matterDevice; + await env.load(MdnsService); - this.#interactionServer = await TransactionalInteractionServer.create(this.owner, matterDevice.sessionManager); - matterDevice.addProtocolHandler(this.#interactionServer); + // Configure network + await this.addTransports(env.get(TransportInterfaceSet)); + await this.addBroadcasters(env.get(DeviceAdvertiser)); - matterDevice.fabricManager.events.added.on(fabric => { - const fabrics = this.#matterDevice?.fabricManager.getFabrics() ?? []; - if (fabrics.length === 1 && fabrics[0].fabricIndex === fabric.fabricIndex) { - this.enableMdnsBroadcasting(); - } - }); + await owner.act("start-network", agent => agent.load(ProductDescriptionServer)); + + // Apply settings to environmental components + env.get(ChannelManager).caseSessionsPerFabricAndNode = + // Note that this is "sessions per fabric and node", so we support more than indicated by capabilityMinima + owner.state.basicInformation.capabilityMinima.caseSessionsPerFabric; + env.get(SessionManager).sessionParameters = { + maxPathsPerInvoke: this.owner.state.basicInformation.maxPathsPerInvoke, + }; - // Expose internal managers for other components in the environment - // TODO - these should live for lifetime of the node and will be managed by node so this is just temporary - this.owner.env.set(SessionManager, matterDevice.sessionManager); - this.owner.env.set(FabricManager, matterDevice.fabricManager); - this.owner.env.set(ExchangeManager, matterDevice.exchangeManager); - this.owner.env.set(DeviceCommissioner, matterDevice.commissioner); - this.owner.env.set(DeviceAdvertiser, matterDevice.advertiser); + // Install our interaction server + this.#interactionServer = await TransactionalInteractionServer.create(this.owner, env.get(SessionManager)); + env.get(ExchangeManager).addProtocolHandler(this.#interactionServer); await this.owner.act("load-sessions", agent => agent.load(SessionsBehavior)); - this.owner.eventsOf(CommissioningBehavior).commissioned.on(() => this.endUncommissionedMode()); - await this.addTransports(matterDevice); - await this.addBroadcasters(matterDevice); + // Monitor CommissioningBehavior to end "uncommissioned" mode when we are commissioned + this.#observers.on(this.owner.eventsOf(CommissioningBehavior).commissioned, this.endUncommissionedMode); + + // Ensure the environment will convey the commissioning configuration to the DeviceCommissioner + if (!env.has(CommissioningConfigProvider)) { + env.set( + CommissioningConfigProvider, + new (class extends CommissioningConfigProvider { + get values() { + return { + ...owner.state.commissioning, + productDescription: owner.state.productDescription, + ble: !!owner.state.network.ble, + }; + } + })(), + ); + } - await this.owner.set({ network: { operationalPort: this.operationalPort } }); + // Initialize the DeviceCommissioner + env.get(DeviceCommissioner); await this.openAdvertisementWindow(); } protected override async stop() { - if (this.#matterDevice) { - this.owner.env.delete(SessionManager, this.#matterDevice.sessionManager); - this.owner.env.delete(FabricManager, this.#matterDevice.fabricManager); - this.owner.env.delete(ExchangeManager, this.#matterDevice.exchangeManager); - this.owner.env.delete(DeviceCommissioner, this.#matterDevice.commissioner); - this.owner.env.delete(DeviceAdvertiser, this.#matterDevice.advertiser); - - await this.#matterDevice.close(); - - this.#matterDevice = undefined; - this.#primaryNetInterface = undefined; - } + this.#observers.close(); - if (this.#primaryNetInterface) { - // If we created the net interface but not the device we need to dispose ourselves - await this.#primaryNetInterface.close(); - this.#primaryNetInterface = undefined; - } + await this.owner.env.close(DeviceCommissioner); + await this.owner.env.close(DeviceAdvertiser); + await this.owner.env.close(ExchangeManager); + await this.owner.env.close(SecureChannelProtocol); + await this.owner.env.close(TransportInterfaceSet); await this.#interactionServer?.[Symbol.asyncDispose](); this.#interactionServer = undefined; - - if (this.#commissionedListener) { - const commissionedListener = this.#commissionedListener; - this.#commissionedListener = undefined; - this.owner.eventsOf(CommissioningBehavior).commissioned.off(commissionedListener); - } } protected override blockNewActivity() { diff --git a/packages/node/src/behaviors/administrator-commissioning/AdministratorCommissioningServer.ts b/packages/node/src/behaviors/administrator-commissioning/AdministratorCommissioningServer.ts index f606ed5485..ba756a89d4 100644 --- a/packages/node/src/behaviors/administrator-commissioning/AdministratorCommissioningServer.ts +++ b/packages/node/src/behaviors/administrator-commissioning/AdministratorCommissioningServer.ts @@ -62,7 +62,7 @@ export class AdministratorCommissioningServer extends AdministratorCommissioning declare state: AdministratorCommissioningServer.State; /** - * This method opens an Enhanced Commissioning Window (A dynamic passcode is used which was provided by the caller). + * This method opens an Enhanced Commissioning Window (a dynamic passcode is used which was provided by the caller). */ override async openCommissioningWindow({ pakePasscodeVerifier, diff --git a/packages/node/src/behaviors/operational-credentials/OperationalCredentialsServer.ts b/packages/node/src/behaviors/operational-credentials/OperationalCredentialsServer.ts index 795595fb98..1f82035b3f 100644 --- a/packages/node/src/behaviors/operational-credentials/OperationalCredentialsServer.ts +++ b/packages/node/src/behaviors/operational-credentials/OperationalCredentialsServer.ts @@ -76,8 +76,8 @@ OperationalCredentials.Cluster.commands = { * This is the default server implementation of OperationalCredentialsBehavior. * * TODO - currently "source of truth" for fabric data is persisted by FabricManager. If we remove some legacy code - * paths we can move source of truth to here. Right now we just sync fabrics with MatterDevice. This sync is only as - * comprehensive as required by current use cases. If fabrics are mutated directly on MatterDevice then this code will + * paths we can move source of truth to here. Right now we just sync fabrics with FabricManager. This sync is only as + * comprehensive as required by current use cases. If fabrics are mutated directly on FabricManager then this code will * require update. */ export class OperationalCredentialsServer extends OperationalCredentialsBehavior { @@ -281,7 +281,7 @@ export class OperationalCredentialsServer extends OperationalCredentialsBehavior ); } } catch (e) { - // Fabric insertion into MatterDevice is not currently transactional so we need to remove manually + // Fabric insertion into FabricManager is not currently transactional so we need to remove manually await fabric.remove(this.session.id); throw e; } diff --git a/packages/node/src/build.config.ts b/packages/node/src/build.config.ts index f2b00ce0f0..ab36bdbd5e 100644 --- a/packages/node/src/build.config.ts +++ b/packages/node/src/build.config.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Project } from "@matter.js/tools"; +import { Project } from "@matter/tools"; export async function before({ project }: Project.Context) { // We must load "load.cjs" or "load.mjs" via self reference to conditional exports, but typescript won't find the diff --git a/packages/node/src/loader/loader.ts b/packages/node/src/loader/loader.ts index 7d6a7c14ce..2addad3fd8 100644 --- a/packages/node/src/loader/loader.ts +++ b/packages/node/src/loader/loader.ts @@ -9,7 +9,7 @@ import { EndpointType } from "#endpoint/type/EndpointType.js"; import { camelize, decamelize, ImplementationError, MaybePromise } from "#general"; // Must load from public export so node selects the correct format -import { load } from "@matter.js/node/load"; +import { load } from "@matter/node/load"; const cache = {} as Record; diff --git a/packages/node/src/node/ServerNode.ts b/packages/node/src/node/ServerNode.ts index 15da3d168e..1bf1a9c035 100644 --- a/packages/node/src/node/ServerNode.ts +++ b/packages/node/src/node/ServerNode.ts @@ -14,6 +14,7 @@ import { EndpointServer } from "#endpoint/EndpointServer.js"; import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js"; import type { Environment } from "#general"; import { Construction, DiagnosticSource, Identity, MatterError, asyncNew, errorOf } from "#general"; +import { EventHandler, FabricManager, SessionManager } from "#protocol"; import { RootEndpoint as BaseRootEndpoint } from "../endpoints/root.js"; import { Node } from "./Node.js"; import { Nodes } from "./Nodes.js"; @@ -98,12 +99,9 @@ export class ServerNode { diff --git a/packages/protocol/src/certificate/CertificateAuthority.ts b/packages/protocol/src/certificate/CertificateAuthority.ts index fe3a09667e..8b2d208c18 100644 --- a/packages/protocol/src/certificate/CertificateAuthority.ts +++ b/packages/protocol/src/certificate/CertificateAuthority.ts @@ -68,11 +68,11 @@ export class CertificateAuthority { this.rootKeyIdentifier = certValues.rootKeyIdentifier; this.rootCertBytes = certValues.rootCertBytes; this.nextCertificateId = BigInt(certValues.nextCertificateId); - logger.debug(`Loaded root certificate with ID ${this.rootCertId} from storage`); + logger.debug(`Loaded stored credentials with ID ${this.rootCertId}`); return; } - logger.debug(`Created new root certificate with ID ${this.rootCertId}`); + logger.debug(`Created new credentials with ID ${this.rootCertId}`); if (options instanceof StorageContext) { await options.set({ @@ -86,7 +86,7 @@ export class CertificateAuthority { }); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const storage = env.get(StorageManager).createContext("certificates"); const instance = new CertificateAuthority(storage); env.set(CertificateAuthority, instance); diff --git a/packages/protocol/src/common/Scanner.ts b/packages/protocol/src/common/Scanner.ts index f44d85d255..53169f37b0 100644 --- a/packages/protocol/src/common/Scanner.ts +++ b/packages/protocol/src/common/Scanner.ts @@ -180,7 +180,7 @@ export class ScannerSet extends BasicSet { ); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new ScannerSet(); env.set(ScannerSet, instance); return instance; diff --git a/packages/protocol/src/fabric/FabricManager.ts b/packages/protocol/src/fabric/FabricManager.ts index cca1b94128..de9901a98e 100644 --- a/packages/protocol/src/fabric/FabricManager.ts +++ b/packages/protocol/src/fabric/FabricManager.ts @@ -80,7 +80,7 @@ export class FabricManager { await this.construction; } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new FabricManager(env.get(StorageManager).createContext("fabrics")); env.set(FabricManager, instance); return instance; @@ -90,6 +90,13 @@ export class FabricManager { return this.#events; } + async clear() { + await this.#construction; + this.#nextFabricIndex = 1; + this.#fabrics.clear(); + await this.#storage?.clear(); + } + for(address: FabricIndex | PeerAddress) { if (typeof address === "object") { address = address.fabricIndex; diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index e7ed702d54..2dde5f35f2 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -12,7 +12,6 @@ export * from "./common/index.js"; export * from "./endpoint/index.js"; export * from "./fabric/index.js"; export * from "./interaction/index.js"; -export * from "./MatterDevice.js"; export * from "./mdns/index.js"; export * from "./peer/index.js"; export * from "./protocol/index.js"; diff --git a/packages/protocol/src/interaction/EventHandler.ts b/packages/protocol/src/interaction/EventHandler.ts index e05702a4de..8a4fe6c8fc 100644 --- a/packages/protocol/src/interaction/EventHandler.ts +++ b/packages/protocol/src/interaction/EventHandler.ts @@ -4,7 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Construction, Logger, MaybePromise, Storage, StorageContext, StorageOperationResult } from "#general"; +import { + Construction, + Environment, + Environmental, + Logger, + MaybePromise, + Storage, + StorageContext, + StorageManager, + StorageOperationResult, +} from "#general"; import { ClusterId, EndpointNumber, @@ -42,13 +52,14 @@ export interface EventStorageData extends EventData { } /** - * Class that collects all triggered events up to a certain limit of events and handle logic - * to handle subscriptions (TBD) + * Class that collects all triggered events up to a certain limit of events and handle logic to handle subscriptions + * (TBD) */ export class EventHandler { - private eventNumber = EventNumber(0); - private storedEventCount = 0; - private readonly events = { + #eventNumber = EventNumber(0); + #storedEventCount = 0; + #eventStorage: StorageContext; + readonly #events = { [EventPriority.Critical]: new Array>(), [EventPriority.Info]: new Array>(), [EventPriority.Debug]: new Array>(), @@ -65,13 +76,30 @@ export class EventHandler { return handler; } - constructor(private readonly eventStorage: StorageContext) { + static [Environmental.create](env: Environment) { + const instance = new EventHandler(env.get(StorageManager).createContext("events")); + env.set(EventHandler, instance); + return instance; + } + + constructor(eventStorage: StorageContext) { + this.#eventStorage = eventStorage; this.#construction = Construction(this, async () => { - this.eventNumber = await this.eventStorage.get("lastEventNumber", this.eventNumber); - logger.debug(`Set/Restore last event number: ${this.eventNumber}`); + this.#eventNumber = await this.#eventStorage.get("lastEventNumber", this.#eventNumber); + logger.debug(`Set/Restore last event number: ${this.#eventNumber}`); }); } + async clear() { + await this.construction; + await this.#eventStorage.clear(); + this.#eventNumber = EventNumber(0); + this.#storedEventCount = 0; + for (const list of Object.values(this.#events)) { + list.length = 0; + } + } + getEvents( eventPath: TypeFromSchema, filters?: TypeFromSchema[], @@ -87,7 +115,7 @@ export class EventHandler { const events = new Array>(); const { endpointId, clusterId, eventId } = eventPath; for (const priority of [EventPriority.Critical, EventPriority.Info, EventPriority.Debug]) { - const eventsToCheck = this.events[priority]; + const eventsToCheck = this.#events[priority]; for (const event of eventsToCheck) { if (endpointId === event.endpointId && clusterId === event.clusterId && eventId === event.eventId) { if (eventFilter(event)) { @@ -109,13 +137,13 @@ export class EventHandler { pushEvent(event: EventData) { const eventData = { - eventNumber: EventNumber(++this.eventNumber), + eventNumber: EventNumber(++this.#eventNumber), ...event, }; logger.debug(`Received event: ${Logger.toJSON(eventData)}`); - this.events[event.priority].push(eventData); - this.storedEventCount++; - const setPromise = this.eventStorage.set("lastEventNumber", this.eventNumber); + this.#events[event.priority].push(eventData); + this.#storedEventCount++; + const setPromise = this.#eventStorage.set("lastEventNumber", this.#eventNumber); if (MaybePromise.is(setPromise)) { return setPromise.then(() => { this.cleanUpEvents(); @@ -127,10 +155,10 @@ export class EventHandler { } cleanUpEvents() { - if (this.storedEventCount < MAX_EVENTS) return; - const eventsToDelete = this.storedEventCount - MAX_EVENTS; // should be always 1, but let's be sure + if (this.#storedEventCount < MAX_EVENTS) return; + const eventsToDelete = this.#storedEventCount - MAX_EVENTS; // should be always 1, but let's be sure for (const priority of [EventPriority.Debug, EventPriority.Info, EventPriority.Critical]) { - const events = this.events[priority]; + const events = this.#events[priority]; if (events.length > 0) { const removedEvents = events.splice(0, events.length - eventsToDelete); logger.debug(`Removed ${removedEvents.length} events from priority ${priority}`); diff --git a/packages/protocol/src/mdns/MdnsService.ts b/packages/protocol/src/mdns/MdnsService.ts index 653ecf0296..a42696393b 100644 --- a/packages/protocol/src/mdns/MdnsService.ts +++ b/packages/protocol/src/mdns/MdnsService.ts @@ -62,11 +62,11 @@ export class MdnsService { } get broadcaster() { - return this.#construction.assert("MDNS broadcaster", this.#broadcaster); + return this.#construction.assert("MDNS service", this.#broadcaster); } get scanner() { - return this.#construction.assert("MDNS scanner", this.#scanner); + return this.#construction.assert("MDNS service", this.#scanner); } get [Diagnostic.value]() { diff --git a/packages/protocol/src/peer/ControllerCommissioner.ts b/packages/protocol/src/peer/ControllerCommissioner.ts index 49b1e09130..b67e3b6943 100644 --- a/packages/protocol/src/peer/ControllerCommissioner.ts +++ b/packages/protocol/src/peer/ControllerCommissioner.ts @@ -97,7 +97,7 @@ export interface ControllerCommissionerContext { netInterfaces: NetInterfaceSet; sessions: SessionManager; exchanges: ExchangeManager; - authority: CertificateAuthority; + ca: CertificateAuthority; } /** @@ -112,14 +112,14 @@ export class ControllerCommissioner { this.#paseClient = new PaseClient(context.sessions); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new ControllerCommissioner({ peers: env.get(PeerSet), scanners: env.get(ScannerSet), netInterfaces: env.get(NetInterfaceSet), sessions: env.get(SessionManager), exchanges: env.get(ExchangeManager), - authority: env.get(CertificateAuthority), + ca: env.get(CertificateAuthority), }); env.set(ControllerCommissioner, instance); return instance; @@ -333,7 +333,7 @@ export class ControllerCommissioner { const commissioningManager = new ControllerCommissioningFlow( // Use the created secure session to do the commissioning new InteractionClient(new ExchangeProvider(this.#context.exchanges, paseSecureMessageChannel), address), - this.#context.authority, + this.#context.ca, fabric, commissioningOptions, async address => { diff --git a/packages/protocol/src/peer/PeerSet.ts b/packages/protocol/src/peer/PeerSet.ts index 98bd9dcd07..e986b39498 100644 --- a/packages/protocol/src/peer/PeerSet.ts +++ b/packages/protocol/src/peer/PeerSet.ts @@ -193,7 +193,7 @@ export class PeerSet implements ImmutableSet, ObservableSet b.close())); + this.#broadcasters.clear(); + const errors = (await closed) + .map(status => (status.status === "rejected" ? status.reason : undefined)) + .filter(reason => reason !== undefined); + if (errors.length) { + throw new MatterAggregateError(errors, "Error closing broadcasters"); + } + } } diff --git a/packages/protocol/src/protocol/DeviceCommissioner.ts b/packages/protocol/src/protocol/DeviceCommissioner.ts index ff77967932..8be4124e46 100644 --- a/packages/protocol/src/protocol/DeviceCommissioner.ts +++ b/packages/protocol/src/protocol/DeviceCommissioner.ts @@ -97,7 +97,7 @@ export class DeviceCommissioner { }); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new DeviceCommissioner({ fabrics: env.get(FabricManager), sessions: env.get(SessionManager), diff --git a/packages/protocol/src/protocol/ExchangeManager.ts b/packages/protocol/src/protocol/ExchangeManager.ts index fada4d13f2..87b8e23764 100644 --- a/packages/protocol/src/protocol/ExchangeManager.ts +++ b/packages/protocol/src/protocol/ExchangeManager.ts @@ -137,7 +137,7 @@ export class ExchangeManager { }); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new ExchangeManager({ transportInterfaces: env.get(TransportInterfaceSet), sessionManager: env.get(SessionManager), diff --git a/packages/protocol/src/securechannel/SecureChannelProtocol.ts b/packages/protocol/src/securechannel/SecureChannelProtocol.ts index 82d9cbfd40..e3d6e6398f 100644 --- a/packages/protocol/src/securechannel/SecureChannelProtocol.ts +++ b/packages/protocol/src/securechannel/SecureChannelProtocol.ts @@ -5,7 +5,8 @@ */ import { FabricManager } from "#fabric/FabricManager.js"; -import { AsyncObservable, Logger, MatterFlowError } from "#general"; +import { AsyncObservable, Environment, Environmental, Logger, MatterFlowError } from "#general"; +import { ExchangeManager } from "#protocol/ExchangeManager.js"; import { SessionManager } from "#session/SessionManager.js"; import { GeneralStatusCode, @@ -100,6 +101,13 @@ export class SecureChannelProtocol extends StatusReportOnlySecureChannelProtocol this.#caseCommissioner = new CaseServer(sessions, fabrics); } + static [Environmental.create](env: Environment) { + const instance = new SecureChannelProtocol(env.get(SessionManager), env.get(FabricManager)); + env.get(ExchangeManager).addProtocolHandler(instance); + env.set(SecureChannelProtocol, instance); + return instance; + } + /** * Emitted when the active PASE session hits the maximum error threshold. */ diff --git a/packages/protocol/src/session/SessionManager.ts b/packages/protocol/src/session/SessionManager.ts index cb1c3995a6..99d181f261 100644 --- a/packages/protocol/src/session/SessionManager.ts +++ b/packages/protocol/src/session/SessionManager.ts @@ -114,7 +114,7 @@ export class SessionManager { #resumptionRecords = new PeerAddressMap(); readonly #globalUnencryptedMessageCounter = new MessageCounter(); readonly #subscriptionsChanged = Observable<[session: SecureSession, subscription: Subscription]>(); - readonly #sessionParameters; + #sessionParameters; readonly #resubmissionStarted = new Observable<[session: Session]>(); readonly #construction: Construction; readonly #observers = new ObserverGroup(); @@ -132,7 +132,7 @@ export class SessionManager { this.#construction = Construction(this, () => this.#initialize()); } - [Environmental.create](env: Environment) { + static [Environmental.create](env: Environment) { const instance = new SessionManager({ storage: env.get(StorageManager).createContext("sessions"), fabrics: env.get(FabricManager), @@ -167,10 +167,23 @@ export class SessionManager { * Our session parameters. These are the parameters we provide during session negotiation. The peer may specify * different parameters. */ - get sessionParameters() { + get sessionParameters(): SessionParameters { return this.#sessionParameters; } + /** + * Change session parameters. + * + * Parameters values you omit in {@link parameters} will retain their current values. This only affects new + * sessions. + */ + set sessionParameters(parameters: Partial) { + this.#sessionParameters = { + ...this.#sessionParameters, + ...parameters, + }; + } + /** * Emits when there is a change to the subscription set. */ @@ -529,6 +542,11 @@ export class SessionManager { } } + async clear() { + await this.close(); + await this.#context.storage.clear(); + } + updateAllSubscriptions() { this.#subscriptionUpdateMutex.run(async () => { for (const session of this.#sessions) { From bb6638499dcd9f67233eef6d6401a5f772bdae58 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Sun, 6 Oct 2024 19:32:54 -0700 Subject: [PATCH 04/11] Add component for managing controlled fabric "FabricAuthority" works in conjunction with CertificateAuthority and FabricManager to manage fabrics that we control. --- .../examples/src/examples/ControllerNode.ts | 4 +- packages/matter.js/src/MatterController.ts | 19 +-- packages/matter.js/src/compat/protocol.ts | 2 +- packages/node/src/node/Nodes.ts | 7 +- .../nodejs/test/fabric/FabricManagerTest.ts | 32 ++-- .../protocol/src/common/FailsafeContext.ts | 4 +- .../protocol/src/fabric/FabricAuthority.ts | 141 ++++++++++++++++++ packages/protocol/src/fabric/FabricManager.ts | 7 +- packages/protocol/src/fabric/index.ts | 1 + .../src/peer/ControllerCommissioner.ts | 6 +- .../src/peer/ControllerCommissioningFlow.ts | 16 +- 11 files changed, 193 insertions(+), 46 deletions(-) create mode 100644 packages/protocol/src/fabric/FabricAuthority.ts diff --git a/packages/examples/src/examples/ControllerNode.ts b/packages/examples/src/examples/ControllerNode.ts index 1bbcfaf427..5771bdf71a 100644 --- a/packages/examples/src/examples/ControllerNode.ts +++ b/packages/examples/src/examples/ControllerNode.ts @@ -13,7 +13,7 @@ import { Environment, Logger, singleton, StorageService, Time } from "@matter/main"; import { BasicInformationCluster, DescriptorCluster, GeneralCommissioning, OnOff } from "@matter/main/clusters"; -import { Ble, ClusterClientObj, ControllingCommissioningFlowOptions } from "@matter/main/protocol"; +import { Ble, ClusterClientObj, ControllerCommissioningFlowOptions } from "@matter/main/protocol"; import { ManualPairingCodeCodec, NodeId } from "@matter/main/types"; import { NodeJsBle } from "@matter/nodejs-ble"; import { CommissioningController, NodeCommissioningOptions } from "@project-chip/matter.js"; @@ -89,7 +89,7 @@ class ControllerNode { } // Collect commissioning options from commandline parameters - const commissioningOptions: ControllingCommissioningFlowOptions = { + const commissioningOptions: ControllerCommissioningFlowOptions = { regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor, regulatoryCountryCode: "XX", }; diff --git a/packages/matter.js/src/MatterController.ts b/packages/matter.js/src/MatterController.ts index cb1e7931f0..592c3ee272 100644 --- a/packages/matter.js/src/MatterController.ts +++ b/packages/matter.js/src/MatterController.ts @@ -33,6 +33,8 @@ import { ClusterClient, CommissioningError, ControllerCommissioner, + DEFAULT_ADMIN_VENDOR_ID, + DEFAULT_FABRIC_ID, DiscoveryData, DiscoveryOptions, ExchangeManager, @@ -67,9 +69,7 @@ export type CommissionedNodeDetails = { deviceData?: DeviceInformationData; }; -const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1); const DEFAULT_FABRIC_INDEX = FabricIndex(1); -const DEFAULT_FABRIC_ID = FabricId(1); const CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE = 3; const CONTROLLER_MAX_PATHS_PER_INVOKE = 10; @@ -110,7 +110,7 @@ export class MatterController { caseAuthenticatedTags, } = options; - const certificateManager = await CertificateAuthority.create(caStorage); + const ca = await CertificateAuthority.create(caStorage); let controller: MatterController; // Check if we have a fabric stored in the storage, if yes initialize this one, else build a new one @@ -122,7 +122,7 @@ export class MatterController { nodesStorage, scanners, netInterfaces, - certificateManager, + certificateManager: ca, fabric, sessionClosedCallback, }); @@ -130,17 +130,12 @@ export class MatterController { const rootNodeId = NodeId.randomOperationalNodeId(); const ipkValue = Crypto.getRandomData(CRYPTO_SYMMETRIC_KEY_LENGTH); const fabricBuilder = new FabricBuilder() - .setRootCert(certificateManager.rootCert) + .setRootCert(ca.rootCert) .setRootNodeId(rootNodeId) .setIdentityProtectionKey(ipkValue) .setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID); fabricBuilder.setOperationalCert( - certificateManager.generateNoc( - fabricBuilder.publicKey, - adminFabricId, - rootNodeId, - caseAuthenticatedTags, - ), + ca.generateNoc(fabricBuilder.publicKey, adminFabricId, rootNodeId, caseAuthenticatedTags), ); const fabric = await fabricBuilder.build(adminFabricIndex); @@ -150,7 +145,7 @@ export class MatterController { nodesStorage, scanners, netInterfaces, - certificateManager, + certificateManager: ca, fabric, sessionClosedCallback, }); diff --git a/packages/matter.js/src/compat/protocol.ts b/packages/matter.js/src/compat/protocol.ts index 3e830f287d..e6e9a1f676 100644 --- a/packages/matter.js/src/compat/protocol.ts +++ b/packages/matter.js/src/compat/protocol.ts @@ -27,7 +27,7 @@ export { NoChannelError, PairRetransmissionLimitReachedError, UnexpectedMessageError, - type ControllingCommissioningFlowOptions as CommissioningOptions, + type ControllerCommissioningFlowOptions as CommissioningOptions, type ExchangeSendOptions, type ProtocolHandler, } from "#protocol"; diff --git a/packages/node/src/node/Nodes.ts b/packages/node/src/node/Nodes.ts index cc09032729..532b0f84c3 100644 --- a/packages/node/src/node/Nodes.ts +++ b/packages/node/src/node/Nodes.ts @@ -6,6 +6,7 @@ import { EndpointContainer } from "#endpoint/properties/EndpointContainer.js"; import { Construction } from "#general"; +import { CertificateAuthority } from "@matter/protocol"; import { ClientNode } from "./ClientNode.js"; import { type Node } from "./Node.js"; import { ServerNode } from "./ServerNode.js"; @@ -41,5 +42,9 @@ export class Nodes extends EndpointContainer { return super.endpoint as ServerNode; } - async commission() {} + async commission() { + await this.#construction; + + const ca = await this.endpoint.env.load(CertificateAuthority); + } } diff --git a/packages/nodejs/test/fabric/FabricManagerTest.ts b/packages/nodejs/test/fabric/FabricManagerTest.ts index 04e5f9a2ad..ff852405cb 100644 --- a/packages/nodejs/test/fabric/FabricManagerTest.ts +++ b/packages/nodejs/test/fabric/FabricManagerTest.ts @@ -89,70 +89,70 @@ describe("FabricManager", () => { describe("determine next FabricIndex", () => { it("get next fabric index initially", async () => { - assert.equal(fabricManager.getNextFabricIndex(), 1); + assert.equal(fabricManager.allocateFabricIndex(), 1); }); it("get next fabric index after adding fabric", async () => { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); - assert.equal(fabricManager.getNextFabricIndex(), 2); + assert.equal(fabricManager.allocateFabricIndex(), 2); }); it("get next fabric index after adding fabric and removing it", async () => { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); await fabricManager.removeFabric(fabric.fabricIndex); - assert.equal(fabricManager.getNextFabricIndex(), 2); + assert.equal(fabricManager.allocateFabricIndex(), 2); }); it("get next fabric index after adding fabric and revoking it", async () => { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); await fabricManager.revokeFabric(fabric.fabricIndex); - assert.equal(fabricManager.getNextFabricIndex(), 2); + assert.equal(fabricManager.allocateFabricIndex(), 2); }); it("get jumps over one fabric index after adding fabric and revoking it", async () => { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); // Index 1 + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); // Index 1 fabricManager.addFabric(fabric); - const fabric2 = await buildFabric(fabricManager.getNextFabricIndex()); // Index 2 + const fabric2 = await buildFabric(fabricManager.allocateFabricIndex()); // Index 2 fabricManager.addFabric(fabric2); await fabricManager.revokeFabric(fabric.fabricIndex); // We do not remove the last generated Index - assert.equal(fabricManager.getNextFabricIndex(), 3); // So index now changed + assert.equal(fabricManager.allocateFabricIndex(), 3); // So index now changed }); it("it find first open slot for next index", async () => { for (let i = 1; i < 255; i++) { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); } await fabricManager.removeFabric(FabricIndex(100)); - assert.equal(fabricManager.getNextFabricIndex(), 100); + assert.equal(fabricManager.allocateFabricIndex(), 100); }); it("it find first open slot for next index id including overflow", async () => { for (let i = 1; i < 255; i++) { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); } await fabricManager.removeFabric(FabricIndex(1)); - assert.equal(fabricManager.getNextFabricIndex(), 1); + assert.equal(fabricManager.allocateFabricIndex(), 1); }); it("throws when table is full", async () => { for (let i = 1; i < 255; i++) { - const fabric = await buildFabric(fabricManager.getNextFabricIndex()); + const fabric = await buildFabric(fabricManager.allocateFabricIndex()); fabricManager.addFabric(fabric); } assert.throws( - () => fabricManager.getNextFabricIndex(), + () => fabricManager.allocateFabricIndex(), new FabricTableFullError("No free fabric index available."), ); }); diff --git a/packages/protocol/src/common/FailsafeContext.ts b/packages/protocol/src/common/FailsafeContext.ts index 5fdde141c5..c8bb716601 100644 --- a/packages/protocol/src/common/FailsafeContext.ts +++ b/packages/protocol/src/common/FailsafeContext.ts @@ -131,7 +131,7 @@ export abstract class FailsafeContext { } getNextFabricIndex() { - return this.#fabrics.getNextFabricIndex(); + return this.#fabrics.allocateFabricIndex(); } async addFabric(fabric: Fabric) { @@ -239,7 +239,7 @@ export abstract class FailsafeContext { .setRootVendorId(adminVendorId) .setIdentityProtectionKey(ipkValue) .setRootNodeId(caseAdminSubject) - .build(this.#fabrics.getNextFabricIndex()); + .build(this.#fabrics.allocateFabricIndex()); } async #failSafeExpired() { diff --git a/packages/protocol/src/fabric/FabricAuthority.ts b/packages/protocol/src/fabric/FabricAuthority.ts new file mode 100644 index 0000000000..b7bac354da --- /dev/null +++ b/packages/protocol/src/fabric/FabricAuthority.ts @@ -0,0 +1,141 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CertificateAuthority } from "#certificate/CertificateAuthority.js"; +import { + Bytes, + Crypto, + CRYPTO_SYMMETRIC_KEY_LENGTH, + Environment, + Environmental, + ImplementationError, + Logger, +} from "#general"; +import { CaseAuthenticatedTag, FabricId, FabricIndex, NodeId, VendorId } from "#types"; +import { FabricBuilder } from "./Fabric.js"; +import { FabricManager } from "./FabricManager.js"; + +const logger = Logger.get("FabricAuthority"); + +/** + * Configuration for fabrics controlled by a FabricAuthority. + */ +interface FabricAuthorityConfiguration { + adminVendorId?: VendorId; + fabricIndex?: FabricIndex; + fabricId?: FabricId; + caseAuthenticatedTags?: CaseAuthenticatedTag[]; +} + +/** + * Concrete {@link FabricAuthorityConfiguration} for environmental configuration. + */ +export class FabricAuthorityConfigurationProvider implements FabricAuthorityConfiguration {} + +/** + * Interfaces FabricAuthority with other components. + */ +export interface FabricAuthorityContext { + ca: CertificateAuthority; + fabrics: FabricManager; + config: FabricAuthorityConfiguration; +} + +export const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1); +export const DEFAULT_FABRIC_ID = FabricId(1); + +/** + * Manages fabrics controlled locally associated with a specific CA. + */ +export class FabricAuthority { + #ca: CertificateAuthority; + #fabrics: FabricManager; + #config: FabricAuthorityConfiguration; + + constructor(context: FabricAuthorityContext) { + this.#ca = context.ca; + this.#fabrics = context.fabrics; + this.#config = context.config; + } + + /** + * Obtain the default fabric for this authority. + */ + async defaultFabric() { + // First search for a fabric associated with the CA's root certificate + const fabric = this.fabrics[0]; + if (fabric !== undefined) { + return fabric; + } + + // Create a new fabric + return await this.createFabric(); + } + + /** + * List all controlled fabrics. + */ + get fabrics() { + return Array.from(this.#fabrics).filter(fabric => { + if (Bytes.areEqual(fabric.rootCert, this.#ca.rootCert)) { + return true; + } + }); + } + + /** + * Create a new fabric under our control. + */ + async createFabric() { + const rootNodeId = NodeId.randomOperationalNodeId(); + const ipkValue = Crypto.getRandomData(CRYPTO_SYMMETRIC_KEY_LENGTH); + + let vendorId = this.#config.adminVendorId; + if (vendorId === undefined) { + vendorId = DEFAULT_ADMIN_VENDOR_ID; + logger.warn(`Using test vendor ID 0x${vendorId.toString(16)} for controller fabric`); + } + + const fabricBuilder = new FabricBuilder() + .setRootCert(this.#ca.rootCert) + .setRootNodeId(rootNodeId) + .setIdentityProtectionKey(ipkValue) + .setRootVendorId(this.#config.adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID); + + fabricBuilder.setOperationalCert( + this.#ca.generateNoc( + fabricBuilder.publicKey, + this.#config.fabricId ?? DEFAULT_FABRIC_ID, + rootNodeId, + this.#config.caseAuthenticatedTags, + ), + ); + + let index = this.#config.fabricIndex; + if (index === undefined) { + index = this.#fabrics.allocateFabricIndex(); + } else if (this.#fabrics.findByIndex(index) !== undefined) { + throw new ImplementationError(`Cannot allocate controller fabric ${index} because index is in use`); + } + + const fabric = await fabricBuilder.build(index); + this.#fabrics.addFabric(fabric); + + logger.debug(`Created new controller fabric ${index}`); + + return fabric; + } + + static [Environmental.create](env: Environment) { + const instance = new FabricAuthority({ + ca: env.get(CertificateAuthority), + fabrics: env.get(FabricManager), + config: env.get(FabricAuthorityConfigurationProvider), + }); + env.set(FabricAuthority, instance); + return instance; + } +} diff --git a/packages/protocol/src/fabric/FabricManager.ts b/packages/protocol/src/fabric/FabricManager.ts index de9901a98e..911419ac0f 100644 --- a/packages/protocol/src/fabric/FabricManager.ts +++ b/packages/protocol/src/fabric/FabricManager.ts @@ -32,6 +32,7 @@ export enum FabricAction { Removed, Updated, } + export class FabricManager { #nextFabricIndex = 1; readonly #fabrics = new Map(); @@ -108,7 +109,7 @@ export class FabricManager { return fabric; } - getNextFabricIndex() { + allocateFabricIndex() { this.#construction.assert(); for (let i = 0; i < 254; i++) { @@ -178,6 +179,10 @@ export class FabricManager { this.#events.deleted.emit(fabric); } + [Symbol.iterator]() { + return this.#fabrics.values(); + } + getFabrics() { this.#construction.assert(); diff --git a/packages/protocol/src/fabric/index.ts b/packages/protocol/src/fabric/index.ts index 6c73768220..0f77b4c9dc 100644 --- a/packages/protocol/src/fabric/index.ts +++ b/packages/protocol/src/fabric/index.ts @@ -5,4 +5,5 @@ */ export * from "./Fabric.js"; +export * from "./FabricAuthority.js"; export * from "./FabricManager.js"; diff --git a/packages/protocol/src/peer/ControllerCommissioner.ts b/packages/protocol/src/peer/ControllerCommissioner.ts index b67e3b6943..b39f6ca75b 100644 --- a/packages/protocol/src/peer/ControllerCommissioner.ts +++ b/packages/protocol/src/peer/ControllerCommissioner.ts @@ -20,7 +20,7 @@ import { ServerAddress, } from "#general"; import { MdnsScanner } from "#mdns/MdnsScanner.js"; -import { ControllerCommissioningFlow, ControllingCommissioningFlowOptions } from "#peer/ControllerCommissioningFlow.js"; +import { ControllerCommissioningFlow, ControllerCommissioningFlowOptions } from "#peer/ControllerCommissioningFlow.js"; import { ControllerDiscovery, PairRetransmissionLimitReachedError } from "#peer/ControllerDiscovery.js"; import { ExchangeManager, ExchangeProvider, MessageChannel } from "#protocol/ExchangeManager.js"; import { PaseClient } from "#session/index.js"; @@ -35,7 +35,7 @@ const logger = Logger.get("PeerCommissioner"); /** * Options needed to commission a new node */ -export interface PeerCommissioningOptions extends Partial { +export interface PeerCommissioningOptions extends Partial { /** The fabric into which to commission. */ fabric: Fabric; @@ -296,7 +296,7 @@ export class ControllerCommissioner { */ async #commissionDiscoveredNode( paseSecureMessageChannel: MessageChannel, - commissioningOptions: PeerCommissioningOptions & ControllingCommissioningFlowOptions, + commissioningOptions: PeerCommissioningOptions & ControllerCommissioningFlowOptions, discoveryData?: DiscoveryData, ): Promise { const { fabric, performCaseCommissioning } = commissioningOptions; diff --git a/packages/protocol/src/peer/ControllerCommissioningFlow.ts b/packages/protocol/src/peer/ControllerCommissioningFlow.ts index f708621dc0..185c71a6bb 100644 --- a/packages/protocol/src/peer/ControllerCommissioningFlow.ts +++ b/packages/protocol/src/peer/ControllerCommissioningFlow.ts @@ -34,7 +34,7 @@ const logger = Logger.get("ControllerCommissioner"); /** * User specific options for the Commissioning process */ -export type ControllingCommissioningFlowOptions = { +export type ControllerCommissioningFlowOptions = { /** * The regulatory location (indoor or outdoor) where the device is used. */ @@ -138,10 +138,10 @@ const DEFAULT_FAILSAFE_TIME_MS = 60_000; // 60 seconds */ export class ControllerCommissioningFlow { #interactionClient: InteractionClient; - readonly #certificateManager: CertificateAuthority; + readonly #ca: CertificateAuthority; readonly #fabric: Fabric; readonly #transitionToCase: (peerAddress: PeerAddress) => Promise; - readonly #commissioningOptions: ControllingCommissioningFlowOptions; + readonly #commissioningOptions: ControllerCommissioningFlowOptions; readonly #commissioningSteps = new Array(); readonly #commissioningStepResults = new Map(); readonly #clusterClients = new Map(); @@ -157,19 +157,19 @@ export class ControllerCommissioningFlow { interactionClient: InteractionClient, /** CertificateManager of the controller. */ - certificateManager: CertificateAuthority, + ca: CertificateAuthority, /** Fabric of the controller. */ fabric: Fabric, /** Commissioning options for the commissioning process. */ - commissioningOptions: ControllingCommissioningFlowOptions, + commissioningOptions: ControllerCommissioningFlowOptions, /** Callback that establishes CASE connection or handles final commissioning */ transitionToCase: (peerAddress: PeerAddress) => Promise, ) { this.#interactionClient = interactionClient; - this.#certificateManager = certificateManager; + this.#ca = ca; this.#fabric = fabric; this.#transitionToCase = transitionToCase; this.#commissioningOptions = commissioningOptions; @@ -706,11 +706,11 @@ export class ControllerCommissioningFlow { await operationalCredentialsClusterClient.addTrustedRootCertificate( { - rootCaCertificate: this.#certificateManager.rootCert, + rootCaCertificate: this.#ca.rootCert, }, { useExtendedFailSafeMessageResponseTimeout: true }, ); - const peerOperationalCert = this.#certificateManager.generateNoc( + const peerOperationalCert = this.#ca.generateNoc( operationalPublicKey, this.#fabric.fabricId, this.#interactionClient.address.nodeId, From 423f909f0d44c833222b7b1096f174f4ff20e676 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Sun, 6 Oct 2024 23:59:24 -0700 Subject: [PATCH 05/11] Partial implementatoin of ClientNode commissioning --- .../endpoint/properties/EndpointContainer.ts | 10 +- packages/node/src/node/ClientNode.ts | 29 ++++- packages/node/src/node/Nodes.ts | 104 +++++++++++++++++- packages/node/src/node/ServerNode.ts | 17 +-- .../node/src/node/server/ServerEnvironment.ts | 38 +++++++ packages/nodejs-ble/src/BleBroadcaster.ts | 3 +- .../src/interaction/InteractionServer.ts | 2 +- 7 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 packages/node/src/node/server/ServerEnvironment.ts diff --git a/packages/node/src/endpoint/properties/EndpointContainer.ts b/packages/node/src/endpoint/properties/EndpointContainer.ts index 078c1a746c..8420b651bd 100644 --- a/packages/node/src/endpoint/properties/EndpointContainer.ts +++ b/packages/node/src/endpoint/properties/EndpointContainer.ts @@ -11,8 +11,8 @@ import { Endpoint } from "../Endpoint.js"; /** * Manages parent-child relationships between endpoints. */ -export class EndpointContainer implements MutableSet, ObservableSet { - #children = new BasicSet(); +export class EndpointContainer implements MutableSet, ObservableSet { + #children = new BasicSet(); #endpoint: Endpoint; constructor(endpoint: Endpoint) { @@ -27,7 +27,7 @@ export class EndpointContainer implements MutableSet, Observ } } - add(endpoint: Endpoint) { + add(endpoint: T) { if (endpoint.lifecycle.hasId) { this.assertIdAvailable(endpoint.id, endpoint); } @@ -42,7 +42,7 @@ export class EndpointContainer implements MutableSet, Observ this.#children.add(endpoint); } - delete(endpoint: Endpoint) { + delete(endpoint: T) { return this.#children.delete(endpoint); } @@ -50,7 +50,7 @@ export class EndpointContainer implements MutableSet, Observ this.#children.clear(); } - has(endpoint: Endpoint) { + has(endpoint: T) { return this.#children.has(endpoint); } diff --git a/packages/node/src/node/ClientNode.ts b/packages/node/src/node/ClientNode.ts index eff4ff5969..39de1d32fb 100644 --- a/packages/node/src/node/ClientNode.ts +++ b/packages/node/src/node/ClientNode.ts @@ -6,8 +6,8 @@ import { NetworkRuntime } from "#behavior/system/network/NetworkRuntime.js"; import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js"; -import { ImplementationError, NotImplementedError } from "#general"; -import { PeerAddress } from "#protocol"; +import { ImplementationError, NotImplementedError, SupportedStorageTypes } from "#general"; +import { OperationalPeer, PeerAddress } from "#protocol"; import { ClientEndpointInitializer } from "./client/ClientEndpointInitializer.js"; import { ClientNodeLifecycle } from "./ClientNodeLifecycle.js"; import { Node } from "./Node.js"; @@ -19,6 +19,8 @@ import { ClientNodeStore } from "./storage/ClientNodeStore.js"; */ export class ClientNode extends Node { #address: PeerAddress; + #operationalAddress: Partial = {}; + #store: ClientNodeStore; constructor({ owner, store }: ClientNode.Options) { const { address } = store; @@ -29,14 +31,37 @@ export class ClientNode extends Node { owner, }); this.#address = store.address; + this.#store = store; this.env.set(EndpointInitializer, new ClientEndpointInitializer(store)); } + override async initialize() { + await super.initialize(); + + const discovery = this.#store.discoveryStorage; + const operationalAddress = {} as Record; + for (const key of await discovery.keys()) { + operationalAddress[key] = await discovery.get(key); + } + this.#operationalAddress = operationalAddress as Partial; + } + get address() { return this.#address; } + get operationalAddress(): OperationalPeer { + return { ...this.#operationalAddress, address: this.#address }; + } + + updateOperationalAddress(operationalAddress: Partial) { + operationalAddress = { ...operationalAddress }; + delete operationalAddress.address; + this.#operationalAddress = operationalAddress; + return this.#store.discoveryStorage.set(operationalAddress as Record); + } + override get lifecycle() { return super.lifecycle as ClientNodeLifecycle; } diff --git a/packages/node/src/node/Nodes.ts b/packages/node/src/node/Nodes.ts index 532b0f84c3..264b2b1f1e 100644 --- a/packages/node/src/node/Nodes.ts +++ b/packages/node/src/node/Nodes.ts @@ -5,8 +5,17 @@ */ import { EndpointContainer } from "#endpoint/properties/EndpointContainer.js"; -import { Construction } from "#general"; -import { CertificateAuthority } from "@matter/protocol"; +import { Construction, InternalError } from "#general"; +import { + ControllerCommissioner, + Fabric, + FabricAuthority, + FabricAuthorityConfigurationProvider, + OperationalPeer, + PeerAddress, + PeerCommissioningOptions, + PeerStore, +} from "#protocol"; import { ClientNode } from "./ClientNode.js"; import { type Node } from "./Node.js"; import { ServerNode } from "./ServerNode.js"; @@ -15,8 +24,9 @@ import { ServerNodeStore } from "./storage/ServerNodeStore.js"; /** * Manages the set of known endpoints that share a fabric with a {@link Node}. */ -export class Nodes extends EndpointContainer { +export class Nodes extends EndpointContainer { #construction: Construction; + #controllerFabric?: Fabric; get construction() { return this.#construction; @@ -25,6 +35,8 @@ export class Nodes extends EndpointContainer { constructor(owner: ServerNode) { super(owner); + this.#configureController(); + this.#construction = Construction(this, async () => { const stores = await this.endpoint.env.get(ServerNodeStore).allPeerStores(); for (const store of stores) { @@ -38,13 +50,95 @@ export class Nodes extends EndpointContainer { }); } + override get(id: string | PeerAddress) { + if (typeof id !== "string") { + id = PeerAddress(id).toString(); + } + return super.get(id); + } + + set controllerFabric(fabric: Fabric) { + this.#controllerFabric = fabric; + } + override get endpoint() { return super.endpoint as ServerNode; } - async commission() { + async commission(options: Nodes.CommissioningOptions) { await this.#construction; - const ca = await this.endpoint.env.load(CertificateAuthority); + let commissionerOptions: PeerCommissioningOptions; + if (options.fabric !== undefined) { + commissionerOptions = options as PeerCommissioningOptions; + } else { + let controllerFabric = this.#controllerFabric; + if (controllerFabric === undefined) { + controllerFabric = this.#controllerFabric = await this.endpoint.env + .get(FabricAuthority) + .defaultFabric(); + } + commissionerOptions = { + ...options, + fabric: controllerFabric, + }; + } + + const commissioner = this.endpoint.env.get(ControllerCommissioner); + + const address = await commissioner.commission(commissionerOptions); + const node = this.get(address); + if (node === undefined) { + throw new InternalError(`Commissioned node ${PeerAddress(address)} but no ClientNode installed`); + } + + return node; + } + + #configureController() { + const { endpoint: owner } = this; + + if (!owner.env.has(FabricAuthorityConfigurationProvider)) { + owner.env.set( + FabricAuthorityConfigurationProvider, + new (class extends FabricAuthorityConfigurationProvider { + get vendorId() { + return owner.state.basicInformation.vendorId; + } + })(), + ); + } + + const nodes = this; + + owner.env.set( + PeerStore, + new (class extends PeerStore { + async loadPeers() { + await nodes.construction; + return [...nodes].map(node => ({ address: node.address })); + } + + async updatePeer(peer: OperationalPeer) { + await nodes.construction; + const node = nodes.get(peer.address); + await node?.updateOperationalAddress(peer); + } + + async deletePeer(address: PeerAddress) { + await nodes.construction; + const node = nodes.get(address); + if (node) { + nodes.delete(node); + } + } + })(), + ); + } +} + +export namespace Nodes { + export interface CommissioningOptions extends Omit { + fabric?: Fabric; } } diff --git a/packages/node/src/node/ServerNode.ts b/packages/node/src/node/ServerNode.ts index 1bf1a9c035..f66dff69f3 100644 --- a/packages/node/src/node/ServerNode.ts +++ b/packages/node/src/node/ServerNode.ts @@ -11,15 +11,13 @@ import { ProductDescriptionServer } from "#behavior/system/product-description/P import { SessionsBehavior } from "#behavior/system/sessions/SessionsBehavior.js"; import { Endpoint } from "#endpoint/Endpoint.js"; import { EndpointServer } from "#endpoint/EndpointServer.js"; -import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js"; import type { Environment } from "#general"; import { Construction, DiagnosticSource, Identity, MatterError, asyncNew, errorOf } from "#general"; import { EventHandler, FabricManager, SessionManager } from "#protocol"; import { RootEndpoint as BaseRootEndpoint } from "../endpoints/root.js"; import { Node } from "./Node.js"; import { Nodes } from "./Nodes.js"; -import { IdentityService } from "./server/IdentityService.js"; -import { ServerEndpointInitializer } from "./server/ServerEndpointInitializer.js"; +import { ServerEnvironment } from "./server/ServerEnvironment.js"; import { ServerNodeStore } from "./storage/ServerNodeStore.js"; /** @@ -99,9 +97,7 @@ export class ServerNode Date: Mon, 7 Oct 2024 11:07:48 -0700 Subject: [PATCH 06/11] Do not display prebuild messages --- packages/tools/src/building/builder.ts | 10 +++++----- packages/tools/src/building/graph.ts | 22 +++++++--------------- packages/tools/src/building/project.ts | 5 +---- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/tools/src/building/builder.ts b/packages/tools/src/building/builder.ts index 58ba62f401..bdef51431a 100644 --- a/packages/tools/src/building/builder.ts +++ b/packages/tools/src/building/builder.ts @@ -54,12 +54,12 @@ export class Builder { return this.options.targets && this.options.targets.length > 0; } - public async configure(project: Project, progress: Progress) { + public async configure(project: Project) { if (!project.pkg.hasConfig) { return; } - await project.configure(progress); + await project.configure(); } public async build(project: Project) { @@ -89,9 +89,9 @@ export class Builder { const info: BuildInformation = {}; - const config = await project.configure(progress); + const config = await project.configure(); - await config?.before?.({ project, progress }); + await config?.before?.({ project }); // If available we use graph to access dependency API shas const graph = this.graph ?? (await Graph.forProject(project.pkg.path)); @@ -164,7 +164,7 @@ export class Builder { await this.#transpile(project, progress, Target.cjs); } - await config?.after?.({ project, progress }); + await config?.after?.({ project }); // Only update build information when there are no explicit targets so we know it's a full build if (!this.options.targets?.length) { diff --git a/packages/tools/src/building/graph.ts b/packages/tools/src/building/graph.ts index cb7fff5122..cef5102ada 100644 --- a/packages/tools/src/building/graph.ts +++ b/packages/tools/src/building/graph.ts @@ -78,18 +78,14 @@ export class Graph { const needsConfig = this.nodes.find(node => node.pkg.hasConfig); if (builder.hasClean || needsConfig) { - const progress = Package.workspace.start("Prebuild"); - try { // We clean all packages before engaging typescript because otherwise it seems to get confused if (builder.hasClean) { - await progress.run("Clean", async () => { - builder.clearClean(); - for (const node of this.nodes) { - await node.project.clean(); - node.info = {}; - } - }); + builder.clearClean(); + for (const node of this.nodes) { + await node.project.clean(); + node.info = {}; + } } // We configure each build before building so that any generated files are in place before we initiate the build @@ -97,16 +93,12 @@ export class Graph { if (!node.pkg.hasConfig) { continue; } - await progress.run(`Configure ${progress.emphasize(node.pkg.name)}`, async () => { - await builder.configure(node.project, progress); - }); + await builder.configure(node.project); } } catch (e) { - progress.shutdown(); console.error("Terminating due to prebuild error:", e); + process.exit(1); } - - progress.shutdown(); } while (toBuild.size) { diff --git a/packages/tools/src/building/project.ts b/packages/tools/src/building/project.ts index 60b8f3ca2d..078aad578d 100644 --- a/packages/tools/src/building/project.ts +++ b/packages/tools/src/building/project.ts @@ -12,7 +12,6 @@ import { platform } from "os"; import { dirname, join } from "path"; import { ignoreError } from "../util/errors.js"; import { CONFIG_PATH, Package } from "../util/package.js"; -import { Progress } from "../util/progress.js"; export const BUILD_INFO_LOCATION = "build/info.json"; @@ -119,7 +118,7 @@ export class Project { await writeFile(this.pkg.resolve(BUILD_INFO_LOCATION), JSON.stringify(info, undefined, 4)); } - async configure(progress: Progress) { + async configure() { if (this.#configured) { return this.#config; } @@ -143,7 +142,6 @@ export class Project { if (this.#config?.startup) { await this.#config?.startup({ project: this, - progress, }); } @@ -234,7 +232,6 @@ export class Project { export namespace Project { export interface Context { project: Project; - progress: Progress; } export interface Config { From 70e973afcfc7356b00481240bba5655d7f6e47f4 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Tue, 8 Oct 2024 13:12:30 -0700 Subject: [PATCH 07/11] JS-based command line tool (WIP) --- codegen/bin/repl.js | 3 - codegen/package.json | 5 +- codegen/src/repl.ts | 28 -- codegen/src/tsconfig.json | 4 +- models/package.json | 4 +- package-lock.json | 74 ++++- package.json | 3 +- packages/cli-tool/README.md | 1 + packages/cli-tool/bin/matter.js | 3 + packages/cli-tool/package.json | 69 +++++ packages/cli-tool/src/cli.ts | 21 ++ packages/cli-tool/src/commands/cat.ts | 19 ++ packages/cli-tool/src/commands/cd.ts | 28 ++ packages/cli-tool/src/commands/cwd.ts | 18 ++ packages/cli-tool/src/commands/help.ts | 14 + packages/cli-tool/src/commands/index.ts | 13 + packages/cli-tool/src/commands/ls.ts | 232 ++++++++++++++++ packages/cli-tool/src/commands/rm.ts | 25 ++ packages/cli-tool/src/commands/set.ts | 32 +++ packages/cli-tool/src/domain.ts | 221 +++++++++++++++ packages/cli-tool/src/errors.ts | 54 ++++ packages/cli-tool/src/globals.ts | 29 ++ packages/cli-tool/src/location.ts | 193 +++++++++++++ packages/cli-tool/src/parser.ts | 254 ++++++++++++++++++ packages/cli-tool/src/providers/endpoint.ts | 44 +++ packages/cli-tool/src/providers/index.ts | 9 + packages/cli-tool/src/providers/model.ts | 45 ++++ packages/cli-tool/src/providers/module.ts | 30 +++ packages/cli-tool/src/repl.ts | 168 ++++++++++++ packages/cli-tool/src/stat.ts | 121 +++++++++ packages/cli-tool/src/tsconfig.json | 27 ++ packages/cli-tool/tsconfig.json | 5 + packages/general/src/log/Diagnostic.ts | 9 +- packages/general/src/log/LogFormat.ts | 3 +- packages/general/src/util/Promises.ts | 7 +- packages/main/package.json | 6 +- packages/main/src/tsconfig.json | 4 +- packages/main/tsconfig.json | 2 +- packages/matter.js/package.json | 6 +- packages/matter.js/src/tsconfig.json | 4 +- packages/matter.js/test/tsconfig.json | 4 +- packages/matter.js/tsconfig.json | 2 +- packages/model/package.json | 4 +- packages/model/src/models/Model.ts | 28 +- .../endpoint/properties/EndpointContainer.ts | 8 + packages/nodejs-shell/package.json | 4 +- packages/nodejs-shell/src/tsconfig.json | 4 +- packages/nodejs/package.json | 4 +- packages/nodejs/src/tsconfig.json | 4 +- packages/nodejs/test/tsconfig.json | 4 +- packages/protocol/package.json | 4 +- packages/react-native/package.json | 2 +- packages/react-native/src/tsconfig.json | 4 +- packages/types/package.json | 4 +- tsconfig.json | 3 + 55 files changed, 1838 insertions(+), 82 deletions(-) delete mode 100644 codegen/bin/repl.js delete mode 100644 codegen/src/repl.ts create mode 100644 packages/cli-tool/README.md create mode 100755 packages/cli-tool/bin/matter.js create mode 100644 packages/cli-tool/package.json create mode 100644 packages/cli-tool/src/cli.ts create mode 100644 packages/cli-tool/src/commands/cat.ts create mode 100644 packages/cli-tool/src/commands/cd.ts create mode 100644 packages/cli-tool/src/commands/cwd.ts create mode 100644 packages/cli-tool/src/commands/help.ts create mode 100644 packages/cli-tool/src/commands/index.ts create mode 100644 packages/cli-tool/src/commands/ls.ts create mode 100644 packages/cli-tool/src/commands/rm.ts create mode 100644 packages/cli-tool/src/commands/set.ts create mode 100644 packages/cli-tool/src/domain.ts create mode 100644 packages/cli-tool/src/errors.ts create mode 100644 packages/cli-tool/src/globals.ts create mode 100644 packages/cli-tool/src/location.ts create mode 100644 packages/cli-tool/src/parser.ts create mode 100644 packages/cli-tool/src/providers/endpoint.ts create mode 100644 packages/cli-tool/src/providers/index.ts create mode 100644 packages/cli-tool/src/providers/model.ts create mode 100644 packages/cli-tool/src/providers/module.ts create mode 100644 packages/cli-tool/src/repl.ts create mode 100644 packages/cli-tool/src/stat.ts create mode 100644 packages/cli-tool/src/tsconfig.json create mode 100644 packages/cli-tool/tsconfig.json diff --git a/codegen/bin/repl.js b/codegen/bin/repl.js deleted file mode 100644 index 8bd06c41c2..0000000000 --- a/codegen/bin/repl.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env matter-run -import { main } from "../dist/esm/repl.js"; -await main(); diff --git a/codegen/package.json b/codegen/package.json index 2fe80a72cd..549f3e2825 100644 --- a/codegen/package.json +++ b/codegen/package.json @@ -8,7 +8,6 @@ "clean": "matter-build clean", "build": "matter-build", "build-clean": "matter-build --clean", - "console": "matter-run bin/repl.ts", "generate-spec": "matter-run bin/generate-spec.js", "generate-chip": "matter-run bin/generate-chip.js", "generate-model": "matter-run bin/generate-model.js", @@ -33,9 +32,9 @@ "homepage": "https://github.com/project-chip/matter.js#readme", "dependencies": { "@matter/general": "*", + "@matter/intermediate-models": "*", "@matter/model": "*", - "@matter/tools": "*", - "@matter/intermediate-models": "*" + "@matter/tools": "*" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/codegen/src/repl.ts b/codegen/src/repl.ts deleted file mode 100644 index e437fa25e1..0000000000 --- a/codegen/src/repl.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright 2022-2024 Matter.js Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -// Run a TypeScript REPL with key objects loaded - -import { homedir } from "os"; -import { join } from "path"; -import repl from "repl"; - -import * as model from "#model"; - -export async function main() { - (global as any).model = model; - (global as any).matter = new model.MatterModel(); - - const server = repl.start({ prompt: "matter.js > " }); - - const historyPath = process.env.MATTER_REPL_HISTORY || join(homedir(), ".matter-repl-history"); - server.setupHistory(historyPath, error => { - if (error) { - console.error(error); - process.exit(1); - } - }); -} diff --git a/codegen/src/tsconfig.json b/codegen/src/tsconfig.json index 4832da004d..182132434d 100644 --- a/codegen/src/tsconfig.json +++ b/codegen/src/tsconfig.json @@ -10,10 +10,10 @@ "path": "../../packages/general/src" }, { - "path": "../../packages/model/src" + "path": "../../models/src" }, { - "path": "../../models/src" + "path": "../../packages/model/src" } ] } \ No newline at end of file diff --git a/models/package.json b/models/package.json index 411c40d715..a486a9f9d6 100644 --- a/models/package.json +++ b/models/package.json @@ -34,7 +34,7 @@ }, "homepage": "https://github.com/project-chip/matter.js#readme", "devDependencies": { - "@matter/tools": "*", - "@matter/model": "*" + "@matter/model": "*", + "@matter/tools": "*" } } diff --git a/package-lock.json b/package-lock.json index fc8bdd4770..169496a9cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "packages/nodejs-shell", "packages/examples", "packages/react-native", + "packages/cli-tool", "models", "codegen", "chip-testing", @@ -4987,6 +4988,10 @@ "resolved": "chip-testing", "link": true }, + "node_modules/@matter/cli-tool": { + "resolved": "packages/cli-tool", + "link": true + }, "node_modules/@matter/codegen": { "resolved": "codegen", "link": true @@ -6708,6 +6713,12 @@ "@types/ssh2": "*" } }, + "node_modules/@types/escodegen": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.10.tgz", + "integrity": "sha512-IVvcNLEFbiL17qiGRGzyfx/u9K6lA5w6wcQSIgv2h4JG3ZAFIY1Be9ITTSPuARIxRpzW54s8OvcF6PdonBbDzg==", + "license": "MIT" + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -10393,6 +10404,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -10759,7 +10791,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -20652,6 +20683,47 @@ "url": "https://github.com/sponsors/wooorm" } }, + "packages/cli": { + "name": "@matter/cli", + "version": "0.0.0-git", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@matter/model": "*", + "@matter/node": "*" + }, + "bin": { + "matter": "bin/matter.js" + }, + "devDependencies": { + "@matter/tools": "*" + } + }, + "packages/cli-tool": { + "name": "@matter/cli-tool", + "version": "0.0.0-git", + "license": "Apache-2.0", + "dependencies": { + "@matter/general": "*", + "@matter/model": "*", + "@matter/node": "*", + "@matter/protocol": "*", + "@matter/tools": "*", + "@matter/types": "*", + "@types/escodegen": "^0.0.10", + "acorn": "^8.12.1", + "ansi-colors": "^4.1.3", + "escodegen": "^2.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "matter": "bin/matter.js" + }, + "devDependencies": { + "@matter/tools": "*", + "@types/yargs": "^17.0.33" + } + }, "packages/examples": { "name": "@matter/examples", "version": "0.0.0-git", diff --git a/package.json b/package.json index c2c6a335da..73e3e276c0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "packages/nodejs-shell", "packages/examples", "packages/react-native", + "packages/cli-tool", "models", "codegen", "chip-testing", @@ -40,7 +41,7 @@ "matter-multidevice": "matter-run packages/examples/src/examples/MultiDeviceNode.ts", "matter-controller": "matter-run packages/examples/src/examples/ControllerNode.ts", "shell": "matter-run packages/nodejs-shell/src/app.ts" -}, + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.18.0", diff --git a/packages/cli-tool/README.md b/packages/cli-tool/README.md new file mode 100644 index 0000000000..08c0e23757 --- /dev/null +++ b/packages/cli-tool/README.md @@ -0,0 +1 @@ +A matter.js-based command line utility for interacting with Matter. diff --git a/packages/cli-tool/bin/matter.js b/packages/cli-tool/bin/matter.js new file mode 100755 index 0000000000..5313f4cae9 --- /dev/null +++ b/packages/cli-tool/bin/matter.js @@ -0,0 +1,3 @@ +#!/usr/bin/env matter-run +import { main } from "../dist/esm/cli.js"; +await main(process.argv); diff --git a/packages/cli-tool/package.json b/packages/cli-tool/package.json new file mode 100644 index 0000000000..97c4d53ede --- /dev/null +++ b/packages/cli-tool/package.json @@ -0,0 +1,69 @@ +{ + "name": "@matter/cli-tool", + "version": "0.0.0-git", + "description": "Command line tool for interacting with Matter", + "keywords": [ + "iot", + "home automation", + "matter", + "smart device" + ], + "license": "Apache-2.0", + "author": "matter.js authors", + "contributors": [ + "Greg Lauckhart " + ], + "bugs": { + "url": "https://github.com/project-chip/matter.js/issues" + }, + "homepage": "https://github.com/project-chip/matter.js", + "repository": { + "type": "git", + "url": "git+https://github.com/project-chip/matter.js.git" + }, + "scripts": { + "clean": "matter-build clean", + "build": "matter-build", + "build-clean": "matter-build --clean", + "matter": "matter-run bin/matter.js" + }, + "bin": { + "matter": "bin/matter.js" + }, + "imports": { + "#tools": "@matter/tools", + "#general": "@matter/general", + "#model": "@matter/model", + "#types": "@matter/types", + "#protocol": "@matter/protocol", + "#node": "@matter/node", + "#*": "./src/*" + }, + "dependencies": { + "@matter/general": "*", + "@matter/model": "*", + "@matter/node": "*", + "@matter/protocol": "*", + "@matter/tools": "*", + "@matter/types": "*", + "@types/escodegen": "^0.0.10", + "acorn": "^8.12.1", + "ansi-colors": "^4.1.3", + "escodegen": "^2.1.0", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@matter/tools": "*", + "@types/yargs": "^17.0.33" + }, + "files": [ + "dist/**/*", + "src/**/*", + "LICENSE", + "README.md" + ], + "type": "module", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli-tool/src/cli.ts b/packages/cli-tool/src/cli.ts new file mode 100644 index 0000000000..dc277f6485 --- /dev/null +++ b/packages/cli-tool/src/cli.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { repl } from "#repl.js"; +import colors from "ansi-colors"; +import { stdout } from "process"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +export async function main(argv: string[]) { + colors.enabled = stdout.isTTY; + + await yargs(hideBin(argv)).usage("Interact with the local Matter environment.").strict().argv; + + // TODO - REPL is enough for testing but need proper CLI + + await repl(); +} diff --git a/packages/cli-tool/src/commands/cat.ts b/packages/cli-tool/src/commands/cat.ts new file mode 100644 index 0000000000..41fb1e6cd7 --- /dev/null +++ b/packages/cli-tool/src/commands/cat.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { bin } from "#globals.js"; +import { stdout } from "process"; +import { inspect } from "util"; + +bin.cat = async function (...args: unknown[]) { + const locations = await Promise.all(args.map(arg => this.location.at(`${arg}`))); + for (const location of locations) { + stdout.write(inspect(location.definition, false, 1, stdout.isTTY)); + stdout.write("\n"); + } +}; + +bin.inspect = bin.cat; diff --git a/packages/cli-tool/src/commands/cd.ts b/packages/cli-tool/src/commands/cd.ts new file mode 100644 index 0000000000..0c60a3af13 --- /dev/null +++ b/packages/cli-tool/src/commands/cd.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotADirectoryError, TooManyArgsError } from "#errors.js"; +import { bin } from "#globals.js"; + +bin.cd = async function (path, ...extraArgs: unknown[]) { + if (extraArgs.length) { + throw new TooManyArgsError("cd"); + } + + if (path === undefined) { + path = "/"; + } else { + path = `${path}`; + } + + const location = await this.location.at(path as string); + + if (location.kind !== "directory") { + throw new NotADirectoryError(path); + } + + this.location = location; +}; diff --git a/packages/cli-tool/src/commands/cwd.ts b/packages/cli-tool/src/commands/cwd.ts new file mode 100644 index 0000000000..0858720907 --- /dev/null +++ b/packages/cli-tool/src/commands/cwd.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TooManyArgsError } from "#errors.js"; +import { bin } from "#globals.js"; +import { stdout } from "process"; + +bin.cwd = function (...args: unknown[]) { + if (args.length) { + throw new TooManyArgsError("cwd"); + } + + stdout.write(this.location.path); + stdout.write("\n"); +}; diff --git a/packages/cli-tool/src/commands/help.ts b/packages/cli-tool/src/commands/help.ts new file mode 100644 index 0000000000..3a7192cdc8 --- /dev/null +++ b/packages/cli-tool/src/commands/help.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { bin } from "#globals.js"; + +bin.help = function () { + // TODO - be helpful + process.stdout.write("Help is on the way!\n"); +}; + +bin.man = bin.help; diff --git a/packages/cli-tool/src/commands/index.ts b/packages/cli-tool/src/commands/index.ts new file mode 100644 index 0000000000..778c012a71 --- /dev/null +++ b/packages/cli-tool/src/commands/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import "./cat.js"; +import "./cd.js"; +import "./cwd.js"; +import "./help.js"; +import "./ls.js"; +import "./rm.js"; +import "./set.js"; diff --git a/packages/cli-tool/src/commands/ls.ts b/packages/cli-tool/src/commands/ls.ts new file mode 100644 index 0000000000..25cead7052 --- /dev/null +++ b/packages/cli-tool/src/commands/ls.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { bin } from "#globals.js"; +import { Location } from "#location.js"; +import colors from "ansi-colors"; +import { stdout } from "process"; +import yargs from "yargs"; + +bin.ls = async function (...rawArgs: unknown[]) { + const args = await yargs(rawArgs.map(arg => `${arg}`)) + .usage("Show available objects (objects in the current path by default)") + .option("l", { type: "boolean", description: "use a long listing format" }) + .exitProcess(false) + .parse(); + + const locations = Array(); + for (const str of args._) { + const input = `${str}`; + locations.push(DisplayLocation(await this.location.at(input), input)); + } + + const files = Array(); + const dirs = Array(); + + if (locations.length) { + for (const location of locations) { + if (location.kind === "directory") { + dirs.push(location); + } else { + files.push(location); + } + } + } else { + for (const basename of await this.location.paths) { + files.push(DisplayLocation(await this.location.at(basename))); + } + } + + let displayedSomething = false; + + if (files.length) { + display(files, ""); + displayedSomething = true; + } + + for (const dir of dirs) { + if (displayedSomething) { + stdout.write("\n"); + } + let linePrefix; + if (displayedSomething || dirs.length > 1) { + stdout.write(`${dir.displayName ?? dir.basename}:\n`); + linePrefix = " "; + } else { + linePrefix = ""; + } + display( + (await Promise.all((await dir.paths).map(path => dir.at(path)))).map(location => DisplayLocation(location)), + linePrefix, + ); + } + + function display(files: DisplayLocation[], linePrefix: string) { + if (args.l) { + displayList(files, linePrefix); + } else { + const { grid, colWidth } = gridFor(files, linePrefix); + displayGrid(grid, colWidth, linePrefix); + } + } + + function displayList(files: DisplayLocation[], linePrefix: string) { + for (const location of files) { + const { name, length } = formatName(location); + + let charsAvailable = stdout.columns ?? 80; + + const { name: longName, tag } = location; + + stdout.write(linePrefix); + stdout.write(name); + stdout.write(" "); + stdout.write(colors.dim(tag)); + charsAvailable -= length + tag.length + linePrefix.length + 1; + + let { id, summary } = location; + + if (id !== undefined) { + id = `#${id}`; + stdout.write(colors.dim(id)); + charsAvailable -= id.length + 1; + } + + if (longName) { + stdout.write(' "'); + stdout.write(colors.cyan(longName)); + stdout.write('"'); + charsAvailable -= longName.length + 3; + } + + if (summary !== undefined && charsAvailable > 10) { + stdout.write(" "); + charsAvailable -= 1; + + summary = summary.split("\n")[0].trim(); + if (summary.length > charsAvailable) { + summary = summary.slice(0, charsAvailable - 1) + "…"; + } + + stdout.write(summary); + } + stdout.write("\n"); + } + } + + function gridFor(files: DisplayLocation[], linePrefix: string) { + const columns = (stdout.columns ?? 80) - linePrefix.length; + + // Maximum width of columns + 1 for special char and +2 for between columns + const maxWidth = maxWidthOf(files) + 3; + + let colCount = Math.floor(columns / maxWidth); + if (colCount === 0) { + colCount = 1; + } + + const rowCount = Math.ceil(files.length / colCount); + + const grid = Array(); + + for (let i = 0; i < files.length; i++) { + const colNum = Math.floor(i / rowCount); + const rowNum = i - colNum * rowCount; + (grid[rowNum] ??= [])[colNum] = files[i]; + } + + const colWidth = Math.floor(columns / colCount); + + return { grid, colWidth }; + } + + function displayGrid(grid: Array, colWidth: number, linePrefix: string) { + for (const row of grid) { + stdout.write(linePrefix); + for (const location of row) { + const { name, length } = formatName(location); + stdout.write(name); + stdout.write("".padStart(colWidth - length)); + } + stdout.write("\n"); + } + } + + function formatName(location: DisplayLocation) { + const name = location.displayName ?? location.basename; + const length = name.length; + + if (location.kind === "directory") { + return { name: `${colors.blue(name)}/`, length: length + 1 }; + } + + if (location.kind === "command") { + return { name: `${colors.green(name)}*`, length: length + 1 }; + } + + return { name, length }; + } + + function maxWidthOf(locations: DisplayLocation[]) { + return Math.max(...locations.map(file => (file.displayName ?? file.basename).length)); + } +}; + +export interface DisplayLocation extends Location { + displayName: string; +} + +function DisplayLocation(location: Location, displayName?: string): DisplayLocation { + return { + get kind() { + return location.kind; + }, + + get basename() { + return location.basename; + }, + + get name() { + return location.name; + }, + + get id() { + return location.id; + }, + + get path() { + return location.path; + }, + + get paths() { + return location.paths; + }, + + get tag() { + return location.tag; + }, + + get summary() { + return location.summary; + }, + + get definition() { + return location.definition; + }, + + get displayName() { + return displayName ?? location.basename; + }, + + get at() { + return location.at; + }, + + get maybeAt() { + return location.maybeAt; + }, + }; +} diff --git a/packages/cli-tool/src/commands/rm.ts b/packages/cli-tool/src/commands/rm.ts new file mode 100644 index 0000000000..dcacf701a9 --- /dev/null +++ b/packages/cli-tool/src/commands/rm.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InvalidArgumentError } from "#errors.js"; +import { bin } from "#globals.js"; +import { Location } from "#location.js"; + +bin.rm = async function (...paths: unknown[]) { + const toDelete = Array(); + + for (const path of paths) { + const location = await this.location.at(`${path}`); + if (!location.parent) { + throw new InvalidArgumentError(`Invalid argument: Can't delete ${location.path}`); + } + toDelete.push(location); + } + + for (const location of toDelete) { + delete (location.parent!.definition as Record)[location.basename]; + } +}; diff --git a/packages/cli-tool/src/commands/set.ts b/packages/cli-tool/src/commands/set.ts new file mode 100644 index 0000000000..74078b3649 --- /dev/null +++ b/packages/cli-tool/src/commands/set.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InvalidArgumentError, TooManyArgsError } from "#errors.js"; +import { bin } from "#globals.js"; +import { Environment, VariableService } from "@matter/general"; + +bin.set = function (keyOrAssignment: unknown, value: unknown) { + switch (arguments.length) { + case 0: + return Environment.default.vars.vars; + + case 1: + const assignment = `${keyOrAssignment}`; + const equalPos = assignment.indexOf("="); + if (equalPos === -1) { + throw new InvalidArgumentError(`Invalid argument: parameter must be of the form key=value`); + } + Environment.default.vars.set(assignment.slice(0, equalPos), assignment.slice(equalPos + 1)); + break; + + case 2: + Environment.default.vars.set(`${keyOrAssignment}`, value as VariableService.Value); + break; + + default: + throw new TooManyArgsError("set"); + } +}; diff --git a/packages/cli-tool/src/domain.ts b/packages/cli-tool/src/domain.ts new file mode 100644 index 0000000000..3ca8718e5f --- /dev/null +++ b/packages/cli-tool/src/domain.ts @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BadCommandError, IncompleteError, NotACommandError, NotADirectoryError, NotFoundError } from "#errors.js"; +import { InternalError, MaybePromise } from "#general"; +import { bin, globals as defaultGlobals } from "#globals.js"; +import { Location } from "#location.js"; +import { parseInput } from "#parser.js"; +import { Directory } from "#stat.js"; +import { createContext, runInContext, RunningCodeOptions } from "vm"; + +export interface Domain { + location: Location; + execute(input: string): Promise; +} + +/** + * Maintains state and executes commands. + */ +export function Domain(): Domain { + const hiddenGlobals = Object.keys(globalThis); + + const globals: Record = Object.defineProperties( + {}, + { + ...Object.getOwnPropertyDescriptors(globalThis), + ...Object.getOwnPropertyDescriptors(defaultGlobals), + bin: { value: { ...bin }, enumerable: true, writable: false, configurable: false }, + }, + ); + + globals.global = globals.globalThis = globals; + + const domain: Domain = { + location: Location( + "", + globals, + Directory({ + paths() { + const result = new Set(Object.keys(globals)); + for (const name of hiddenGlobals) { + result.delete(name); + } + return [...result]; + }, + + definitionAt(path: string) { + // Crypto has specialized constraints on its "this" + if (path === "crypto") { + return globalThis.crypto; + } + return globals[path]; + }, + }), + undefined, + ), + + async execute(inputStr: string) { + const input = parseInput(inputStr); + + switch (input.kind) { + case "empty": + return; + + case "incomplete": + throw new IncompleteError(input.error); + + case "command": + break; + + case "statement": + return await evaluate(input.js); + + default: + throw new InternalError(`Unknown internal command type ${(input as any).kind}`); + } + + const { name, args } = input; + + let location; + try { + location = await this.location.at(name); + } catch (e) { + if ((e instanceof NotFoundError || e instanceof NotADirectoryError) && name.indexOf("/") === -1) { + // "path" search + try { + location = await this.location.at(Location.join("/bin", name)); + } catch (e2) { + if (e instanceof NotFoundError || e instanceof NotADirectoryError) { + // Throw original error + throw e; + } + throw e2; + } + } else { + throw e; + } + } + + const fn = location?.definition; + + if (location === undefined || fn === undefined) { + throw new NotACommandError(name); + } + + if (typeof fn !== "function") { + if (args.length) { + // If there are arguments it must be a call; otherwise it's just inspection + throw new BadCommandError(name); + } + return fn; + } + + const argvals = args.map(arg => { + return evaluate(arg.js, { + lineOffset: arg.line - 1, + columnOffset: arg.column, + }); + }); + + let scope = location.parent?.definition ?? globals; + if (scope === globals.bin) { + scope = this; + } + + return fn.apply(scope, argvals); + }, + }; + + const context = createContext( + new Proxy( + {}, + { + get(_target, key, _receiver) { + if (!(typeof key === "string")) { + return; + } + const value = domain.location.maybeAt(key); + + if (MaybePromise.is(value)) { + return value.then(value => { + if (value === undefined) { + return globals[key]; + } + return value.definition; + }); + } + + if (value === undefined) { + return globals[key]; + } + + return value.definition; + }, + + set(target, key, newValue, _receiver) { + const { definition } = domain.location; + if (definition === undefined || definition === null) { + // Shouldn't happen + return false; + } + target = definition; + (target as any)[key] = newValue; + return true; + }, + + has(_target, key) { + const { definition } = domain.location; + if (definition === undefined || definition === null || typeof definition !== "object") { + // Shouldn't happen + return false; + } + return key in definition; + }, + + defineProperty(_target, property, attributes) { + Object.defineProperty(domain.location.definition, property, attributes); + return true; + }, + + deleteProperty(_target, property) { + const { definition } = domain.location; + if (definition === undefined || definition === null) { + // Shouldn't happen + return false; + } + Reflect.deleteProperty(definition, property); + return true; + }, + + ownKeys(_target) { + return [ + ...new Set([...Reflect.ownKeys(globals), ...Reflect.ownKeys(domain.location.definition as {})]), + ]; + }, + + getOwnPropertyDescriptor(_target, p) { + const descriptor = Reflect.getOwnPropertyDescriptor(domain.location.definition as {}, p); + if (descriptor) { + return descriptor; + } + return Reflect.getOwnPropertyDescriptor(globals, p); + }, + }, + ), + ); + + return domain; + + function evaluate(js: string, options: RunningCodeOptions = {}) { + return runInContext(js, context, { + breakOnSigint: true, + filename: "matter-cli-eval", + displayErrors: false, + ...options, + }); + } +} diff --git a/packages/cli-tool/src/errors.ts b/packages/cli-tool/src/errors.ts new file mode 100644 index 0000000000..246384464e --- /dev/null +++ b/packages/cli-tool/src/errors.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MatterError } from "#general"; + +export class CliError extends MatterError { + // Error class won't be the same across vm contexts so make detection easier + isCliError = true; +} + +export class NotFoundError extends CliError { + constructor(what: unknown) { + super(`Not found: ${what}`); + } +} + +export class NotADirectoryError extends CliError { + constructor(what: unknown) { + super(`Not a directory: ${what}`); + } +} + +export class BadCommandError extends CliError { + constructor(what: unknown) { + super(`Bad command: ${what}`); + } +} + +export class NotACommandError extends CliError { + constructor(what: unknown) { + super(`Not a command: ${what}`); + } +} + +export class TooManyArgsError extends CliError { + constructor(what: string) { + super(`Too many arguments: ${what}`); + } +} + +export class InvalidArgumentError extends CliError {} + +/** + * Thrown when statement appears incomplete and REPL should wait for more input. + */ +export class IncompleteError extends Error { + constructor(cause: Error) { + super(cause.message, { cause }); + this.cause = cause; + } +} diff --git a/packages/cli-tool/src/globals.ts b/packages/cli-tool/src/globals.ts new file mode 100644 index 0000000000..d388f24aa8 --- /dev/null +++ b/packages/cli-tool/src/globals.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Domain } from "#domain.js"; +import * as general from "#general"; +import * as model from "#model"; +import { Matter as matter } from "#model"; +import * as node from "#node"; +import * as protocol from "#protocol"; +import * as tools from "#tools"; +import * as types from "#types"; + +export type GlobalCommand = (this: Domain, ...args: unknown[]) => unknown; + +export const bin: Record = {}; + +export const globals: Record = { + general, + tools, + protocol, + node, + types, + matter, + model, + bin, +}; diff --git a/packages/cli-tool/src/location.ts b/packages/cli-tool/src/location.ts new file mode 100644 index 0000000000..300b44d00d --- /dev/null +++ b/packages/cli-tool/src/location.ts @@ -0,0 +1,193 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotADirectoryError, NotFoundError } from "#errors.js"; +import { decamelize, MaybePromise } from "#general"; +import { Stat } from "#stat.js"; + +/** + * We support a quick-and-dirty virtual "filesystem" for navigation of our object model. + * + * This is the interface to this filesystem. + */ +export interface Location { + kind: "directory" | "command" | "file"; + basename: string; + path: string; + tag: string; + id?: string | number; + name?: string; + summary?: string; + parent?: Location; + definition: unknown; + paths: MaybePromise; + at(path: string, searchedAs?: string): MaybePromise; + maybeAt(path: string, searchedAs?: string): MaybePromise; +} + +function isClass(fn: {}) { + return !Object.getOwnPropertyDescriptor(fn, "prototype")?.writable; +} + +export function Location(basename: string, definition: unknown, stat: Stat, parent: undefined | Location): Location { + let { tag } = stat; + + if (tag === undefined) { + if (definition === undefined) { + tag = "undefined"; + } else if (typeof definition === "object") { + if (definition === null) { + tag = "null"; + } else if (Symbol.toStringTag in definition) { + tag = `${definition[Symbol.toStringTag]}`; + } else if (Array.isArray(definition)) { + tag = "array"; + } else if (ArrayBuffer.isView(definition)) { + tag = "bytes"; + } else if (definition && definition.constructor.name !== "Object") { + tag = definition.constructor.name; + } else { + tag = "object"; + } + } else if (typeof definition === "function") { + if (isClass(definition)) { + tag = "constructor"; + } else { + tag = "function"; + } + } else { + tag = typeof definition; + } + } + + tag = decamelize(tag); + + return { + kind: typeof definition === "function" && !isClass(definition) ? "command" : stat.kind, + basename, + name: stat.name, + summary: stat.summary, + id: stat.id, + tag, + parent, + definition, + + get path() { + if (!this.parent) { + return "/"; + } + + let path = ""; + let location = this; + while (location.parent) { + path = `/${location.basename}${path}`; + location = location.parent; + } + return path; + }, + + get paths() { + if (stat.kind !== "directory") { + throw new NotADirectoryError(this.path); + } + return stat.paths; + }, + + at(path, searchedAs): MaybePromise { + const location = this.maybeAt(path); + + if (MaybePromise.is(location)) { + return location.then(accept); + } + + return accept(location); + + function accept(location: Location | undefined) { + if (location === undefined) { + throw new NotFoundError(searchedAs ?? path); + } + + return location; + } + }, + + maybeAt(path, searchedAs): MaybePromise { + if (stat.kind !== "directory") { + throw new NotADirectoryError(searchedAs ?? path); + } + + if (path === undefined || path === "") { + return this; + } + + const segments = path.split("/"); + + // Handle absolute path. Does not happen on recursion + if (segments[0] === "") { + let location = this; + while (location.parent) { + location = location.parent; + } + return location.maybeAt(segments.slice(1).join("/"), "/"); + } + + while (segments[0] === "" || segments[0] === ".") { + searchedAs += `${segments.shift()}/`; + } + + if (segments[0] === "..") { + return (this.parent ?? this).maybeAt( + segments.slice(1).join("/"), + searchedAs ? Location.join(searchedAs, "..") : "..", + ); + } + + if (!segments.length) { + return this; + } + + const subsearchedAs = searchedAs ? Location.join(searchedAs, segments[0]) : segments[0]; + const definition = stat.definitionAt(decodeURIComponent(segments[0])); + + const accept = (definition: unknown) => { + if (definition === undefined || definition === null) { + if (segments.length > 1) { + throw new NotFoundError(subsearchedAs); + } + return; + } + + const sublocation = Location(segments[0], definition, Stat.of(definition), this); + if (segments.length === 1) { + return sublocation; + } + + return sublocation.at(segments.slice(1).join("/"), subsearchedAs); + }; + + if (MaybePromise.is(definition)) { + return definition.then(accept); + } + + return accept(definition); + }, + }; +} + +export namespace Location { + export function join(path: string, ...paths: string[]) { + for (const other of paths) { + if (other.startsWith("/")) { + path = other; + } else if (path.endsWith("/")) { + path = `${path}${other}`; + } else { + path = `${path}/${other}`; + } + } + return path; + } +} diff --git a/packages/cli-tool/src/parser.ts b/packages/cli-tool/src/parser.ts new file mode 100644 index 0000000000..e1aa4d8777 --- /dev/null +++ b/packages/cli-tool/src/parser.ts @@ -0,0 +1,254 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLineInfo, Options, parse, parseExpressionAt } from "acorn"; +import { generate } from "escodegen"; + +/** + * The result of parsing our command dialect. + * + * We support an expanded JS dialect that allows function invocation with the syntax "command [...text|expr]*". This + * allows us to support both JS less verbose "shell command" syntax that nonetheless supports full JS semantics. + * + * Differentiating between "shell commands" and JS is a bit fiddly but from a high level: + * + * - If it starts with a keyword that starts a JS statement or assignment then it is a statement + * - If it starts with a path (e.g. "foo", "foo/bar", "./foo/bar", etc.) it is a command + * - Otherwise it is a JS statement + */ +export type Input = EmptyInput | CommandInput | StatementInput | IncompleteInput; + +export interface EmptyInput { + kind: "empty"; +} + +export interface CommandArg { + line: number; + column: number; + js: string; +} + +export interface CommandInput { + kind: "command"; + name: string; + args: CommandArg[]; +} + +export interface StatementInput { + kind: "statement"; + js: string; +} + +export interface IncompleteInput { + kind: "incomplete"; + error: SyntaxError; +} + +const ACORN_PARSE_OPTIONS: Options = { + ecmaVersion: "latest", + allowAwaitOutsideFunction: true, +}; + +/** + * Translate user input into a {@link Input}. + */ +export function parseInput(input: string): Input { + const inputIsCommand = isCommand(input); + try { + if (inputIsCommand) { + return parseCommand(input); + } + + // Only parse so we can detect incomplete statements; otherwise let node do the work + parse(input, ACORN_PARSE_OPTIONS); + return { kind: "statement", js: input }; + } catch (e) { + if (errorIndicatesIncomplete(e, input)) { + return { kind: "incomplete", error: e }; + } + if (!inputIsCommand) { + // Let node throw its own error + return { kind: "statement", js: input }; + } + throw e; + } +} + +function errorIndicatesIncomplete(e: any, input: string): e is SyntaxError { + if (e instanceof SyntaxError) { + if (e.message.startsWith("Unexpected token") && (e as unknown as { pos: number }).pos === input.length) { + return true; + } + if (e.message.match(/$Unterminated (?:template literal|group|comment|regular expression|template)/)) { + return true; + } + } + return false; +} + +function parseCommand(command: string): Input { + // Find beginning of name + let start = 0; + while (start < command.length && command[start].match(/\s/)) { + start++; + } + + if (start === command.length) { + return { kind: "empty" }; + } + + // Find end of name. We identified command with regexp so this can be simplistic + const nameStart = start; + while (start < command.length && !command[start].match(/\s/)) { + start++; + } + + // We now know the command name + const name = command.slice(nameStart, start); + + // Extract arguments. These are either unadorned text + const args = Array(); + while (command.trim().length) { + while (start < command.length && command[start].match(/^\s/)) { + start++; + } + + if (start === command.length) { + break; + } + + let isExpression; + switch (command[start]) { + case "(": + case "[": + case "{": + case '"': + case "`": + case "'": + isExpression = true; + break; + + default: + isExpression = false; + break; + } + + const { line, column } = getLineInfo(command, start); + + if (isExpression) { + try { + const ast = parseExpressionAt(command, start, ACORN_PARSE_OPTIONS); + args.push({ js: generate(ast), line, column }); + start = ast.end; + } catch (e) { + // acorn sticks error onto end of message; move it into the stack like Node would + if (e instanceof SyntaxError) { + let message = e.message; + const match = message.match(/(.*) \(\d+:\d+\)$/); + let location; + if (match) { + message = match[1]; + location = message[2]; + } else { + const loc = getLineInfo(command, start); + location = `${loc.line}:${loc.column}`; + } + const error = new SyntaxError(message); + error.stack = `SyntaxError: ${message}\n at matter-cli-input:${location}`; + throw error; + } + } + } else { + let end = start; + + while (end < command.length) { + if (command[end].match(/\s/)) { + break; + } + end++; + } + + const str = command.slice(start, end); + start = end; + + let js; + if (str === "true" || str === "false" || str === "NaN" || str === "Infinity" || isNumeric(str)) { + js = str; + } else { + js = JSON.stringify(str); + } + + args.push({ js, line, column }); + } + } + + return { kind: "command", name, args }; +} + +export function isCommand(input: string) { + if (input.match(isCommand.STATEMENT_DETECTOR)) { + return false; + } + + if (input.match(isCommand.COMMAND_DETECTOR)) { + return true; + } + + return false; +} + +export namespace isCommand { + const STATEMENT_KEYWORDS = [ + "async", + "break", + "class", + "const", + "debugger", + "delete", + "do", + "for", + "function", + "if", + "new", + "return", + "switch", + "throw", + "try", + "let", + "while", + "with", + "export", + "import", + "var", + "void", + "true", + "false", + ]; + + const IDENTIFIER = "[\\p{L}$_][\\p{L}$_0-9]*"; + const EOW = "(?:\\s|$)"; + + const statementStarts = [...STATEMENT_KEYWORDS.map(kw => `${kw}${EOW}`), `${IDENTIFIER}\\s*=`]; + + // If this regexp matches, input is NOT a command + export const STATEMENT_DETECTOR = new RegExp(`^\\s*(?:${statementStarts.join("|")})`, "u"); + + // If above regexp does not match but this one does then input IS a command + export const COMMAND_DETECTOR = new RegExp( + `^\\s*(?:\\.?/)?${IDENTIFIER}(?:\\/(?:\\.?\\.|${IDENTIFIER}))*${EOW}`, + "u", + ); +} + +/** + * {@see https://stackoverflow.com/questions/175739/how-can-i-check-if-a-string-is-a-valid-number} + */ +function isNumeric(str: string) { + return ( + !isNaN(str as unknown as number) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... + !isNaN(parseFloat(str)) + ); // ...and ensure strings of whitespace fail +} diff --git a/packages/cli-tool/src/providers/endpoint.ts b/packages/cli-tool/src/providers/endpoint.ts new file mode 100644 index 0000000000..528c86f028 --- /dev/null +++ b/packages/cli-tool/src/providers/endpoint.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Endpoint } from "#node"; +import { Directory, Stat } from "#stat.js"; + +Stat.provide(endpoint => { + if (!isEndpoint(endpoint)) { + return; + } + + return Directory({ + definitionAt(path) { + const part = endpoint.parts.get(path); + + if (part) { + if (!part.lifecycle.isReady) { + return part.construction.then(() => Stat.of(part)); + } + return Stat.of(part); + } + }, + + paths() { + if (!endpoint.lifecycle.isPartsReady) { + return endpoint.construction.then(() => endpoint.parts.map(part => part.id)); + } + return endpoint.parts.map(part => part.id); + }, + }); +}); + +function isEndpoint(item: unknown): item is Endpoint { + return item instanceof Endpoint; +} diff --git a/packages/cli-tool/src/providers/index.ts b/packages/cli-tool/src/providers/index.ts new file mode 100644 index 0000000000..27c66da3df --- /dev/null +++ b/packages/cli-tool/src/providers/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./endpoint.js"; +export * from "./model.js"; +export * from "./module.js"; diff --git a/packages/cli-tool/src/providers/model.ts b/packages/cli-tool/src/providers/model.ts new file mode 100644 index 0000000000..4251ceac2f --- /dev/null +++ b/packages/cli-tool/src/providers/model.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Model } from "#model"; +import { Directory, Stat } from "#stat.js"; + +Stat.provide(model => { + if (!isModel(model)) { + return; + } + + const details: Stat.Base = { + name: model.description, + summary: model.details, + id: model.id, + tag: model.tag, + }; + + if (!model.children.length) { + return { + kind: "file", + ...details, + }; + } + + return Directory({ + ...details, + paths() { + return model.children.map(child => child.name); + }, + definitionAt(name: string) { + return model.children.find(model => model.name === name); + }, + }); +}); + +function isModel(model: unknown): model is Model { + if (model instanceof Model) { + return true; + } + return false; +} diff --git a/packages/cli-tool/src/providers/module.ts b/packages/cli-tool/src/providers/module.ts new file mode 100644 index 0000000000..01f0423894 --- /dev/null +++ b/packages/cli-tool/src/providers/module.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Directory, Stat } from "#stat.js"; + +Stat.provide(definition => { + if (!isModule(definition)) { + return; + } + + return Directory({ + paths() { + return Object.keys(definition); + }, + + definitionAt(path: string) { + return definition[path]; + }, + }); +}); + +function isModule(definition: unknown): definition is Record { + if (typeof definition !== "object" || definition === null) { + return false; + } + return (definition as any)[Symbol.toStringTag] === "Module"; +} diff --git a/packages/cli-tool/src/repl.ts b/packages/cli-tool/src/repl.ts new file mode 100644 index 0000000000..f781f937d1 --- /dev/null +++ b/packages/cli-tool/src/repl.ts @@ -0,0 +1,168 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Domain } from "#domain.js"; +import { IncompleteError } from "#errors.js"; +import { Diagnostic, LogFormat } from "#general"; +import { isCommand } from "#parser.js"; +import colors from "ansi-colors"; +import { homedir } from "os"; +import { join } from "path"; +import { AsyncCompleter, CompleterResult } from "readline"; +import { Recoverable, REPLEval, REPLServer, start } from "repl"; +import "./commands/index.js"; +import "./providers/index.js"; + +// Node.js repl implementation does good stuff for us so want to keep it but we don't want the "." commands and it has +// no way to disable those. So use this prefix as a hack to prevent it from noticing lines that start with "." +const LINE_PROTECTOR_CHAR = "\u0001"; + +export async function repl() { + const domain = Domain(); + + let server: REPLServer | undefined = undefined; + + const doEval: REPLEval = function (this, evalCmd, _context, _file, cb: (err: Error | null, result: any) => void) { + // See comment below r.e. "realEmit". We can't just strip first character because the line protector will + // appear multiple times if there are multiple lines + evalCmd = evalCmd.replace(new RegExp(LINE_PROTECTOR_CHAR, "g"), ""); + + if (evalCmd.endsWith("\n")) { + evalCmd = evalCmd.slice(0, evalCmd.length - 1); + } + const result: Promise = domain.execute(evalCmd); + result.then(handleSuccess, handleError); + + function handleSuccess(result: unknown) { + server?.setPrompt(createPrompt()); + cb(null, result); + } + + function handleError(error: Error) { + server?.setPrompt(createPrompt()); + + if (error.constructor.name === "IncompleteError") { + cb(new Recoverable((error as IncompleteError).cause as Error), undefined); + return; + } + + // Stack frames following our special matter-cli-* "filenames" are just cruft. And if the first filename + // then just remove the stack and place location at end of message + const stack = error.stack; + if (stack !== undefined) { + const lines = stack.split("\n"); + let specialLoc: string | undefined; + let specialLine; + if ("isCliError" in error) { + // These are thrown at the top level and should not display a stack trace + specialLine = 1; + } else { + // Look for the "matter-cli-" marker which we prefix on the "filename" + specialLine = lines.findIndex(line => { + const match = line.match(/at matter-cli-(?:[a-z]+):([0-9]+:[0-9]+)?/); + if (match) { + specialLoc = match[1]; + return true; + } + }); + } + + if (specialLine === 1) { + if (specialLoc) { + error.message += ` (${specialLoc})`; + } + error.stack = `${error.constructor.name}: ${error.message}`; + } else if (specialLine !== -1) { + error.stack = lines.slice(0, specialLine + 1).join("\n"); + } + } + + // Display the error ourselves so is pretty and captures all details + const diagnostic = Diagnostic.error(error); + const formatted = LogFormat[colors.enabled ? "ansi" : "plain"](diagnostic); + process.stderr.write(`${formatted}\n`); + + // Do not report the error to node + cb(null, undefined); + } + }; + + server = start({ + prompt: createPrompt(), + eval: doEval, + ignoreUndefined: true, + }); + + const historyPath = process.env.MATTER_REPL_HISTORY || join(homedir(), ".matter-cli-history"); + server.setupHistory(historyPath, error => { + if (error) { + console.error(error); + process.exit(1); + } + }); + + const realEmit = server.emit as (...args: unknown[]) => boolean; + server.emit = (event, ...args: any[]) => { + if (event === "line") { + args[0] = `${LINE_PROTECTOR_CHAR}${args[0]}`; + } + return realEmit.call(server, event, ...args); + }; + + const complete: AsyncCompleter = (line, callback) => { + findCompletions(line).then(result => { + if (result) { + callback(null, result); + } else { + nodeCompleter.call(server, line, callback); + } + }, callback); + }; + + const nodeCompleter = server.completer; + Object.defineProperty(server, "completer", { value: complete }); + + function createPrompt() { + return `${colors.dim("matter")} ${colors.yellow(domain.location.path)} ❯ `; + } + + async function findCompletions(line: string): Promise { + if (line.endsWith("/") ? !isCommand(line.slice(0, line.length - 1)) : !isCommand(line)) { + return; + } + + const possiblePath = line.replace(/^.*\s/u, ""); + if (!possiblePath.match(/^[/0-9\p{L}$_%]*$/u)) { + return; + } + + const pathsToSearch = Array(); + + const slashPos = possiblePath.lastIndexOf("/"); + let partial; + if (slashPos === -1) { + pathsToSearch.push(""); + pathsToSearch.push("/bin"); + partial = possiblePath; + } else { + pathsToSearch.push(possiblePath.slice(0, slashPos)); + partial = possiblePath.slice(slashPos + 1); + } + + const completions = Array(); + + for (const path of pathsToSearch) { + const location = await domain.location.maybeAt(path); + if (location?.kind !== "directory") { + continue; + } + + completions.push(...(await location.paths).filter(path => path.startsWith(partial))); + } + + return [completions.sort(), partial]; + } +} diff --git a/packages/cli-tool/src/stat.ts b/packages/cli-tool/src/stat.ts new file mode 100644 index 0000000000..dfac993af4 --- /dev/null +++ b/packages/cli-tool/src/stat.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2022-2024 Matter.js Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MaybePromise } from "@matter/general"; + +/** + * An object that does not contain subobjects in our virtual filesystem. + */ +export interface File extends Stat.Base { + kind: "file"; +} + +/** + * An object that contains sub-objects in our virtual filesystem. + */ +export interface Directory extends Stat.Base { + kind: "directory"; + paths: MaybePromise; + definitionAt(path: string): MaybePromise; +} + +export function Directory(options: { + id?: number | string; + tag?: string; + name?: string; + summary?: string; + paths: () => MaybePromise; + definitionAt: (path: string) => MaybePromise; +}): Directory { + const { name, summary, id, tag, paths, definitionAt } = options; + return { + kind: "directory", + name, + summary, + id, + tag, + + get paths() { + return paths(); + }, + + definitionAt(path: string) { + return definitionAt(path); + }, + }; +} + +export type Stat = File | Directory; + +/** + * Augments information about "filesystem" locations. + */ +export interface StatProvider { + (definition: unknown): undefined | Stat; +} + +const providers = Array(); + +export namespace Stat { + export interface Base { + name?: string; + summary?: string; + id?: number | string; + tag?: string; + } + + /** + * Obtain an Inode for a JS value. + */ + export function of(definition: unknown): Stat { + for (const provider of providers) { + const stat = provider(definition); + if (stat) { + return stat; + } + } + + if (isDirectory(definition)) { + return { + kind: "directory", + + get paths() { + return Object.keys(definition); + }, + + definitionAt(path: string) { + if (path in definition) { + return definition[path]; + } + }, + }; + } + + return { + kind: "file", + }; + } + + /** + * Register a provider. + */ + export function provide(provider: StatProvider) { + providers.push(provider); + } + + /** + * Determine if we consider an object a "directory". + */ + export function isDirectory(definition: unknown): definition is Record { + return ( + typeof definition === "object" && + definition !== null && + !Array.isArray(definition) && + !ArrayBuffer.isView(definition) && + !(definition instanceof Date) + ); + } +} diff --git a/packages/cli-tool/src/tsconfig.json b/packages/cli-tool/src/tsconfig.json new file mode 100644 index 0000000000..dfeb7fd69e --- /dev/null +++ b/packages/cli-tool/src/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tools/tsc/tsconfig.app.json", + "compilerOptions": { + "allowJs": true, + "types": [ + "globals", + "node" + ] + }, + "references": [ + { + "path": "../../general/src" + }, + { + "path": "../../model/src" + }, + { + "path": "../../node/src" + }, + { + "path": "../../protocol/src" + }, + { + "path": "../../types/src" + } + ] +} \ No newline at end of file diff --git a/packages/cli-tool/tsconfig.json b/packages/cli-tool/tsconfig.json new file mode 100644 index 0000000000..60582e0cd5 --- /dev/null +++ b/packages/cli-tool/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "composite": true }, + "files": [], + "references": [{ "path": "src" }] +} diff --git a/packages/general/src/log/Diagnostic.ts b/packages/general/src/log/Diagnostic.ts index a2da9636d5..5ecf5c9e4f 100644 --- a/packages/general/src/log/Diagnostic.ts +++ b/packages/general/src/log/Diagnostic.ts @@ -339,17 +339,16 @@ function messageAndStackFor(error: any, parentStack?: string[]) { let message: string | undefined; let rawStack: string | undefined; if (error !== undefined && error !== null) { - if (error instanceof Error) { - message = error.message; - rawStack = error.stack; + if ("message" in error) { + ({ message, stack: rawStack } = error); } else if (error.message) { message = typeof error.message === "string" ? message : error.toString(); } } if (message === undefined || message === null || message === "") { - if (error instanceof Error) { + if (error !== undefined && error !== null) { message = error.constructor.name; - if (message === "Error") { + if (!message || message === "Error") { message = "(unknown error)"; } } else { diff --git a/packages/general/src/log/LogFormat.ts b/packages/general/src/log/LogFormat.ts index 3133496839..44d8c9e581 100644 --- a/packages/general/src/log/LogFormat.ts +++ b/packages/general/src/log/LogFormat.ts @@ -354,8 +354,7 @@ function formatAnsi(diagnostic: unknown, indents = 0) { return ansiEscape(...codes); } - // Apply style codes. Maintains color state (via escapes) so values must - // be rendered sequentially as they appear + // Apply style codes. Maintains color state (via escapes) so values must be rendered sequentially as they appear function style(style: StyleName, text: string) { if (text === "") { return text; diff --git a/packages/general/src/util/Promises.ts b/packages/general/src/util/Promises.ts index 681c677e3c..c118199fb2 100644 --- a/packages/general/src/util/Promises.ts +++ b/packages/general/src/util/Promises.ts @@ -148,7 +148,12 @@ export const MaybePromise = { */ is(value: MaybePromise): value is PromiseLike { // We cannot use isObject because this could collide with valid values here - return typeof value === "object" && value !== null && typeof (value as { then?: unknown }).then === "function"; + return ( + typeof value === "object" && + value !== null && + typeof (value as { then?: unknown }).then === "function" && + value !== this + ); }, /** diff --git a/packages/main/package.json b/packages/main/package.json index 7e97a33048..a3536781d0 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -41,12 +41,12 @@ "#*": "./src/*" }, "dependencies": { - "@noble/curves": "^1.5.0", "@matter/general": "*", "@matter/model": "*", - "@matter/types": "*", + "@matter/node": "*", "@matter/protocol": "*", - "@matter/node": "*" + "@matter/types": "*", + "@noble/curves": "^1.5.0" }, "optionalDependencies": { "@matter/nodejs": "*", diff --git a/packages/main/src/tsconfig.json b/packages/main/src/tsconfig.json index 4a9ad0bff5..80fb71738a 100644 --- a/packages/main/src/tsconfig.json +++ b/packages/main/src/tsconfig.json @@ -14,13 +14,13 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { "path": "../../protocol/src" }, { - "path": "../../node/src" + "path": "../../types/src" }, { "path": "../../nodejs/src" diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json index b3e7fa2976..60582e0cd5 100644 --- a/packages/main/tsconfig.json +++ b/packages/main/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { "composite": true }, "files": [], - "references": [{ "path": "src" }, { "path": "test" }, { "path": "build/src" }] + "references": [{ "path": "src" }] } diff --git a/packages/matter.js/package.json b/packages/matter.js/package.json index bc105036c0..a047eb66f8 100644 --- a/packages/matter.js/package.json +++ b/packages/matter.js/package.json @@ -45,12 +45,12 @@ "#*": "./src/*" }, "dependencies": { - "@noble/curves": "^1.5.0", "@matter/general": "*", "@matter/model": "*", - "@matter/types": "*", + "@matter/node": "*", "@matter/protocol": "*", - "@matter/node": "*" + "@matter/types": "*", + "@noble/curves": "^1.5.0" }, "devDependencies": { "@matter/tools": "*" diff --git a/packages/matter.js/src/tsconfig.json b/packages/matter.js/src/tsconfig.json index b9329b4ff0..2990e822e7 100644 --- a/packages/matter.js/src/tsconfig.json +++ b/packages/matter.js/src/tsconfig.json @@ -14,13 +14,13 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { "path": "../../protocol/src" }, { - "path": "../../node/src" + "path": "../../types/src" } ] } \ No newline at end of file diff --git a/packages/matter.js/test/tsconfig.json b/packages/matter.js/test/tsconfig.json index 3550af3abf..81d6fea7b9 100644 --- a/packages/matter.js/test/tsconfig.json +++ b/packages/matter.js/test/tsconfig.json @@ -15,13 +15,13 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { "path": "../../protocol/src" }, { - "path": "../../node/src" + "path": "../../types/src" }, { "path": "../src" diff --git a/packages/matter.js/tsconfig.json b/packages/matter.js/tsconfig.json index 1a833f6035..325dec2cc5 100644 --- a/packages/matter.js/tsconfig.json +++ b/packages/matter.js/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { "composite": true }, "files": [], - "references": [{ "path": "./src" }, { "path": "./test" }, { "path": "./build/src" }] + "references": [{ "path": "./src" }, { "path": "./test" }] } diff --git a/packages/model/package.json b/packages/model/package.json index 7f39cc949f..b63fbfdf0e 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -33,8 +33,8 @@ "embed-examples": "embedme **/README.md" }, "dependencies": { - "@noble/curves": "^1.5.0", - "@matter/general": "*" + "@matter/general": "*", + "@noble/curves": "^1.5.0" }, "devDependencies": { "@matter/tools": "*" diff --git a/packages/model/src/models/Model.ts b/packages/model/src/models/Model.ts index 4867c144dc..9dc1758a35 100644 --- a/packages/model/src/models/Model.ts +++ b/packages/model/src/models/Model.ts @@ -4,12 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { camelize, ImplementationError, InternalError } from "#general"; +import { camelize, decamelize, ImplementationError, InternalError } from "#general"; import { DefinitionError, ElementTag, Metatype, Specification } from "../common/index.js"; import { AnyElement, BaseElement } from "../elements/index.js"; import { ModelTraversal } from "../logic/ModelTraversal.js"; import { Children } from "./Children.js"; +const inspect = Symbol.for("nodejs.util.inspect.custom"); + /** * A "model" is a class that implements runtime functionality associated with the corresponding element type. * @@ -500,6 +502,26 @@ export abstract class Model { }, }); } + + [inspect](_depth: any, options: any, inspect: any) { + const json = this.valueOf() as Record; + const props = { + name: json.name, + } as Record; + if (json.id !== undefined) { + props.id = json.id; + } + for (const key in json) { + if (key === "id" || key === "name" || key === "tag") { + continue; + } + props[key] = json[key]; + } + if (this.#children !== undefined && this.#children.length) { + props.children = this.#children.length; + } + return `${inspect(props, options)}`.replace("{", `${decamelize(this.tag)} {`); + } } export namespace Model { @@ -555,5 +577,9 @@ export namespace Model { } return (this.instances[key] = new CrossReference(xref)); } + + [inspect](_depth: any, options: any, inspect: any) { + return inspect(this.toString(), options); + } } } diff --git a/packages/node/src/endpoint/properties/EndpointContainer.ts b/packages/node/src/endpoint/properties/EndpointContainer.ts index 8420b651bd..288086f57f 100644 --- a/packages/node/src/endpoint/properties/EndpointContainer.ts +++ b/packages/node/src/endpoint/properties/EndpointContainer.ts @@ -66,6 +66,14 @@ export class EndpointContainer implements Mutable return this.#children.size; } + map(fn: (part: Endpoint) => T) { + return this.#children.map(fn); + } + + filter(predicate: (part: Endpoint) => boolean) { + return this.#children.filter(predicate); + } + [Symbol.iterator]() { return this.#children[Symbol.iterator](); } diff --git a/packages/nodejs-shell/package.json b/packages/nodejs-shell/package.json index 2ebda666bc..5b52b6f34c 100644 --- a/packages/nodejs-shell/package.json +++ b/packages/nodejs-shell/package.json @@ -36,9 +36,9 @@ "#types": "@matter/types" }, "dependencies": { - "@matter/nodejs-ble": "*", - "@matter/nodejs": "*", "@matter/general": "*", + "@matter/nodejs": "*", + "@matter/nodejs-ble": "*", "@matter/tools": "*", "@project-chip/matter.js": "*", "yargs": "^17.7.2" diff --git a/packages/nodejs-shell/src/tsconfig.json b/packages/nodejs-shell/src/tsconfig.json index 579ac40fe2..bbd7f08753 100644 --- a/packages/nodejs-shell/src/tsconfig.json +++ b/packages/nodejs-shell/src/tsconfig.json @@ -7,13 +7,13 @@ }, "references": [ { - "path": "../../nodejs-ble/src" + "path": "../../general/src" }, { "path": "../../nodejs/src" }, { - "path": "../../general/src" + "path": "../../nodejs-ble/src" }, { "path": "../../matter.js/src" diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index ac2d6b35c5..ec22b5b853 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -45,9 +45,9 @@ }, "dependencies": { "@matter/general": "*", - "@matter/types": "*", - "@matter/protocol": "*", "@matter/node": "*", + "@matter/protocol": "*", + "@matter/types": "*", "@project-chip/matter.js": "*", "node-localstorage": "^3.0.5" }, diff --git a/packages/nodejs/src/tsconfig.json b/packages/nodejs/src/tsconfig.json index c82df2e8f8..28c16ae787 100644 --- a/packages/nodejs/src/tsconfig.json +++ b/packages/nodejs/src/tsconfig.json @@ -10,13 +10,13 @@ "path": "../../general/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { "path": "../../protocol/src" }, { - "path": "../../node/src" + "path": "../../types/src" }, { "path": "../../matter.js/src" diff --git a/packages/nodejs/test/tsconfig.json b/packages/nodejs/test/tsconfig.json index 6225446ffc..f0c60dbf23 100644 --- a/packages/nodejs/test/tsconfig.json +++ b/packages/nodejs/test/tsconfig.json @@ -12,13 +12,13 @@ "path": "../../general/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { "path": "../../protocol/src" }, { - "path": "../../node/src" + "path": "../../types/src" }, { "path": "../../matter.js/src" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 191b6daeb7..ab6ea96ce4 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -40,10 +40,10 @@ "#*": "./src/*" }, "dependencies": { - "@noble/curves": "^1.5.0", "@matter/general": "*", "@matter/model": "*", - "@matter/types": "*" + "@matter/types": "*", + "@noble/curves": "^1.5.0" }, "devDependencies": { "@matter/tools": "*" diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 8f4720b99a..321c276b42 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -36,8 +36,8 @@ "#*": "./src/*" }, "dependencies": { - "@matter/nodejs": "*", "@matter/general": "*", + "@matter/nodejs": "*", "@matter/protocol": "*", "@react-native-async-storage/async-storage": "^2.0.0", "@react-native-community/netinfo": "^11.3.2", diff --git a/packages/react-native/src/tsconfig.json b/packages/react-native/src/tsconfig.json index f02e8cad4a..38f8252871 100644 --- a/packages/react-native/src/tsconfig.json +++ b/packages/react-native/src/tsconfig.json @@ -5,10 +5,10 @@ }, "references": [ { - "path": "../../nodejs/src" + "path": "../../general/src" }, { - "path": "../../general/src" + "path": "../../nodejs/src" }, { "path": "../../protocol/src" diff --git a/packages/types/package.json b/packages/types/package.json index 3e65388d80..312375da5e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -33,9 +33,9 @@ "embed-examples": "embedme **/README.md" }, "dependencies": { - "@noble/curves": "^1.5.0", "@matter/general": "*", - "@matter/model": "*" + "@matter/model": "*", + "@noble/curves": "^1.5.0" }, "devDependencies": { "@matter/tools": "*" diff --git a/tsconfig.json b/tsconfig.json index b3e59c0cd3..fe70a53c3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,9 @@ { "path": "packages/react-native" }, + { + "path": "packages/cli-tool" + }, { "path": "models" }, From 5679fb3b1f1c96c42a7070178012e9b28be26b95 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Fri, 11 Oct 2024 23:37:08 -0700 Subject: [PATCH 08/11] Sort references by name Previously we would sometimes reorder references based on how dependencies were loaded by the tool. We now always order them alphabetically. --- chip-testing/test/tsconfig.json | 4 ++-- codegen/src/tsconfig.json | 4 ++-- compat/matter-node.js-examples/src/tsconfig.json | 4 ++-- compat/matter-node.js/src/tsconfig.json | 4 ++-- packages/examples/src/tsconfig.json | 4 ++-- packages/general/test/tsconfig.json | 4 ++-- packages/main/src/tsconfig.json | 8 ++++---- packages/matter.js/test/tsconfig.json | 6 +++--- packages/model/test/tsconfig.json | 4 ++-- packages/node/src/tsconfig.json | 4 ++-- packages/node/test/tsconfig.json | 8 ++++---- packages/nodejs-ble/src/BleBroadcaster.ts | 2 +- packages/nodejs-shell/src/tsconfig.json | 4 ++-- packages/nodejs/src/tsconfig.json | 10 +++++----- packages/nodejs/test/tsconfig.json | 14 +++++++------- packages/protocol/test/tsconfig.json | 6 +++--- packages/tools/src/building/tsconfig.ts | 4 +++- packages/types/test/tsconfig.json | 4 ++-- 18 files changed, 50 insertions(+), 48 deletions(-) diff --git a/chip-testing/test/tsconfig.json b/chip-testing/test/tsconfig.json index 23603b2ad4..68041f5fb2 100644 --- a/chip-testing/test/tsconfig.json +++ b/chip-testing/test/tsconfig.json @@ -15,10 +15,10 @@ "path": "../../packages/matter.js/src" }, { - "path": "../src" + "path": "../../packages/tools/src" }, { - "path": "../../packages/tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/codegen/src/tsconfig.json b/codegen/src/tsconfig.json index 182132434d..e98c4f4394 100644 --- a/codegen/src/tsconfig.json +++ b/codegen/src/tsconfig.json @@ -7,10 +7,10 @@ }, "references": [ { - "path": "../../packages/general/src" + "path": "../../models/src" }, { - "path": "../../models/src" + "path": "../../packages/general/src" }, { "path": "../../packages/model/src" diff --git a/compat/matter-node.js-examples/src/tsconfig.json b/compat/matter-node.js-examples/src/tsconfig.json index 2ab04b142c..9e596af645 100644 --- a/compat/matter-node.js-examples/src/tsconfig.json +++ b/compat/matter-node.js-examples/src/tsconfig.json @@ -7,10 +7,10 @@ }, "references": [ { - "path": "../../matter-node-ble.js/src" + "path": "../../../packages/matter.js/src" }, { - "path": "../../../packages/matter.js/src" + "path": "../../matter-node-ble.js/src" } ] } \ No newline at end of file diff --git a/compat/matter-node.js/src/tsconfig.json b/compat/matter-node.js/src/tsconfig.json index afa245033a..880bc89aad 100644 --- a/compat/matter-node.js/src/tsconfig.json +++ b/compat/matter-node.js/src/tsconfig.json @@ -10,10 +10,10 @@ "path": "../../../packages/general/src" }, { - "path": "../../../packages/nodejs/src" + "path": "../../../packages/matter.js/src" }, { - "path": "../../../packages/matter.js/src" + "path": "../../../packages/nodejs/src" } ] } \ No newline at end of file diff --git a/packages/examples/src/tsconfig.json b/packages/examples/src/tsconfig.json index c2fc9ce423..e08d79bf58 100644 --- a/packages/examples/src/tsconfig.json +++ b/packages/examples/src/tsconfig.json @@ -10,10 +10,10 @@ "path": "../../main/src" }, { - "path": "../../nodejs/src" + "path": "../../nodejs-ble/src" }, { - "path": "../../nodejs-ble/src" + "path": "../../nodejs/src" } ] } \ No newline at end of file diff --git a/packages/general/test/tsconfig.json b/packages/general/test/tsconfig.json index f2de597897..b2d88dd5e3 100644 --- a/packages/general/test/tsconfig.json +++ b/packages/general/test/tsconfig.json @@ -9,10 +9,10 @@ }, "references": [ { - "path": "../src" + "path": "../../tools/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/main/src/tsconfig.json b/packages/main/src/tsconfig.json index 80fb71738a..375cccc783 100644 --- a/packages/main/src/tsconfig.json +++ b/packages/main/src/tsconfig.json @@ -17,16 +17,16 @@ "path": "../../node/src" }, { - "path": "../../protocol/src" + "path": "../../nodejs/src" }, { - "path": "../../types/src" + "path": "../../protocol/src" }, { - "path": "../../nodejs/src" + "path": "../../react-native/src" }, { - "path": "../../react-native/src" + "path": "../../types/src" } ] } \ No newline at end of file diff --git a/packages/matter.js/test/tsconfig.json b/packages/matter.js/test/tsconfig.json index 81d6fea7b9..a5155d7afe 100644 --- a/packages/matter.js/test/tsconfig.json +++ b/packages/matter.js/test/tsconfig.json @@ -21,13 +21,13 @@ "path": "../../protocol/src" }, { - "path": "../../types/src" + "path": "../../tools/src" }, { - "path": "../src" + "path": "../../types/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/model/test/tsconfig.json b/packages/model/test/tsconfig.json index 2b3703d37d..249158c814 100644 --- a/packages/model/test/tsconfig.json +++ b/packages/model/test/tsconfig.json @@ -12,10 +12,10 @@ "path": "../../general/src" }, { - "path": "../src" + "path": "../../tools/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/node/src/tsconfig.json b/packages/node/src/tsconfig.json index 39746c74e0..79db4203bf 100644 --- a/packages/node/src/tsconfig.json +++ b/packages/node/src/tsconfig.json @@ -15,10 +15,10 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../protocol/src" }, { - "path": "../../protocol/src" + "path": "../../types/src" } ] } \ No newline at end of file diff --git a/packages/node/test/tsconfig.json b/packages/node/test/tsconfig.json index a8e9a95ae5..abb2612e6b 100644 --- a/packages/node/test/tsconfig.json +++ b/packages/node/test/tsconfig.json @@ -15,16 +15,16 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../protocol/src" }, { - "path": "../../protocol/src" + "path": "../../tools/src" }, { - "path": "../src" + "path": "../../types/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/nodejs-ble/src/BleBroadcaster.ts b/packages/nodejs-ble/src/BleBroadcaster.ts index 001ad115b8..50df8ac228 100644 --- a/packages/nodejs-ble/src/BleBroadcaster.ts +++ b/packages/nodejs-ble/src/BleBroadcaster.ts @@ -5,6 +5,7 @@ */ import { ImplementationError, Logger } from "@matter/general"; +import { CommissioningMode } from "@matter/protocol"; import { BtpCodec } from "@project-chip/matter.js/codec"; import { VendorId } from "@project-chip/matter.js/datatype"; import { @@ -12,7 +13,6 @@ import { CommissioningModeInstanceData, InstanceBroadcaster, } from "@project-chip/matter.js/fabric"; -import { CommissioningMode } from "../../protocol/src/common/InstanceBroadcaster.js"; import { BlenoBleServer } from "./BlenoBleServer.js"; const logger = Logger.get("BleBroadcaster"); diff --git a/packages/nodejs-shell/src/tsconfig.json b/packages/nodejs-shell/src/tsconfig.json index bbd7f08753..7acf535bb7 100644 --- a/packages/nodejs-shell/src/tsconfig.json +++ b/packages/nodejs-shell/src/tsconfig.json @@ -10,13 +10,13 @@ "path": "../../general/src" }, { - "path": "../../nodejs/src" + "path": "../../matter.js/src" }, { "path": "../../nodejs-ble/src" }, { - "path": "../../matter.js/src" + "path": "../../nodejs/src" } ] } \ No newline at end of file diff --git a/packages/nodejs/src/tsconfig.json b/packages/nodejs/src/tsconfig.json index 28c16ae787..31f019ecd6 100644 --- a/packages/nodejs/src/tsconfig.json +++ b/packages/nodejs/src/tsconfig.json @@ -10,19 +10,19 @@ "path": "../../general/src" }, { - "path": "../../node/src" + "path": "../../matter.js/src" }, { - "path": "../../protocol/src" + "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { - "path": "../../matter.js/src" + "path": "../../protocol/src" }, { - "path": "../../model/src" + "path": "../../types/src" } ] } \ No newline at end of file diff --git a/packages/nodejs/test/tsconfig.json b/packages/nodejs/test/tsconfig.json index f0c60dbf23..90129f79db 100644 --- a/packages/nodejs/test/tsconfig.json +++ b/packages/nodejs/test/tsconfig.json @@ -12,25 +12,25 @@ "path": "../../general/src" }, { - "path": "../../node/src" + "path": "../../matter.js/src" }, { - "path": "../../protocol/src" + "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../node/src" }, { - "path": "../../matter.js/src" + "path": "../../protocol/src" }, { - "path": "../../model/src" + "path": "../../tools/src" }, { - "path": "../src" + "path": "../../types/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/protocol/test/tsconfig.json b/packages/protocol/test/tsconfig.json index 3140dd0950..28400d6ca5 100644 --- a/packages/protocol/test/tsconfig.json +++ b/packages/protocol/test/tsconfig.json @@ -15,13 +15,13 @@ "path": "../../model/src" }, { - "path": "../../types/src" + "path": "../../tools/src" }, { - "path": "../src" + "path": "../../types/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file diff --git a/packages/tools/src/building/tsconfig.ts b/packages/tools/src/building/tsconfig.ts index feac361d1e..e4526cf383 100644 --- a/packages/tools/src/building/tsconfig.ts +++ b/packages/tools/src/building/tsconfig.ts @@ -60,7 +60,9 @@ async function syncSubproject(node: Graph.Node, path: string, ...extraRefs: stri const desired = [...new Set([...deps, ...extraRefs])]; - const newReferences = desired.map(ref => ({ path: relative(path, ref).replace(/\\/g, "/") })); + const newReferences = desired + .map(ref => ({ path: relative(path, ref).replace(/\\/g, "/") })) + .sort((ref1, ref2) => ref1.path.localeCompare(ref2.path)); if (referencesChanged(tsconfig.references, newReferences)) { tsconfig.references = newReferences; diff --git a/packages/types/test/tsconfig.json b/packages/types/test/tsconfig.json index 6e91db8954..66f79b6344 100644 --- a/packages/types/test/tsconfig.json +++ b/packages/types/test/tsconfig.json @@ -15,10 +15,10 @@ "path": "../../model/src" }, { - "path": "../src" + "path": "../../tools/src" }, { - "path": "../../tools/src" + "path": "../src" } ] } \ No newline at end of file From 55e81bba07e4e6711e3c618bc5f7e04dbda1c113 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Sat, 12 Oct 2024 00:17:27 -0700 Subject: [PATCH 09/11] Make tooling happy --- package-lock.json | 787 +++++++++++++---------------- packages/cli-tool/src/location.ts | 2 +- packages/cli-tool/src/parser.ts | 2 +- packages/model/src/models/Model.ts | 2 +- packages/node/src/build.config.ts | 2 +- 5 files changed, 355 insertions(+), 440 deletions(-) diff --git a/package-lock.json b/package-lock.json index 169496a9cf..045231c150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,18 +165,18 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", - "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", - "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -185,10 +185,10 @@ "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-module-transforms": "^7.25.7", "@babel/helpers": "^7.25.7", - "@babel/parser": "^7.25.7", + "@babel/parser": "^7.25.8", "@babel/template": "^7.25.7", "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -655,12 +655,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -807,13 +807,12 @@ } }, "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.7.tgz", - "integrity": "sha512-Egdiuy7pLTyaPkIr6rItNyFVbblTmx3VgqY+72KiS9BzcA+SMyrS9zSumQeSANo8uE3Kax0ZUMkpNh0Q+mbNwg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.8.tgz", + "integrity": "sha512-5SLPHA/Gk7lNdaymtSVS9jH77Cs7yuHTR3dYj+9q+M7R7tNLXhNuvnmOfafRIzpWL+dtMibuu1I4ofrc768Gkw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-export-default-from": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -953,35 +952,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz", @@ -1024,18 +994,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.25.7.tgz", @@ -1083,32 +1041,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", @@ -1196,37 +1128,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", @@ -1275,15 +1176,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.7.tgz", - "integrity": "sha512-4B6OhTrwYKHYYgcwErvZjbmH9X5TxQBsaBHdzEIB4l71gR5jh/tuHGlb9in47udL2+wVUcOz5XXhhfhVJwEpEg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz", + "integrity": "sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==", "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.7", "@babel/helper-remap-async-to-generator": "^7.25.7", - "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/traverse": "^7.25.7" }, "engines": { @@ -1359,15 +1259,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.7.tgz", - "integrity": "sha512-rvUUtoVlkDWtDWxGAiiQj0aNktTPn3eFynBcMC2IhsXweehwgdI9ODe+XjWw515kEmv22sSOTp/rxIRuTiB7zg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "license": "MIT", "peer": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.7", - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1487,14 +1386,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.7.tgz", - "integrity": "sha512-UvcLuual4h7/GfylKm2IAA3aph9rwvAM2XBA0uPKU3lca+Maai4jBjjEVUS568ld6kJcgbouuumCBhMd/Yz17w==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1521,13 +1419,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.7.tgz", - "integrity": "sha512-h3MDAP5l34NQkkNulsTNyjdaR+OiB0Im67VU//sFupouP8Q6m9Spy7l66DcaAQxtmCqGdanPByLsnwFttxKISQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1587,14 +1484,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.7.tgz", - "integrity": "sha512-Ot43PrL9TEAiCe8C/2erAjXMeVSnE/BLEx6eyrKLNFCCw5jvhTHKyHxdI1pA0kz5njZRYAnMO2KObGqOCRDYSA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1619,14 +1515,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.7.tgz", - "integrity": "sha512-iImzbA55BjiovLyG2bggWS+V+OLkaBorNvc/yJoeeDQGztknRnDdYfp2d/UPmunZYEnZi6Lg8QcTmNMHOB0lGA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1754,14 +1649,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.7.tgz", - "integrity": "sha512-FbuJ63/4LEL32mIxrxwYaqjJxpbzxPVQj5a+Ebrc8JICV6YX8nE53jY+K0RZT3um56GoNWgkS2BQ/uLGTjtwfw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1771,14 +1665,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.7.tgz", - "integrity": "sha512-8CbutzSSh4hmD+jJHIA8vdTNk15kAzOnFLVVgBSMGr28rt85ouT01/rezMecks9pkU939wDInImwCKv4ahU4IA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1788,14 +1681,13 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.7.tgz", - "integrity": "sha512-1JdVKPhD7Y5PvgfFy0Mv2brdrolzpzSoUq2pr6xsR+m+3viGGeHEokFKsCgOkbeFOQxfB1Vt2F0cPJLRpFI4Zg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { @@ -1823,14 +1715,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.7.tgz", - "integrity": "sha512-m9obYBA39mDPN7lJzD5WkGGb0GO54PPLXsbcnj1Hyeu8mSRz7Gb4b1A6zxNX32ZuUySDK4G6it8SDFWD1nCnqg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1840,15 +1731,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.7.tgz", - "integrity": "sha512-h39agClImgPWg4H8mYVAbD1qP9vClFbEjqoJmt87Zen8pjqK8FTPUwrOXAvqu5soytwxrLMd2fx2KSCp2CHcNg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1889,15 +1779,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.7.tgz", - "integrity": "sha512-LzA5ESzBy7tqj00Yjey9yWfs3FKy4EmJyKOSWld144OxkTji81WWnUT8nkLUn+imN/zHL8ZQlOu/MTUAhHaX3g==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.7", "@babel/helper-create-class-features-plugin": "^7.25.7", - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2243,13 +2132,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.7.tgz", - "integrity": "sha512-Gibz4OUdyNqqLj+7OAvBZxOD7CklCtMA5/j0JgUEwOnaRULsPDXmic2iKxL2DX2vQduPR5wH2hjZas/Vr/Oc0g==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.8.tgz", + "integrity": "sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==", "license": "MIT", "peer": true, "dependencies": { - "@babel/compat-data": "^7.25.7", + "@babel/compat-data": "^7.25.8", "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-plugin-utils": "^7.25.7", "@babel/helper-validator-option": "^7.25.7", @@ -2259,45 +2148,30 @@ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.25.7", "@babel/plugin-syntax-import-attributes": "^7.25.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.8", "@babel/plugin-transform-async-to-generator": "^7.25.7", "@babel/plugin-transform-block-scoped-functions": "^7.25.7", "@babel/plugin-transform-block-scoping": "^7.25.7", "@babel/plugin-transform-class-properties": "^7.25.7", - "@babel/plugin-transform-class-static-block": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.8", "@babel/plugin-transform-classes": "^7.25.7", "@babel/plugin-transform-computed-properties": "^7.25.7", "@babel/plugin-transform-destructuring": "^7.25.7", "@babel/plugin-transform-dotall-regex": "^7.25.7", "@babel/plugin-transform-duplicate-keys": "^7.25.7", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", - "@babel/plugin-transform-dynamic-import": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.8", "@babel/plugin-transform-exponentiation-operator": "^7.25.7", - "@babel/plugin-transform-export-namespace-from": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.8", "@babel/plugin-transform-for-of": "^7.25.7", "@babel/plugin-transform-function-name": "^7.25.7", - "@babel/plugin-transform-json-strings": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.8", "@babel/plugin-transform-literals": "^7.25.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.8", "@babel/plugin-transform-member-expression-literals": "^7.25.7", "@babel/plugin-transform-modules-amd": "^7.25.7", "@babel/plugin-transform-modules-commonjs": "^7.25.7", @@ -2305,15 +2179,15 @@ "@babel/plugin-transform-modules-umd": "^7.25.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", "@babel/plugin-transform-new-target": "^7.25.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.7", - "@babel/plugin-transform-numeric-separator": "^7.25.7", - "@babel/plugin-transform-object-rest-spread": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.8", + "@babel/plugin-transform-numeric-separator": "^7.25.8", + "@babel/plugin-transform-object-rest-spread": "^7.25.8", "@babel/plugin-transform-object-super": "^7.25.7", - "@babel/plugin-transform-optional-catch-binding": "^7.25.7", - "@babel/plugin-transform-optional-chaining": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.8", + "@babel/plugin-transform-optional-chaining": "^7.25.8", "@babel/plugin-transform-parameters": "^7.25.7", "@babel/plugin-transform-private-methods": "^7.25.7", - "@babel/plugin-transform-private-property-in-object": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.8", "@babel/plugin-transform-property-literals": "^7.25.7", "@babel/plugin-transform-regenerator": "^7.25.7", "@babel/plugin-transform-reserved-words": "^7.25.7", @@ -2522,9 +2396,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.7", @@ -3315,9 +3189,9 @@ } }, "node_modules/@expo/cli/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", + "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3351,6 +3225,18 @@ "node": ">=4" } }, + "node_modules/@expo/cli/node_modules/hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/cli/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3436,6 +3322,18 @@ "node": ">=4" } }, + "node_modules/@expo/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/cli/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3469,6 +3367,27 @@ "node": "*" } }, + "node_modules/@expo/cli/node_modules/npm-package-arg": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", + "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^3.0.2", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/@expo/cli/node_modules/npm-package-arg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@expo/cli/node_modules/onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -3659,6 +3578,15 @@ "node": ">= 4.0.0" } }, + "node_modules/@expo/cli/node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "license": "ISC", + "dependencies": { + "builtins": "^1.0.3" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", @@ -4296,6 +4224,18 @@ "node": ">=4" } }, + "node_modules/@expo/package-manager/node_modules/hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/package-manager/node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4359,6 +4299,18 @@ "node": ">=4" } }, + "node_modules/@expo/package-manager/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/package-manager/node_modules/mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -4368,6 +4320,18 @@ "node": ">=4" } }, + "node_modules/@expo/package-manager/node_modules/npm-package-arg": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", + "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^3.0.2", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, "node_modules/@expo/package-manager/node_modules/onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -4448,6 +4412,15 @@ "node": ">=4" } }, + "node_modules/@expo/package-manager/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@expo/package-manager/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4487,6 +4460,15 @@ "integrity": "sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==", "license": "MIT" }, + "node_modules/@expo/package-manager/node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "license": "ISC", + "dependencies": { + "builtins": "^1.0.3" + } + }, "node_modules/@expo/plist": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.1.3.tgz", @@ -5143,6 +5125,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^8.0.0", "ini": "^5.0.0", @@ -5158,18 +5141,11 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@npmcli/git/node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@npmcli/git/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", "engines": { "node": ">=16" } @@ -5178,6 +5154,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -5192,6 +5169,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-4.0.1.tgz", "integrity": "sha512-g5H8ljH7Z+4T1ASsfcL09gZl4YGw6M4GbjzPt6HgE+pCRSKC4nlNc4nY75zshi88eEHcdoh3Q8XgWFkGKoVOPw==", + "license": "ISC", "dependencies": { "@npmcli/name-from-folder": "^3.0.0", "@npmcli/package-json": "^6.0.0", @@ -5254,6 +5232,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-3.0.0.tgz", "integrity": "sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==", + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -5262,6 +5241,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.0.1.tgz", "integrity": "sha512-YW6PZ99sc1Q4DINEY2td5z9Z3rwbbsx7CyCnOc7UXUUdePXh5gPi1UeaoQVmKQMVbIU7aOwX2l1OG5ZfjgGi5g==", + "license": "ISC", "dependencies": { "@npmcli/git": "^6.0.0", "glob": "^10.2.2", @@ -5279,6 +5259,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -5294,29 +5275,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.0.tgz", - "integrity": "sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/@npmcli/promise-spawn": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.1.tgz", "integrity": "sha512-ZscqKtJqy7oj6MgXEJcHQ1om4utU0Q84QtC28UVuiO6ALSO9sDPanXdu6Wd1oYhItW8fx2u96zRFUE8BuPlAjA==", + "license": "ISC", "dependencies": { "which": "^5.0.0" }, @@ -5328,6 +5291,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", "engines": { "node": ">=16" } @@ -5336,6 +5300,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -6089,9 +6054,9 @@ } }, "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/@types/node": { - "version": "18.19.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", - "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -6422,47 +6387,47 @@ "optional": true }, "node_modules/@shikijs/core": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.21.1.tgz", - "integrity": "sha512-scBQo4V4O4WZLEDg11e75UPmXoCMq4Ya2A16U6efi/aTiR4o7T/GMNWZs2rq1U8dEvFKGxJZxiUy+tXgmr/4vw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "1.21.1", - "@shikijs/engine-oniguruma": "1.21.1", - "@shikijs/types": "1.21.1", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.3" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.21.1.tgz", - "integrity": "sha512-29EG4KYKlAona8yikEx8uoKbK7N2YoXUO26LS1GOIxpMMIAlQS9UFONg95lkGmIfp1rRcvCvSpYYIJ/blsQxvg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.1", + "@shikijs/types": "1.22.0", "@shikijs/vscode-textmate": "^9.3.0", "oniguruma-to-js": "0.4.3" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.21.1.tgz", - "integrity": "sha512-PvfEtXCDbQZc9ud0SC0bPiuMbul44Cv0Ky2go4SsvVkYAAKYJsMe/Hx7nxThW8yS0r+w8USa0WfOtQKsD9DU9A==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.1", + "@shikijs/types": "1.22.0", "@shikijs/vscode-textmate": "^9.3.0" } }, "node_modules/@shikijs/types": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.21.1.tgz", - "integrity": "sha512-yLuTJTCHmYznerJ0nxF+f2rBKHQf2FMAd08QL/3du2xNBy/7yQ8CjuKN4Zc+Pk0vfIFzdBoxdzvEXE4JtXoR4Q==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", "dev": true, "license": "MIT", "dependencies": { @@ -6851,9 +6816,9 @@ "license": "MIT" }, "node_modules/@types/mocha": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.8.tgz", - "integrity": "sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==", + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", "dev": true, "license": "MIT" }, @@ -6865,9 +6830,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -7024,9 +6989,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", - "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", "dev": true, "license": "MIT", "dependencies": { @@ -7987,9 +7952,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-734b737-20241003", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-734b737-20241003.tgz", - "integrity": "sha512-jdcHsQwYAPuB2u/wpyCXCMI2B9n4weLAx8csvjNwYBw9drXYv4GmoxMyboigR9NJqDdcpIgjCBvt9qnIZM7AhA==", + "version": "0.0.0-experimental-592953e-20240517", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-592953e-20240517.tgz", + "integrity": "sha512-OjG1SVaeQZaJrqkMFJatg8W/MTow8Ak5rx2SI0ETQBO1XvOk/XZGMbltNCPdFJLKghBYoBjC+Y3Ap/Xr7B01mA==", "license": "MIT", "dependencies": { "@babel/generator": "7.2.0", @@ -8157,9 +8122,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "11.0.14", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-11.0.14.tgz", - "integrity": "sha512-4BVYR0Sc2sSNxYTiE/OLSnPiOp+weFNy8eV+hX3aD6YAIbBnw+VubKRWqJV/sOJauzOLz0SgYAYyFciYMqizRA==", + "version": "11.0.15", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-11.0.15.tgz", + "integrity": "sha512-rgiMTYwqIPULaO7iZdqyL7aAff9QLOX6OWUtLZBlOrOTreGY1yHah/5+l8MvI6NVc/8Zj5LY4Y5uMSnJIuzTLw==", "license": "MIT", "dependencies": { "@babel/plugin-proposal-decorators": "^7.12.9", @@ -8169,7 +8134,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.74.87", - "babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517", + "babel-plugin-react-compiler": "0.0.0-experimental-592953e-20240517", "babel-plugin-react-native-web": "~0.19.10", "react-refresh": "^0.14.2" } @@ -8764,9 +8729,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "funding": [ { "type": "opencollective", @@ -9332,9 +9297,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10017,9 +9982,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.32", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", - "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "license": "ISC" }, "node_modules/elliptic": { @@ -10870,9 +10835,9 @@ "peer": true }, "node_modules/expo": { - "version": "51.0.36", - "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.36.tgz", - "integrity": "sha512-eQIC0l6fz3p4cU/hV8+QcyKSacyROhaoA1oohfCD6I3F09dxmC8b3SESpzGqHfuq8wsgcUc4q8ckX7ec25IV1g==", + "version": "51.0.37", + "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.37.tgz", + "integrity": "sha512-zMdfTiGNgNWG0HOOFA3zRreS94iQ7fDxxgEIR6wdQCbncTpbeYj+5mscTAlHE9JJ+oBkcNyJXrLSjE/YVbFERg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", @@ -10881,7 +10846,7 @@ "@expo/config-plugins": "8.0.10", "@expo/metro-config": "0.18.11", "@expo/vector-icons": "^14.0.3", - "babel-preset-expo": "~11.0.14", + "babel-preset-expo": "~11.0.15", "expo-asset": "~10.0.10", "expo-file-system": "~17.0.1", "expo-font": "~12.0.10", @@ -11021,9 +10986,9 @@ "peer": true }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -11031,7 +10996,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -11385,9 +11350,9 @@ "peer": true }, "node_modules/flow-parser": { - "version": "0.247.1", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.247.1.tgz", - "integrity": "sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ==", + "version": "0.248.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.248.1.tgz", + "integrity": "sha512-fkCfVPelbTzSVp+jVwSvEyc+I4WG8MNhRG/EWSZZTlgHAMEdhXJaFEbfErXxMktboMhVGchvEFhWxkzNGM1m2A==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -11425,9 +11390,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { @@ -12085,27 +12050,15 @@ } }, "node_modules/hosted-git-info": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", - "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.0.tgz", + "integrity": "sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==", "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "lru-cache": "^10.0.1" }, "engines": { - "node": ">=10" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/html-encoding-sniffer": { @@ -12326,10 +12279,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/internal-ip": { "version": "4.3.0", @@ -13424,11 +13380,13 @@ "peer": true }, "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "license": "MIT", - "peer": true + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/json-schema-deref-sync": { "version": "0.13.0", @@ -15537,9 +15495,9 @@ } }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "license": "MIT", "optional": true }, @@ -15620,9 +15578,9 @@ "peer": true }, "node_modules/node-addon-api": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", - "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz", + "integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==", "license": "MIT", "optional": true, "engines": { @@ -15886,6 +15844,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.0.tgz", "integrity": "sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==", + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^8.0.0", "semver": "^7.3.5", @@ -15895,17 +15854,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.0.tgz", - "integrity": "sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15919,6 +15867,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.0.tgz", "integrity": "sha512-bkTildVlofeMX7wiOaWk3PlW7YcBXAuEc7TWpOxwUgalG5ZvgT/ms+6OX9zt7iGLv4+VhKbRZhpOfgQJzk1YAw==", + "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, @@ -15930,35 +15879,31 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-package-arg": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", - "integrity": "sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", + "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", "license": "ISC", "dependencies": { - "hosted-git-info": "^3.0.2", - "osenv": "^0.1.5", - "semver": "^5.6.0", - "validate-npm-package-name": "^3.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-pick-manifest": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "license": "ISC", "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", @@ -15969,39 +15914,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-pick-manifest/node_modules/hosted-git-info": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.0.tgz", - "integrity": "sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-pick-manifest/node_modules/npm-package-arg": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", - "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-pick-manifest/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", - "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -16429,6 +16341,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, "node_modules/parse-png": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", @@ -16728,12 +16647,12 @@ } }, "node_modules/playwright": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", - "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.47.2" + "playwright-core": "1.48.0" }, "bin": { "playwright": "cli.js" @@ -16746,9 +16665,9 @@ } }, "node_modules/playwright-core": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", - "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -16968,6 +16887,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -17210,6 +17130,12 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -17233,9 +17159,9 @@ } }, "node_modules/react-devtools-core": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.1.tgz", - "integrity": "sha512-7FSb9meX0btdBQLwdFOwt6bGqvRPabmVMMslv8fgoSPqXyuGpgQe36kx8gR86XPw7aV1yVouTp6fyZ0EH+NfUw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.2.tgz", + "integrity": "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==", "license": "MIT", "peer": true, "dependencies": { @@ -18212,16 +18138,16 @@ } }, "node_modules/shiki": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.21.1.tgz", - "integrity": "sha512-jSOKRHyQJxGOW3kJflmwzHJbp/kjg6hP8LYuVbCPw5oyX+fSNNoCywvcCD3w9eHbj2rvNljt7YMa5BP5Xi+nHg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "1.21.1", - "@shikijs/engine-javascript": "1.21.1", - "@shikijs/engine-oniguruma": "1.21.1", - "@shikijs/types": "1.21.1", + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } @@ -18524,6 +18450,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -18532,12 +18459,14 @@ "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -18546,7 +18475,8 @@ "node_modules/spdx-license-ids": { "version": "3.0.20", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==" + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "license": "CC0-1.0" }, "node_modules/split": { "version": "1.0.1", @@ -19658,9 +19588,9 @@ } }, "node_modules/typedoc": { - "version": "0.26.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.8.tgz", - "integrity": "sha512-QBF0BMbnNeUc6U7pRHY7Jb8pjhmiNWZNQT8LU6uk9qP9t3goP9bJptdlNqMC0wBB2w9sQrxjZt835bpRSSq1LA==", + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.9.tgz", + "integrity": "sha512-Rc7QpWL7EtmrT8yxV0GmhOR6xHgFnnhphbD9Suti3fz3um7ZOrou6q/g9d6+zC5PssTLZmjaW4Upmzv8T1rCcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -19704,9 +19634,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -20086,18 +20016,19 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "node_modules/validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", "license": "ISC", - "dependencies": { - "builtins": "^1.0.3" + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/vary": { @@ -20352,9 +20283,9 @@ "license": "MIT" }, "node_modules/word-list": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/word-list/-/word-list-4.0.0.tgz", - "integrity": "sha512-K5ziGoPUuJAq30nkpRfK29fkwYxtr8VQzhGB8R/XqP0aucU7Lxd9m1TcLW4pGS2y6cLu7HX7714VxdesArmMUg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/word-list/-/word-list-4.1.0.tgz", + "integrity": "sha512-4vtKyH8lGfaY8DHLTKQDfDxPixdK6BC/2W38TsPE07Wc8uPrMIbEO3quxBFSa2BfsDyG6pW47UEN/9tfYxN6BA==", "dev": true, "license": "MIT", "engines": { @@ -20683,22 +20614,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "packages/cli": { - "name": "@matter/cli", - "version": "0.0.0-git", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@matter/model": "*", - "@matter/node": "*" - }, - "bin": { - "matter": "bin/matter.js" - }, - "devDependencies": { - "@matter/tools": "*" - } - }, "packages/cli-tool": { "name": "@matter/cli-tool", "version": "0.0.0-git", diff --git a/packages/cli-tool/src/location.ts b/packages/cli-tool/src/location.ts index 300b44d00d..0eed7fd714 100644 --- a/packages/cli-tool/src/location.ts +++ b/packages/cli-tool/src/location.ts @@ -47,7 +47,7 @@ export function Location(basename: string, definition: unknown, stat: Stat, pare tag = "array"; } else if (ArrayBuffer.isView(definition)) { tag = "bytes"; - } else if (definition && definition.constructor.name !== "Object") { + } else if (definition.constructor.name !== "Object") { tag = definition.constructor.name; } else { tag = "object"; diff --git a/packages/cli-tool/src/parser.ts b/packages/cli-tool/src/parser.ts index e1aa4d8777..21775be7a6 100644 --- a/packages/cli-tool/src/parser.ts +++ b/packages/cli-tool/src/parser.ts @@ -82,7 +82,7 @@ function errorIndicatesIncomplete(e: any, input: string): e is SyntaxError { if (e.message.startsWith("Unexpected token") && (e as unknown as { pos: number }).pos === input.length) { return true; } - if (e.message.match(/$Unterminated (?:template literal|group|comment|regular expression|template)/)) { + if (e.message.match(/^Unterminated (?:template literal|group|comment|regular expression|template)/)) { return true; } } diff --git a/packages/model/src/models/Model.ts b/packages/model/src/models/Model.ts index 9dc1758a35..f198bc4688 100644 --- a/packages/model/src/models/Model.ts +++ b/packages/model/src/models/Model.ts @@ -520,7 +520,7 @@ export abstract class Model { if (this.#children !== undefined && this.#children.length) { props.children = this.#children.length; } - return `${inspect(props, options)}`.replace("{", `${decamelize(this.tag)} {`); + return `${inspect(props, options)}`.replace(/^{/, `${decamelize(this.tag)} {`); } } diff --git a/packages/node/src/build.config.ts b/packages/node/src/build.config.ts index ab36bdbd5e..83dfffe45a 100644 --- a/packages/node/src/build.config.ts +++ b/packages/node/src/build.config.ts @@ -9,5 +9,5 @@ import { Project } from "@matter/tools"; export async function before({ project }: Project.Context) { // We must load "load.cjs" or "load.mjs" via self reference to conditional exports, but typescript won't find the // type definition this way unless it's in the dist location. So copy it there prior to build - project.copyToDist("src/loader/load.d.ts", "loader/load.d.ts"); + await project.copyToDist("src/loader/load.d.ts", "loader/load.d.ts"); } From 38534752bad25cf1aae06b849b5be3c463ac10f9 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Sat, 12 Oct 2024 00:44:06 -0700 Subject: [PATCH 10/11] Add missing CA cert initialization --- packages/protocol/src/fabric/Fabric.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/src/fabric/Fabric.ts b/packages/protocol/src/fabric/Fabric.ts index 30a303bbb3..267bee9091 100644 --- a/packages/protocol/src/fabric/Fabric.ts +++ b/packages/protocol/src/fabric/Fabric.ts @@ -118,6 +118,7 @@ export class Fabric { this.rootCert = config.rootCert; this.identityProtectionKey = config.identityProtectionKey; this.operationalIdentityProtectionKey = config.operationalIdentityProtectionKey; + this.intermediateCACert = config.intermediateCACert; this.operationalCert = config.operationalCert; this.label = config.label; From a1a41be5c6e591c73ec6a20633ed2682a3789348 Mon Sep 17 00:00:00 2001 From: Greg Lauckhart Date: Sat, 12 Oct 2024 16:39:02 -0700 Subject: [PATCH 11/11] Address review feedback. --- CHANGELOG.md | 7 +++++++ packages/node/src/node/storage/ClientNodeStore.ts | 9 +++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d69c69275..28208eb825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,11 +69,18 @@ The main work (all changes without a GitHub username in brackets in the below li - @matter/examples: - Enhancement: Adds a new example to show a PlugIn-Socket with Energy and Power measurement +- @matter/cli-tool: + - Feature: This new package offers a specialized JS environment for interacting with Matter and matter.js + - The "matter" command supports standard JS syntax and a "shell" style syntax that emulates common shell commands + - The virtual filesystem exposed by the tool allows you to navigate matter.js's packages and active subsystems + - This is an alpha feature. We'll add command line control and additional functionality over time + - Matter-Core functionality: - Enhancement: Allow to discover VendorId + ProductId together optionally - Matter.js clusters: - Adds convenience helper method for ElectricalEnergyMeasurement cluster (usage see new example MeasuredSocketDevice) to set measurements and also trigger the needed events when imported and exported values changed in the measurement and events are required by specification + - matter.js Controller API: - Breaking: PairedNode instances are now created and directly returned also when the node is not et connected. This do not block code flows anymore for offline devices - Breaking: Because of this "getConnectedNode()" got renamed to "getPairedNode()" diff --git a/packages/node/src/node/storage/ClientNodeStore.ts b/packages/node/src/node/storage/ClientNodeStore.ts index c7b00973a4..eeeefbb336 100644 --- a/packages/node/src/node/storage/ClientNodeStore.ts +++ b/packages/node/src/node/storage/ClientNodeStore.ts @@ -36,14 +36,15 @@ export class ClientNodeStore extends NodeStore { const stores = Array(); for (const addrStr of addresses) { - const addrComponents = addrStr.match(/^([0-9]+)-([0-9a-f])/i); - if (!addrComponents) { + const addrComponents = addrStr.split("-"); + const fabricIndex = FabricIndex(Number.parseInt(addrComponents[0])); + const nodeId = NodeId(Number.parseInt(addrComponents[1], 16)); + + if (addrComponents.length !== 2 || Number.isNaN(fabricIndex) || Number.isNaN(nodeId)) { logger.warn(`Ignoring peer storage context "${addrStr}" due to invalid name format`); continue; } - const fabricIndex = FabricIndex(Number.parseInt(addrComponents[1])); - const nodeId = NodeId(Number.parseInt(addrComponents[2], 16)); const addr = PeerAddress({ fabricIndex, nodeId }); stores.push(new ClientNodeStore(addr, peerStorage.createContext(addrStr)));