@camstack/addon-matter-broker 0.2.155 → 0.2.156

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.156] - 2026-11-04
11
+
12
+ ### Changed
13
+
14
+ - The integration states its role (devices, services or both) to the Integrations page
15
+
10
16
  ## [0.2.155] - 2026-11-04
11
17
 
12
18
  ### 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-BkzQdpzF.mjs
5998
+ //#region ../types/dist/sleep-BaWhXZFm.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).
@@ -8831,6 +8831,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8831
8831
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8832
8832
  * (`auth.accessToken`, `headers[].value`).
8833
8833
  *
8834
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8835
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8836
+ * key releases exactly that key's secret.
8837
+ *
8834
8838
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8835
8839
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8836
8840
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8843,11 +8847,13 @@ function parseSecretPath(path) {
8843
8847
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8844
8848
  return path.split(".").map((raw) => {
8845
8849
  const each = raw.endsWith("[]");
8846
- const key = each ? raw.slice(0, -2) : raw;
8850
+ const map = raw.endsWith("{}");
8851
+ const key = each || map ? raw.slice(0, -2) : raw;
8847
8852
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8848
8853
  return {
8849
8854
  key,
8850
- each
8855
+ each,
8856
+ map
8851
8857
  };
8852
8858
  });
8853
8859
  }
@@ -8899,6 +8905,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8899
8905
  [segment.key]: mapped
8900
8906
  };
8901
8907
  }
