@camstack/addon-matter-broker 0.2.157 → 0.2.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.159] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - The integration form is declared as the instance form (the connection form); fabrics and their stored values are unchanged.
15
+
16
+ ## [0.2.158] - 2026-11-05
17
+
18
+ ### Changed
19
+
20
+ - The Matter manifest declares multiple instances, as the integration already allowed.
21
+
10
22
  ## [0.2.157] - 2026-11-05
11
23
 
12
24
  ### Changed
package/dist/addon.js CHANGED
@@ -5995,7 +5995,7 @@ var ZodIssueCode = {
5995
5995
  var ZodFirstPartyTypeKind;
5996
5996
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5997
5997
  //#endregion
5998
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5998
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5999
5999
  /**
6000
6000
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6001
6001
  * window to float samples (D455).
@@ -6804,6 +6804,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6804
6804
  }
6805
6805
  return out;
6806
6806
  }
6807
+ /** Every scope, in the order the guard and the docs list them. */
6808
+ var SCOPED_SETTINGS_SCOPES = [
6809
+ "cluster",
6810
+ "device-defaults",
6811
+ "node"
6812
+ ];
6807
6813
  /**
6808
6814
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6809
6815
  *
@@ -7695,6 +7701,13 @@ var BaseAddon = class {
7695
7701
  */
7696
7702
  secretStatePaths = [];
7697
7703
  _addonSecretTree = null;
7704
+ /**
7705
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7706
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7707
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7708
+ */
7709
+ _scopedByCap = /* @__PURE__ */ new Map();
7710
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7698
7711
  _deviceSecretTree = null;
7699
7712
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7700
7713
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7764,6 +7777,7 @@ var BaseAddon = class {
7764
7777
  this._ctx = context;
7765
7778
  this._secretSettings = null;
7766
7779
  }
7780
+ await this.loadScopedSchemas();
7767
7781
  await this.resolveConfig();
7768
7782
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7769
7783
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7773,6 +7787,7 @@ var BaseAddon = class {
7773
7787
  const normalized = normalizeAddonInitResult(result);
7774
7788
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7775
7789
  this._registeredCapNames = providers.map((p) => p.capability.name);
7790
+ await this.loadScopedSchemas();
7776
7791
  return normalized;
7777
7792
  }
7778
7793
  /**
@@ -7861,6 +7876,16 @@ var BaseAddon = class {
7861
7876
  schema(schema) {
7862
7877
  return schema;
7863
7878
  }
7879
+ /**
7880
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7881
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7882
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7883
+ * must already know which keys are per node. Names the providers register at init are added
7884
+ * once known.
7885
+ */
7886
+ scopedSettingsCapabilities() {
7887
+ return [];
7888
+ }
7864
7889
  /** Override to provide global-level settings UI schema. */
7865
7890
  globalSettingsSchema(_cap) {
7866
7891
  return null;
@@ -7904,7 +7929,8 @@ var BaseAddon = class {
7904
7929
  return [];
7905
7930
  }
7906
7931
  async getGlobalSettings(overlay, cap, nodeId) {
7907
- const schema = this.globalSettingsSchema(cap);
7932
+ await this.loadScopedSchemas(cap);
7933
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7908
7934
  if (!schema) return { sections: [] };
7909
7935
  const projected = await this.readRawProjectedStore(nodeId, cap);
7910
7936
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8035,6 +8061,7 @@ var BaseAddon = class {
8035
8061
  * resolved blob back is a no-op for every unchanged secret.
8036
8062
  */
8037
8063
  async resolveGlobalStore(nodeId, cap) {
8064
+ await this.loadScopedSchemas(cap);
8038
8065
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8039
8066
  return this.projectForNode(raw, nodeId, cap);
8040
8067
  }
@@ -8045,6 +8072,7 @@ var BaseAddon = class {
8045
8072
  }
8046
8073
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8047
8074
  async readRawProjectedStore(nodeId, cap) {
8075
+ await this.loadScopedSchemas(cap);
8048
8076
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8049
8077
  return this.projectForNode(raw, nodeId, cap);
8050
8078
  }
@@ -8147,6 +8175,7 @@ var BaseAddon = class {
8147
8175
  await this.applyGlobalPatch(patch, nodeId);
8148
8176
  }
8149
8177
  async applyGlobalPatch(barePatch, nodeId) {
8178
+ await this.loadScopedSchemas();
8150
8179
  const keys = this.perNodeKeys();
8151
8180
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8152
8181
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8160,7 +8189,8 @@ var BaseAddon = class {
8160
8189
  await this.resolveConfig();
8161
8190
  await this.onConfigChanged();
8162
8191
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8163
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8192
+ const legacy = this.globalSettingsSchema();
8193
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8164
8194
  }
8165
8195
  /**
8166
8196
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8168,14 +8198,67 @@ var BaseAddon = class {
8168
8198
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8169
8199
  * settings API behaves exactly like the legacy node-agnostic one.
8170
8200
  */
8171
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8201
+ /**
8202
+ * Load the scoped schemas of every capability this addon declares (the override and the
8203
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8204
+ * derived from the previous set. Idempotent.
8205
+ */
8206
+ async loadScopedSchemas(extraCap) {
8207
+ const caps = new Set([
8208
+ ...this.scopedSettingsCapabilities(),
8209
+ ...this._registeredCapNames,
8210
+ ...extraCap === void 0 ? [] : [extraCap]
8211
+ ]);
8212
+ let added = false;
8213
+ for (const cap of caps) {
8214
+ if (this._scopedByCap.has(cap)) continue;
8215
+ const loaded = [];
8216
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8217
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8218
+ if (schema !== null) loaded.push({
8219
+ scope,
8220
+ schema
8221
+ });
8222
+ }
8223
+ this._scopedByCap.set(cap, loaded);
8224
+ added = true;
8225
+ }
8226
+ if (!added) return;
8227
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8228
+ this._addonSecretTree = null;
8229
+ }
8230
+ /**
8231
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8232
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8233
+ */
8234
+ scopedFormSchema(cap) {
8235
+ const byId = /* @__PURE__ */ new Map();
8236
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8237
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8238
+ }
8239
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8240
+ loadedScopedSchemas(cap) {
8241
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8242
+ return [...this._scopedByCap.values()].flat();
8243
+ }
8244
+ /**
8245
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8246
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8247
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8248
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8249
+ * Call {@link loadScopedSchemas} first (the async seams do).
8250
+ */
8172
8251
  perNodeKeys(cap) {
8173
8252
  const cacheKey = cap ?? "";
8174
- const cached = this._perNodeKeysCache.get(cacheKey);
8253
+ const cached = this._nodeKeysByCap.get(cacheKey);
8175
8254
  if (cached) return cached;
8176
- const schema = this.globalSettingsSchema(cap);
8177
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8178
- this._perNodeKeysCache.set(cacheKey, keys);
8255
+ const legacy = this.globalSettingsSchema(cap);
8256
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8257
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8258
+ if (scope !== "node") continue;
8259
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8260
+ }
8261
+ this._nodeKeysByCap.set(cacheKey, keys);
8179
8262
  return keys;
8180
8263
  }
8181
8264
  /**
@@ -8185,10 +8268,9 @@ var BaseAddon = class {
8185
8268
  * time to return its response before the addon is torn down and
8186
8269
  * re-initialised by `AddonRegistryService.restartAddon`.
8187
8270
  */
8188
- maybeAutoRestart(patchKeys, schema) {
8189
- if (!schema) return;
8271
+ maybeAutoRestart(patchKeys, schemas) {
8190
8272
  const restartKeys = /* @__PURE__ */ new Set();
8191
- for (const section of schema.sections) for (const field of section.fields) {
8273
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8192
8274
  if (field.type === "separator" || field.type === "info") continue;
8193
8275
  if (field.requiresRestart) restartKeys.add(field.key);
8194
8276
  }
@@ -8384,12 +8466,14 @@ var BaseAddon = class {
8384
8466
  * here, in the addon's own process, so the hub keeps no copy of it.
8385
8467
  */
8386
8468
  async getDeclaredSettingsKeys() {
8469
+ await this.loadScopedSchemas();
8387
8470
  return { keys: [...this.declaredKeySet()].toSorted() };
8388
8471
  }
8389
8472
  declaredKeySet() {
8390
8473
  const keys = new Set(Object.keys(this.defaults));
8391
8474
  const schema = this.globalSettingsSchema();
8392
8475
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8476
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8393
8477
  for (const key of this._declaredStateKeys) keys.add(key);
8394
8478
  for (const path of this.secretStatePaths) {
8395
8479
  const root = path.split(/[.[]/, 1)[0];
@@ -8485,7 +8569,11 @@ var BaseAddon = class {
8485
8569
  }
8486
8570
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8487
8571
  addonSecretTree() {
8488
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8572
+ if (this._addonSecretTree === null) {
8573
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8574
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8575
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8576
+ }
8489
8577
  return this._addonSecretTree;
8490
8578
  }
8491
8579
  deviceSecretTree() {
@@ -8735,6 +8823,23 @@ function dropPerNodeFields(fields) {
8735
8823
  }
8736
8824
  return kept;
8737
8825
  }
8826
+ /** The keys of the value-holding fields, recursing into layout containers. */
8827
+ function collectValueFieldKeys(fields) {
8828
+ const collected = [];
8829
+ for (const field of fields) {
8830
+ if (field.type === "group") {
8831
+ collected.push(...collectValueFieldKeys(field.fields));
8832
+ continue;
8833
+ }
8834
+ if (field.type === "sub-tabs") {
8835
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8836
+ continue;
8837
+ }
8838
+ if (field.type === "separator" || field.type === "info") continue;
8839
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8840
+ }
8841
+ return collected;
8842
+ }
8738
8843
  function collectFieldKeys(fields) {
8739
8844
  const collected = [];
8740
8845
  for (const field of fields) {
@@ -13403,12 +13508,22 @@ var RecordIntegrationInstances = class {
13403
13508
  ...this.hooks.description,
13404
13509
  mintsId: true
13405
13510
  };
13406
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13511
+ /**
13512
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13513
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13514
+ * record id answers as it is. An id that names nothing comes back unchanged.
13515
+ */
13516
+ async resolveRecordId(id) {
13517
+ if (await this.handle.get(id) !== null) return id;
13518
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13519
+ return linked === void 0 ? id : linked.id;
13520
+ }
13521
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13407
13522
  listInstances = async () => {
13408
13523
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13409
13524
  };
13410
13525
  getInstance = async ({ id }) => {
13411
- const record = await this.handle.get(id);
13526
+ const record = await this.handle.get(await this.resolveRecordId(id));
13412
13527
  return record === null ? null : this.viewOf(record);
13413
13528
  };
13414
13529
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13425,7 +13540,8 @@ var RecordIntegrationInstances = class {
13425
13540
  await this.hooks.onChanged?.(minted, "created");
13426
13541
  return { id: minted };
13427
13542
  };
13428
- updateInstance = async ({ id, name, values }) => {
13543
+ updateInstance = async ({ id: askedId, name, values }) => {
13544
+ const id = await this.resolveRecordId(askedId);
13429
13545
  const patch = {};
13430
13546
  for (const [key, value] of Object.entries(values)) {
13431
13547
  if (value === void 0) continue;
@@ -13460,7 +13576,10 @@ var RecordIntegrationInstances = class {
13460
13576
  status: "unsupported",
13461
13577
  error: "This integration declares no connection test"
13462
13578
  };
13463
- return this.hooks.test(input);
13579
+ return this.hooks.test(input.id === null ? input : {
13580
+ ...input,
13581
+ id: await this.resolveRecordId(input.id)
13582
+ });
13464
13583
  };
13465
13584
  /** The flat provider object to hand to `registerProvider`. */
13466
13585
  toProvider() {
@@ -97060,6 +97179,111 @@ object({
97060
97179
  commands: record(string$2(), unknown())
97061
97180
  });
97062
97181
  //#endregion
97182
+ //#region src/config.ts
97183
+ /**
97184
+ * Matter broker connection settings for ONE fabric/controller (one broker entry).
97185
+ * v1 is intentionally minimal: optional Thread / Wi-Fi credentials supplied up
97186
+ * front for wireless commissioning. The pairing code itself is NOT stored here —
97187
+ * it is passed transiently to the commissioning action.
97188
+ *
97189
+ * The fabric credentials (root cert / NOC / node registry) live in the addon
97190
+ * `dataDir` via the controller's storage, NEVER in this typed KV.
97191
+ */
97192
+ var matterConnectionSchema = object({
97193
+ /** Optional Thread operational dataset (hex) for Thread device onboarding. */
97194
+ threadDataset: string$2().default("").describe("Thread operational dataset (hex) — required only for Thread devices").meta({ secret: true }),
97195
+ /** Optional Wi-Fi SSID for Wi-Fi device onboarding over BLE. */
97196
+ wifiSsid: string$2().default("").describe("Wi-Fi SSID — required only for BLE Wi-Fi onboarding"),
97197
+ /** Optional Wi-Fi password for Wi-Fi device onboarding over BLE. */
97198
+ wifiPassword: string$2().default("").describe("Wi-Fi password (optional)").meta({ secret: true }),
97199
+ /** PULL-reconcile interval for the commissioned-node inventory. */
97200
+ reconcileIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(3e5)).describe("Node-inventory reconcile interval (ms)")
97201
+ });
97202
+ /** A single registered Matter fabric ("broker"). */
97203
+ var MatterBrokerEntrySchema = object({
97204
+ id: string$2().min(1),
97205
+ name: string$2().min(1),
97206
+ connection: matterConnectionSchema,
97207
+ integrationId: string$2().optional(),
97208
+ /** Absent means on; the instances page switches a fabric off without deleting it. */
97209
+ enabled: boolean().optional()
97210
+ });
97211
+ /** Top-level addon config — the fabrics are records, so nothing is left here. */
97212
+ var matterAddonConfigSchema = object({});
97213
+ /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
97214
+ function toRecord(value) {
97215
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
97216
+ return {};
97217
+ }
97218
+ /** Coerce a loose settings blob through the connection schema, applying defaults. */
97219
+ function settingsToMatterConnection(settings) {
97220
+ return matterConnectionSchema.parse(toRecord(settings));
97221
+ }
97222
+ /**
97223
+ * Settings readers see an empty string for both secrets. The stored value
97224
+ * stays in the keychain (records `secret` meta). An empty round-trip must
97225
+ * not wipe it — see {@link mergeMatterSecrets}.
97226
+ */
97227
+ function publicMatterConnection(connection) {
97228
+ return {
97229
+ ...connection,
97230
+ wifiPassword: "",
97231
+ threadDataset: ""
97232
+ };
97233
+ }
97234
+ /** Keep a stored secret when the form sent back the redacted empty string. */
97235
+ function mergeMatterSecrets(existing, incoming) {
97236
+ return {
97237
+ ...incoming,
97238
+ wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
97239
+ threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
97240
+ };
97241
+ }
97242
+ /** Hand-written connection form for the broker/integration creation UI. */
97243
+ function buildConnectionFormSchema() {
97244
+ return { sections: [{
97245
+ id: "matter-fabric",
97246
+ title: "Matter fabric",
97247
+ titleKey: "matter-broker:connection.s_matter-fabric.title",
97248
+ description: "CamStack owns a Matter fabric on this hub. Add wireless onboarding credentials only if you will commission Thread or BLE-Wi-Fi devices. On-network (Ethernet/Wi-Fi) Matter devices need none of these.",
97249
+ descriptionKey: "matter-broker:connection.s_matter-fabric.description",
97250
+ columns: 1,
97251
+ fields: [
97252
+ {
97253
+ type: "password",
97254
+ key: "threadDataset",
97255
+ secret: true,
97256
+ showToggle: true,
97257
+ label: "Thread operational dataset (hex, optional)",
97258
+ labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
97259
+ placeholder: "0e08000000000001…"
97260
+ },
97261
+ {
97262
+ type: "text",
97263
+ key: "wifiSsid",
97264
+ label: "Wi-Fi SSID (optional)",
97265
+ labelKey: "matter-broker:connection.s_matter-fabric.wifiSsid.label"
97266
+ },
97267
+ {
97268
+ type: "password",
97269
+ key: "wifiPassword",
97270
+ label: "Wi-Fi password (optional)",
97271
+ labelKey: "matter-broker:connection.s_matter-fabric.wifiPassword.label",
97272
+ showToggle: true
97273
+ },
97274
+ {
97275
+ type: "number",
97276
+ key: "reconcileIntervalMs",
97277
+ label: "Node reconcile interval (ms)",
97278
+ labelKey: "matter-broker:connection.s_matter-fabric.reconcileIntervalMs.label",
97279
+ min: 1e4,
97280
+ max: 864e5,
97281
+ default: 3e5
97282
+ }
97283
+ ]
97284
+ }] };
97285
+ }
97286
+ //#endregion
97063
97287
  //#region ../../node_modules/@matter/general/dist/esm/net/tcp/TcpConnection.js
97064
97288
  /**
97065
97289
  * @license
@@ -207386,8 +207610,8 @@ function sameConnection(a, b) {
207386
207610
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207387
207611
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207388
207612
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207389
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207390
- * fabric, but the namespace tolerates multiple.
207613
+ * registry. The integration is `instanceMode: multiple`: the common case is one
207614
+ * fabric, and an operator may add more.
207391
207615
  */
207392
207616
  var MatterBrokerRegistry = class {
207393
207617
  #logger;
@@ -207575,111 +207799,6 @@ var MatterBrokerRegistry = class {
207575
207799
  }
207576
207800
  };
207577
207801
  //#endregion
207578
- //#region src/config.ts
207579
- /**
207580
- * Matter broker connection settings for ONE fabric/controller (one broker entry).
207581
- * v1 is intentionally minimal: optional Thread / Wi-Fi credentials supplied up
207582
- * front for wireless commissioning. The pairing code itself is NOT stored here —
207583
- * it is passed transiently to the commissioning action.
207584
- *
207585
- * The fabric credentials (root cert / NOC / node registry) live in the addon
207586
- * `dataDir` via the controller's storage, NEVER in this typed KV.
207587
- */
207588
- var matterConnectionSchema = object({
207589
- /** Optional Thread operational dataset (hex) for Thread device onboarding. */
207590
- threadDataset: string$2().default("").describe("Thread operational dataset (hex) — required only for Thread devices").meta({ secret: true }),
207591
- /** Optional Wi-Fi SSID for Wi-Fi device onboarding over BLE. */
207592
- wifiSsid: string$2().default("").describe("Wi-Fi SSID — required only for BLE Wi-Fi onboarding"),
207593
- /** Optional Wi-Fi password for Wi-Fi device onboarding over BLE. */
207594
- wifiPassword: string$2().default("").describe("Wi-Fi password (optional)").meta({ secret: true }),
207595
- /** PULL-reconcile interval for the commissioned-node inventory. */
207596
- reconcileIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(3e5)).describe("Node-inventory reconcile interval (ms)")
207597
- });
207598
- /** A single registered Matter fabric ("broker"). */
207599
- var MatterBrokerEntrySchema = object({
207600
- id: string$2().min(1),
207601
- name: string$2().min(1),
207602
- connection: matterConnectionSchema,
207603
- integrationId: string$2().optional(),
207604
- /** Absent means on; the instances page switches a fabric off without deleting it. */
207605
- enabled: boolean().optional()
207606
- });
207607
- /** Top-level addon config — the fabrics are records, so nothing is left here. */
207608
- var matterAddonConfigSchema = object({});
207609
- /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
207610
- function toRecord(value) {
207611
- if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
207612
- return {};
207613
- }
207614
- /** Coerce a loose settings blob through the connection schema, applying defaults. */
207615
- function settingsToMatterConnection(settings) {
207616
- return matterConnectionSchema.parse(toRecord(settings));
207617
- }
207618
- /**
207619
- * Settings readers see an empty string for both secrets. The stored value
207620
- * stays in the keychain (records `secret` meta). An empty round-trip must
207621
- * not wipe it — see {@link mergeMatterSecrets}.
207622
- */
207623
- function publicMatterConnection(connection) {
207624
- return {
207625
- ...connection,
207626
- wifiPassword: "",
207627
- threadDataset: ""
207628
- };
207629
- }
207630
- /** Keep a stored secret when the form sent back the redacted empty string. */
207631
- function mergeMatterSecrets(existing, incoming) {
207632
- return {
207633
- ...incoming,
207634
- wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
207635
- threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
207636
- };
207637
- }
207638
- /** Hand-written connection form for the broker/integration creation UI. */
207639
- function buildConnectionFormSchema() {
207640
- return { sections: [{
207641
- id: "matter-fabric",
207642
- title: "Matter fabric",
207643
- titleKey: "matter-broker:connection.s_matter-fabric.title",
207644
- description: "CamStack owns a Matter fabric on this hub. Add wireless onboarding credentials only if you will commission Thread or BLE-Wi-Fi devices. On-network (Ethernet/Wi-Fi) Matter devices need none of these.",
207645
- descriptionKey: "matter-broker:connection.s_matter-fabric.description",
207646
- columns: 1,
207647
- fields: [
207648
- {
207649
- type: "password",
207650
- key: "threadDataset",
207651
- secret: true,
207652
- showToggle: true,
207653
- label: "Thread operational dataset (hex, optional)",
207654
- labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
207655
- placeholder: "0e08000000000001…"
207656
- },
207657
- {
207658
- type: "text",
207659
- key: "wifiSsid",
207660
- label: "Wi-Fi SSID (optional)",
207661
- labelKey: "matter-broker:connection.s_matter-fabric.wifiSsid.label"
207662
- },
207663
- {
207664
- type: "password",
207665
- key: "wifiPassword",
207666
- label: "Wi-Fi password (optional)",
207667
- labelKey: "matter-broker:connection.s_matter-fabric.wifiPassword.label",
207668
- showToggle: true
207669
- },
207670
- {
207671
- type: "number",
207672
- key: "reconcileIntervalMs",
207673
- label: "Node reconcile interval (ms)",
207674
- labelKey: "matter-broker:connection.s_matter-fabric.reconcileIntervalMs.label",
207675
- min: 1e4,
207676
- max: 864e5,
207677
- default: 3e5
207678
- }
207679
- ]
207680
- }] };
207681
- }
207682
- //#endregion
207683
207802
  //#region src/matter-commissioning.ts
207684
207803
  /** A clear validation error for a malformed pairing code. */
207685
207804
  var InvalidPairingCodeError = class extends Error {
@@ -209241,22 +209360,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209241
209360
  }
209242
209361
  });
209243
209362
  }
209244
- globalSettingsSchema() {
209245
- return this.schema({ sections: [{
209246
- id: "matter-fabric",
209247
- title: "Matter fabric",
209248
- titleKey: "matter-broker:globalSettings.s_matter-fabric.title",
209249
- description: "Matter fabrics are managed in the External systems → Brokers tab.",
209250
- descriptionKey: "matter-broker:globalSettings.s_matter-fabric.description",
209251
- columns: 1,
209252
- fields: [{
209253
- type: "info",
209254
- key: "fabricHelp",
209255
- label: "Fabrics are managed separately",
209256
- labelKey: "matter-broker:globalSettings.s_matter-fabric.fabricHelp.label",
209257
- content: "This integration links to a Matter fabric broker. Add, edit, or remove fabrics from External systems → Brokers, then commission devices into the fabric. The integration only stores a reference to its broker."
209258
- }]
209259
- }] });
209363
+ /** The connection form a record carries; the values live on the record, never in `addon-settings`. */
209364
+ async integrationFormSchema(_instance) {
209365
+ return buildConnectionFormSchema();
209260
209366
  }
209261
209367
  async supportsManualCreation() {
209262
209368
  return false;
package/dist/addon.mjs CHANGED
@@ -5993,7 +5993,7 @@ var ZodIssueCode = {
5993
5993
  var ZodFirstPartyTypeKind;
5994
5994
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5995
5995
  //#endregion
5996
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5996
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5997
5997
  /**
5998
5998
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5999
5999
  * window to float samples (D455).
@@ -6802,6 +6802,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6802
6802
  }
6803
6803
  return out;
6804
6804
  }
6805
+ /** Every scope, in the order the guard and the docs list them. */
6806
+ var SCOPED_SETTINGS_SCOPES = [
6807
+ "cluster",
6808
+ "device-defaults",
6809
+ "node"
6810
+ ];
6805
6811
  /**
6806
6812
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6807
6813
  *
@@ -7693,6 +7699,13 @@ var BaseAddon = class {
7693
7699
  */
7694
7700
  secretStatePaths = [];
7695
7701
  _addonSecretTree = null;
7702
+ /**
7703
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7704
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7705
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7706
+ */
7707
+ _scopedByCap = /* @__PURE__ */ new Map();
7708
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7696
7709
  _deviceSecretTree = null;
7697
7710
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7698
7711
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7762,6 +7775,7 @@ var BaseAddon = class {
7762
7775
  this._ctx = context;
7763
7776
  this._secretSettings = null;
7764
7777
  }
7778
+ await this.loadScopedSchemas();
7765
7779
  await this.resolveConfig();
7766
7780
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7767
7781
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7771,6 +7785,7 @@ var BaseAddon = class {
7771
7785
  const normalized = normalizeAddonInitResult(result);
7772
7786
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7773
7787
  this._registeredCapNames = providers.map((p) => p.capability.name);
7788
+ await this.loadScopedSchemas();
7774
7789
  return normalized;
7775
7790
  }
7776
7791
  /**
@@ -7859,6 +7874,16 @@ var BaseAddon = class {
7859
7874
  schema(schema) {
7860
7875
  return schema;
7861
7876
  }
7877
+ /**
7878
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7879
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7880
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7881
+ * must already know which keys are per node. Names the providers register at init are added
7882
+ * once known.
7883
+ */
7884
+ scopedSettingsCapabilities() {
7885
+ return [];
7886
+ }
7862
7887
  /** Override to provide global-level settings UI schema. */
7863
7888
  globalSettingsSchema(_cap) {
7864
7889
  return null;
@@ -7902,7 +7927,8 @@ var BaseAddon = class {
7902
7927
  return [];
7903
7928
  }
7904
7929
  async getGlobalSettings(overlay, cap, nodeId) {
7905
- const schema = this.globalSettingsSchema(cap);
7930
+ await this.loadScopedSchemas(cap);
7931
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7906
7932
  if (!schema) return { sections: [] };
7907
7933
  const projected = await this.readRawProjectedStore(nodeId, cap);
7908
7934
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8033,6 +8059,7 @@ var BaseAddon = class {
8033
8059
  * resolved blob back is a no-op for every unchanged secret.
8034
8060
  */
8035
8061
  async resolveGlobalStore(nodeId, cap) {
8062
+ await this.loadScopedSchemas(cap);
8036
8063
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8037
8064
  return this.projectForNode(raw, nodeId, cap);
8038
8065
  }
@@ -8043,6 +8070,7 @@ var BaseAddon = class {
8043
8070
  }
8044
8071
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8045
8072
  async readRawProjectedStore(nodeId, cap) {
8073
+ await this.loadScopedSchemas(cap);
8046
8074
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8047
8075
  return this.projectForNode(raw, nodeId, cap);
8048
8076
  }
@@ -8145,6 +8173,7 @@ var BaseAddon = class {
8145
8173
  await this.applyGlobalPatch(patch, nodeId);
8146
8174
  }
8147
8175
  async applyGlobalPatch(barePatch, nodeId) {
8176
+ await this.loadScopedSchemas();
8148
8177
  const keys = this.perNodeKeys();
8149
8178
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8150
8179
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8158,7 +8187,8 @@ var BaseAddon = class {
8158
8187
  await this.resolveConfig();
8159
8188
  await this.onConfigChanged();
8160
8189
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8161
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8190
+ const legacy = this.globalSettingsSchema();
8191
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8162
8192
  }
8163
8193
  /**
8164
8194
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8166,14 +8196,67 @@ var BaseAddon = class {
8166
8196
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8167
8197
  * settings API behaves exactly like the legacy node-agnostic one.
8168
8198
  */
8169
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8199
+ /**
8200
+ * Load the scoped schemas of every capability this addon declares (the override and the
8201
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8202
+ * derived from the previous set. Idempotent.
8203
+ */
8204
+ async loadScopedSchemas(extraCap) {
8205
+ const caps = new Set([
8206
+ ...this.scopedSettingsCapabilities(),
8207
+ ...this._registeredCapNames,
8208
+ ...extraCap === void 0 ? [] : [extraCap]
8209
+ ]);
8210
+ let added = false;
8211
+ for (const cap of caps) {
8212
+ if (this._scopedByCap.has(cap)) continue;
8213
+ const loaded = [];
8214
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8215
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8216
+ if (schema !== null) loaded.push({
8217
+ scope,
8218
+ schema
8219
+ });
8220
+ }
8221
+ this._scopedByCap.set(cap, loaded);
8222
+ added = true;
8223
+ }
8224
+ if (!added) return;
8225
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8226
+ this._addonSecretTree = null;
8227
+ }
8228
+ /**
8229
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8230
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8231
+ */
8232
+ scopedFormSchema(cap) {
8233
+ const byId = /* @__PURE__ */ new Map();
8234
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8235
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8236
+ }
8237
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8238
+ loadedScopedSchemas(cap) {
8239
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8240
+ return [...this._scopedByCap.values()].flat();
8241
+ }
8242
+ /**
8243
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8244
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8245
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8246
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8247
+ * Call {@link loadScopedSchemas} first (the async seams do).
8248
+ */
8170
8249
  perNodeKeys(cap) {
8171
8250
  const cacheKey = cap ?? "";
8172
- const cached = this._perNodeKeysCache.get(cacheKey);
8251
+ const cached = this._nodeKeysByCap.get(cacheKey);
8173
8252
  if (cached) return cached;
8174
- const schema = this.globalSettingsSchema(cap);
8175
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8176
- this._perNodeKeysCache.set(cacheKey, keys);
8253
+ const legacy = this.globalSettingsSchema(cap);
8254
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8255
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8256
+ if (scope !== "node") continue;
8257
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8258
+ }
8259
+ this._nodeKeysByCap.set(cacheKey, keys);
8177
8260
  return keys;
8178
8261
  }
8179
8262
  /**
@@ -8183,10 +8266,9 @@ var BaseAddon = class {
8183
8266
  * time to return its response before the addon is torn down and
8184
8267
  * re-initialised by `AddonRegistryService.restartAddon`.
8185
8268
  */
8186
- maybeAutoRestart(patchKeys, schema) {
8187
- if (!schema) return;
8269
+ maybeAutoRestart(patchKeys, schemas) {
8188
8270
  const restartKeys = /* @__PURE__ */ new Set();
8189
- for (const section of schema.sections) for (const field of section.fields) {
8271
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8190
8272
  if (field.type === "separator" || field.type === "info") continue;
8191
8273
  if (field.requiresRestart) restartKeys.add(field.key);
8192
8274
  }
@@ -8382,12 +8464,14 @@ var BaseAddon = class {
8382
8464
  * here, in the addon's own process, so the hub keeps no copy of it.
8383
8465
  */
8384
8466
  async getDeclaredSettingsKeys() {
8467
+ await this.loadScopedSchemas();
8385
8468
  return { keys: [...this.declaredKeySet()].toSorted() };
8386
8469
  }
8387
8470
  declaredKeySet() {
8388
8471
  const keys = new Set(Object.keys(this.defaults));
8389
8472
  const schema = this.globalSettingsSchema();
8390
8473
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8474
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8391
8475
  for (const key of this._declaredStateKeys) keys.add(key);
8392
8476
  for (const path of this.secretStatePaths) {
8393
8477
  const root = path.split(/[.[]/, 1)[0];
@@ -8483,7 +8567,11 @@ var BaseAddon = class {
8483
8567
  }
8484
8568
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8485
8569
  addonSecretTree() {
8486
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8570
+ if (this._addonSecretTree === null) {
8571
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8572
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8573
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8574
+ }
8487
8575
  return this._addonSecretTree;
8488
8576
  }
8489
8577
  deviceSecretTree() {
@@ -8733,6 +8821,23 @@ function dropPerNodeFields(fields) {
8733
8821
  }
8734
8822
  return kept;
8735
8823
  }
8824
+ /** The keys of the value-holding fields, recursing into layout containers. */
8825
+ function collectValueFieldKeys(fields) {
8826
+ const collected = [];
8827
+ for (const field of fields) {
8828
+ if (field.type === "group") {
8829
+ collected.push(...collectValueFieldKeys(field.fields));
8830
+ continue;
8831
+ }
8832
+ if (field.type === "sub-tabs") {
8833
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8834
+ continue;
8835
+ }
8836
+ if (field.type === "separator" || field.type === "info") continue;
8837
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8838
+ }
8839
+ return collected;
8840
+ }
8736
8841
  function collectFieldKeys(fields) {
8737
8842
  const collected = [];
8738
8843
  for (const field of fields) {
@@ -13401,12 +13506,22 @@ var RecordIntegrationInstances = class {
13401
13506
  ...this.hooks.description,
13402
13507
  mintsId: true
13403
13508
  };
13404
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13509
+ /**
13510
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13511
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13512
+ * record id answers as it is. An id that names nothing comes back unchanged.
13513
+ */
13514
+ async resolveRecordId(id) {
13515
+ if (await this.handle.get(id) !== null) return id;
13516
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13517
+ return linked === void 0 ? id : linked.id;
13518
+ }
13519
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13405
13520
  listInstances = async () => {
13406
13521
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13407
13522
  };
13408
13523
  getInstance = async ({ id }) => {
13409
- const record = await this.handle.get(id);
13524
+ const record = await this.handle.get(await this.resolveRecordId(id));
13410
13525
  return record === null ? null : this.viewOf(record);
13411
13526
  };
13412
13527
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13423,7 +13538,8 @@ var RecordIntegrationInstances = class {
13423
13538
  await this.hooks.onChanged?.(minted, "created");
13424
13539
  return { id: minted };
13425
13540
  };
13426
- updateInstance = async ({ id, name, values }) => {
13541
+ updateInstance = async ({ id: askedId, name, values }) => {
13542
+ const id = await this.resolveRecordId(askedId);
13427
13543
  const patch = {};
13428
13544
  for (const [key, value] of Object.entries(values)) {
13429
13545
  if (value === void 0) continue;
@@ -13458,7 +13574,10 @@ var RecordIntegrationInstances = class {
13458
13574
  status: "unsupported",
13459
13575
  error: "This integration declares no connection test"
13460
13576
  };
13461
- return this.hooks.test(input);
13577
+ return this.hooks.test(input.id === null ? input : {
13578
+ ...input,
13579
+ id: await this.resolveRecordId(input.id)
13580
+ });
13462
13581
  };
13463
13582
  /** The flat provider object to hand to `registerProvider`. */
13464
13583
  toProvider() {
@@ -97058,6 +97177,111 @@ object({
97058
97177
  commands: record(string$2(), unknown())
97059
97178
  });
97060
97179
  //#endregion
97180
+ //#region src/config.ts
97181
+ /**
97182
+ * Matter broker connection settings for ONE fabric/controller (one broker entry).
97183
+ * v1 is intentionally minimal: optional Thread / Wi-Fi credentials supplied up
97184
+ * front for wireless commissioning. The pairing code itself is NOT stored here —
97185
+ * it is passed transiently to the commissioning action.
97186
+ *
97187
+ * The fabric credentials (root cert / NOC / node registry) live in the addon
97188
+ * `dataDir` via the controller's storage, NEVER in this typed KV.
97189
+ */
97190
+ var matterConnectionSchema = object({
97191
+ /** Optional Thread operational dataset (hex) for Thread device onboarding. */
97192
+ threadDataset: string$2().default("").describe("Thread operational dataset (hex) — required only for Thread devices").meta({ secret: true }),
97193
+ /** Optional Wi-Fi SSID for Wi-Fi device onboarding over BLE. */
97194
+ wifiSsid: string$2().default("").describe("Wi-Fi SSID — required only for BLE Wi-Fi onboarding"),
97195
+ /** Optional Wi-Fi password for Wi-Fi device onboarding over BLE. */
97196
+ wifiPassword: string$2().default("").describe("Wi-Fi password (optional)").meta({ secret: true }),
97197
+ /** PULL-reconcile interval for the commissioned-node inventory. */
97198
+ reconcileIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(3e5)).describe("Node-inventory reconcile interval (ms)")
97199
+ });
97200
+ /** A single registered Matter fabric ("broker"). */
97201
+ var MatterBrokerEntrySchema = object({
97202
+ id: string$2().min(1),
97203
+ name: string$2().min(1),
97204
+ connection: matterConnectionSchema,
97205
+ integrationId: string$2().optional(),
97206
+ /** Absent means on; the instances page switches a fabric off without deleting it. */
97207
+ enabled: boolean().optional()
97208
+ });
97209
+ /** Top-level addon config — the fabrics are records, so nothing is left here. */
97210
+ var matterAddonConfigSchema = object({});
97211
+ /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
97212
+ function toRecord(value) {
97213
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
97214
+ return {};
97215
+ }
97216
+ /** Coerce a loose settings blob through the connection schema, applying defaults. */
97217
+ function settingsToMatterConnection(settings) {
97218
+ return matterConnectionSchema.parse(toRecord(settings));
97219
+ }
97220
+ /**
97221
+ * Settings readers see an empty string for both secrets. The stored value
97222
+ * stays in the keychain (records `secret` meta). An empty round-trip must
97223
+ * not wipe it — see {@link mergeMatterSecrets}.
97224
+ */
97225
+ function publicMatterConnection(connection) {
97226
+ return {
97227
+ ...connection,
97228
+ wifiPassword: "",
97229
+ threadDataset: ""
97230
+ };
97231
+ }
97232
+ /** Keep a stored secret when the form sent back the redacted empty string. */
97233
+ function mergeMatterSecrets(existing, incoming) {
97234
+ return {
97235
+ ...incoming,
97236
+ wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
97237
+ threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
97238
+ };
97239
+ }
97240
+ /** Hand-written connection form for the broker/integration creation UI. */
97241
+ function buildConnectionFormSchema() {
97242
+ return { sections: [{
97243
+ id: "matter-fabric",
97244
+ title: "Matter fabric",
97245
+ titleKey: "matter-broker:connection.s_matter-fabric.title",
97246
+ description: "CamStack owns a Matter fabric on this hub. Add wireless onboarding credentials only if you will commission Thread or BLE-Wi-Fi devices. On-network (Ethernet/Wi-Fi) Matter devices need none of these.",
97247
+ descriptionKey: "matter-broker:connection.s_matter-fabric.description",
97248
+ columns: 1,
97249
+ fields: [
97250
+ {
97251
+ type: "password",
97252
+ key: "threadDataset",
97253
+ secret: true,
97254
+ showToggle: true,
97255
+ label: "Thread operational dataset (hex, optional)",
97256
+ labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
97257
+ placeholder: "0e08000000000001…"
97258
+ },
97259
+ {
97260
+ type: "text",
97261
+ key: "wifiSsid",
97262
+ label: "Wi-Fi SSID (optional)",
97263
+ labelKey: "matter-broker:connection.s_matter-fabric.wifiSsid.label"
97264
+ },
97265
+ {
97266
+ type: "password",
97267
+ key: "wifiPassword",
97268
+ label: "Wi-Fi password (optional)",
97269
+ labelKey: "matter-broker:connection.s_matter-fabric.wifiPassword.label",
97270
+ showToggle: true
97271
+ },
97272
+ {
97273
+ type: "number",
97274
+ key: "reconcileIntervalMs",
97275
+ label: "Node reconcile interval (ms)",
97276
+ labelKey: "matter-broker:connection.s_matter-fabric.reconcileIntervalMs.label",
97277
+ min: 1e4,
97278
+ max: 864e5,
97279
+ default: 3e5
97280
+ }
97281
+ ]
97282
+ }] };
97283
+ }
97284
+ //#endregion
97061
97285
  //#region ../../node_modules/@matter/general/dist/esm/net/tcp/TcpConnection.js
97062
97286
  /**
97063
97287
  * @license
@@ -207384,8 +207608,8 @@ function sameConnection(a, b) {
207384
207608
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207385
207609
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207386
207610
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207387
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207388
- * fabric, but the namespace tolerates multiple.
207611
+ * registry. The integration is `instanceMode: multiple`: the common case is one
207612
+ * fabric, and an operator may add more.
207389
207613
  */
207390
207614
  var MatterBrokerRegistry = class {
207391
207615
  #logger;
@@ -207573,111 +207797,6 @@ var MatterBrokerRegistry = class {
207573
207797
  }
207574
207798
  };
207575
207799
  //#endregion
207576
- //#region src/config.ts
207577
- /**
207578
- * Matter broker connection settings for ONE fabric/controller (one broker entry).
207579
- * v1 is intentionally minimal: optional Thread / Wi-Fi credentials supplied up
207580
- * front for wireless commissioning. The pairing code itself is NOT stored here —
207581
- * it is passed transiently to the commissioning action.
207582
- *
207583
- * The fabric credentials (root cert / NOC / node registry) live in the addon
207584
- * `dataDir` via the controller's storage, NEVER in this typed KV.
207585
- */
207586
- var matterConnectionSchema = object({
207587
- /** Optional Thread operational dataset (hex) for Thread device onboarding. */
207588
- threadDataset: string$2().default("").describe("Thread operational dataset (hex) — required only for Thread devices").meta({ secret: true }),
207589
- /** Optional Wi-Fi SSID for Wi-Fi device onboarding over BLE. */
207590
- wifiSsid: string$2().default("").describe("Wi-Fi SSID — required only for BLE Wi-Fi onboarding"),
207591
- /** Optional Wi-Fi password for Wi-Fi device onboarding over BLE. */
207592
- wifiPassword: string$2().default("").describe("Wi-Fi password (optional)").meta({ secret: true }),
207593
- /** PULL-reconcile interval for the commissioned-node inventory. */
207594
- reconcileIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(3e5)).describe("Node-inventory reconcile interval (ms)")
207595
- });
207596
- /** A single registered Matter fabric ("broker"). */
207597
- var MatterBrokerEntrySchema = object({
207598
- id: string$2().min(1),
207599
- name: string$2().min(1),
207600
- connection: matterConnectionSchema,
207601
- integrationId: string$2().optional(),
207602
- /** Absent means on; the instances page switches a fabric off without deleting it. */
207603
- enabled: boolean().optional()
207604
- });
207605
- /** Top-level addon config — the fabrics are records, so nothing is left here. */
207606
- var matterAddonConfigSchema = object({});
207607
- /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
207608
- function toRecord(value) {
207609
- if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
207610
- return {};
207611
- }
207612
- /** Coerce a loose settings blob through the connection schema, applying defaults. */
207613
- function settingsToMatterConnection(settings) {
207614
- return matterConnectionSchema.parse(toRecord(settings));
207615
- }
207616
- /**
207617
- * Settings readers see an empty string for both secrets. The stored value
207618
- * stays in the keychain (records `secret` meta). An empty round-trip must
207619
- * not wipe it — see {@link mergeMatterSecrets}.
207620
- */
207621
- function publicMatterConnection(connection) {
207622
- return {
207623
- ...connection,
207624
- wifiPassword: "",
207625
- threadDataset: ""
207626
- };
207627
- }
207628
- /** Keep a stored secret when the form sent back the redacted empty string. */
207629
- function mergeMatterSecrets(existing, incoming) {
207630
- return {
207631
- ...incoming,
207632
- wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
207633
- threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
207634
- };
207635
- }
207636
- /** Hand-written connection form for the broker/integration creation UI. */
207637
- function buildConnectionFormSchema() {
207638
- return { sections: [{
207639
- id: "matter-fabric",
207640
- title: "Matter fabric",
207641
- titleKey: "matter-broker:connection.s_matter-fabric.title",
207642
- description: "CamStack owns a Matter fabric on this hub. Add wireless onboarding credentials only if you will commission Thread or BLE-Wi-Fi devices. On-network (Ethernet/Wi-Fi) Matter devices need none of these.",
207643
- descriptionKey: "matter-broker:connection.s_matter-fabric.description",
207644
- columns: 1,
207645
- fields: [
207646
- {
207647
- type: "password",
207648
- key: "threadDataset",
207649
- secret: true,
207650
- showToggle: true,
207651
- label: "Thread operational dataset (hex, optional)",
207652
- labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
207653
- placeholder: "0e08000000000001…"
207654
- },
207655
- {
207656
- type: "text",
207657
- key: "wifiSsid",
207658
- label: "Wi-Fi SSID (optional)",
207659
- labelKey: "matter-broker:connection.s_matter-fabric.wifiSsid.label"
207660
- },
207661
- {
207662
- type: "password",
207663
- key: "wifiPassword",
207664
- label: "Wi-Fi password (optional)",
207665
- labelKey: "matter-broker:connection.s_matter-fabric.wifiPassword.label",
207666
- showToggle: true
207667
- },
207668
- {
207669
- type: "number",
207670
- key: "reconcileIntervalMs",
207671
- label: "Node reconcile interval (ms)",
207672
- labelKey: "matter-broker:connection.s_matter-fabric.reconcileIntervalMs.label",
207673
- min: 1e4,
207674
- max: 864e5,
207675
- default: 3e5
207676
- }
207677
- ]
207678
- }] };
207679
- }
207680
- //#endregion
207681
207800
  //#region src/matter-commissioning.ts
207682
207801
  /** A clear validation error for a malformed pairing code. */
207683
207802
  var InvalidPairingCodeError = class extends Error {
@@ -209239,22 +209358,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209239
209358
  }
209240
209359
  });
209241
209360
  }
209242
- globalSettingsSchema() {
209243
- return this.schema({ sections: [{
209244
- id: "matter-fabric",
209245
- title: "Matter fabric",
209246
- titleKey: "matter-broker:globalSettings.s_matter-fabric.title",
209247
- description: "Matter fabrics are managed in the External systems → Brokers tab.",
209248
- descriptionKey: "matter-broker:globalSettings.s_matter-fabric.description",
209249
- columns: 1,
209250
- fields: [{
209251
- type: "info",
209252
- key: "fabricHelp",
209253
- label: "Fabrics are managed separately",
209254
- labelKey: "matter-broker:globalSettings.s_matter-fabric.fabricHelp.label",
209255
- content: "This integration links to a Matter fabric broker. Add, edit, or remove fabrics from External systems → Brokers, then commission devices into the fabric. The integration only stores a reference to its broker."
209256
- }]
209257
- }] });
209361
+ /** The connection form a record carries; the values live on the record, never in `addon-settings`. */
209362
+ async integrationFormSchema(_instance) {
209363
+ return buildConnectionFormSchema();
209258
209364
  }
209259
209365
  async supportsManualCreation() {
209260
209366
  return false;
@@ -209303,4 +209409,4 @@ function endpointsEqual(a, b) {
209303
209409
  return true;
209304
209410
  }
209305
209411
  //#endregion
209306
- export { MatterBrokerAddon, parsePairingCode as a, matterConnectionSchema as c, endpointLabel as d, mapMatterEndpointToCamStack as f, createMatterController as h, isValidPairingCode as i, settingsToMatterConnection as l, StubMatterController as m, endpointNativeId as n, buildConnectionFormSchema as o, MatterControllerNotWiredError as p, InvalidPairingCodeError as r, matterAddonConfigSchema as s, buildMatterNodeCandidate as t, MatterClusterId as u };
209412
+ export { MatterBrokerAddon, parsePairingCode as a, mapMatterEndpointToCamStack as c, createMatterController as d, buildConnectionFormSchema as f, settingsToMatterConnection as h, isValidPairingCode as i, MatterControllerNotWiredError as l, matterConnectionSchema as m, endpointNativeId as n, MatterClusterId as o, matterAddonConfigSchema as p, InvalidPairingCodeError as r, endpointLabel as s, buildMatterNodeCandidate as t, StubMatterController as u };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { MatterBrokerAddon, a as parsePairingCode, c as matterConnectionSchema, d as endpointLabel, f as mapMatterEndpointToCamStack, h as createMatterController, i as isValidPairingCode, l as settingsToMatterConnection, m as StubMatterController, n as endpointNativeId, o as buildConnectionFormSchema, p as MatterControllerNotWiredError, r as InvalidPairingCodeError, s as matterAddonConfigSchema, t as buildMatterNodeCandidate, u as MatterClusterId } from "./addon.mjs";
1
+ import { MatterBrokerAddon, a as parsePairingCode, c as mapMatterEndpointToCamStack, d as createMatterController, f as buildConnectionFormSchema, h as settingsToMatterConnection, i as isValidPairingCode, l as MatterControllerNotWiredError, m as matterConnectionSchema, n as endpointNativeId, o as MatterClusterId, p as matterAddonConfigSchema, r as InvalidPairingCodeError, s as endpointLabel, t as buildMatterNodeCandidate, u as StubMatterController } from "./addon.mjs";
2
2
  export { InvalidPairingCodeError, MatterBrokerAddon, MatterClusterId, MatterControllerNotWiredError, StubMatterController, buildConnectionFormSchema, buildMatterNodeCandidate, createMatterController, endpointLabel, endpointNativeId, isValidPairingCode, mapMatterEndpointToCamStack, matterAddonConfigSchema, matterConnectionSchema, parsePairingCode, settingsToMatterConnection };
package/i18n/en.json CHANGED
@@ -1,7 +1,4 @@
1
1
  {
2
- "globalSettings.s_matter-fabric.title": "Matter fabric",
3
- "globalSettings.s_matter-fabric.description": "Matter fabrics are managed in the External systems → Brokers tab.",
4
- "globalSettings.s_matter-fabric.fabricHelp.label": "Fabrics are managed separately",
5
2
  "connection.s_matter-fabric.title": "Matter fabric",
6
3
  "connection.s_matter-fabric.description": "CamStack owns a Matter fabric on this hub. Add wireless onboarding credentials only if you will commission Thread or BLE-Wi-Fi devices. On-network (Ethernet/Wi-Fi) Matter devices need none of these.",
7
4
  "connection.s_matter-fabric.threadDataset.label": "Thread operational dataset (hex, optional)",
package/i18n/it.json CHANGED
@@ -1,7 +1,4 @@
1
1
  {
2
- "globalSettings.s_matter-fabric.title": "Fabric Matter",
3
- "globalSettings.s_matter-fabric.description": "I fabric Matter si gestiscono nella scheda Sistemi esterni → Broker.",
4
- "globalSettings.s_matter-fabric.fabricHelp.label": "I fabric si gestiscono separatamente",
5
2
  "connection.s_matter-fabric.title": "Fabric Matter",
6
3
  "connection.s_matter-fabric.description": "CamStack gestisce un fabric Matter su questo hub. Aggiungi le credenziali di onboarding wireless solo se metterai in servizio dispositivi Thread o BLE-Wi-Fi. I dispositivi Matter sulla rete (Ethernet/Wi-Fi) non ne hanno bisogno.",
7
4
  "connection.s_matter-fabric.threadDataset.label": "Dataset operativo Thread (esadecimale, facoltativo)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.157",
3
+ "version": "0.2.159",
4
4
  "description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -43,7 +43,7 @@
43
43
  "entry": "./dist/addon.js",
44
44
  "icon": "assets/icon.svg",
45
45
  "color": "#3b6ea5",
46
- "instanceMode": "unique",
46
+ "instanceMode": "multiple",
47
47
  "brokerKind": "matter",
48
48
  "execution": {
49
49
  "placement": "hub-only"
@@ -65,7 +65,7 @@
65
65
  }
66
66
  ],
67
67
  "requires": {
68
- "server": ">=1.2.770"
68
+ "server": ">=1.2.777"
69
69
  }
70
70
  },
71
71
  "files": [