@camstack/addon-import-alexa 0.2.182 → 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/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-DV4qxEyD.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).
@@ -6192,9 +6192,16 @@ function isSecretMarker(value) {
6192
6192
  const ref = value["$secret"];
6193
6193
  return typeof ref === "string" && ref.length > 0;
6194
6194
  }
6195
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6196
+ function isSecretMarkerLike(value) {
6197
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6198
+ }
6195
6199
  function isSecretClear(value) {
6196
6200
  return isRecord$7(value) && value["$secretClear"] === true;
6197
6201
  }
6202
+ function isSecretFieldView(value) {
6203
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6204
+ }
6198
6205
  /**
6199
6206
  * The UI view of a stored secret field value: a marker becomes "set", inline
6200
6207
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6844,11 +6851,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6844
6851
  }
6845
6852
  return out;
6846
6853
  }
6854
+ /**
6855
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6856
+ *
6857
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6858
+ * scope (global, one node, one device, one integration, one row of a business
6859
+ * collection) and the field path inside that scope — so saving the same field
6860
+ * twice updates the same ref, and the ref itself says which settings blob
6861
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6862
+ * copied onto another ref does not decrypt.
6863
+ *
6864
+ * Array items are addressed by ITEM ID, never by index
6865
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6866
+ *
6867
+ * Pure and browser-safe: no node builtins, no I/O.
6868
+ */
6869
+ /** The URI scheme of every keychain ref. */
6870
+ var SECRET_REF_SCHEME = "secret://";
6847
6871
  var GLOBAL_SCOPE = { kind: "global" };
6848
6872
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6849
6873
  function globalSecretScope() {
6850
6874
  return GLOBAL_SCOPE;
6851
6875
  }
6876
+ /**
6877
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6878
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6879
+ * addon running forked.
6880
+ */
6881
+ function normalizeSecretOwner(addonId) {
6882
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6883
+ }
6852
6884
  function encodeSegment(value) {
6853
6885
  return encodeURIComponent(value);
6854
6886
  }
@@ -6859,6 +6891,16 @@ function decodeSegment(value) {
6859
6891
  return null;
6860
6892
  }
6861
6893
  }
6894
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6895
+ function formatSecretScope(scope) {
6896
+ switch (scope.kind) {
6897
+ case "global": return "global";
6898
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6899
+ case "device": return `device/${scope.deviceId}`;
6900
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6901
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6902
+ }
6903
+ }
6862
6904
  /**
6863
6905
  * The field path of a value inside an array item, addressed by the item's id:
6864
6906
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6867,6 +6909,13 @@ function decodeSegment(value) {
6867
6909
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6868
6910
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6869
6911
  }
6912
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6913
+ function buildSecretRef(parts) {
6914
+ const owner = normalizeSecretOwner(parts.owner);
6915
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6916
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6917
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6918
+ }
6870
6919
  function parseScope(segments) {
6871
6920
  switch (segments[0]) {
6872
6921
  case "global": return {
@@ -8824,11 +8873,17 @@ var RECORD_BATCH_MAX = 1e3;
8824
8873
  var RECORD_QUERY_MAX = 1e3;
8825
8874
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8826
8875
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8876
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8877
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8827
8878
  /**
8828
8879
  * Secret fields of a record, addressed the way a Zod schema declares them
8829
8880
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8830
8881
  * (`auth.accessToken`, `headers[].value`).
8831
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
+ *
8832
8887
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8833
8888
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8834
8889
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8841,11 +8896,13 @@ function parseSecretPath(path) {
8841
8896
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8842
8897
  return path.split(".").map((raw) => {
8843
8898
  const each = raw.endsWith("[]");
8844
- const key = each ? raw.slice(0, -2) : raw;
8899
+ const map = raw.endsWith("{}");
8900
+ const key = each || map ? raw.slice(0, -2) : raw;
8845
8901
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8846
8902
  return {
8847
8903
  key,
8848
- each
8904
+ each,
8905
+ map
8849
8906
  };
8850
8907
  });
8851
8908
  }
@@ -8860,6 +8917,128 @@ function secretArrayPrefixes(path) {
8860
8917
  }
8861
8918
  return prefixes;
8862
8919
  }
8920
+ function isRecord$4$1(value) {
8921
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8922
+ }
8923
+ function elementId(element, idKey, declared, prefix) {
8924
+ const id = isRecord$4$1(element) ? element[idKey] : void 0;
8925
+ if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
8926
+ return id;
8927
+ }
8928
+ /**
8929
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8930
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8931
+ * result is a new body, `data` is untouched.
8932
+ */
8933
+ var REMOVE_LEAF = Symbol("remove-leaf");
8934
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8935
+ if (!isRecord$4$1(node)) return node;
8936
+ const segment = segments[index];
8937
+ if (segment === void 0) return node;
8938
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8939
+ const child = node[segment.key];
8940
+ const keys = [...walkedKeys, segment.key];
8941
+ const last = index === segments.length - 1;
8942
+ if (segment.each) {
8943
+ if (!Array.isArray(child)) return node;
8944
+ const prefix = keys.join(".");
8945
+ const idKey = arrayKeys[prefix];
8946
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8947
+ const mapped = child.map((element) => {
8948
+ const id = elementId(element, idKey, declared, prefix);
8949
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8950
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8951
+ });
8952
+ return {
8953
+ ...node,
8954
+ [segment.key]: mapped
8955
+ };
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
+ }
8973
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8974
+ if (last) {
8975
+ const replacement = visit({
8976
+ declared,
8977
+ concrete,
8978
+ value: child
8979
+ });
8980
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8981
+ return {
8982
+ ...node,
8983
+ [segment.key]: replacement
8984
+ };
8985
+ }
8986
+ return {
8987
+ ...node,
8988
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8989
+ };
8990
+ }
8991
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8992
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8993
+ let current = data;
8994
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8995
+ return current;
8996
+ }
8997
+ /**
8998
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8999
+ * record's refs never collide with the refs a legacy collection of the same name
9000
+ * already holds during an adoption.
9001
+ */
9002
+ function recordSecretCollection(kind) {
9003
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
9004
+ }
9005
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
9006
+ function recordSecretRef(owner, kind, rowId, concretePath) {
9007
+ return buildSecretRef({
9008
+ owner,
9009
+ scope: {
9010
+ kind: "row",
9011
+ collection: recordSecretCollection(kind),
9012
+ rowId
9013
+ },
9014
+ fieldPath: concretePath
9015
+ });
9016
+ }
9017
+ /**
9018
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
9019
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
9020
+ * token. The form hands it to the secret field, which reveals the value by ref
9021
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
9022
+ * stored secret (the records door reads a view as KEEP).
9023
+ *
9024
+ * ONE implementation for the entries service and for every addon that serves
9025
+ * its own instances (`RecordIntegrationInstances`).
9026
+ */
9027
+ function isRecord$3$1(value) {
9028
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9029
+ }
9030
+ function secretViewsOf(owner, kind, id, decl, data) {
9031
+ if (decl.secretPaths.length === 0) return data;
9032
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
9033
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
9034
+ return {
9035
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
9036
+ v: 1,
9037
+ set: true
9038
+ };
9039
+ });
9040
+ return isRecord$3$1(shown) ? shown : data;
9041
+ }
8863
9042
  /**
8864
9043
  * The share-view scope kinds a share-link token can carry. The hub mints
8865
9044
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8932,7 +9111,8 @@ function isZodSecret(schema) {
8932
9111
  }
8933
9112
  /**
8934
9113
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8935
- * 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
8936
9116
  * object or array is reported as one path and not descended into — everything
8937
9117
  * under it is secret.
8938
9118
  */
@@ -8943,6 +9123,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8943
9123
  const def = defOf(layer);
