/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { ActionContext } from "#behavior/context/ActionContext.js"; import { CommissioningServer } from "#behavior/system/commissioning/CommissioningServer.js"; import { EventsBehavior } from "#behavior/system/events/EventsBehavior.js"; import { NetworkServer } from "#behavior/system/network/NetworkServer.js"; import { ServerNetworkRuntime } from "#behavior/system/network/ServerNetworkRuntime.js"; import { ProductDescriptionServer } from "#behavior/system/product-description/ProductDescriptionServer.js"; import { SessionsBehavior } from "#behavior/system/sessions/SessionsBehavior.js"; import { SubscriptionsServer } from "#behavior/system/subscriptions/SubscriptionsServer.js"; import { Endpoint } from "#endpoint/Endpoint.js"; import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js"; import { ServerNodeStore } from "#storage/server/ServerNodeStore.js"; import type { Environment } from "@matter/general"; import { asyncNew, Construction, Diagnostic, DiagnosticSource, errorOf, Identity, MatterAggregateError, MatterError, } from "@matter/general"; import { FabricManager, Interactable, OccurrenceManager, PeerSet, ServerInteraction, SessionManager, } from "@matter/protocol"; import { AccessControlServer } from "../behaviors/access-control/AccessControlServer.js"; import { GroupcastServer } from "../behaviors/groupcast/GroupcastServer.js"; import { RootEndpoint as BaseRootEndpoint } from "../endpoints/root.js"; import { Peers } from "./client/Peers.js"; import { Node } from "./Node.js"; import { Plugins } from "./Plugins.js"; import { DeviceTypeConformanceService } from "./server/DeviceTypeConformanceService.js"; import { IdentityService } from "./server/IdentityService.js"; import { ServerEnvironment } from "./server/ServerEnvironment.js"; /** * Thrown when there is an error during factory reset. */ class FactoryResetError extends MatterError { constructor(message: string, cause: any) { super(message); this.cause = errorOf(cause); } } /** * A server-side Matter {@link Node}. * * The Matter specification often refers to server-side nodes as "devices". */ export class ServerNode extends Node { #peers?: Peers; #interaction?: Interactable; #plugins: Plugins; /** * Construct a new ServerNode. * * You can use {@link create} to construct the node and wait for it to initialize fully. * * @param type the variation of {@link RootEndpoint} that defines the root endpoint's behavior * @param options root endpoint options and the node's environment */ constructor(type?: T, options?: Node.Options); /** * Construct a new ServerNode. * * You can use {@link create} to construct the node and wait for it to initialize fully. * * @param config a {@link Endpoint.Configuration} for the root endpoint */ constructor(config: Partial>); constructor(definition?: T | Node.Configuration, options?: Node.Options) { const config = Node.nodeConfigFor(ServerNode.RootEndpoint as T, definition, options ?? ({} as Node.Options)); super(config); this.#plugins = new Plugins(this); this.env.set(Node, this); this.env.set(ServerNode, this); DiagnosticSource.add(this); this.construction.start(); } /** * Create a new ServerNode. * * @param type the variation of {@link RootEndpoint} that defines the root endpoint's behavior * @param options root endpoint configuration and, optionally, the node's environment */ static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, type?: T, options?: Node.Options): Promise>; /** * Create a new ServerNode. * * @param config root endpoint configuration and, optionally, the node's {@link Environment} */ static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, config: Partial>): Promise>; static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, definition?: T | Node.Configuration, options?: Node.Options) { return await asyncNew(this, definition, options); } protected createRuntime(): ServerNetworkRuntime { return new ServerNetworkRuntime(this); } override async [Construction.destruct]() { await MatterAggregateError.settleSeries( [() => this.#peers?.close(), () => super[Construction.destruct](), () => ServerEnvironment.close(this)], `Error destroying server node ${this}`, ); } protected override get diagnosticSections() { const peers = this.#peers; if (peers === undefined || peers.size === 0) { return []; } return [ Diagnostic.strong("Peers"), Diagnostic.list([...peers].map(peer => `${peer} ${peer.lifecycle.isOnline ? "online" : "offline"}`)), ]; } override async prepareRuntimeShutdown() { await this.env.maybeGet(PeerSet)?.disconnect(); await this.env.get(SessionManager).closeAllSessions(); } /** * Perform a factory reset of the node. */ override async erase() { using _erasing = this.construction.join("erasing"); await this.lifecycle.mutex.produce(this.eraseWithMutex.bind(this)); } protected async eraseWithMutex() { try { await this.construction; // Go offline before performing reset const isOnline = this.lifecycle.isOnline; if (isOnline) { await this.cancelWithMutex(); } // Inform user this.statusUpdate("resetting to factory defaults"); // Reset in-memory state await this.resetWithMutex(); // Reset persistent state await this.resetStorage(); // The node's services outlive the reset, so what they hold for the state it discarded does not. This is // deliberately not part of resetStorage(), which an application may override await this.resetServiceState(); // Reset reverts node to inactive state; now reinitialize this.construction.start(); // Go back online if we were online at time of reset and should still be online, otherwise just await reinitialization if (isOnline && this.lifecycle.shouldBeOnline) { await this.startWithMutex(); } else { await this.construction.ready; } } catch (e) { this.construction.crash(); throw new FactoryResetError(`Error during factory reset of ${this}`, e); } } /** * Access other nodes on the fabric. */ get peers() { if (!this.#peers) { try { this.#peers = new Peers(this); this.#peers.initialize(); } catch (e) { this.#peers = undefined; throw e; } } return this.#peers; } get interaction() { if (this.#interaction === undefined) { this.#interaction = new ServerInteraction(this.protocol); } return this.#interaction; } override readonly nodeType = "server" as const; protected override async initialize() { await ServerEnvironment.initialize(this); await this.#plugins.load(); await super.initialize(); } /** * By default, on factory reset we erase all stored data. * * If this is inappropriate for your application, you may override to alter the behavior. Matter requires that all * "security- and privacy-related data and key material" is removed on factory reset. * * @see {@link MatterSpecification.v161.Core} § 13.4 */ protected async resetStorage() { await MatterAggregateError.settleSeries( [ // Peers first so their teardown still sees the fabrics and sessions it depends on () => this.#peers?.erase(), () => this.env.get(SessionManager).clear(), () => this.env.get(FabricManager).clear(), () => this.env.get(OccurrenceManager).clear(), () => ServerEnvironment.eraseCredentials(this), () => this.env.get(ServerNodeStore).erase(), ], `Error erasing storage of ${this}`, ); } /** * Discard the in-memory state the node's services hold for what a factory reset erases. */ private async resetServiceState() { this.env.get(IdentityService).releaseReservedPeerAddresses(); this.env.get(DeviceTypeConformanceService).reset(); this.env.get(EndpointInitializer).variableService?.invalidate(); } /** * Normal endpoints must have an owner to complete construction but server nodes have no such precondition for * construction. */ protected override assertConstructable() {} } export namespace ServerNode { /** * The root endpoint of a server node without the Groupcast cluster. Its Access Control server has no Auxiliary * feature. Controllers, which need neither, use this as their root. * * A node with a Groups server on any endpoint does not conform to Matter 1.6.1 without Groupcast on its root, so * use this only for nodes without Groups or that deliberately model a pre-Groupcast device. */ export const RootEndpointWithoutGroupcast = BaseRootEndpoint.with( CommissioningServer, NetworkServer, ProductDescriptionServer, SubscriptionsServer, SessionsBehavior, EventsBehavior, ); export interface RootEndpointWithoutGroupcast extends Identity {} /** * The default root endpoint of a server node. * * @see {@link MatterSpecification.v161.Device} § 2.1 */ export const RootEndpoint: RootEndpoint = RootEndpointWithoutGroupcast.with( // Groupcast Listener requires the Auxiliary ACL feature AccessControlServer.with("Extension", "Auxiliary"), GroupcastServer, ); /** * Typed without Groupcast so a node type may select any Groupcast features, or leave the cluster out, and still * satisfy this type. Use `node.stateOf(GroupcastServer)` for the Groupcast state of a default node. */ export interface RootEndpoint extends Identity {} } Object.freeze(ServerNode.RootEndpointWithoutGroupcast); Object.freeze(ServerNode.RootEndpoint);