import { OperationalCredentials } from "@matter/main/clusters/operational-credentials"; import type { CommissioningController } from "@project-chip/matter.js"; import type { PairedNode } from "@project-chip/matter.js/device"; /** * Read-only view of the device's Operational Credentials fabric table. * * Every commissioning writes a fabric entry into the device's limited fabric * table. Entries never expire; identities whose local storage was deleted * leave orphans behind. These helpers make the table visible and flag likely * orphans. Removal is deliberately not offered: this controller does not use * its admin rights against other fabrics' entries. Clean stale entries up * from the device's primary ecosystem (e.g. Apple Home) or by factory reset; * our own live entry is removed with "decommission". */ export interface FabricEntry { fabricIndex: number; fabricId: bigint; nodeId: bigint; vendorId: number; label: string; /** True when the entry belongs to this controller's current identity. */ isOurs: boolean; } export interface FabricTable { supportedFabrics: number; commissionedFabrics: number; entries: FabricEntry[]; } const KNOWN_VENDORS: Record = { 0x1349: "Apple", 0x6006: "Google", 0x10e1: "SmartThings", 0x117c: "IKEA", }; function vendorName(vendorId: number): string { const known = KNOWN_VENDORS[vendorId]; if (known !== undefined) return known; if (vendorId >= 0xfff1 && vendorId <= 0xfff4) return "test vendor"; return "unknown vendor"; } export async function readFabricTable( controller: CommissioningController, node: PairedNode, ): Promise { const client = node.getRootClusterClient(OperationalCredentials.Complete); if (client === undefined) { throw new Error("Operational Credentials cluster not found on the device's root endpoint"); } // Read from the device with fabric filtering off, so entries from all // fabrics are returned, not just our own. const fabrics = await client.attributes.fabrics.get(true, false); const supportedFabrics = (await client.attributes.supportedFabrics.get(true)) ?? 0; const commissionedFabrics = (await client.attributes.commissionedFabrics.get(true)) ?? 0; if (fabrics === undefined) { throw new Error("Device returned no fabric list"); } const ourFabric = controller.fabric; const entries = fabrics.map(fabric => ({ fabricIndex: Number(fabric.fabricIndex), fabricId: BigInt(fabric.fabricId), nodeId: BigInt(fabric.nodeId), vendorId: Number(fabric.vendorId), label: fabric.label, isOurs: ourFabric.matchesFabricIdAndRootPublicKey(fabric.fabricId, fabric.rootPublicKey), })); return { supportedFabrics, commissionedFabrics, entries }; } export function formatFabricTable(nodeId: bigint, table: FabricTable): string { const lines: string[] = []; lines.push( `Fabric table on node ${nodeId} (${table.commissionedFabrics} of ${table.supportedFabrics} slots used):`, ); for (const entry of table.entries) { const vendor = `0x${entry.vendorId.toString(16).padStart(4, "0")} (${vendorName(entry.vendorId)})`; const marker = entry.isOurs ? " [this controller]" : ""; lines.push( ` index ${entry.fabricIndex}: label ${JSON.stringify(entry.label)} vendor ${vendor} ` + `fabricId ${entry.fabricId} nodeId ${entry.nodeId}${marker}`, ); } const strays = findLikelyStrays(table); if (strays.length > 0) { lines.push(""); lines.push( `Likely stale entries (our label, but not our current identity): ` + `${strays.map(entry => `index ${entry.fabricIndex}`).join(", ")}`, ); lines.push(`Remove them from the device's primary ecosystem (e.g. Apple Home: device settings,`); lines.push(`Connected Services) or by factory-resetting and re-pairing the device.`); } return lines.join("\n"); } /** * Entries carrying our fabric label but not our current identity: orphans * from a commissioning whose local storage was deleted or replaced. */ export function findLikelyStrays(table: FabricTable): FabricEntry[] { const ourLabel = table.entries.find(entry => entry.isOurs)?.label; return table.entries.filter(entry => !entry.isOurs && ourLabel !== undefined && entry.label === ourLabel); }