8944
9124
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8945
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}{}`));
8946
9127
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8947
9128
  else if (def.type === "intersection") {
8948
9129
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9082,7 +9263,7 @@ function defineRecordKinds(specs) {
9082
9263
  retention: spec.retention,
9083
9264
  deviceRemoval: spec.deviceRemoval,
9084
9265
  maxDataBytes: spec.maxDataBytes,
9085
- secretPaths: [...secretPaths],
9266
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9086
9267
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9087
9268
  transactFamily: spec.transactFamily,
9088
9269
  entry: spec.entry === void 0 ? void 0 : {
@@ -9127,6 +9308,7 @@ function defineEntryKind(name, spec) {
9127
9308
  ...spec.database === void 0 ? {} : { database: spec.database },
9128
9309
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9129
9310
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9311
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9130
9312
  entry: {
9131
9313
  title: spec.title,
9132
9314
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11792,7 +11974,7 @@ object({
11792
11974
  params: record(string(), union([string(), number$1()])).optional()
11793
11975
  });
11794
11976
  //#endregion
11795
- //#region ../types/dist/composition-CD1GdFuv.mjs
11977
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11796
11978
  /**
11797
11979
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11798
11980
  *
@@ -12555,38 +12737,6 @@ function validateExpressionSource(src) {
12555
12737
  return null;
12556
12738
  }
12557
12739
  /**
12558
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12559
- *
12560
- * Standby of the control board is under 5 W; a motor or a heater is far
12561
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12562
- * a cycle, so running starts only after 30 s above the line.
12563
- *
12564
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12565
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12566
- * report "finished" in the middle of the program, so the stop dwell is
12567
- * 10 minutes on every type. The number is stored per type so one machine
12568
- * can move later without moving the others.
12569
- */
12570
- var APPLIANCE_TEMPLATE_ID = "appliance";
12571
- var APPLIANCE_KINDS = [
12572
- "washer",
12573
- "dryer",
12574
- "dishwasher"
12575
- ];
12576
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12577
- /**
12578
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12579
- *
12580
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12581
- * (D967), so the heater switches exactly as the plug does. Running is the
12582
- * plug being on; when a power reading is also named, running is the draw
12583
- * staying above the line instead (a plug left on with the stove's own
12584
- * thermostat open is not heating). 50 W is far above any standby and below
12585
- * the smallest radiator.
12586
- */
12587
- var HEATER_TEMPLATE_ID = "heater";
12588
- DeviceType.Heater;
12589
- /**
12590
12740
  * Statistics and integrals over a history read (D944).
12591
12741
  *
12592
12742
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12730,34 +12880,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12730
12880
  /** Actuator groups only. Readings leave this unset. */
12731
12881
  active: ActiveCriterionSchema.optional()
12732
12882
  }),
12733
- object({
12734
- kind: literal("appliance"),
12735
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12736
- appliance: _enum(APPLIANCE_KINDS),
12737
- thresholdWatts: number$1().positive(),
12738
- startDwellMs: number$1().int().nonnegative(),
12739
- stopDwellMs: number$1().int().nonnegative(),
12740
- member: HelperMemberSchema,
12741
- /**
12742
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12743
- * composition carries a `power-source` feature, so the hub can cut and restore
12744
- * that plug (power-source link of a NEW-target composition).
12745
- */
12746
- powerSwitch: HelperMemberSchema.optional(),
12747
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12748
- offSeconds: number$1().optional()
12749
- }),
12750
- object({
12751
- kind: literal("heater"),
12752
- templateId: literal(HEATER_TEMPLATE_ID),
12753
- /** The plug, read and commanded at `switch.on`. */
12754
- plug: HelperMemberSchema,
12755
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12756
- meter: HelperMemberSchema.optional(),
12757
- thresholdWatts: number$1().positive(),
12758
- startDwellMs: number$1().int().nonnegative(),
12759
- stopDwellMs: number$1().int().nonnegative()
12760
- }),
12761
12883
  object({
12762
12884
  kind: literal("statistic"),
12763
12885
  stat: _enum(STATISTIC_STATS),
@@ -13316,6 +13438,205 @@ function buildCapturingReply(envelope) {
13316
13438
  };
13317
13439
  return wrapper;
13318
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
+ */
13454
+ /** The most instances one addon lists (an integrations page, not a table). */
13455
+ var INSTANCES_LIST_MAX = 1e3;
13456
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13457
+ function topLevelSecretKeys(paths) {
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;
13484
+ }
13485
+ var RecordIntegrationInstances = class {
13486
+ records;
13487
+ kind;
13488
+ hooks;
13489
+ handle;
13490
+ secretKeys;
13491
+ secretMaps;
13492
+ nameField;
13493
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13494
+ registryOwnsEnabled;
13495
+ constructor(records, kind, hooks) {
13496
+ this.records = records;
13497
+ this.kind = kind;
13498
+ this.hooks = hooks;
13499
+ this.handle = records.kind(kind);
13500
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13501
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
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"`);
13505
+ }
13506
+ /**
13507
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13508
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13509
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13510
+ */
13511
+ async readResolved(id) {
13512
+ try {
13513
+ const record = await this.handle.getResolved(id);
13514
+ return record === null ? null : {
13515
+ id,
13516
+ body: record.data,
13517
+ enabled: record.data.enabled !== false,
13518
+ secrets: "resolved"
13519
+ };
13520
+ } catch (err) {
13521
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13522
+ const stored = await this.handle.get(id);
13523
+ if (stored === null) return null;
13524
+ return {
13525
+ id,
13526
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13527
+ enabled: stored.data.enabled !== false,
13528
+ secrets: "unavailable"
13529
+ };
13530
+ }
13531
+ }
13532
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13533
+ async getResolved(id) {
13534
+ return (await this.readResolved(id))?.body ?? null;
13535
+ }
13536
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13537
+ async listResolved() {
13538
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13539
+ const out = [];
13540
+ for (const record of page.records) {
13541
+ const read = await this.readResolved(record.id);
13542
+ if (read !== null) out.push(read);
13543
+ }
13544
+ return out;
13545
+ }
13546
+ viewOf(record) {
13547
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13548
+ const rawName = record.data[this.nameField];
13549
+ const kind = this.hooks.kindOf?.(record.data);
13550
+ const health = this.hooks.health?.(record.data, record.id);
13551
+ return {
13552
+ id: record.id,
13553
+ name: typeof rawName === "string" ? rawName : record.id,
13554
+ configured: this.hooks.configured(record.data),
13555
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13556
+ formValues: shown,
13557
+ ...kind !== void 0 ? { kind } : {},
13558
+ ...health?.status !== void 0 ? { status: health.status } : {},
13559
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13560
+ };
13561
+ }
13562
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13563
+ ...this.hooks.description,
13564
+ mintsId: true
13565
+ };
13566
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13567
+ listInstances = async () => {
13568
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13569
+ };
13570
+ getInstance = async ({ id }) => {
13571
+ const record = await this.handle.get(id);
13572
+ return record === null ? null : this.viewOf(record);
13573
+ };
13574
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13575
+ createInstance = async ({ id, name, kind }) => {
13576
+ const minted = id ?? this.hooks.mintId?.({
13577
+ name,
13578
+ ...kind !== void 0 ? { kind } : {}
13579
+ });
13580
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13581
+ await this.handle.put({
13582
+ id: minted,
13583
+ data: this.hooks.initialBody(name, kind)
13584
+ }, { ifRev: 0 });
13585
+ await this.hooks.onChanged?.(minted, "created");
13586
+ return { id: minted };
13587
+ };
13588
+ updateInstance = async ({ id, name, values }) => {
13589
+ const patch = {};
13590
+ for (const [key, value] of Object.entries(values)) {
13591
+ if (value === void 0) continue;
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;
13596
+ else if (isSecretClear(value)) patch[key] = null;
13597
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13598
+ }
13599
+ if (name !== void 0) patch[this.nameField] = name;
13600
+ await this.handle.patch(id, patch);
13601
+ await this.hooks.onChanged?.(id, "updated");
13602
+ return { ok: true };
13603
+ };
13604
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
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`);
13607
+ await this.handle.patch(id, { enabled });
13608
+ await this.hooks.onChanged?.(id, "enabled");
13609
+ return { ok: true };
13610
+ };
13611
+ /** The row and its keychain refs go together (the records door). */
13612
+ deleteInstance = async ({ id }) => {
13613
+ await this.handle.delete(id);
13614
+ await this.hooks.onChanged?.(id, "deleted");
13615
+ return { ok: true };
13616
+ };
13617
+ testInstance = async (input) => {
13618
+ if (this.hooks.test === void 0) return {
13619
+ success: false,
13620
+ status: "unsupported",
13621
+ error: "This integration declares no connection test"
13622
+ };
13623
+ return this.hooks.test(input);
13624
+ };
13625
+ /** The flat provider object to hand to `registerProvider`. */
13626
+ toProvider() {
13627
+ return {
13628
+ describe: this.describe,
13629
+ instanceFormSchema: this.instanceFormSchema,
13630
+ listInstances: this.listInstances,
13631
+ getInstance: this.getInstance,
13632
+ createInstance: this.createInstance,
13633
+ updateInstance: this.updateInstance,
13634
+ setInstanceEnabled: this.setInstanceEnabled,
13635
+ deleteInstance: this.deleteInstance,
13636
+ testInstance: this.testInstance
13637
+ };
13638
+ }
13639
+ };
13319
13640
  var EncodeProfileSchema = object({
13320
13641
  video: object({
13321
13642
  codec: _enum([
@@ -14384,6 +14705,58 @@ var MigrationOwnerStepSchema = object({
14384
14705
  /** The ledger state the engine found: `pending` has no row yet. */
14385
14706
  state: union([literal("pending"), MigrationStatusSchema])
14386
14707
  });
14708
+ function isPlain(value) {
14709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14710
+ }
14711
+ async function flattenNestedRecords(ports, spec) {
14712
+ const ids = await ports.listNested(spec.legacyKey);
14713
+ if (ids.length === 0) return {
14714
+ outcome: "already-done",
14715
+ summary: { converted: 0 }
14716
+ };
14717
+ const rows = /* @__PURE__ */ new Map();
14718
+ for (const id of ids) try {
14719
+ rows.set(id, await ports.readResolved(id));
14720
+ } catch (err) {
14721
+ const why = err instanceof Error ? err.message : String(err);
14722
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14723
+ }
14724
+ for (const [id, row] of rows) {
14725
+ const legacy = row[spec.legacyKey];
14726
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14727
+ const flat = spec.toFlat(legacy, row);
14728
+ await ports.patch(id, flat);
14729
+ const back = await ports.readResolved(id);
14730
+ const wrong = spec.flatSecretKeys.filter((key) => {
14731
+ const sent = flat[key];
14732
+ const got = back[key];
14733
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14734
+ });
14735
+ if (wrong.length > 0) {
14736
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14737
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14738
+ }
14739
+ await ports.patch(id, { [spec.legacyKey]: null });
14740
+ }
14741
+ return {
14742
+ outcome: "done",
14743
+ summary: { converted: rows.size }
14744
+ };
14745
+ }
14746
+ /** The ports over one kind's records handle. */
14747
+ function flattenPortsOver(handle) {
14748
+ return {
14749
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14750
+ readResolved: async (id) => {
14751
+ const row = await handle.getResolved(id);
14752
+ if (row === null) throw new Error("the record is gone");
14753
+ return row.data;
14754
+ },
14755
+ patch: async (id, patch) => {
14756
+ await handle.patch(id, patch);
14757
+ }
14758
+ };
14759
+ }
14387
14760
  /**
14388
14761
  * The trainer contract — what the hub hands the cloud trainer and what it
14389
14762
  * gets back (cloud training, D805+).
@@ -49914,117 +50287,6 @@ function actuator(input) {
49914
50287
  };
49915
50288
  }
49916
50289
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49917
- var PowerSourceRefusalReasonSchema = _enum([
49918
- "no-power-source",
49919
- "switch-missing",
49920
- "switch-offline",
49921
- "switch-unknown",
49922
- "restart-in-progress",
49923
- "switch-call-failed"
49924
- ]);
49925
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
49926
- accepted: literal(false),
49927
- reason: PowerSourceRefusalReasonSchema,
49928
- /** One line for the operator, naming the switch. */
49929
- message: string()
49930
- })]);
49931
- var PowerSourceInfoSchema = object({
49932
- /** False when the device has no power source linked; the rest is then empty. */
49933
- linked: boolean(),
49934
- switchDeviceId: number$1().nullable(),
49935
- switchName: string().nullable(),
49936
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
49937
- switchOn: boolean().nullable(),
49938
- switchOnline: boolean().nullable(),
49939
- restartInProgress: boolean(),
49940
- offSeconds: number$1().int()
49941
- });
49942
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
49943
- var PowerSourceStateSchema = object({
49944
- /** Null while the switch has not reported: unknown is never `false` (D393). */
49945
- switchOn: boolean().nullable(),
49946
- offSeconds: number$1().int(),
49947
- lastFetchedAt: number$1()
49948
- });
49949
- var POWER_SOURCE_DEVICE_TYPES = [
49950
- DeviceType.Gateway,
49951
- DeviceType.Hub,
49952
- DeviceType.Camera,
49953
- DeviceType.Sensor,
49954
- DeviceType.MediaPlayer,
49955
- DeviceType.Vacuum,
49956
- DeviceType.Generic,
49957
- DeviceType.Washer,
49958
- DeviceType.Dryer,
49959
- DeviceType.Dishwasher,
49960
- DeviceType.PetFountain,
49961
- DeviceType.Fan,
49962
- DeviceType.Humidifier,
49963
- DeviceType.Climate,
49964
- DeviceType.WaterHeater
49965
- ];
49966
- var powerSourceCustomize = {
49967
- version: 1,
49968
- targets: [{
49969
- id: "switch",
49970
- cardinality: "scalar",
49971
- vocabulary: {
49972
- cap: "power-source",
49973
- stateField: "switchOn"
49974
- },
49975
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49976
- apply: "add",
49977
- path: "switchOn",
49978
- sources: [{
49979
- kind: "switch",
49980
- transforms: [{ kind: "identity" }]
49981
- }],
49982
- companions: [{
49983
- kind: "constant",
49984
- path: "offSeconds",
49985
- value: 10,
49986
- when: "add"
49987
- }, {
49988
- kind: "clock",
49989
- path: "lastFetchedAt",
49990
- when: "always"
49991
- }],
49992
- sourceReport: { status: "assignment-status" }
49993
- }]
49994
- };
49995
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
49996
- var powerSourceCapability = {
49997
- name: "power-source",
49998
- scope: "device",
49999
- mode: "singleton",
50000
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
50001
- methods: {
50002
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
50003
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50004
- kind: "mutation",
50005
- auth: "admin"
50006
- }),
50007
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50008
- kind: "mutation",
50009
- auth: "admin"
50010
- }),
50011
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50012
- kind: "mutation",
50013
- auth: "admin"
50014
- })
50015
- },
50016
- runtimeState: PowerSourceStateSchema,
50017
- customize: powerSourceCustomize,
50018
- /**
50019
- * Runtime-state durability: **session** — mirrored from the linked switch on
50020
- * its next report; a restored value would outlive the reading it names.
50021
- *
50022
- * See `RuntimeStateDurability`. Enforced by
50023
- * `scripts/check-runtime-state-durability.ts`.
50024
- */
50025
- durability: "session",
50026
- history: "none"
50027
- };
50028
50290
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
50029
50291
  DeviceType.Camera;
50030
50292
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -50052,6 +50314,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
50052
50314
  }).optional()
50053
50315
  })) });
50054
50316
  /**
50317
+ * A saved Customize assignment (D936). The schema is the type: callers use
50318
+ * `z.infer`, they do not restate the shape.
50319
+ */
50320
+ var OverrideSourceSchema = object({
50321
+ addonId: string().min(1),
50322
+ stableId: string().min(1),
50323
+ cap: string().min(1),
50324
+ fieldPath: string().min(1)
50325
+ });
50326
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50327
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50328
+ var ThresholdTransformDraftSchema = object({
50329
+ kind: literal("threshold"),
50330
+ comparison: _enum(["gte", "lte"]),
50331
+ threshold: number$1(),
50332
+ startDwellMs: number$1().nonnegative(),
50333
+ stopDwellMs: number$1().nonnegative()
50334
+ });
50335
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50336
+ IdentityTransformDraftSchema,
50337
+ InvertTransformDraftSchema,
50338
+ ThresholdTransformDraftSchema,
50339
+ object({
50340
+ kind: literal("linear"),
50341
+ scale: number$1(),
50342
+ offset: number$1(),
50343
+ clampMin: number$1().optional(),
50344
+ clampMax: number$1().optional()
50345
+ }),
50346
+ object({
50347
+ kind: literal("enum-map"),
50348
+ mapping: record(string(), string().nullable())
50349
+ }),
50350
+ object({ kind: literal("number") })
50351
+ ]);
50352
+ var ScalarOverrideDraftSchema = object({
50353
+ kind: literal("scalar"),
50354
+ cap: string().min(1),
50355
+ targetId: string().min(1),
50356
+ source: OverrideSourceSchema,
50357
+ transform: CustomizeTransformDraftSchema
50358
+ });
50359
+ var KeyedArrayOverrideDraftSchema = object({
50360
+ kind: literal("keyedArray"),
50361
+ cap: string().min(1),
50362
+ targetId: string().min(1),
50363
+ key: string().min(1),
50364
+ label: string().min(1),
50365
+ fieldPath: string().min(1),
50366
+ source: OverrideSourceSchema,
50367
+ transform: CustomizeTransformDraftSchema
50368
+ });
50369
+ var PulseOverrideDraftSchema = object({
50370
+ kind: literal("pulse"),
50371
+ cap: string().min(1),
50372
+ targetId: string().min(1),
50373
+ enabled: boolean(),
50374
+ debounceMs: number$1().nonnegative(),
50375
+ edges: array(_enum([
50376
+ "rising",
50377
+ "button",
50378
+ "native-event"
50379
+ ])).min(1).optional(),
50380
+ sources: array(OverrideSourceSchema).min(1)
50381
+ });
50382
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50383
+ "in-addition",
50384
+ "confirm",
50385
+ "replace"
50386
+ ]).optional() });
50387
+ discriminatedUnion("kind", [
50388
+ ScalarOverrideDraftSchema,
50389
+ KeyedArrayOverrideDraftSchema,
50390
+ PulseOverrideDraftSchema,
50391
+ object({
50392
+ kind: literal("span"),
50393
+ cap: string().min(1),
50394
+ targetId: string().min(1),
50395
+ timeoutMs: number$1().nonnegative(),
50396
+ cooldownMs: number$1().nonnegative().optional(),
50397
+ sources: array(SpanSourceDraftSchema)
50398
+ })
50399
+ ]);
50400
+ object({
50401
+ targetId: string().min(1),
50402
+ configured: boolean(),
50403
+ source: _enum(["native", "assigned"]),
50404
+ state: _enum([
50405
+ "active",
50406
+ "stale",
50407
+ "released",
50408
+ "unavailable"
50409
+ ]),
50410
+ reason: string().nullable()
50411
+ });
50412
+ /**
50413
+ * What a running-threshold proposal says (planning#405). The schema is the
50414
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50415
+ *
50416
+ * A proposal is read-only advice: it carries the numbers it was derived from
50417
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50418
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50419
+ */
50420
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50421
+ var RunningProposalConfidenceSchema = _enum([
50422
+ "high",
50423
+ "medium",
50424
+ "low"
50425
+ ]);
50426
+ var RunningProposalInsufficientReasonSchema = _enum([
50427
+ "no-history",
50428
+ "span-too-short",
50429
+ "truncated",
50430
+ "no-idle-floor",
50431
+ "no-runs",
50432
+ "no-separation"
50433
+ ]);
50434
+ var RunningProposalReplaySchema = object({
50435
+ /** Intervals the real dwell expression held true over the window. */
50436
+ intervals: number$1().int().nonnegative(),
50437
+ /** Intervals outside every run, or a second interval inside one. */
50438
+ extra: number$1().int().nonnegative(),
50439
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50440
+ missed: number$1().int().nonnegative()
50441
+ });
50442
+ var RunningProposalEvidenceSchema = object({
50443
+ tier: RunningProposalTierSchema,
50444
+ spanMs: number$1().nonnegative(),
50445
+ from: number$1(),
50446
+ to: number$1(),
50447
+ points: number$1().int().nonnegative(),
50448
+ runsFound: number$1().int().nonnegative(),
50449
+ pausesFound: number$1().int().nonnegative(),
50450
+ longestPauseMs: number$1().nonnegative(),
50451
+ idleFloorW: number$1(),
50452
+ noiseCeilingW: number$1(),
50453
+ plateauW: number$1(),
50454
+ replay: RunningProposalReplaySchema
50455
+ });
50456
+ var RunningProposalInsufficientEvidenceSchema = object({
50457
+ spanMs: number$1().nonnegative(),
50458
+ points: number$1().int().nonnegative(),
50459
+ runsFound: number$1().int().nonnegative()
50460
+ });
50461
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50462
+ state: literal("proposal"),
50463
+ transform: ThresholdTransformDraftSchema,
50464
+ evidence: RunningProposalEvidenceSchema,
50465
+ confidence: RunningProposalConfidenceSchema,
50466
+ /** Why the confidence is not higher; empty when nothing held it back. */
50467
+ notes: array(string())
50468
+ }), object({
50469
+ state: literal("insufficient"),
50470
+ reason: RunningProposalInsufficientReasonSchema,
50471
+ evidence: RunningProposalInsufficientEvidenceSchema,
50472
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50473
+ fallback: ThresholdTransformDraftSchema
50474
+ })]);
50475
+ /**
50055
50476
  * The report of a one-shot "move this owner's plaintext secrets into the
50056
50477
  * keychain" run (D878). COUNTS only - never a value, never a ref.
50057
50478
  *
@@ -50341,6 +50762,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
50341
50762
  kind: "mutation",
50342
50763
  auth: "admin",
50343
50764
  access: "view"
50765
+ }), method(object({
50766
+ deviceId: number$1().int(),
50767
+ cap: string().min(1),
50768
+ fieldPath: string().min(1),
50769
+ deviceType: _enum(DeviceType).optional()
50770
+ }), RunningThresholdProposalSchema, {
50771
+ auth: "admin",
50772
+ access: "view"
50344
50773
  }), method(object({
50345
50774
  blockId: string().min(1),
50346
50775
  trace: AutomationRunTraceSchema.optional(),
@@ -51296,7 +51725,27 @@ method(object({
51296
51725
  * exposes only the COMMON surface: link state, supported device
51297
51726
  * kinds (so the UI can filter the device picker), and per-device
51298
51727
  * expose/unexpose.
51299
- */
51728
+ *
51729
+ * INSTANCES. One export addon can serve several independent targets (a
51730
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51731
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51732
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51733
+ * default instance's); present = that instance; an id the provider does not
51734
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51735
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51736
+ * change always names the target it changes. A single-instance provider
51737
+ * accepts an absent id on a read or its own id, and refuses any other.
51738
+ */
51739
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51740
+ var InstanceIdSchema$1 = string().min(1);
51741
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51742
+ /** One independent export target of an addon. */
51743
+ var ExportInstanceSchema = object({
51744
+ instanceId: InstanceIdSchema$1,
51745
+ name: string(),
51746
+ enabled: boolean(),
51747
+ kind: string().optional()
51748
+ });
51300
51749
  var LinkStateSchema = _enum([
51301
51750
  "unlinked",
51302
51751
  "linked",
@@ -51347,10 +51796,14 @@ var ExposedDeviceSchema = object({
51347
51796
  capabilities: array(string()).optional()
51348
51797
  });
51349
51798
  var ExposeInputSchema = object({
51799
+ instanceId: InstanceIdSchema$1,
51350
51800
  deviceId: string(),
51351
51801
  capabilities: array(string()).optional()
51352
51802
  });
51353
- var UnexposeInputSchema = object({ deviceId: string() });
51803
+ var UnexposeInputSchema = object({
51804
+ instanceId: InstanceIdSchema$1,
51805
+ deviceId: string()
51806
+ });
51354
51807
  /**
51355
51808
  * What an exporter may expose besides devices (D968). An action and a
51356
51809
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -51379,15 +51832,21 @@ var ExposedEntitySchema = object({
51379
51832
  entity: ExportEntityRefSchema,
51380
51833
  exposedAs: string().optional()
51381
51834
  });
51382
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51383
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51384
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51835
+ var ExposeEntityInputSchema = object({
51836
+ instanceId: InstanceIdSchema$1,
51837
+ entity: ExportEntityRefSchema
51838
+ });
51839
+ var UnexposeEntityInputSchema = object({
51840
+ instanceId: InstanceIdSchema$1,
51841
+ entity: ExportEntityRefSchema
51842
+ });
51843
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51385
51844
  kind: "mutation",
51386
51845
  auth: "admin"
51387
51846
  }), method(UnexposeInputSchema, _void(), {
51388
51847
  kind: "mutation",
51389
51848
  auth: "admin"
51390
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51849
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51391
51850
  kind: "mutation",
51392
51851
  auth: "admin"
51393
51852
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -54978,15 +55437,15 @@ var FanPresetCaptureResultSchema = object({
54978
55437
  items: array(FanCaptureItemSchema),
54979
55438
  omitted: array(FanCaptureOmissionSchema)
54980
55439
  });
54981
- var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
55440
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
54982
55441
  DeviceType.Fan;
54983
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55442
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54984
55443
  kind: "mutation",
54985
55444
  auth: "admin"
54986
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55445
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
54987
55446
  kind: "mutation",
54988
55447
  auth: "protected"
54989
- }), method(deviceInput$1.extend({
55448
+ }), method(deviceInput$2.extend({
54990
55449
  fields: array(FanCaptureFieldSchema).optional(),
54991
55450
  steps: array(ActionStepSchema).max(20).default([])
54992
55451
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -55897,13 +56356,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55897
56356
  */
55898
56357
  var ConfigSchemaPassthrough$3 = custom();
55899
56358
  var IntegrationInstanceIdSchema = string().min(1);
56359
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56360
+ /**
56361
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56362
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56363
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56364
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56365
+ * instance id.
56366
+ */
56367
+ var IntegrationInstanceKindSchema = object({
56368
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56369
+ id: string().min(1),
56370
+ label: string().min(1),
56371
+ /** The kind signs in through a server-held interactive session. */
56372
+ signIn: object({
56373
+ basePath: string().min(1),
56374
+ methods: array(_enum(["device-code", "proxy"])).optional()
56375
+ }).optional(),
56376
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56377
+ connectPath: string().min(1).optional(),
56378
+ /** Relative hub path to disconnect that account. */
56379
+ disconnectPath: string().min(1).optional()
56380
+ });
55900
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(),
55901
56397
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55902
- instanceMode: _enum(["single", "multiple"]),
56398
+ instanceMode: IntegrationInstanceModeSchema,
55903
56399
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55904
56400
  kind: string(),
55905
56401
  /** True when `testInstance` really probes the remote. */
55906
- canTest: boolean()
56402
+ canTest: boolean(),
56403
+ /**
56404
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56405
+ * Add flow asks which first and sends it as `createInstance.kind`.
56406
+ */
56407
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56408
+ /**
56409
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56410
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56411
+ */
56412
+ allowRestoreId: boolean().optional(),
56413
+ /**
56414
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56415
+ * send an id (the UI then mints an opaque one).
56416
+ */
56417
+ mintsId: boolean().optional()
55907
56418
  });
55908
56419
  var IntegrationInstanceStatusSchema = object({
55909
56420
  state: _enum([
@@ -55912,7 +56423,9 @@ var IntegrationInstanceStatusSchema = object({
55912
56423
  "error",
55913
56424
  "unknown"
55914
56425
  ]),
55915
- detail: string().optional()
56426
+ detail: string().optional(),
56427
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56428
+ label: string().optional()
55916
56429
  });
55917
56430
  var IntegrationInstanceSchema = object({
55918
56431
  id: IntegrationInstanceIdSchema,
@@ -55920,13 +56433,18 @@ var IntegrationInstanceSchema = object({
55920
56433
  /** The addon's own rule (an URL is present, a client id is present...). */
55921
56434
  configured: boolean(),
55922
56435
  /**
55923
- * The instance's own on/off switch (D996): always present, derived from the record body
55924
- * (`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.
55925
56439
  */
