@camstack/addon-import-alexa 0.2.183 → 0.2.184

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.184] - 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.183] - 2026-11-04
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -6044,7 +6044,7 @@ function number(params) {
6044
6044
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6045
6045
  }
6046
6046
  //#endregion
6047
- //#region ../types/dist/sleep-BkzQdpzF.mjs
6047
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
6048
6048
  /**
6049
6049
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6050
6050
  * window to float samples (D455).
@@ -8880,6 +8880,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8880
8880
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8881
8881
  * (`auth.accessToken`, `headers[].value`).
8882
8882
  *
8883
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8884
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8885
+ * key releases exactly that key's secret.
8886
+ *
8883
8887
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8884
8888
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8885
8889
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8892,11 +8896,13 @@ function parseSecretPath(path) {
8892
8896
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8893
8897
  return path.split(".").map((raw) => {
8894
8898
  const each = raw.endsWith("[]");
8895
- const key = each ? raw.slice(0, -2) : raw;
8899
+ const map = raw.endsWith("{}");
8900
+ const key = each || map ? raw.slice(0, -2) : raw;
8896
8901
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8897
8902
  return {
8898
8903
  key,
8899
- each
8904
+ each,
8905
+ map
8900
8906
  };
8901
8907
  });
8902
8908
  }
@@ -8948,6 +8954,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8948
8954
  [segment.key]: mapped
8949
8955
  };
8950
8956
  }
8957
+ if (segment.map) {
8958
+ if (!isRecord$4$1(child)) return node;
8959
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8960
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8961
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8962
+ return [mapKey, visit({
8963
+ declared,
8964
+ concrete,
8965
+ value
8966
+ })];
8967
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8968
+ return {
8969
+ ...node,
8970
+ [segment.key]: Object.fromEntries(kept)
8971
+ };
8972
+ }
8951
8973
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8952
8974
  if (last) {
8953
8975
  const replacement = visit({
@@ -9089,7 +9111,8 @@ function isZodSecret(schema) {
9089
9111
  }
9090
9112
  /**
9091
9113
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9092
- * element of this array" (`connection.password`, `rows[].token`). A declared
9114
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9115
+ * map" (`applianceSecrets{}`). A declared
9093
9116
  * object or array is reported as one path and not descended into — everything
9094
9117
  * under it is secret.
9095
9118
  */
@@ -9100,6 +9123,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9100
9123
  const def = defOf(layer);
9101
9124
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9102
9125
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9126
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9103
9127
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9104
9128
  else if (def.type === "intersection") {
9105
9129
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13414,11 +13438,49 @@ function buildCapturingReply(envelope) {
13414
13438
  };
13415
13439
  return wrapper;
13416
13440
  }
13441
+ /**
13442
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13443
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13444
+ *
13445
+ * Every instance is one record of the addon's own kind, keyed by the
13446
+ * `integrations` registry id. The records door does the secret work: a field the
13447
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13448
+ * deleting the row deletes the secret, and one row's value is never another's.
13449
+ *
13450
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13451
+ * token or the view the form was given KEEP the stored secret;
13452
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13453
+ */
13417
13454
  /** The most instances one addon lists (an integrations page, not a table). */
13418
13455
  var INSTANCES_LIST_MAX = 1e3;
13419
13456
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13420
13457
  function topLevelSecretKeys(paths) {
13421
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13458
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13459
+ }
13460
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13461
+ function secretMapKeys(paths) {
13462
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13463
+ }
13464
+ /**
13465
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13466
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13467
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13468
+ */
13469
+ function secretMapPatch(value) {
13470
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13471
+ const out = {};
13472
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13473
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13474
+ return Object.keys(out).length > 0 ? out : null;
13475
+ }
13476
+ /** Does the kind's body schema carry an `enabled` field? */
13477
+ function declaresEnabled(kind) {
13478
+ const json = toJSONSchema(kind.schema, {
13479
+ unrepresentable: "any",
13480
+ io: "input"
13481
+ });
13482
+ const properties = Reflect.get(json, "properties");
13483
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13422
13484
  }