8908
+ if (segment.map) {
8909
+ if (!isRecord$4$1(child)) return node;
8910
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8911
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8912
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8913
+ return [mapKey, visit({
8914
+ declared,
8915
+ concrete,
8916
+ value
8917
+ })];
8918
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8919
+ return {
8920
+ ...node,
8921
+ [segment.key]: Object.fromEntries(kept)
8922
+ };
8923
+ }
8902
8924
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8903
8925
  if (last) {
8904
8926
  const replacement = visit({
@@ -9040,7 +9062,8 @@ function isZodSecret(schema) {
9040
9062
  }
9041
9063
  /**
9042
9064
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9043
- * element of this array" (`connection.password`, `rows[].token`). A declared
9065
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9066
+ * map" (`applianceSecrets{}`). A declared
9044
9067
  * object or array is reported as one path and not descended into — everything
9045
9068
  * under it is secret.
9046
9069
  */
@@ -9051,6 +9074,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9051
9074
  const def = defOf(layer);
9052
9075
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9053
9076
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9077
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9054
9078
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9055
9079
  else if (def.type === "intersection") {
9056
9080
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13248,11 +13272,49 @@ var IntegrationInstanceRefSchema = object({
13248
13272
  ]),
13249
13273
  candidateStableId: string$2().optional()
13250
13274
  });
13275
+ /**
13276
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13277
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13278
+ *
13279
+ * Every instance is one record of the addon's own kind, keyed by the
13280
+ * `integrations` registry id. The records door does the secret work: a field the
13281
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13282
+ * deleting the row deletes the secret, and one row's value is never another's.
13283
+ *
13284
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13285
+ * token or the view the form was given KEEP the stored secret;
13286
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13287
+ */
13251
13288
  /** The most instances one addon lists (an integrations page, not a table). */
13252
13289
  var INSTANCES_LIST_MAX = 1e3;
13253
13290
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13254
13291
  function topLevelSecretKeys(paths) {
13255
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13292
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13293
+ }
13294
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13295
+ function secretMapKeys(paths) {
13296
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13297
+ }
13298
+ /**
13299
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13300
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13301
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13302
+ */
13303
+ function secretMapPatch(value) {
13304
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13305
+ const out = {};
13306
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13307
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13308
+ return Object.keys(out).length > 0 ? out : null;
13309
+ }
13310
+ /** Does the kind's body schema carry an `enabled` field? */
13311
+ function declaresEnabled(kind) {
13312
+ const json = toJSONSchema(kind.schema, {
13313
+ unrepresentable: "any",
13314
+ io: "input"
13315
+ });
13316
+ const properties = Reflect.get(json, "properties");
13317
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13256
13318
  }
13257
13319
  var RecordIntegrationInstances = class {
13258
13320
  records;
@@ -13260,14 +13322,20 @@ var RecordIntegrationInstances = class {
13260
13322
  hooks;
13261
13323
  handle;
13262
13324
  secretKeys;
13325
+ secretMaps;
13263
13326
  nameField;
13327
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13328
+ registryOwnsEnabled;
13264
13329
  constructor(records, kind, hooks) {
13265
13330
  this.records = records;
13266
13331
  this.kind = kind;
13267
13332
  this.hooks = hooks;
13268
13333
  this.handle = records.kind(kind);
13269
13334
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13335
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13270
13336
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13337
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13338
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13271
13339
  }
13272
13340
  /**
13273
13341
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13318,7 +13386,7 @@ var RecordIntegrationInstances = class {
13318
13386
  id: record.id,
13319
13387
  name: typeof rawName === "string" ? rawName : record.id,
13320
13388
  configured: this.hooks.configured(record.data),
13321
- enabled: record.data.enabled !== false,
13389
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13322
13390
  formValues: shown,
13323
13391
  ...kind !== void 0 ? { kind } : {},
13324
13392
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13348,32 +13416,36 @@ var RecordIntegrationInstances = class {
13348
13416
  id: minted,
13349
13417
  data: this.hooks.initialBody(name, kind)
13350
13418
  }, { ifRev: 0 });
13351
- await this.hooks.onChanged?.(minted);
13419
+ await this.hooks.onChanged?.(minted, "created");
13352
13420
  return { id: minted };
13353
13421
  };
13354
13422
  updateInstance = async ({ id, name, values }) => {
13355
13423
  const patch = {};
13356
13424
  for (const [key, value] of Object.entries(values)) {
13357
13425
  if (value === void 0) continue;
13358
- if (!this.secretKeys.has(key)) patch[key] = value;
13426
+ if (this.secretMaps.has(key)) {
13427
+ const entries = secretMapPatch(value);
13428
+ if (entries !== null) patch[key] = entries;
13429
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13359
13430
  else if (isSecretClear(value)) patch[key] = null;
13360
13431
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13361
13432
  }
13362
13433
  if (name !== void 0) patch[this.nameField] = name;
13363
13434
  await this.handle.patch(id, patch);
13364
- await this.hooks.onChanged?.(id);
13435
+ await this.hooks.onChanged?.(id, "updated");
13365
13436
  return { ok: true };
13366
13437
  };
13367
13438
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13368
13439
  setInstanceEnabled = async ({ id, enabled }) => {
13440
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13369
13441
  await this.handle.patch(id, { enabled });
13370
- await this.hooks.onChanged?.(id);
13442
+ await this.hooks.onChanged?.(id, "enabled");
13371
13443
  return { ok: true };
13372
13444
  };
13373
13445
  /** The row and its keychain refs go together (the records door). */
13374
13446
  deleteInstance = async ({ id }) => {
13375
13447
  await this.handle.delete(id);
13376
- await this.hooks.onChanged?.(id);
13448
+ await this.hooks.onChanged?.(id, "deleted");
13377
13449
  return { ok: true };
13378
13450
  };
13379
13451
  testInstance = async (input) => {
@@ -56100,6 +56172,21 @@ var IntegrationInstanceKindSchema = object({
56100
56172
  disconnectPath: string$2().min(1).optional()
56101
56173
  });
56102
56174
  var IntegrationInstanceDescriptionSchema = object({
56175
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56176
+ role: _enum([
56177
+ "devices",
56178
+ "services",
56179
+ "both"
56180
+ ]),
56181
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56182
+ discovery: object({
56183
+ mode: _enum([
56184
+ "none",
56185
+ "lan",
56186
+ "account"
56187
+ ]),
56188
+ scopedToInstance: boolean()
56189
+ }).optional(),
56103
56190
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56104
56191
  instanceMode: IntegrationInstanceModeSchema,
56105
56192
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56139,10 +56226,11 @@ var IntegrationInstanceSchema = object({
56139
56226
  /** The addon's own rule (an URL is present, a client id is present...). */
56140
56227
  configured: boolean(),
56141
56228
  /**
56142
- * The instance's own on/off switch (D996): always present, derived from the record body
56143
- * (`enabled !== false`, so a row written before the field existed is on).
56229
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56230
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56231
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56144
56232
  */
56145
- enabled: boolean(),
56233
+ enabled: boolean().optional(),
56146
56234
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56147
56235
  kind: string$2().optional(),
56148
56236
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -208592,6 +208680,7 @@ function statusOf(info) {
208592
208680
  function buildMatterInstances(records, deps) {
208593
208681
  return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208594
208682
  description: {
208683
+ role: "both",
208595
208684
  instanceMode: "multiple",
208596
208685
  kind: "connection",
208597
208686
  canTest: true,
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-BkzQdpzF.mjs
5996
+ //#region ../types/dist/sleep-BaWhXZFm.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).
@@ -8829,6 +8829,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8829
8829
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8830
8830
  * (`auth.accessToken`, `headers[].value`).
8831
8831
  *
8832
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8833
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8834
+ * key releases exactly that key's secret.
8835
+ *
8832
8836
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8833
8837
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8834
8838
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8841,11 +8845,13 @@ function parseSecretPath(path) {
8841
8845
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8842
8846
  return path.split(".").map((raw) => {
8843
8847
  const each = raw.endsWith("[]");
8844
- const key = each ? raw.slice(0, -2) : raw;
8848
+ const map = raw.endsWith("{}");
8849
+ const key = each || map ? raw.slice(0, -2) : raw;
8845
8850
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8846
8851
  return {
8847
8852
  key,
8848
- each
8853
+ each,
8854
+ map
8849
8855
  };
8850
8856
  });
8851
8857
  }
@@ -8897,6 +8903,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8897
8903
  [segment.key]: mapped
8898
8904
  };
8899
8905
  }
8906
+ if (segment.map) {
8907
+ if (!isRecord$4$1(child)) return node;
8908
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8909
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8910
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8911
+ return [mapKey, visit({
8912
+ declared,
8913
+ concrete,
8914
+ value
8915
+ })];
8916
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8917
+ return {
8918
+ ...node,
8919
+ [segment.key]: Object.fromEntries(kept)
8920
+ };
8921
+ }
8900
8922
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8901
8923
  if (last) {
8902
8924
  const replacement = visit({
@@ -9038,7 +9060,8 @@ function isZodSecret(schema) {
9038
9060
  }
9039
9061
  /**
9040
9062
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9041
- * element of this array" (`connection.password`, `rows[].token`). A declared
9063
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9064
+ * map" (`applianceSecrets{}`). A declared
9042
9065
  * object or array is reported as one path and not descended into — everything
9043
9066
  * under it is secret.
9044
9067
  */
@@ -9049,6 +9072,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9049
9072
  const def = defOf(layer);
9050
9073
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9051
9074
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9075
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9052
9076
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9053
9077
  else if (def.type === "intersection") {
9054
9078
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13246,11 +13270,49 @@ var IntegrationInstanceRefSchema = object({
13246
13270
  ]),
13247
13271
  candidateStableId: string$2().optional()
13248
13272
  });
13273
+ /**
13274
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13275
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13276
+ *
13277
+ * Every instance is one record of the addon's own kind, keyed by the
13278
+ * `integrations` registry id. The records door does the secret work: a field the
13279
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13280
+ * deleting the row deletes the secret, and one row's value is never another's.
13281
+ *
13282
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13283
+ * token or the view the form was given KEEP the stored secret;
13284
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13285
+ */
13249
13286
  /** The most instances one addon lists (an integrations page, not a table). */
13250
13287
  var INSTANCES_LIST_MAX = 1e3;
13251
13288
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13252
13289
  function topLevelSecretKeys(paths) {
13253
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13290
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13291
+ }
13292
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13293
+ function secretMapKeys(paths) {
13294
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13295
+ }
13296
+ /**
13297
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13298
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13299
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13300
+ */
13301
+ function secretMapPatch(value) {
13302
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13303
+ const out = {};
13304
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13305
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13306
+ return Object.keys(out).length > 0 ? out : null;
13307
+ }
13308
+ /** Does the kind's body schema carry an `enabled` field? */
13309
+ function declaresEnabled(kind) {
13310
+ const json = toJSONSchema(kind.schema, {
13311
+ unrepresentable: "any",
13312
+ io: "input"
13313
+ });
13314
+ const properties = Reflect.get(json, "properties");
13315
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13254
13316
  }
13255
13317
  var RecordIntegrationInstances = class {
13256
13318
  records;
@@ -13258,14 +13320,20 @@ var RecordIntegrationInstances = class {
13258
13320
  hooks;
13259
13321
  handle;
13260
13322
  secretKeys;
13323
+ secretMaps;
13261
13324
  nameField;
13325
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13326
+ registryOwnsEnabled;
13262
13327
  constructor(records, kind, hooks) {
13263
13328
  this.records = records;
13264
13329
  this.kind = kind;
13265
13330
  this.hooks = hooks;
13266
13331
  this.handle = records.kind(kind);
13267
13332
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13333
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13268
13334
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13335
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13336
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13269
13337
  }
13270
13338
  /**
13271
13339
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13316,7 +13384,7 @@ var RecordIntegrationInstances = class {
13316
13384
  id: record.id,
13317
13385
  name: typeof rawName === "string" ? rawName : record.id,
13318
13386
  configured: this.hooks.configured(record.data),
13319
- enabled: record.data.enabled !== false,
13387
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13320
13388
  formValues: shown,
13321
13389
  ...kind !== void 0 ? { kind } : {},
13322
13390
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13346,32 +13414,36 @@ var RecordIntegrationInstances = class {
13346
13414
  id: minted,
13347
13415
  data: this.hooks.initialBody(name, kind)
13348
13416
  }, { ifRev: 0 });
13349
- await this.hooks.onChanged?.(minted);
13417
+ await this.hooks.onChanged?.(minted, "created");
13350
13418
  return { id: minted };
13351
13419
  };
13352
13420
  updateInstance = async ({ id, name, values }) => {
13353
13421
  const patch = {};
13354
13422
  for (const [key, value] of Object.entries(values)) {
13355
13423
  if (value === void 0) continue;
13356
- if (!this.secretKeys.has(key)) patch[key] = value;
13424
+ if (this.secretMaps.has(key)) {
13425
+ const entries = secretMapPatch(value);
13426
+ if (entries !== null) patch[key] = entries;
13427
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13357
13428
  else if (isSecretClear(value)) patch[key] = null;
13358
13429
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13359
13430
  }
13360
13431
  if (name !== void 0) patch[this.nameField] = name;
13361
13432
  await this.handle.patch(id, patch);
13362
- await this.hooks.onChanged?.(id);
13433
+ await this.hooks.onChanged?.(id, "updated");
13363
13434
  return { ok: true };
13364
13435
  };
13365
13436
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13366
13437
  setInstanceEnabled = async ({ id, enabled }) => {
13438
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13367
13439
  await this.handle.patch(id, { enabled });
13368
- await this.hooks.onChanged?.(id);
13440
+ await this.hooks.onChanged?.(id, "enabled");
13369
13441
  return { ok: true };
13370
13442
  };
13371
13443
  /** The row and its keychain refs go together (the records door). */
13372
13444
  deleteInstance = async ({ id }) => {
13373
13445
  await this.handle.delete(id);
13374
- await this.hooks.onChanged?.(id);
13446
+ await this.hooks.onChanged?.(id, "deleted");
13375
13447
  return { ok: true };
13376
13448
  };
13377
13449
  testInstance = async (input) => {
@@ -56098,6 +56170,21 @@ var IntegrationInstanceKindSchema = object({
56098
56170
  disconnectPath: string$2().min(1).optional()
56099
56171
  });
56100
56172
  var IntegrationInstanceDescriptionSchema = object({
56173
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56174
+ role: _enum([
56175
+ "devices",
56176
+ "services",
56177
+ "both"
56178
+ ]),
56179
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56180
+ discovery: object({
56181
+ mode: _enum([
56182
+ "none",
56183
+ "lan",
56184
+ "account"
56185
+ ]),
56186
+ scopedToInstance: boolean()
56187
+ }).optional(),
56101
56188
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56102
56189
  instanceMode: IntegrationInstanceModeSchema,
56103
56190
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56137,10 +56224,11 @@ var IntegrationInstanceSchema = object({
56137
56224
  /** The addon's own rule (an URL is present, a client id is present...). */
56138
56225
  configured: boolean(),
56139
56226
  /**
56140
- * The instance's own on/off switch (D996): always present, derived from the record body
56141
- * (`enabled !== false`, so a row written before the field existed is on).
56227
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56228
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56229
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56142
56230
  */
56143
- enabled: boolean(),
56231
+ enabled: boolean().optional(),
56144
56232
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56145
56233
  kind: string$2().optional(),
56146
56234
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -208590,6 +208678,7 @@ function statusOf(info) {
208590
208678
  function buildMatterInstances(records, deps) {
208591
208679
  return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208592
208680
  description: {
208681
+ role: "both",
208593
208682
  instanceMode: "multiple",
208594
208683
  kind: "connection",
208595
208684
  canTest: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.155",
3
+ "version": "0.2.156",
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",
@@ -65,7 +65,7 @@
65
65
  }
66
66
  ],
67
67
  "requires": {
68
- "server": ">=1.2.761"
68
+ "server": ">=1.2.762"
69
69
  }
70
70
  },
71
71
  "files": [