55926
- enabled: boolean(),
56440
+ enabled: boolean().optional(),
56441
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56442
+ kind: string().optional(),
55927
56443
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55928
56444
  formValues: record(string(), unknown()),
55929
- status: IntegrationInstanceStatusSchema.optional()
56445
+ status: IntegrationInstanceStatusSchema.optional(),
56446
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56447
+ lastCheckedAt: number$1().optional()
55930
56448
  });
55931
56449
  var IntegrationInstancePatchSchema = object({
55932
56450
  id: IntegrationInstanceIdSchema,
@@ -55934,31 +56452,57 @@ var IntegrationInstancePatchSchema = object({
55934
56452
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55935
56453
  values: record(string(), unknown())
55936
56454
  });
55937
- method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
55938
- id: IntegrationInstanceIdSchema,
55939
- name: string().min(1)
55940
- }), object({ id: string() }), {
55941
- kind: "mutation",
55942
- auth: "admin"
55943
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55944
- kind: "mutation",
55945
- auth: "admin"
55946
- }), method(object({
55947
- id: IntegrationInstanceIdSchema,
55948
- enabled: boolean()
55949
- }), object({ ok: literal(true) }), {
55950
- kind: "mutation",
55951
- auth: "admin"
55952
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55953
- kind: "mutation",
55954
- auth: "admin"
55955
- }), optionalMethod(object({
55956
- id: IntegrationInstanceIdSchema.nullable(),
55957
- values: record(string(), unknown())
55958
- }), TestConnectionResultSchema$1, {
55959
- kind: "mutation",
55960
- auth: "admin"
55961
- });
56455
+ var integrationInstancesCapability = {
56456
+ name: "integration-instances",
56457
+ scope: "system",
56458
+ mode: "collection",
56459
+ methods: {
56460
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56461
+ /** The form; `id: null` is the create form. */
56462
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56463
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56464
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56465
+ /**
56466
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56467
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56468
+ */
56469
+ createInstance: method(object({
56470
+ id: IntegrationInstanceIdSchema.optional(),
56471
+ name: string().min(1),
56472
+ kind: string().min(1).optional()
56473
+ }), object({ id: string() }), {
56474
+ kind: "mutation",
56475
+ auth: "admin"
56476
+ }),
56477
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56478
+ kind: "mutation",
56479
+ auth: "admin"
56480
+ }),
56481
+ /**
56482
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56483
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56484
+ */
56485
+ setInstanceEnabled: method(object({
56486
+ id: IntegrationInstanceIdSchema,
56487
+ enabled: boolean()
56488
+ }), object({ ok: literal(true) }), {
56489
+ kind: "mutation",
56490
+ auth: "admin"
56491
+ }),
56492
+ /** Delete the record and its keychain refs together. */
56493
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56494
+ kind: "mutation",
56495
+ auth: "admin"
56496
+ }),
56497
+ testInstance: optionalMethod(object({
56498
+ id: IntegrationInstanceIdSchema.nullable(),
56499
+ values: record(string(), unknown())
56500
+ }), TestConnectionResultSchema$1, {
56501
+ kind: "mutation",
56502
+ auth: "admin"
56503
+ })
56504
+ }
56505
+ };
55962
56506
  /**
55963
56507
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55964
56508
  * session (server-initiated offer pattern, identical to the main
@@ -57976,6 +58520,11 @@ var LogEntrySchema = object({
57976
58520
  * flight, and not asking for the next slice ends the scan.
57977
58521
  */