13423
13485
  var RecordIntegrationInstances = class {
13424
13486
  records;
@@ -13426,14 +13488,20 @@ var RecordIntegrationInstances = class {
13426
13488
  hooks;
13427
13489
  handle;
13428
13490
  secretKeys;
13491
+ secretMaps;
13429
13492
  nameField;
13493
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13494
+ registryOwnsEnabled;
13430
13495
  constructor(records, kind, hooks) {
13431
13496
  this.records = records;
13432
13497
  this.kind = kind;
13433
13498
  this.hooks = hooks;
13434
13499
  this.handle = records.kind(kind);
13435
13500
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13501
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13436
13502
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13503
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13504
+ 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"`);
13437
13505
  }
13438
13506
  /**
13439
13507
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13484,7 +13552,7 @@ var RecordIntegrationInstances = class {
13484
13552
  id: record.id,
13485
13553
  name: typeof rawName === "string" ? rawName : record.id,
13486
13554
  configured: this.hooks.configured(record.data),
13487
- enabled: record.data.enabled !== false,
13555
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13488
13556
  formValues: shown,
13489
13557
  ...kind !== void 0 ? { kind } : {},
13490
13558
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13514,32 +13582,36 @@ var RecordIntegrationInstances = class {
13514
13582
  id: minted,
13515
13583
  data: this.hooks.initialBody(name, kind)
13516
13584
  }, { ifRev: 0 });
13517
- await this.hooks.onChanged?.(minted);
13585
+ await this.hooks.onChanged?.(minted, "created");
13518
13586
  return { id: minted };
13519
13587
  };
13520
13588
  updateInstance = async ({ id, name, values }) => {
13521
13589
  const patch = {};
13522
13590
  for (const [key, value] of Object.entries(values)) {
13523
13591
  if (value === void 0) continue;
13524
- if (!this.secretKeys.has(key)) patch[key] = value;
13592
+ if (this.secretMaps.has(key)) {
13593
+ const entries = secretMapPatch(value);
13594
+ if (entries !== null) patch[key] = entries;
13595
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13525
13596
  else if (isSecretClear(value)) patch[key] = null;
13526
13597
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13527
13598
  }
13528
13599
  if (name !== void 0) patch[this.nameField] = name;
13529
13600
  await this.handle.patch(id, patch);
13530
- await this.hooks.onChanged?.(id);
13601
+ await this.hooks.onChanged?.(id, "updated");
13531
13602
  return { ok: true };
13532
13603
  };
13533
13604
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13534
13605
  setInstanceEnabled = async ({ id, enabled }) => {
13606
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13535
13607
  await this.handle.patch(id, { enabled });
13536
- await this.hooks.onChanged?.(id);
13608
+ await this.hooks.onChanged?.(id, "enabled");
13537
13609
  return { ok: true };
13538
13610
  };
13539
13611
  /** The row and its keychain refs go together (the records door). */
13540
13612
  deleteInstance = async ({ id }) => {
13541
13613
  await this.handle.delete(id);
13542
- await this.hooks.onChanged?.(id);
13614
+ await this.hooks.onChanged?.(id, "deleted");
13543
13615
  return { ok: true };
13544
13616
  };
13545
13617
  testInstance = async (input) => {
@@ -56307,6 +56379,21 @@ var IntegrationInstanceKindSchema = object({
56307
56379
  disconnectPath: string().min(1).optional()
56308
56380
  });
56309
56381
  var IntegrationInstanceDescriptionSchema = object({
56382
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56383
+ role: _enum([
56384
+ "devices",
56385
+ "services",
56386
+ "both"
56387
+ ]),
56388
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56389
+ discovery: object({
56390
+ mode: _enum([
56391
+ "none",
56392
+ "lan",
56393
+ "account"
56394
+ ]),
56395
+ scopedToInstance: boolean()
56396
+ }).optional(),
56310
56397
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56311
56398
  instanceMode: IntegrationInstanceModeSchema,
56312
56399
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56346,10 +56433,11 @@ var IntegrationInstanceSchema = object({
56346
56433
  /** The addon's own rule (an URL is present, a client id is present...). */
56347
56434
  configured: boolean(),
56348
56435
  /**
56349
- * The instance's own on/off switch (D996): always present, derived from the record body
56350
- * (`enabled !== false`, so a row written before the field existed is on).
56436
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56437
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56438
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56351
56439
  */
56352
- enabled: boolean(),
56440
+ enabled: boolean().optional(),
56353
56441
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56354
56442
  kind: string().optional(),
56355
56443
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -143315,6 +143403,7 @@ function statusOf(info, configured) {
143315
143403
  function buildAlexaInstances(records, deps) {
143316
143404
  return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
143317
143405
  description: {
143406
+ role: "both",
143318
143407
  instanceMode: "multiple",
143319
143408
  kind: "account",
143320
143409
  canTest: true,
package/dist/addon.mjs CHANGED
@@ -6042,7 +6042,7 @@ function number(params) {
6042
6042
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6043
6043
  }
6044
6044
  //#endregion
6045
- //#region ../types/dist/sleep-BkzQdpzF.mjs
6045
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
6046
6046
  /**
6047
6047
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6048
6048
  * window to float samples (D455).
@@ -8878,6 +8878,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8878
8878
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8879
8879
  * (`auth.accessToken`, `headers[].value`).
8880
8880
  *
8881
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8882
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8883
+ * key releases exactly that key's secret.
8884
+ *
8881
8885
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8882
8886
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8883
8887
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8890,11 +8894,13 @@ function parseSecretPath(path) {
8890
8894
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8891
8895
  return path.split(".").map((raw) => {
8892
8896
  const each = raw.endsWith("[]");
8893
- const key = each ? raw.slice(0, -2) : raw;
8897
+ const map = raw.endsWith("{}");
8898
+ const key = each || map ? raw.slice(0, -2) : raw;
8894
8899
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8895
8900
  return {
8896
8901
  key,
8897
- each
8902
+ each,
8903
+ map
8898
8904
  };
8899
8905
  });
8900
8906
  }
@@ -8946,6 +8952,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8946
8952
  [segment.key]: mapped
8947
8953
  };
8948
8954
  }
8955
+ if (segment.map) {
8956
+ if (!isRecord$4$1(child)) return node;
8957
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8958
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8959
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8960
+ return [mapKey, visit({
8961
+ declared,
8962
+ concrete,
8963
+ value
8964
+ })];
8965
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8966
+ return {
8967
+ ...node,
8968
+ [segment.key]: Object.fromEntries(kept)
8969
+ };
8970
+ }
8949
8971
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8950
8972
  if (last) {
8951
8973
  const replacement = visit({
@@ -9087,7 +9109,8 @@ function isZodSecret(schema) {
9087
9109
  }
9088
9110
  /**
9089
9111
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9090
- * element of this array" (`connection.password`, `rows[].token`). A declared
9112
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9113
+ * map" (`applianceSecrets{}`). A declared
9091
9114
  * object or array is reported as one path and not descended into — everything
9092
9115
  * under it is secret.
9093
9116
  */
@@ -9098,6 +9121,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9098
9121
  const def = defOf(layer);
9099
9122
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9100
9123
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9124
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9101
9125
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9102
9126
  else if (def.type === "intersection") {
9103
9127
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13412,11 +13436,49 @@ function buildCapturingReply(envelope) {
13412
13436
  };
13413
13437
  return wrapper;
13414
13438
  }
13439
+ /**
13440
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13441
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13442
+ *
13443
+ * Every instance is one record of the addon's own kind, keyed by the
13444
+ * `integrations` registry id. The records door does the secret work: a field the
13445
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13446
+ * deleting the row deletes the secret, and one row's value is never another's.
13447
+ *
13448
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13449
+ * token or the view the form was given KEEP the stored secret;
13450
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13451
+ */
13415
13452
  /** The most instances one addon lists (an integrations page, not a table). */
13416
13453
  var INSTANCES_LIST_MAX = 1e3;
13417
13454
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13418
13455
  function topLevelSecretKeys(paths) {
13419
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13456
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13457
+ }
13458
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13459
+ function secretMapKeys(paths) {
13460
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13461
+ }
13462
+ /**
13463
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13464
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13465
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13466
+ */
13467
+ function secretMapPatch(value) {
13468
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13469
+ const out = {};
13470
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13471
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13472
+ return Object.keys(out).length > 0 ? out : null;
13473
+ }
13474
+ /** Does the kind's body schema carry an `enabled` field? */
13475
+ function declaresEnabled(kind) {
13476
+ const json = toJSONSchema(kind.schema, {
13477
+ unrepresentable: "any",
13478
+ io: "input"
13479
+ });
13480
+ const properties = Reflect.get(json, "properties");
13481
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13420
13482
  }
13421
13483
  var RecordIntegrationInstances = class {
13422
13484
  records;
@@ -13424,14 +13486,20 @@ var RecordIntegrationInstances = class {
13424
13486
  hooks;
13425
13487
  handle;
13426
13488
  secretKeys;
13489
+ secretMaps;
13427
13490
  nameField;
13491
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13492
+ registryOwnsEnabled;
13428
13493
  constructor(records, kind, hooks) {
13429
13494
  this.records = records;
13430
13495
  this.kind = kind;
13431
13496
  this.hooks = hooks;
13432
13497
  this.handle = records.kind(kind);
13433
13498
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13499
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13434
13500
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13501
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13502
+ 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"`);
13435
13503
  }
13436
13504
  /**
13437
13505
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13482,7 +13550,7 @@ var RecordIntegrationInstances = class {
13482
13550
  id: record.id,
13483
13551
  name: typeof rawName === "string" ? rawName : record.id,
13484
13552
  configured: this.hooks.configured(record.data),
13485
- enabled: record.data.enabled !== false,
13553
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13486
13554
  formValues: shown,
13487
13555
  ...kind !== void 0 ? { kind } : {},
13488
13556
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13512,32 +13580,36 @@ var RecordIntegrationInstances = class {
13512
13580
  id: minted,
13513
13581
  data: this.hooks.initialBody(name, kind)
13514
13582
  }, { ifRev: 0 });
13515
- await this.hooks.onChanged?.(minted);
13583
+ await this.hooks.onChanged?.(minted, "created");
13516
13584
  return { id: minted };
13517
13585
  };
13518
13586
  updateInstance = async ({ id, name, values }) => {
13519
13587
  const patch = {};
13520
13588
  for (const [key, value] of Object.entries(values)) {
13521
13589
  if (value === void 0) continue;
13522
- if (!this.secretKeys.has(key)) patch[key] = value;
13590
+ if (this.secretMaps.has(key)) {
13591
+ const entries = secretMapPatch(value);
13592
+ if (entries !== null) patch[key] = entries;
13593
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13523
13594
  else if (isSecretClear(value)) patch[key] = null;
13524
13595
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13525
13596
  }
13526
13597
  if (name !== void 0) patch[this.nameField] = name;
13527
13598
  await this.handle.patch(id, patch);
13528
- await this.hooks.onChanged?.(id);
13599
+ await this.hooks.onChanged?.(id, "updated");
13529
13600
  return { ok: true };
13530
13601
  };
13531
13602
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13532
13603
  setInstanceEnabled = async ({ id, enabled }) => {
13604
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13533
13605
  await this.handle.patch(id, { enabled });
13534
- await this.hooks.onChanged?.(id);
13606
+ await this.hooks.onChanged?.(id, "enabled");
13535
13607
  return { ok: true };
13536
13608
  };
13537
13609
  /** The row and its keychain refs go together (the records door). */
13538
13610
  deleteInstance = async ({ id }) => {
13539
13611
  await this.handle.delete(id);
13540
- await this.hooks.onChanged?.(id);
13612
+ await this.hooks.onChanged?.(id, "deleted");
13541
13613
  return { ok: true };
13542
13614
  };
13543
13615
  testInstance = async (input) => {
@@ -56305,6 +56377,21 @@ var IntegrationInstanceKindSchema = object({
56305
56377
  disconnectPath: string().min(1).optional()
56306
56378
  });
56307
56379
  var IntegrationInstanceDescriptionSchema = object({
56380
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56381
+ role: _enum([
56382
+ "devices",
56383
+ "services",
56384
+ "both"
56385
+ ]),
56386
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56387
+ discovery: object({
56388
+ mode: _enum([
56389
+ "none",
56390
+ "lan",
56391
+ "account"
56392
+ ]),
56393
+ scopedToInstance: boolean()
56394
+ }).optional(),
56308
56395
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56309
56396
  instanceMode: IntegrationInstanceModeSchema,
56310
56397
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56344,10 +56431,11 @@ var IntegrationInstanceSchema = object({
56344
56431
  /** The addon's own rule (an URL is present, a client id is present...). */
56345
56432
  configured: boolean(),
56346
56433
  /**
56347
- * The instance's own on/off switch (D996): always present, derived from the record body
56348
- * (`enabled !== false`, so a row written before the field existed is on).
56434
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56435
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56436
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56349
56437
  */
56350
- enabled: boolean(),
56438
+ enabled: boolean().optional(),
56351
56439
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56352
56440
  kind: string().optional(),
56353
56441
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -143313,6 +143401,7 @@ function statusOf(info, configured) {
143313
143401
  function buildAlexaInstances(records, deps) {
143314
143402
  return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
143315
143403
  description: {
143404
+ role: "both",
143316
143405
  instanceMode: "multiple",
143317
143406
  kind: "account",
143318
143407
  canTest: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.183",
3
+ "version": "0.2.184",
4
4
  "description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -72,7 +72,7 @@
72
72
  }
73
73
  ],
74
74
  "requires": {
75
- "server": ">=1.2.761"
75
+ "server": ">=1.2.762"
76
76
  }
77
77
  },
78
78
  "files": [