@camstack/addon-provider-homeassistant 1.2.655 → 1.2.657

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.
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-BkzQdpzF.mjs
5984
+ //#region ../types/dist/sleep-Cv4Zmie9.mjs
5985
5985
  /**
5986
5986
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5987
5987
  * window to float samples (D455).
@@ -6591,7 +6591,7 @@ var NOT_RUN = {
6591
6591
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6592
6592
  * because the hub (and this addon's boot) must not hang on a migration.
6593
6593
  */
6594
- async function runAddonMigrations(host, migrations, logger) {
6594
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6595
6595
  const first = migrations[0];
6596
6596
  if (first === void 0) return NOT_RUN;
6597
6597
  const owner = first.meta.owner;
@@ -6620,7 +6620,9 @@ async function runAddonMigrations(host, migrations, logger) {
6620
6620
  else if (status === "blocked") blocked.push(id);
6621
6621
  else skipped.push(id);
6622
6622
  };
6623
+ const selected = options.only === void 0 ? null : new Set(options.only);
6623
6624
  for (const step of declaration.steps) {
6625
+ if (selected !== null && !selected.has(step.id)) continue;
6624
6626
  const bound = byId.get(step.id);
6625
6627
  if (bound === void 0 || step.action === "skip") {
6626
6628
  skipped.push(step.id);
@@ -7780,14 +7782,18 @@ var BaseAddon = class {
7780
7782
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7781
7783
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7782
7784
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7785
+ *
7786
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7787
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7788
+ * omitted applied ids "newer").
7783
7789
  */
7784
- async runMigrations(migrations) {
7790
+ async runMigrations(migrations, options) {
7785
7791
  const api = this.ctx.api.migrations;
7786
7792
  const report = await runAddonMigrations({
7787
7793
  declareOwner: (input) => api.declareOwner.mutate(input),
7788
7794
  startMigration: (input) => api.startMigration.mutate(input),
7789
7795
  finishMigration: (input) => api.finishMigration.mutate(input)
7790
- }, migrations, this.ctx.logger);
7796
+ }, migrations, this.ctx.logger, options);
7791
7797
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7792
7798
  return report;
7793
7799
  }
@@ -8783,6 +8789,13 @@ var RecordsError = class extends Error {
8783
8789
  this.detail = detail;
8784
8790
  }
8785
8791
  };
8792
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8793
+ var RevConflictError = class extends RecordsError {
8794
+ constructor(detail) {
8795
+ super("rev-conflict", detail);
8796
+ this.name = "RevConflictError";
8797
+ }
8798
+ };
8786
8799
  /** The keychain could not store or serve a value: explicit, never an empty string. */
8787
8800
  var RecordSecretUnavailableError = class extends RecordsError {
8788
8801
  constructor(detail) {
@@ -8827,6 +8840,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8827
8840
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8828
8841
  * (`auth.accessToken`, `headers[].value`).
8829
8842
  *
8843
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8844
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8845
+ * key releases exactly that key's secret.
8846
+ *
8830
8847
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8831
8848
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8832
8849
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8839,11 +8856,13 @@ function parseSecretPath(path) {
8839
8856
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8840
8857
  return path.split(".").map((raw) => {
8841
8858
  const each = raw.endsWith("[]");
8842
- const key = each ? raw.slice(0, -2) : raw;
8859
+ const map = raw.endsWith("{}");
8860
+ const key = each || map ? raw.slice(0, -2) : raw;
8843
8861
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8844
8862
  return {
8845
8863
  key,
8846
- each
8864
+ each,
8865
+ map
8847
8866
  };
8848
8867
  });
8849
8868
  }
@@ -8895,6 +8914,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8895
8914
  [segment.key]: mapped
8896
8915
  };
8897
8916
  }
8917
+ if (segment.map) {
8918
+ if (!isRecord$4(child)) return node;
8919
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8920
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8921
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8922
+ return [mapKey, visit({
8923
+ declared,
8924
+ concrete,
8925
+ value
8926
+ })];
8927
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8928
+ return {
8929
+ ...node,
8930
+ [segment.key]: Object.fromEntries(kept)
8931
+ };
8932
+ }
8898
8933
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8899
8934
  if (last) {
8900
8935
  const replacement = visit({
@@ -9036,7 +9071,8 @@ function isZodSecret(schema) {
9036
9071
  }
9037
9072
  /**
9038
9073
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9039
- * element of this array" (`connection.password`, `rows[].token`). A declared
9074
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9075
+ * map" (`applianceSecrets{}`). A declared
9040
9076
  * object or array is reported as one path and not descended into — everything
9041
9077
  * under it is secret.
9042
9078
  */
@@ -9047,6 +9083,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9047
9083
  const def = defOf(layer);
9048
9084
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9049
9085
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9086
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9050
9087
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9051
9088
  else if (def.type === "intersection") {
9052
9089
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13423,11 +13460,49 @@ function buildCapturingReply(envelope) {
13423
13460
  };
13424
13461
  return wrapper;
13425
13462
  }
13463
+ /**
13464
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13465
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13466
+ *
13467
+ * Every instance is one record of the addon's own kind, keyed by the
13468
+ * `integrations` registry id. The records door does the secret work: a field the
13469
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13470
+ * deleting the row deletes the secret, and one row's value is never another's.
13471
+ *
13472
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13473
+ * token or the view the form was given KEEP the stored secret;
13474
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13475
+ */
13426
13476
  /** The most instances one addon lists (an integrations page, not a table). */
13427
13477
  var INSTANCES_LIST_MAX = 1e3;
13428
13478
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13429
13479
  function topLevelSecretKeys(paths) {
13430
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13480
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13481
+ }
13482
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13483
+ function secretMapKeys(paths) {
13484
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13485
+ }
13486
+ /**
13487
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13488
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13489
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13490
+ */
13491
+ function secretMapPatch(value) {
13492
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13493
+ const out = {};
13494
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13495
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13496
+ return Object.keys(out).length > 0 ? out : null;
13497
+ }
13498
+ /** Does the kind's body schema carry an `enabled` field? */
13499
+ function declaresEnabled(kind) {
13500
+ const json = toJSONSchema(kind.schema, {
13501
+ unrepresentable: "any",
13502
+ io: "input"
13503
+ });
13504
+ const properties = Reflect.get(json, "properties");
13505
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13431
13506
  }
13432
13507
  var RecordIntegrationInstances = class {
13433
13508
  records;
@@ -13435,14 +13510,20 @@ var RecordIntegrationInstances = class {
13435
13510
  hooks;
13436
13511
  handle;
13437
13512
  secretKeys;
13513
+ secretMaps;
13438
13514
  nameField;
13515
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13516
+ registryOwnsEnabled;
13439
13517
  constructor(records, kind, hooks) {
13440
13518
  this.records = records;
13441
13519
  this.kind = kind;
13442
13520
  this.hooks = hooks;
13443
13521
  this.handle = records.kind(kind);
13444
13522
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13523
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13445
13524
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13525
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13526
+ 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"`);
13446
13527
  }
13447
13528
  /**
13448
13529
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13493,7 +13574,7 @@ var RecordIntegrationInstances = class {
13493
13574
  id: record.id,
13494
13575
  name: typeof rawName === "string" ? rawName : record.id,
13495
13576
  configured: this.hooks.configured(record.data),
13496
- enabled: record.data.enabled !== false,
13577
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13497
13578
  formValues: shown,
13498
13579
  ...kind !== void 0 ? { kind } : {},
13499
13580
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13523,32 +13604,36 @@ var RecordIntegrationInstances = class {
13523
13604
  id: minted,
13524
13605
  data: this.hooks.initialBody(name, kind)
13525
13606
  }, { ifRev: 0 });
13526
- await this.hooks.onChanged?.(minted);
13607
+ await this.hooks.onChanged?.(minted, "created");
13527
13608
  return { id: minted };
13528
13609
  };
13529
13610
  updateInstance = async ({ id, name, values }) => {
13530
13611
  const patch = {};
13531
13612
  for (const [key, value] of Object.entries(values)) {
13532
13613
  if (value === void 0) continue;
13533
- if (!this.secretKeys.has(key)) patch[key] = value;
13614
+ if (this.secretMaps.has(key)) {
13615
+ const entries = secretMapPatch(value);
13616
+ if (entries !== null) patch[key] = entries;
13617
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13534
13618
  else if (isSecretClear(value)) patch[key] = null;
13535
13619
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13536
13620
  }
13537
13621
  if (name !== void 0) patch[this.nameField] = name;
13538
13622
  await this.handle.patch(id, patch);
13539
- await this.hooks.onChanged?.(id);
13623
+ await this.hooks.onChanged?.(id, "updated");
13540
13624
  return { ok: true };
13541
13625
  };
13542
13626
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13543
13627
  setInstanceEnabled = async ({ id, enabled }) => {
13628
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13544
13629
  await this.handle.patch(id, { enabled });
13545
- await this.hooks.onChanged?.(id);
13630
+ await this.hooks.onChanged?.(id, "enabled");
13546
13631
  return { ok: true };
13547
13632
  };
13548
13633
  /** The row and its keychain refs go together (the records door). */
13549
13634
  deleteInstance = async ({ id }) => {
13550
13635
  await this.handle.delete(id);
13551
- await this.hooks.onChanged?.(id);
13636
+ await this.hooks.onChanged?.(id, "deleted");
13552
13637
  return { ok: true };
13553
13638
  };
13554
13639
  testInstance = async (input) => {
@@ -14646,7 +14731,13 @@ var MigrationLedgerEntrySchema = object({
14646
14731
  * joined by the engine from the catalogues it knows this boot. Absent when no
14647
14732
  * catalogue lists the id (a newer database).
14648
14733
  */
14649
- meta: MigrationMetaSchema.optional()
14734
+ meta: MigrationMetaSchema.optional(),
14735
+ /**
14736
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14737
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14738
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14739
+ */
14740
+ ownerNotLoaded: boolean().optional()
14650
14741
  });
14651
14742
  /** A catalogued migration that is not applied, with where it stands. */
14652
14743
  var MigrationPendingEntrySchema = object({
@@ -56775,6 +56866,21 @@ var IntegrationInstanceKindSchema = object({
56775
56866
  disconnectPath: string().min(1).optional()
56776
56867
  });
56777
56868
  var IntegrationInstanceDescriptionSchema = object({
56869
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56870
+ role: _enum([
56871
+ "devices",
56872
+ "services",
56873
+ "both"
56874
+ ]),
56875
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56876
+ discovery: object({
56877
+ mode: _enum([
56878
+ "none",
56879
+ "lan",
56880
+ "account"
56881
+ ]),
56882
+ scopedToInstance: boolean()
56883
+ }).optional(),
56778
56884
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56779
56885
  instanceMode: IntegrationInstanceModeSchema,
56780
56886
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56814,10 +56920,11 @@ var IntegrationInstanceSchema = object({
56814
56920
  /** The addon's own rule (an URL is present, a client id is present...). */
56815
56921
  configured: boolean(),
56816
56922
  /**
56817
- * The instance's own on/off switch (D996): always present, derived from the record body
56818
- * (`enabled !== false`, so a row written before the field existed is on).
56923
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56924
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56925
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56819
56926
  */
56820
- enabled: boolean(),
56927
+ enabled: boolean().optional(),
56821
56928
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56822
56929
  kind: string().optional(),
56823
56930
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -98300,21 +98407,6 @@ function haInstanceSettingsPortsOver(api) {
98300
98407
  }
98301
98408
  };
98302
98409
  }
98303
- /** The brokerId → settings view of the instances: what a reader keeps per broker. */
98304
- function settingsByBroker(rows) {
98305
- const out = /* @__PURE__ */ new Map();
98306
- for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
98307
- return out;
98308
- }
98309
- /** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
98310
- async function listInstancesOrNothing(ports, onError) {
98311
- try {
98312
- return await ports.listInstances();
98313
- } catch (err) {
98314
- onError(errMsg(err));
98315
- return [];
98316
- }
98317
- }
98318
98410
  //#endregion
98319
98411
  Object.defineProperty(exports, "AlarmArmModeSchema", {
98320
98412
  enumerable: true,
@@ -98472,6 +98564,12 @@ Object.defineProperty(exports, "RecordSecretUnavailableError", {
98472
98564
  return RecordSecretUnavailableError;
98473
98565
  }
98474
98566
  });
98567
+ Object.defineProperty(exports, "RevConflictError", {
98568
+ enumerable: true,
98569
+ get: function() {
98570
+ return RevConflictError;
98571
+ }
98572
+ });
98475
98573
  Object.defineProperty(exports, "TargetSchema", {
98476
98574
  enumerable: true,
98477
98575
  get: function() {
@@ -98808,12 +98906,6 @@ Object.defineProperty(exports, "listHomeAssistantBrokers", {
98808
98906
  return listHomeAssistantBrokers;
98809
98907
  }
98810
98908
  });
98811
- Object.defineProperty(exports, "listInstancesOrNothing", {
98812
- enumerable: true,
98813
- get: function() {
98814
- return listInstancesOrNothing;
98815
- }
98816
- });
98817
98909
  Object.defineProperty(exports, "literal", {
98818
98910
  enumerable: true,
98819
98911
  get: function() {
@@ -98994,12 +99086,6 @@ Object.defineProperty(exports, "sendToExportedTarget", {
98994
99086
  return sendToExportedTarget;
98995
99087
  }
98996
99088
  });
98997
- Object.defineProperty(exports, "settingsByBroker", {
98998
- enumerable: true,
98999
- get: function() {
99000
- return settingsByBroker;
99001
- }
99002
- });
99003
99089
  Object.defineProperty(exports, "smokeCapability", {
99004
99090
  enumerable: true,
99005
99091
  get: function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.655",
3
+ "version": "1.2.657",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -134,7 +134,7 @@
134
134
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
135
135
  },
136
136
  "requires": {
137
- "server": ">=1.2.761"
137
+ "server": ">=1.2.763"
138
138
  }
139
139
  },
140
140
  "files": [