57978
58522
  var LogReadQuerySchema = object({
58523
+ /**
58524
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58525
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58526
+ */
58527
+ instanceId: string().min(1).max(200).optional(),
57979
58528
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57980
58529
  requestId: string().min(1).max(64).optional(),
57981
58530
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -58038,6 +58587,8 @@ var LogReadResultSchema = object({
58038
58587
  /** True when `cancelRead` ended this slice early. */
58039
58588
  cancelled: boolean().optional()
58040
58589
  });
58590
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58591
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
58041
58592
  var LogReadCapabilitiesSchema = object({
58042
58593
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
58043
58594
  readable: boolean(),
@@ -58056,7 +58607,7 @@ var LogReadCapabilitiesSchema = object({
58056
58607
  /** How far back the store reaches, when the provider knows (days). */
58057
58608
  retentionDays: number$1().nullable()
58058
58609
  });
58059
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
58610
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
58060
58611
  kind: "mutation",
58061
58612
  auth: "admin"
58062
58613
  });
@@ -70450,6 +71001,117 @@ var powerMeterCapability = {
70450
71001
  volatileStateFields: ["lastFetchedAt"],
70451
71002
  customize: powerMeterCustomize
70452
71003
  };
71004
+ var PowerSourceRefusalReasonSchema = _enum([
71005
+ "no-power-source",
71006
+ "switch-missing",
71007
+ "switch-offline",
71008
+ "switch-unknown",
71009
+ "restart-in-progress",
71010
+ "switch-call-failed"
71011
+ ]);
71012
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
71013
+ accepted: literal(false),
71014
+ reason: PowerSourceRefusalReasonSchema,
71015
+ /** One line for the operator, naming the switch. */
71016
+ message: string()
71017
+ })]);
71018
+ var PowerSourceInfoSchema = object({
71019
+ /** False when the device has no power source linked; the rest is then empty. */
71020
+ linked: boolean(),
71021
+ switchDeviceId: number$1().nullable(),
71022
+ switchName: string().nullable(),
71023
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
71024
+ switchOn: boolean().nullable(),
71025
+ switchOnline: boolean().nullable(),
71026
+ restartInProgress: boolean(),
71027
+ offSeconds: number$1().int()
71028
+ });
71029
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
71030
+ var PowerSourceStateSchema = object({
71031
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
71032
+ switchOn: boolean().nullable(),
71033
+ offSeconds: number$1().int(),
71034
+ lastFetchedAt: number$1()
71035
+ });
71036
+ var POWER_SOURCE_DEVICE_TYPES = [
71037
+ DeviceType.Gateway,
71038
+ DeviceType.Hub,
71039
+ DeviceType.Camera,
71040
+ DeviceType.Sensor,
71041
+ DeviceType.MediaPlayer,
71042
+ DeviceType.Vacuum,
71043
+ DeviceType.Generic,
71044
+ DeviceType.Washer,
71045
+ DeviceType.Dryer,
71046
+ DeviceType.Dishwasher,
71047
+ DeviceType.PetFountain,
71048
+ DeviceType.Fan,
71049
+ DeviceType.Humidifier,
71050
+ DeviceType.Climate,
71051
+ DeviceType.WaterHeater
71052
+ ];
71053
+ var powerSourceCustomize = {
71054
+ version: 1,
71055
+ targets: [{
71056
+ id: "switch",
71057
+ cardinality: "scalar",
71058
+ vocabulary: {
71059
+ cap: "power-source",
71060
+ stateField: "switchOn"
71061
+ },
71062
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71063
+ apply: "add",
71064
+ path: "switchOn",
71065
+ sources: [{
71066
+ kind: "switch",
71067
+ transforms: [{ kind: "identity" }]
71068
+ }],
71069
+ companions: [{
71070
+ kind: "constant",
71071
+ path: "offSeconds",
71072
+ value: 10,
71073
+ when: "add"
71074
+ }, {
71075
+ kind: "clock",
71076
+ path: "lastFetchedAt",
71077
+ when: "always"
71078
+ }],
71079
+ sourceReport: { status: "assignment-status" }
71080
+ }]
71081
+ };
71082
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
71083
+ var powerSourceCapability = {
71084
+ name: "power-source",
71085
+ scope: "device",
71086
+ mode: "singleton",
71087
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71088
+ methods: {
71089
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71090
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71091
+ kind: "mutation",
71092
+ auth: "admin"
71093
+ }),
71094
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71095
+ kind: "mutation",
71096
+ auth: "admin"
71097
+ }),
71098
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71099
+ kind: "mutation",
71100
+ auth: "admin"
71101
+ })
71102
+ },
71103
+ runtimeState: PowerSourceStateSchema,
71104
+ customize: powerSourceCustomize,
71105
+ /**
71106
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71107
+ * its next report; a restored value would outlive the reading it names.
71108
+ *
71109
+ * See `RuntimeStateDurability`. Enforced by
71110
+ * `scripts/check-runtime-state-durability.ts`.
71111
+ */
71112
+ durability: "session",
71113
+ history: "none"
71114
+ };
70453
71115
  /**
70454
71116
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
70455
71117
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -79811,6 +80473,25 @@ function buildDescriptor(capName, kind) {
79811
80473
  * contributes one entry per bound cap present in this map.
79812
80474
  */
79813
80475
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80476
+ /**
80477
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80478
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80479
+ * client resolves its name from `label` (English and Italian side by side) and
80480
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80481
+ * server sends an id, a client maps it to its own icon set.
80482
+ *
80483
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80484
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80485
+ * to `icon`.
80486
+ */
80487
+ function digit(n) {
80488
+ return {
80489
+ label: t(n, n),
80490
+ icon: "hash",
80491
+ legend: n
80492
+ };
80493
+ }
80494
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
79814
80495
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79815
80496
  var btn = (idSuffix, row, col, init) => ({
79816
80497
  idSuffix,
@@ -79938,6 +80619,34 @@ discriminatedUnion("kind", [object({
79938
80619
  kind: literal("pronto"),
79939
80620
  code: string()
79940
80621
  })]);
80622
+ /**
80623
+ * Propose a running threshold from a device's own power history (planning#405).
80624
+ *
80625
+ * Pure and deterministic: the same series gives the same answer. The series is
80626
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80627
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80628
+ * count of samples.
80629
+ *
80630
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80631
+ * near F. A device that is not near one level most of the time has no floor.
80632
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80633
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80634
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80635
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80636
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80637
+ * covers the longest standby blip above T.
80638
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80639
+ * run must hold exactly one true interval and nothing may flap outside.
80640
+ */
80641
+ var SECOND_MS = 1e3;
80642
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80643
+ 24 * (60 * MINUTE_MS$1);
80644
+ 10 * MINUTE_MS$1;
80645
+ 30 * SECOND_MS;
80646
+ 60 * SECOND_MS;
80647
+ 20 * MINUTE_MS$1;
80648
+ 10 * SECOND_MS;
80649
+ 10 * SECOND_MS;
79941
80650
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79942
80651
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79943
80652
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83043,6 +83752,13 @@ Object.freeze({
83043
83752
  access: "create",
83044
83753
  role: "admin"
83045
83754
  },
83755
+ "coreBlocks.proposeRunningThreshold": {
83756
+ capName: "core-blocks",
83757
+ capScope: "system",
83758
+ addonId: null,
83759
+ access: "view",
83760
+ role: "admin"
83761
+ },
83046
83762
  "coreBlocks.reportAutomationRun": {
83047
83763
  capName: "core-blocks",
83048
83764
  capScope: "system",
@@ -83554,6 +84270,13 @@ Object.freeze({
83554
84270
  access: "view",
83555
84271
  role: "admin"
83556
84272
  },
84273
+ "deviceExport.listInstances": {
84274
+ capName: "device-export",
84275
+ capScope: "system",
84276
+ addonId: null,
84277
+ access: "view",
84278
+ role: "admin"
84279
+ },
83557
84280
  "deviceExport.listSupportedDeviceKinds": {
83558
84281
  capName: "device-export",
83559
84282
  capScope: "system",
@@ -92501,6 +93224,11 @@ Object.freeze({
92501
93224
  form: "single",
92502
93225
  optional: false
92503
93226
  }],
93227
+ "coreBlocks.proposeRunningThreshold": [{
93228
+ name: "deviceId",
93229
+ form: "single",
93230
+ optional: false
93231
+ }],
92504
93232
  "cover.close": [{
92505
93233
  name: "deviceId",
92506
93234
  form: "single",
@@ -95164,99 +95892,6 @@ Object.freeze({
95164
95892
  "network-access": "ingress",
95165
95893
  "smtp-provider": "email"
95166
95894
  });
95167
- /**
95168
- * A saved Customize assignment (D936). The schema is the type: callers use
95169
- * `z.infer`, they do not restate the shape.
95170
- */
95171
- var OverrideSourceSchema = object({
95172
- addonId: string().min(1),
95173
- stableId: string().min(1),
95174
- cap: string().min(1),
95175
- fieldPath: string().min(1)
95176
- });
95177
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
95178
- object({ kind: literal("identity") }),
95179
- object({ kind: literal("invert") }),
95180
- object({
95181
- kind: literal("threshold"),
95182
- comparison: _enum(["gte", "lte"]),
95183
- threshold: number$1(),
95184
- startDwellMs: number$1().nonnegative(),
95185
- stopDwellMs: number$1().nonnegative()
95186
- }),
95187
- object({
95188
- kind: literal("linear"),
95189
- scale: number$1(),
95190
- offset: number$1(),
95191
- clampMin: number$1().optional(),
95192
- clampMax: number$1().optional()
95193
- }),
95194
- object({
95195
- kind: literal("enum-map"),
95196
- mapping: record(string(), string().nullable())
95197
- }),
95198
- object({ kind: literal("number") })
95199
- ]);
95200
- var ScalarOverrideDraftSchema = object({
95201
- kind: literal("scalar"),
95202
- cap: string().min(1),
95203
- targetId: string().min(1),
95204
- source: OverrideSourceSchema,
95205
- transform: CustomizeTransformDraftSchema
95206
- });
95207
- var KeyedArrayOverrideDraftSchema = object({
95208
- kind: literal("keyedArray"),
95209
- cap: string().min(1),
95210
- targetId: string().min(1),
95211
- key: string().min(1),
95212
- label: string().min(1),
95213
- fieldPath: string().min(1),
95214
- source: OverrideSourceSchema,
95215
- transform: CustomizeTransformDraftSchema
95216
- });
95217
- var PulseOverrideDraftSchema = object({
95218
- kind: literal("pulse"),
95219
- cap: string().min(1),
95220
- targetId: string().min(1),
95221
- enabled: boolean(),
95222
- debounceMs: number$1().nonnegative(),
95223
- edges: array(_enum([
95224
- "rising",
95225
- "button",
95226
- "native-event"
95227
- ])).min(1).optional(),
95228
- sources: array(OverrideSourceSchema).min(1)
95229
- });
95230
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
95231
- "in-addition",
95232
- "confirm",
95233
- "replace"
95234
- ]).optional() });
95235
- discriminatedUnion("kind", [
95236
- ScalarOverrideDraftSchema,
95237
- KeyedArrayOverrideDraftSchema,
95238
- PulseOverrideDraftSchema,
95239
- object({
95240
- kind: literal("span"),
95241
- cap: string().min(1),
95242
- targetId: string().min(1),
95243
- timeoutMs: number$1().nonnegative(),
95244
- cooldownMs: number$1().nonnegative().optional(),
95245
- sources: array(SpanSourceDraftSchema)
95246
- })
95247
- ]);
95248
- object({
95249
- targetId: string().min(1),
95250
- configured: boolean(),
95251
- source: _enum(["native", "assigned"]),
95252
- state: _enum([
95253
- "active",
95254
- "stale",
95255
- "released",
95256
- "unavailable"
95257
- ]),
95258
- reason: string().nullable()
95259
- });
95260
95895
  var G711_SCALE_CORRECTION_DB = {
95261
95896
  PCMU: 20 * Math.log10(4),
95262
95897
  PCMA: 20 * Math.log10(8)
@@ -97562,7 +98197,7 @@ var alexaConnectionSchema = object({
97562
98197
  */
97563
98198
  refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)").meta({ secret: true }),
97564
98199
  /** Device-registration secret some flows need. */
97565
- macDms: string().default("").describe("macDms device-registration secret (optional)"),
98200
+ macDms: string().default("").describe("macDms device-registration secret (optional)").meta({ secret: true }),
97566
98201
  /** Poll interval for state sync (Alexa has no clean 3rd-party push). */
97567
98202
  pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number$1().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
97568
98203
  });
@@ -97580,7 +98215,9 @@ var AlexaBrokerEntrySchema = object({
97580
98215
  id: string().min(1),
97581
98216
  name: string().min(1),
97582
98217
  connection: alexaConnectionSchema,
97583
- integrationId: string().optional()
98218
+ integrationId: string().optional(),
98219
+ /** Absent means on; the instances page switches a connection off without deleting it. */
98220
+ enabled: boolean().optional()
97584
98221
  });
97585
98222
  /**
97586
98223
  * Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
@@ -97617,7 +98254,6 @@ function readEchoTargets(config) {
97617
98254
  }
97618
98255
  /** Top-level addon config — broker entries + the proxy-login settings. */
97619
98256
  var alexaAddonConfigSchema = object({
97620
- brokers: array(AlexaBrokerEntrySchema).default([]),
97621
98257
  proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
97622
98258
  echoTargets: array(alexaEchoTargetSchema).default([])
97623
98259
  });
@@ -141162,13 +141798,16 @@ var AlexaBrokerRegistry = class {
141162
141798
  }
141163
141799
  async restore(entries) {
141164
141800
  for (const entry of entries) this.#seedCounter(entry.id);
141165
- for (const entry of entries) try {
141166
- await this.#startManager(entry);
141167
- } catch (err) {
141168
- this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
141169
- tags: { brokerId: entry.id },
141170
- meta: { error: errMsg(err) }
141171
- });
141801
+ for (const entry of entries) {
141802
+ if (entry.enabled === false) continue;
141803
+ try {
141804
+ await this.#startManager(entry);
141805
+ } catch (err) {
141806
+ this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
141807
+ tags: { brokerId: entry.id },
141808
+ meta: { error: errMsg(err) }
141809
+ });
141810
+ }
141172
141811
  }
141173
141812
  }
141174
141813
  async shutdown() {
@@ -141202,7 +141841,8 @@ var AlexaBrokerRegistry = class {
141202
141841
  this.#onStatesPolled = cb;
141203
141842
  }
141204
141843
  async createEntry(name, connection, opts = {}) {
141205
- const id = this.#allocateId();
141844
+ const id = opts.id ?? this.#allocateId();
141845
+ if (opts.id !== void 0) this.#seedCounter(opts.id);
141206
141846
  const entry = {
141207
141847
  id,
141208
141848
  name,
@@ -141215,7 +141855,7 @@ var AlexaBrokerRegistry = class {
141215
141855
  }
141216
141856
  async removeEntry(id) {
141217
141857
  const mgr = this.#managers.get(id);
141218
- if (!mgr) throw new Error(`AlexaBrokerRegistry: unknown broker id "${id}"`);
141858
+ if (!mgr) return;
141219
141859
  for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
141220
141860
  this.#managers.delete(id);
141221
141861
  try {
@@ -141282,6 +141922,10 @@ var AlexaBrokerRegistry = class {
141282
141922
  if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
141283
141923
  await mgr.start();
141284
141924
  }
141925
+ /** The next free broker id, for a record minted before its manager exists. */
141926
+ allocateId() {
141927
+ return this.#allocateId();
141928
+ }
141285
141929
  #allocateId() {
141286
141930
  const id = `alexa_${String(this.#nextId).padStart(3, "0")}`;
141287
141931
  this.#nextId++;
@@ -142703,10 +143347,12 @@ var AlexaEchoDevice = class extends BaseDevice {
142703
143347
  var alexaBrokersEntryKind = defineEntryKind("brokers", {
142704
143348
  title: "Alexa import brokers",
142705
143349
  schema: entryBody({
142706
- connection: AlexaBrokerEntrySchema.shape.connection,
142707
- integrationId: string().optional()
143350
+ ...alexaConnectionSchema.shape,
143351
+ integrationId: string().optional(),
143352
+ enabled: boolean().optional()
142708
143353
  }),
142709
- writeTrigger: "an operator links or removes an Alexa account broker"
143354
+ writeTrigger: "an operator links or removes an Alexa account broker",
143355
+ retiredSecretPaths: ["connection.cookie", "connection.refreshToken"]
142710
143356
  });
142711
143357
  var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
142712
143358
  title: "Alexa Echo speech sessions",
@@ -142719,13 +143365,119 @@ var alexaRecordKinds = {
142719
143365
  "speak-sessions": alexaSpeakSessionsEntryKind
142720
143366
  };
142721
143367
  //#endregion
143368
+ //#region src/alexa-instances.ts
143369
+ /**
143370
+ * The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
143371
+ * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
143372
+ * the form the instances page edits is the stored shape. Signing in (device code or the Amazon
143373
+ * login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
143374
+ */
143375
+ var ALEXA_INSTANCE_KIND = "alexa";
143376
+ var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
143377
+ function isAlexaBodyConfigured(body) {
143378
+ return body.cookie.length > 0 || body.refreshToken.length > 0;
143379
+ }
143380
+ function statusOf(info, configured) {
143381
+ if (!configured) return {
143382
+ state: "unknown",
143383
+ label: "Not signed in"
143384
+ };
143385
+ if (info === null) return {
143386
+ state: "unknown",
143387
+ label: "Not connected"
143388
+ };
143389
+ if (info.status === "connected") return {
143390
+ state: "ok",
143391
+ label: "Connected"
143392
+ };
143393
+ if (info.status === "connecting") return {
143394
+ state: "unknown",
143395
+ label: "Connecting"
143396
+ };
143397
+ return {
143398
+ state: "error",
143399
+ label: info.status,
143400
+ ...info.error !== null ? { detail: info.error } : {}
143401
+ };
143402
+ }
143403
+ function buildAlexaInstances(records, deps) {
143404
+ return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
143405
+ description: {
143406
+ role: "both",
143407
+ instanceMode: "multiple",
143408
+ kind: "account",
143409
+ canTest: true,
143410
+ kinds: [{
143411
+ id: ALEXA_INSTANCE_KIND,
143412
+ label: "Alexa",
143413
+ signIn: {
143414
+ basePath: ALEXA_SIGN_IN_PATH,
143415
+ methods: ["device-code", "proxy"]
143416
+ }
143417
+ }],
143418
+ allowRestoreId: true
143419
+ },
143420
+ formSchema: () => buildConnectionFormSchema(),
143421
+ initialBody: (name) => ({
143422
+ name,
143423
+ amazonDomain: "amazon.com",
143424
+ cookie: "",
143425
+ refreshToken: "",
143426
+ macDms: "",
143427
+ pollIntervalMs: 6e4
143428
+ }),
143429
+ mintId: () => deps.mintId(),
143430
+ kindOf: () => ALEXA_INSTANCE_KIND,
143431
+ health: (body, id) => {
143432
+ const info = deps.infoOf(id);
143433
+ return {
143434
+ status: statusOf(info, isAlexaBodyConfigured(body)),
143435
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
143436
+ };
143437
+ },
143438
+ configured: isAlexaBodyConfigured,
143439
+ onChanged: deps.onChanged,
143440
+ test: async ({ id, values }) => {
143441
+ if (["cookie", "refreshToken"].some((key) => {
143442
+ const v = values[key];
143443
+ return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
143444
+ })) return {
143445
+ success: true,
143446
+ status: "inconclusive",
143447
+ error: "Save the account to check the sign-in"
143448
+ };
143449
+ const info = id === null ? null : deps.infoOf(id);
143450
+ if (info === null) return {
143451
+ success: false,
143452
+ status: "inconclusive",
143453
+ error: "Not connected: sign in first"
143454
+ };
143455
+ return info.status === "connected" ? {
143456
+ success: true,
143457
+ status: "validated"
143458
+ } : {
143459
+ success: false,
143460
+ status: "rejected",
143461
+ error: info.error ?? `status: ${info.status}`
143462
+ };
143463
+ }
143464
+ });
143465
+ }
143466
+ //#endregion
142722
143467
  //#region src/alexa-broker-store.ts
142723
143468
  function rowToEntry(id, data) {
142724
143469
  const parsed = AlexaBrokerEntrySchema.safeParse({
142725
143470
  id,
142726
143471
  name: data.name,
142727
- connection: data.connection,
142728
- integrationId: data.integrationId
143472
+ connection: {
143473
+ amazonDomain: data.amazonDomain,
143474
+ cookie: data.cookie,
143475
+ refreshToken: data.refreshToken,
143476
+ macDms: data.macDms,
143477
+ pollIntervalMs: data.pollIntervalMs
143478
+ },
143479
+ integrationId: data.integrationId,
143480
+ enabled: data.enabled
142729
143481
  });
142730
143482
  return parsed.success ? parsed.data : null;
142731
143483
  }
@@ -142753,16 +143505,20 @@ var AlexaBrokerStore = class AlexaBrokerStore {
142753
143505
  }
142754
143506
  return out;
142755
143507
  }
143508
+ async getById(id) {
143509
+ return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
143510
+ }
142756
143511
  async replaceAll(entries) {
142757
143512
  const page = await this.kind.query({ limit: 1e3 });
142758
143513
  const nextIds = new Set(entries.map((e) => e.id));
142759
143514
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
142760
143515
  for (const entry of entries) {
142761
- const { id, name, connection, integrationId } = entry;
143516
+ const { id, name, connection, integrationId, enabled } = entry;
142762
143517
  const body = {
142763
143518
  name,
142764
- connection,
142765
- ...integrationId === void 0 ? {} : { integrationId }
143519
+ ...connection,
143520
+ ...integrationId === void 0 ? {} : { integrationId },
143521
+ ...enabled === void 0 ? {} : { enabled }
142766
143522
  };
142767
143523
  const parsed = alexaBrokersEntryKind.schema.safeParse(body);
142768
143524
  if (!parsed.success) throw new Error(`invalid Alexa broker '${id}': ${parsed.error.message}`);
@@ -144150,6 +144906,34 @@ var AlexaSpeakController = class {
144150
144906
  }
144151
144907
  };
144152
144908
  //#endregion
144909
+ //#region src/migrations/flatten-broker-records.ts
144910
+ /**
144911
+ * The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
144912
+ * object (`amazonDomain`, `cookie`, `refreshToken`, `macDms`, `pollIntervalMs`) becomes top-level
144913
+ * fields under the SAME record id (`alexa_NNN`). `macDms`, which used to be stored in plain text,
144914
+ * becomes a keychain secret. The work is `flattenNestedRecords`: every secret is read before
144915
+ * anything is written, read back from its new ref before the nested object goes, and the old refs
144916
+ * are deleted last.
144917
+ */
144918
+ var FLATTEN_BROKER_RECORDS_ID = "import-alexa/2026-11-04-flatten-broker-records";
144919
+ async function flattenBrokerRecords(ports) {
144920
+ return flattenNestedRecords(ports, {
144921
+ legacyKey: "connection",
144922
+ flatSecretKeys: [
144923
+ "cookie",
144924
+ "refreshToken",
144925
+ "macDms"
144926
+ ],
144927
+ toFlat: (legacy) => Object.fromEntries([
144928
+ "amazonDomain",
144929
+ "cookie",
144930
+ "refreshToken",
144931
+ "macDms",
144932
+ "pollIntervalMs"
144933
+ ].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
144934
+ });
144935
+ }
144936
+ //#endregion
144153
144937
  //#region src/migrations/strip-echo-target-volume-and-mode.ts
144154
144938
  var RETIRED_KEYS = ["volume", "mode"];
144155
144939
  function isRecord(value) {
@@ -144199,6 +144983,21 @@ var ADDON_MIGRATIONS = [{
144199
144983
  fixtureMark: "the Echo id and the name the migration does not know"
144200
144984
  },
144201
144985
  procedure: stripEchoTargetVolumeAndMode
144986
+ }, {
144987
+ meta: {
144988
+ id: FLATTEN_BROKER_RECORDS_ID,
144989
+ owner: "import-alexa",
144990
+ level: "addon-state",
144991
+ seq: 2,
144992
+ dependsOn: [],
144993
+ store: "settings",
144994
+ scope: "cluster",
144995
+ onFailure: "block-owner",
144996
+ bodyKind: "procedure",
144997
+ description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
144998
+ fixtureMark: "the integration link and the amazon domain of each account"
144999
+ },
145000
+ procedure: flattenBrokerRecords
144202
145001
  }];
144203
145002
  //#endregion
144204
145003
  //#region src/speak/icon-routes.ts
@@ -144253,7 +145052,6 @@ var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
144253
145052
  //#region src/addon.ts
144254
145053
  /** Default config — a fresh install starts with no accounts. */
144255
145054
  var DEFAULTS = {
144256
- brokers: [],
144257
145055
  proxyLogin: { proxyPort: 3456 },
144258
145056
  echoTargets: []
144259
145057
  };
@@ -144301,6 +145099,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144301
145099
  loginPlane = null;
144302
145100
  loginProxy = null;
144303
145101
  brokerStore = null;
145102
+ instances = null;
144304
145103
  activeBrokers = [];
144305
145104
  speak = null;
144306
145105
  constructor() {
@@ -144312,6 +145111,12 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144312
145111
  if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
144313
145112
  await records.declare(alexaRecordKinds);
144314
145113
  this.brokerStore = AlexaBrokerStore.of(records);
145114
+ this.instances = buildAlexaInstances(records, {
145115
+ mintId: () => this.requireRegistry().allocateId(),
145116
+ infoOf: (id) => this.registry?.get(id) ?? null,
145117
+ onChanged: (id) => this.syncBrokerRuntime(id)
145118
+ });
145119
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
144315
145120
  this.activeBrokers = await this.loadPersistedBrokers();
144316
145121
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
144317
145122
  dataDir: this.ctx.dataDir,
@@ -144356,12 +145161,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144356
145161
  deviceCode: this.deviceCodeLogin,
144357
145162
  commitCookie: (input) => this.commitCapturedCookie(input)
144358
145163
  }), ...speakIconRoutes()];
144359
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
144360
- readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
144361
- writeRows: async (rows) => {
144362
- await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
144363
- }
144364
- }));
145164
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
144365
145165
  const speak = new AlexaSpeakController({
144366
145166
  logger: this.ctx.logger.child("speak"),
144367
145167
  readConfig: () => this.config,
@@ -144391,6 +145191,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144391
145191
  capability: brokerCapability,
144392
145192
  provider: this.buildBrokerProvider()
144393
145193
  },
145194
+ {
145195
+ capability: integrationInstancesCapability,
145196
+ provider: this.requireInstances().toProvider()
145197
+ },
144394
145198
  {
144395
145199
  capability: deviceAdoptionCapability,
144396
145200
  provider: this.buildAdoptionProvider()
@@ -144405,19 +145209,65 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144405
145209
  }
144406
145210
  ] };
144407
145211
  }
144408
- async loadPersistedBrokers() {
144409
- const store = this.brokerStore;
144410
- if (store !== null) {
144411
- const fromRecords = await store.listResolved();
144412
- if (fromRecords.length > 0) return fromRecords;
145212
+ migrationPorts(records) {
145213
+ if (this.ctx.secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
145214
+ return {
145215
+ readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
145216
+ writeRows: async (rows) => {
145217
+ await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
145218
+ },
145219
+ ...flattenPortsOver(records.kind(alexaBrokersEntryKind))
145220
+ };
145221
+ }
145222
+ /**
145223
+ * Bring the runtime in line with one record after the instances page changed it: a removed
145224
+ * record stops its manager and cascades its devices, an off record only stops the manager, a
145225
+ * live one re-applies its connection or starts under the record's own id.
145226
+ */
145227
+ async syncBrokerRuntime(id) {
145228
+ const reg = this.requireRegistry();
145229
+ const entry = await this.requireBrokerStore().getById(id);
145230
+ this.activeBrokers = await this.loadPersistedBrokers();
145231
+ if (entry === null) {
145232
+ await reg.removeEntry(id);
145233
+ await this.cascadeRemoveDevicesForBroker(id);
145234
+ this.speak?.brokersChanged();
145235
+ return;
144413
145236
  }
144414
- return this.config.brokers;
145237
+ const configured = entry.connection.cookie.length > 0 || entry.connection.refreshToken.length > 0;
145238
+ if (entry.enabled === false || !configured) {
145239
+ await reg.removeEntry(id);
145240
+ this.speak?.brokersChanged();
145241
+ return;
145242
+ }
145243
+ try {
145244
+ if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
145245
+ else await reg.createEntry(entry.name, entry.connection, {
145246
+ id,
145247
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
145248
+ });
145249
+ } catch (err) {
145250
+ this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
145251
+ brokerId: id,
145252
+ error: errMsg(err)
145253
+ } });
145254
+ }
145255
+ this.speak?.brokersChanged();
145256
+ }
145257
+ requireBrokerStore() {
145258
+ if (this.brokerStore === null) throw new Error("import-alexa: broker store not ready");
145259
+ return this.brokerStore;
145260
+ }
145261
+ requireInstances() {
145262
+ if (this.instances === null) throw new Error("import-alexa: instances not ready");
145263
+ return this.instances;
145264
+ }
145265
+ async loadPersistedBrokers() {
145266
+ return this.requireBrokerStore().listResolved();
144415
145267
  }
144416
145268
  async persistBrokerEntries(entries) {
144417
145269
  this.activeBrokers = [...entries];
144418
- const store = this.brokerStore;
144419
- if (store !== null) await store.replaceAll(entries);
144420
- else await this.updateGlobalSettings({ brokers: [...entries] });
145270
+ await this.requireBrokerStore().replaceAll(entries);
144421
145271
  this.speak?.brokersChanged();
144422
145272
  }
144423
145273
  async onConfigChanged() {
@@ -144436,6 +145286,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
144436
145286
  } });
144437
145287
  }
144438
145288
  for (const entry of persisted) {
145289
+ if (entry.enabled === false) continue;
144439
145290
  if (entry.id && liveIds.has(entry.id)) {
144440
145291
  if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
144441
145292
  try {