@camstack/addon-provider-homeassistant 1.2.648 → 1.2.649

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.649] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - The retired media base URL setting is now removed from the settings table where it is actually stored
15
+
10
16
  ## [1.2.648] - 2026-11-02
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ha_broker_source = require("./ha-broker-source-Ba20mhmb.js");
2
+ const require_ha_broker_source = require("./ha-broker-source-CE3nHkyW.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-iLA_DsW8.mjs";
1
+ import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-D3FBA6uG.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -5972,7 +5972,7 @@ var ZodIssueCode = {
5972
5972
  var ZodFirstPartyTypeKind;
5973
5973
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5974
5974
  //#endregion
5975
- //#region ../types/dist/sleep-Bei295Bz.mjs
5975
+ //#region ../types/dist/sleep-CdJHPm-k.mjs
5976
5976
  /**
5977
5977
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5978
5978
  * window to float samples (D455).
@@ -6089,7 +6089,7 @@ function isKeychainStoredField(field) {
6089
6089
  var SECRET_CLEAR = { $secretClear: true };
6090
6090
  /** The `'***'` token D634 redaction writes for non-admins. */
6091
6091
  var LEGACY_DEVICE_REDACTION = "***";
6092
- function isRecord$6(value) {
6092
+ function isRecord$7(value) {
6093
6093
  return typeof value === "object" && value !== null && !Array.isArray(value);
6094
6094
  }
6095
6095
  function makeSecretMarker(ref) {
@@ -6100,12 +6100,12 @@ function makeSecretMarker(ref) {
6100
6100
  }
6101
6101
  /** A stored marker: an object whose `$secret` is a non-empty string. */
6102
6102
  function isSecretMarker(value) {
6103
- if (!isRecord$6(value)) return false;
6103
+ if (!isRecord$7(value)) return false;
6104
6104
  const ref = value["$secret"];
6105
6105
  return typeof ref === "string" && ref.length > 0;
6106
6106
  }
6107
6107
  function isSecretClear(value) {
6108
- return isRecord$6(value) && value["$secretClear"] === true;
6108
+ return isRecord$7(value) && value["$secretClear"] === true;
6109
6109
  }
6110
6110
  /**
6111
6111
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6858,7 +6858,7 @@ var KeychainWriteRefusedError = class extends Error {
6858
6858
  }
6859
6859
  };
6860
6860
  var KEEP = { kind: "keep" };
6861
- function isRecord$5(value) {
6861
+ function isRecord$6(value) {
6862
6862
  return typeof value === "object" && value !== null && !Array.isArray(value);
6863
6863
  }
6864
6864
  function defaultMintId() {
@@ -6871,7 +6871,7 @@ function collectSecretMarkerRefs(value, out = []) {
6871
6871
  return out;
6872
6872
  }
6873
6873
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6874
- else if (isRecord$5(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6874
+ else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6875
6875
  return out;
6876
6876
  }
6877
6877
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6926,7 +6926,7 @@ function rowId(row, idKey) {
6926
6926
  return null;
6927
6927
  }
6928
6928
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6929
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord$5) : [];
6929
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
6930
6930
  if (!Array.isArray(incoming)) {
6931
6931
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6932
6932
  return {
@@ -6942,7 +6942,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6942
6942
  const kept = /* @__PURE__ */ new Set();
6943
6943
  const rows = [];
6944
6944
  for (const row of incoming) {
6945
- if (!isRecord$5(row)) {
6945
+ if (!isRecord$6(row)) {
6946
6946
  rows.push(row);
6947
6947
  continue;
6948
6948
  }
@@ -6967,7 +6967,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6967
6967
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6968
6968
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6969
6969
  case "object": {
6970
- if (!isRecord$5(incoming)) {
6970
+ if (!isRecord$6(incoming)) {
6971
6971
  if (incoming === void 0) return KEEP;
6972
6972
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6973
6973
  return {
@@ -6975,7 +6975,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6975
6975
  value: incoming
6976
6976
  };
6977
6977
  }
6978
- const storedObj = isRecord$5(stored) ? stored : {};
6978
+ const storedObj = isRecord$6(stored) ? stored : {};
6979
6979
  return {
6980
6980
  kind: "value",
6981
6981
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -7098,7 +7098,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
7098
7098
  value: items
7099
7099
  };
7100
7100
  }
7101
- if (isRecord$5(v)) {
7101
+ if (isRecord$6(v)) {
7102
7102
  const out = {};
7103
7103
  for (const [k, child] of Object.entries(v)) {
7104
7104
  const s = substitute(child);
@@ -7130,7 +7130,7 @@ var SecretOverwriteRefusedError = class extends Error {
7130
7130
  this.name = "SecretOverwriteRefusedError";
7131
7131
  }
7132
7132
  };
7133
- function isRecord$4(value) {
7133
+ function isRecord$5(value) {
7134
7134
  return typeof value === "object" && value !== null && !Array.isArray(value);
7135
7135
  }
7136
7136
  function resolvedValue(pass, marker) {
@@ -7159,7 +7159,7 @@ function inArray(pass, incoming, stored) {
7159
7159
  return incoming.map((item, index) => {
7160
7160
  const twin = stored[index];
7161
7161
  if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
7162
- if (isRecord$4(item) && isRecord$4(twin)) return inArrayRow(pass, item, twin);
7162
+ if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
7163
7163
  if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
7164
7164
  return item;
7165
7165
  });
@@ -7170,7 +7170,7 @@ function inArrayRow(pass, incoming, stored) {
7170
7170
  const twin = stored[key];
7171
7171
  if (isSecretMarker(twin)) {
7172
7172
  if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
7173
- } else if (isRecord$4(value) && isRecord$4(twin)) out[key] = inArrayRow(pass, value, twin);
7173
+ } else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
7174
7174
  else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
7175
7175
  }
7176
7176
  return out;
@@ -7178,7 +7178,7 @@ function inArrayRow(pass, incoming, stored) {
7178
7178
  async function walk(pass, incoming, stored) {
7179
7179
  if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
7180
7180
  if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
7181
- if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
7181
+ if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
7182
7182
  const out = { ...incoming };
7183
7183
  for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
7184
7184
  return out;
@@ -7209,7 +7209,7 @@ async function restoreUnchangedSecrets(input) {
7209
7209
  function isJsonEqual(a, b) {
7210
7210
  if (Object.is(a, b)) return true;
7211
7211
  if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
7212
- if (isRecord$4(a) && isRecord$4(b)) {
7212
+ if (isRecord$5(a) && isRecord$5(b)) {
7213
7213
  const keys = Object.keys(a);
7214
7214
  if (keys.length !== Object.keys(b).length) return false;
7215
7215
  return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
@@ -7233,7 +7233,7 @@ function markerKeysOf(blob, owner) {
7233
7233
  }
7234
7234
  function nodeFromStored(value, owner) {
7235
7235
  if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
7236
- if (!isRecord$4(value)) return null;
7236
+ if (!isRecord$5(value)) return null;
7237
7237
  const children = /* @__PURE__ */ new Map();
7238
7238
  for (const [key, child] of Object.entries(value)) {
7239
7239
  const node = nodeFromStored(child, owner);
@@ -7852,6 +7852,44 @@ var BaseAddon = class {
7852
7852
  return null;
7853
7853
  }
7854
7854
  /**
7855
+ * D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
7856
+ * provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
7857
+ * declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
7858
+ * {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
7859
+ * contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
7860
+ */
7861
+ async clusterSettingsSchema(_cap) {
7862
+ return null;
7863
+ }
7864
+ /**
7865
+ * D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
7866
+ * `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
7867
+ * forgotten node's rows go in one central delete.
7868
+ */
7869
+ async nodeSettingsSchema(_cap) {
7870
+ return null;
7871
+ }
7872
+ /**
7873
+ * D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
7874
+ * or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
7875
+ * as device row, then this default, then the schema default (`resolveDeviceSettings`).
7876
+ */
7877
+ async deviceDefaultsSchema(_cap) {
7878
+ return null;
7879
+ }
7880
+ /**
7881
+ * The host's read of the scoped declarations: the schema one capability declares for `scope`,
7882
+ * or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
7883
+ * host asks this method.
7884
+ */
7885
+ async getScopedSettingsSchema(scope, cap) {
7886
+ switch (scope) {
7887
+ case "cluster": return this.clusterSettingsSchema(cap);
7888
+ case "node": return this.nodeSettingsSchema(cap);
7889
+ case "device-defaults": return this.deviceDefaultsSchema(cap);
7890
+ }
7891
+ }
7892
+ /**
7855
7893
  * What the create wizard and the integration edit page render for one instance: the addon's
7856
7894
  * per-instance schema when it declares one, else its global schema (what they always showed).
7857
7895
  */
@@ -8645,47 +8683,6 @@ function normalizeAddonInitResult(result) {
8645
8683
  if (Array.isArray(result)) return { providers: result };
8646
8684
  return result;
8647
8685
  }
8648
- /**
8649
- * The share-view scope kinds a share-link token can carry. The hub mints
8650
- * and validates them (`ShareViewGrantSchema` builds its
8651
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8652
- */
8653
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8654
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8655
- var PeerBytesTicketSchema = object({
8656
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8657
- url: string().min(1),
8658
- /**
8659
- * The HOST node this URL means something on — the hub or a named agent,
8660
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8661
- * refuses `cross-node` by name when they differ, without dialling.
8662
- */
8663
- hostNodeId: string().min(1),
8664
- expiresAtMs: number().int().nonnegative(),
8665
- /**
8666
- * What the producer DECLARED the body to be, when it knows — `null` when it
8667
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8668
- * must be able to tell "the producer did not say" from "the body is empty".
8669
- */
8670
- declaredBytes: number().int().nonnegative().nullable()
8671
- });
8672
- /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8673
- /** The files a kind may live in. `history` is device-history's (D850), never records. */
8674
- var RECORD_DATABASES = ["settings", "events"];
8675
- /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8676
- var RECORD_CLASSES = [
8677
- "config",
8678
- "ledger",
8679
- "audit",
8680
- "mirror",
8681
- "ephemeral"
8682
- ];
8683
- var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8684
- var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8685
- var RECORD_BATCH_MAX = 1e3;
8686
- var RECORD_QUERY_MAX = 1e3;
8687
- /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8688
- var RECORD_SETTINGS_MAX_ROWS = 5e4;
8689
8686
  var PREFIX = "records/";
8690
8687
  /** The base of every records failure. `detail` is the human part of the message. */
8691
8688
  var RecordsError = class extends Error {
@@ -8718,6 +8715,83 @@ var DeclarationRefusedError = class extends RecordsError {
8718
8715
  this.name = "DeclarationRefusedError";
8719
8716
  }
8720
8717
  };
8718
+ /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8719
+ /** The files a kind may live in. `history` is device-history's (D850), never records. */
8720
+ var RECORD_DATABASES = ["settings", "events"];
8721
+ /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8722
+ var RECORD_CLASSES = [
8723
+ "config",
8724
+ "ledger",
8725
+ "audit",
8726
+ "mirror",
8727
+ "ephemeral"
8728
+ ];
8729
+ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8730
+ var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8731
+ var RECORD_BATCH_MAX = 1e3;
8732
+ var RECORD_QUERY_MAX = 1e3;
8733
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8734
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8735
+ /**
8736
+ * Secret fields of a record, addressed the way a Zod schema declares them
8737
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8738
+ * (`auth.accessToken`, `headers[].value`).
8739
+ *
8740
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8741
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8742
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8743
+ * leaf names its element: `headers[X-Api].value`.
8744
+ *
8745
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8746
+ */
8747
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8748
+ function parseSecretPath(path) {
8749
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8750
+ return path.split(".").map((raw) => {
8751
+ const each = raw.endsWith("[]");
8752
+ const key = each ? raw.slice(0, -2) : raw;
8753
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8754
+ return {
8755
+ key,
8756
+ each
8757
+ };
8758
+ });
8759
+ }
8760
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8761
+ function secretArrayPrefixes(path) {
8762
+ const segments = parseSecretPath(path);
8763
+ const prefixes = [];
8764
+ const walked = [];
8765
+ for (const segment of segments) {
8766
+ walked.push(segment.key);
8767
+ if (segment.each) prefixes.push(walked.join("."));
8768
+ }
8769
+ return prefixes;
8770
+ }
8771
+ /**
8772
+ * The share-view scope kinds a share-link token can carry. The hub mints
8773
+ * and validates them (`ShareViewGrantSchema` builds its
8774
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8775
+ */
8776
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8777
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8778
+ var PeerBytesTicketSchema = object({
8779
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8780
+ url: string().min(1),
8781
+ /**
8782
+ * The HOST node this URL means something on — the hub or a named agent,
8783
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8784
+ * refuses `cross-node` by name when they differ, without dialling.
8785
+ */
8786
+ hostNodeId: string().min(1),
8787
+ expiresAtMs: number().int().nonnegative(),
8788
+ /**
8789
+ * What the producer DECLARED the body to be, when it knows — `null` when it
8790
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8791
+ * must be able to tell "the producer did not say" from "the body is empty".
8792
+ */
8793
+ declaredBytes: number().int().nonnegative().nullable()
8794
+ });
8721
8795
  function defOf(schema) {
8722
8796
  const zodBag = Reflect.get(schema, "_zod");
8723
8797
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -8786,42 +8860,6 @@ function collectZodSecretPaths(schema, prefix = "") {
8786
8860
  return [...new Set(paths)];
8787
8861
  }
8788
8862
  /**
8789
- * Secret fields of a record, addressed the way a Zod schema declares them
8790
- * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8791
- * (`auth.accessToken`, `headers[].value`).
8792
- *
8793
- * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8794
- * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8795
- * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8796
- * leaf names its element: `headers[X-Api].value`.
8797
- *
8798
- * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8799
- */
8800
- /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8801
- function parseSecretPath(path) {
8802
- if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8803
- return path.split(".").map((raw) => {
8804
- const each = raw.endsWith("[]");
8805
- const key = each ? raw.slice(0, -2) : raw;
8806
- if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8807
- return {
8808
- key,
8809
- each
8810
- };
8811
- });
8812
- }
8813
- /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8814
- function secretArrayPrefixes(path) {
8815
- const segments = parseSecretPath(path);
8816
- const prefixes = [];
8817
- const walked = [];
8818
- for (const segment of segments) {
8819
- walked.push(segment.key);
8820
- if (segment.each) prefixes.push(walked.join("."));
8821
- }
8822
- return prefixes;
8823
- }
8824
- /**
8825
8863
  * Record kinds (D881): the declaration an addon makes of one kind of fact, the
8826
8864
  * Zod row schema that describes it, and the helper that turns the two into the
8827
8865
  * JSON the engine stores.
@@ -9786,6 +9824,7 @@ var CAP_NAMES = Object.freeze([
9786
9824
  "humidity-sensor",
9787
9825
  "image",
9788
9826
  "image-settings",
9827
+ "integration-instances",
9789
9828
  "integrations",
9790
9829
  "intercom",
9791
9830
  "ir-transmitter",
@@ -11719,7 +11758,7 @@ object({
11719
11758
  params: record(string(), union([string(), number()])).optional()
11720
11759
  });
11721
11760
  //#endregion
11722
- //#region ../types/dist/composition-kKKOdFIG.mjs
11761
+ //#region ../types/dist/composition-BDUoQsw9.mjs
11723
11762
  /**
11724
11763
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11725
11764
  *
@@ -21356,6 +21395,7 @@ var SYSTEM_CAP_VOCABULARY = {
21356
21395
  "load-contribution": system(INTERNAL),
21357
21396
  "local-network": system(NETWORK),
21358
21397
  "log-channels": system(INTERNAL),
21398
+ "integration-instances": system(INTERNAL),
21359
21399
  "log-destination": system(INTERNAL),
21360
21400
  "login-method": system(IDENTITY),
21361
21401
  "mesh-network": system(NETWORK),
@@ -43656,7 +43696,7 @@ var TargetKindCapsSchema = object({
43656
43696
  * the union is large and not meant for runtime validation here; the exported
43657
43697
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
43658
43698
  */
43659
- var ConfigSchemaPassthrough$3 = unknown();
43699
+ var ConfigSchemaPassthrough$4 = unknown();
43660
43700
  var TargetKindSchema = object({
43661
43701
  kind: string(),
43662
43702
  label: string(),
@@ -43693,7 +43733,7 @@ var TargetKindSchema = object({
43693
43733
  * its raster path, which is the safe default for an unknown image.
43694
43734
  */
43695
43735
  iconMediaType: string().optional(),
43696
- configSchema: ConfigSchemaPassthrough$3,
43736
+ configSchema: ConfigSchemaPassthrough$4,
43697
43737
  /**
43698
43738
  * The config keys that decide WHERE this kind's secrets are sent (server,
43699
43739
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -55701,9 +55741,15 @@ var AvailableIntegrationTypeSchema = object({
55701
55741
  * branch on CAP — never on addon name. `device-adoption` integrations
55702
55742
  * (Home Assistant, …) route through the broker step (Approach A);
55703
55743
  * `device-provider` integrations (Reolink/ONVIF) keep the
55704
- * legacy config → discovery flow.
55744
+ * legacy config → discovery flow. `connection` is an addon that only holds a
55745
+ * connection as typed records (`integration-instances`, D985): no devices,
55746
+ * no discovery.
55705
55747
  */
55706
- kind: _enum(["device-adoption", "device-provider"]),
55748
+ kind: _enum([
55749
+ "device-adoption",
55750
+ "device-provider",
55751
+ "connection"
55752
+ ]),
55707
55753
  /**
55708
55754
  * For `device-adoption` addons: the broker kind to create/link
55709
55755
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55816,6 +55862,67 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55816
55862
  auth: "admin"
55817
55863
  });
55818
55864
  /**
55865
+ * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
55866
+ * (an account, a relay, a destination) as typed records, shown on the
55867
+ * Integrations page like every other integration (D985, D986).
55868
+ *
55869
+ * A collection of providers: each addon that holds connections registers one
55870
+ * and the hub aggregator (`integrations.*`) reaches a provider with the pinned
55871
+ * `addonId`. It is internal and has no route: the operator-facing surface is
55872
+ * the `integrations` cap, which keeps ONE list.
55873
+ *
55874
+ * Identity: the instance id IS the `integrations` registry id (`int_NNNN`,
55875
+ * immutable, already in the `/integrations/<id>` URL). A second operator-chosen
55876
+ * slug would be a second authority over one identity (D224). `name` is the
55877
+ * editable label.
55878
+ *
55879
+ * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55880
+ * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
55881
+ * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
55882
+ * clears it.
55883
+ */
55884
+ var ConfigSchemaPassthrough$3 = custom();
55885
+ var IntegrationInstanceIdSchema = string().min(1);
55886
+ var IntegrationInstanceDescriptionSchema = object({
55887
+ /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55888
+ instanceMode: _enum(["single", "multiple"]),
55889
+ /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55890
+ kind: string(),
55891
+ /** True when `testInstance` really probes the remote. */
55892
+ canTest: boolean()
55893
+ });
55894
+ var IntegrationInstanceStatusSchema = object({
55895
+ state: _enum([
55896
+ "ok",
55897
+ "off",
55898
+ "error",
55899
+ "unknown"
55900
+ ]),
55901
+ detail: string().optional()
55902
+ });
55903
+ var IntegrationInstanceSchema = object({
55904
+ id: IntegrationInstanceIdSchema,
55905
+ name: string(),
55906
+ /** The addon's own rule (an URL is present, a client id is present...). */
55907
+ configured: boolean(),
55908
+ /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55909
+ formValues: record(string(), unknown()),
55910
+ status: IntegrationInstanceStatusSchema.optional()
55911
+ });
55912
+ var IntegrationInstancePatchSchema = object({
55913
+ id: IntegrationInstanceIdSchema,
55914
+ name: string().min(1).optional(),
55915
+ /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55916
+ values: record(string(), unknown())
55917
+ });
55918
+ method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55919
+ id: IntegrationInstanceIdSchema,
55920
+ name: string().min(1)
55921
+ }), object({ id: string() }), { kind: "mutation" }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), { kind: "mutation" }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), { kind: "mutation" }), optionalMethod(object({
55922
+ id: IntegrationInstanceIdSchema.nullable(),
55923
+ values: record(string(), unknown())
55924
+ }), TestConnectionResultSchema$1, { kind: "mutation" });
55925
+ /**
55819
55926
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55820
55927
  * session (server-initiated offer pattern, identical to the main
55821
55928
  * video WebRTC flow) and the browser captures the mic; the server
@@ -85272,6 +85379,62 @@ Object.freeze({
85272
85379
  access: "create",
85273
85380
  role: "admin"
85274
85381
  },
85382
+ "integrationInstances.createInstance": {
85383
+ capName: "integration-instances",
85384
+ capScope: "system",
85385
+ addonId: null,
85386
+ access: "create",
85387
+ role: "admin"
85388
+ },
85389
+ "integrationInstances.deleteInstance": {
85390
+ capName: "integration-instances",
85391
+ capScope: "system",
85392
+ addonId: null,
85393
+ access: "delete",
85394
+ role: "admin"
85395
+ },
85396
+ "integrationInstances.describe": {
85397
+ capName: "integration-instances",
85398
+ capScope: "system",
85399
+ addonId: null,
85400
+ access: "view",
85401
+ role: "admin"
85402
+ },
85403
+ "integrationInstances.getInstance": {
85404
+ capName: "integration-instances",
85405
+ capScope: "system",
85406
+ addonId: null,
85407
+ access: "view",
85408
+ role: "admin"
85409
+ },
85410
+ "integrationInstances.instanceFormSchema": {
85411
+ capName: "integration-instances",
85412
+ capScope: "system",
85413
+ addonId: null,
85414
+ access: "view",
85415
+ role: "admin"
85416
+ },
85417
+ "integrationInstances.listInstances": {
85418
+ capName: "integration-instances",
85419
+ capScope: "system",
85420
+ addonId: null,
85421
+ access: "view",
85422
+ role: "admin"
85423
+ },
85424
+ "integrationInstances.testInstance": {
85425
+ capName: "integration-instances",
85426
+ capScope: "system",
85427
+ addonId: null,
85428
+ access: "create",
85429
+ role: "admin"
85430
+ },
85431
+ "integrationInstances.updateInstance": {
85432
+ capName: "integration-instances",
85433
+ capScope: "system",
85434
+ addonId: null,
85435
+ access: "create",
85436
+ role: "admin"
85437
+ },
85275
85438
  "integrations.create": {
85276
85439
  capName: "integrations",
85277
85440
  capScope: "system",
@@ -5971,7 +5971,7 @@ var ZodIssueCode = {
5971
5971
  var ZodFirstPartyTypeKind;
5972
5972
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5973
5973
  //#endregion
5974
- //#region ../types/dist/sleep-Bei295Bz.mjs
5974
+ //#region ../types/dist/sleep-CdJHPm-k.mjs
5975
5975
  /**
5976
5976
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5977
5977
  * window to float samples (D455).
@@ -6088,7 +6088,7 @@ function isKeychainStoredField(field) {
6088
6088
  var SECRET_CLEAR = { $secretClear: true };
6089
6089
  /** The `'***'` token D634 redaction writes for non-admins. */
6090
6090
  var LEGACY_DEVICE_REDACTION = "***";
6091
- function isRecord$6(value) {
6091
+ function isRecord$7(value) {
6092
6092
  return typeof value === "object" && value !== null && !Array.isArray(value);
6093
6093
  }
6094
6094
  function makeSecretMarker(ref) {
@@ -6099,12 +6099,12 @@ function makeSecretMarker(ref) {
6099
6099
  }
6100
6100
  /** A stored marker: an object whose `$secret` is a non-empty string. */
6101
6101
  function isSecretMarker(value) {
6102
- if (!isRecord$6(value)) return false;
6102
+ if (!isRecord$7(value)) return false;
6103
6103
  const ref = value["$secret"];
6104
6104
  return typeof ref === "string" && ref.length > 0;
6105
6105
  }
6106
6106
  function isSecretClear(value) {
6107
- return isRecord$6(value) && value["$secretClear"] === true;
6107
+ return isRecord$7(value) && value["$secretClear"] === true;
6108
6108
  }
6109
6109
  /**
6110
6110
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6857,7 +6857,7 @@ var KeychainWriteRefusedError = class extends Error {
6857
6857
  }
6858
6858
  };
6859
6859
  var KEEP = { kind: "keep" };
6860
- function isRecord$5(value) {
6860
+ function isRecord$6(value) {
6861
6861
  return typeof value === "object" && value !== null && !Array.isArray(value);
6862
6862
  }
6863
6863
  function defaultMintId() {
@@ -6870,7 +6870,7 @@ function collectSecretMarkerRefs(value, out = []) {
6870
6870
  return out;
6871
6871
  }
6872
6872
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6873
- else if (isRecord$5(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6873
+ else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6874
6874
  return out;
6875
6875
  }
6876
6876
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6925,7 +6925,7 @@ function rowId(row, idKey) {
6925
6925
  return null;
6926
6926
  }
6927
6927
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6928
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord$5) : [];
6928
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
6929
6929
  if (!Array.isArray(incoming)) {
6930
6930
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6931
6931
  return {
@@ -6941,7 +6941,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6941
6941
  const kept = /* @__PURE__ */ new Set();
6942
6942
  const rows = [];
6943
6943
  for (const row of incoming) {
6944
- if (!isRecord$5(row)) {
6944
+ if (!isRecord$6(row)) {
6945
6945
  rows.push(row);
6946
6946
  continue;
6947
6947
  }
@@ -6966,7 +6966,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6966
6966
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6967
6967
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6968
6968
  case "object": {
6969
- if (!isRecord$5(incoming)) {
6969
+ if (!isRecord$6(incoming)) {
6970
6970
  if (incoming === void 0) return KEEP;
6971
6971
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6972
6972
  return {
@@ -6974,7 +6974,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6974
6974
  value: incoming
6975
6975
  };
6976
6976
  }
6977
- const storedObj = isRecord$5(stored) ? stored : {};
6977
+ const storedObj = isRecord$6(stored) ? stored : {};
6978
6978
  return {
6979
6979
  kind: "value",
6980
6980
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -7097,7 +7097,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
7097
7097
  value: items
7098
7098
  };
7099
7099
  }
7100
- if (isRecord$5(v)) {
7100
+ if (isRecord$6(v)) {
7101
7101
  const out = {};
7102
7102
  for (const [k, child] of Object.entries(v)) {
7103
7103
  const s = substitute(child);
@@ -7129,7 +7129,7 @@ var SecretOverwriteRefusedError = class extends Error {
7129
7129
  this.name = "SecretOverwriteRefusedError";
7130
7130
  }
7131
7131
  };
7132
- function isRecord$4(value) {
7132
+ function isRecord$5(value) {
7133
7133
  return typeof value === "object" && value !== null && !Array.isArray(value);
7134
7134
  }
7135
7135
  function resolvedValue(pass, marker) {
@@ -7158,7 +7158,7 @@ function inArray(pass, incoming, stored) {
7158
7158
  return incoming.map((item, index) => {
7159
7159
  const twin = stored[index];
7160
7160
  if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
7161
- if (isRecord$4(item) && isRecord$4(twin)) return inArrayRow(pass, item, twin);
7161
+ if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
7162
7162
  if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
7163
7163
  return item;
7164
7164
  });
@@ -7169,7 +7169,7 @@ function inArrayRow(pass, incoming, stored) {
7169
7169
  const twin = stored[key];
7170
7170
  if (isSecretMarker(twin)) {
7171
7171
  if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
7172
- } else if (isRecord$4(value) && isRecord$4(twin)) out[key] = inArrayRow(pass, value, twin);
7172
+ } else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
7173
7173
  else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
7174
7174
  }
7175
7175
  return out;
@@ -7177,7 +7177,7 @@ function inArrayRow(pass, incoming, stored) {
7177
7177
  async function walk(pass, incoming, stored) {
7178
7178
  if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
7179
7179
  if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
7180
- if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
7180
+ if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
7181
7181
  const out = { ...incoming };
7182
7182
  for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
7183
7183
  return out;
@@ -7208,7 +7208,7 @@ async function restoreUnchangedSecrets(input) {
7208
7208
  function isJsonEqual(a, b) {
7209
7209
  if (Object.is(a, b)) return true;
7210
7210
  if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
7211
- if (isRecord$4(a) && isRecord$4(b)) {
7211
+ if (isRecord$5(a) && isRecord$5(b)) {
7212
7212
  const keys = Object.keys(a);
7213
7213
  if (keys.length !== Object.keys(b).length) return false;
7214
7214
  return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
@@ -7232,7 +7232,7 @@ function markerKeysOf(blob, owner) {
7232
7232
  }
7233
7233
  function nodeFromStored(value, owner) {
7234
7234
  if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
7235
- if (!isRecord$4(value)) return null;
7235
+ if (!isRecord$5(value)) return null;
7236
7236
  const children = /* @__PURE__ */ new Map();
7237
7237
  for (const [key, child] of Object.entries(value)) {
7238
7238
  const node = nodeFromStored(child, owner);
@@ -7851,6 +7851,44 @@ var BaseAddon = class {
7851
7851
  return null;
7852
7852
  }
7853
7853
  /**
7854
+ * D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
7855
+ * provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
7856
+ * declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
7857
+ * {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
7858
+ * contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
7859
+ */
7860
+ async clusterSettingsSchema(_cap) {
7861
+ return null;
7862
+ }
7863
+ /**
7864
+ * D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
7865
+ * `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
7866
+ * forgotten node's rows go in one central delete.
7867
+ */
7868
+ async nodeSettingsSchema(_cap) {
7869
+ return null;
7870
+ }
7871
+ /**
7872
+ * D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
7873
+ * or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
7874
+ * as device row, then this default, then the schema default (`resolveDeviceSettings`).
7875
+ */
7876
+ async deviceDefaultsSchema(_cap) {
7877
+ return null;
7878
+ }
7879
+ /**
7880
+ * The host's read of the scoped declarations: the schema one capability declares for `scope`,
7881
+ * or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
7882
+ * host asks this method.
7883
+ */
7884
+ async getScopedSettingsSchema(scope, cap) {
7885
+ switch (scope) {
7886
+ case "cluster": return this.clusterSettingsSchema(cap);
7887
+ case "node": return this.nodeSettingsSchema(cap);
7888
+ case "device-defaults": return this.deviceDefaultsSchema(cap);
7889
+ }
7890
+ }
7891
+ /**
7854
7892
  * What the create wizard and the integration edit page render for one instance: the addon's
7855
7893
  * per-instance schema when it declares one, else its global schema (what they always showed).
7856
7894
  */
@@ -8644,47 +8682,6 @@ function normalizeAddonInitResult(result) {
8644
8682
  if (Array.isArray(result)) return { providers: result };
8645
8683
  return result;
8646
8684
  }
8647
- /**
8648
- * The share-view scope kinds a share-link token can carry. The hub mints
8649
- * and validates them (`ShareViewGrantSchema` builds its
8650
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8651
- */
8652
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8653
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8654
- var PeerBytesTicketSchema = object({
8655
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8656
- url: string().min(1),
8657
- /**
8658
- * The HOST node this URL means something on — the hub or a named agent,
8659
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8660
- * refuses `cross-node` by name when they differ, without dialling.
8661
- */
8662
- hostNodeId: string().min(1),
8663
- expiresAtMs: number().int().nonnegative(),
8664
- /**
8665
- * What the producer DECLARED the body to be, when it knows — `null` when it
8666
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8667
- * must be able to tell "the producer did not say" from "the body is empty".
8668
- */
8669
- declaredBytes: number().int().nonnegative().nullable()
8670
- });
8671
- /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8672
- /** The files a kind may live in. `history` is device-history's (D850), never records. */
8673
- var RECORD_DATABASES = ["settings", "events"];
8674
- /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8675
- var RECORD_CLASSES = [
8676
- "config",
8677
- "ledger",
8678
- "audit",
8679
- "mirror",
8680
- "ephemeral"
8681
- ];
8682
- var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8683
- var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8684
- var RECORD_BATCH_MAX = 1e3;
8685
- var RECORD_QUERY_MAX = 1e3;
8686
- /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8687
- var RECORD_SETTINGS_MAX_ROWS = 5e4;
8688
8685
  var PREFIX = "records/";
8689
8686
  /** The base of every records failure. `detail` is the human part of the message. */
8690
8687
  var RecordsError = class extends Error {
@@ -8717,6 +8714,83 @@ var DeclarationRefusedError = class extends RecordsError {
8717
8714
  this.name = "DeclarationRefusedError";
8718
8715
  }
8719
8716
  };
8717
+ /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8718
+ /** The files a kind may live in. `history` is device-history's (D850), never records. */
8719
+ var RECORD_DATABASES = ["settings", "events"];
8720
+ /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8721
+ var RECORD_CLASSES = [
8722
+ "config",
8723
+ "ledger",
8724
+ "audit",
8725
+ "mirror",
8726
+ "ephemeral"
8727
+ ];
8728
+ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8729
+ var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8730
+ var RECORD_BATCH_MAX = 1e3;
8731
+ var RECORD_QUERY_MAX = 1e3;
8732
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8733
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8734
+ /**
8735
+ * Secret fields of a record, addressed the way a Zod schema declares them
8736
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8737
+ * (`auth.accessToken`, `headers[].value`).
8738
+ *
8739
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8740
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8741
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8742
+ * leaf names its element: `headers[X-Api].value`.
8743
+ *
8744
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8745
+ */
8746
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8747
+ function parseSecretPath(path) {
8748
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8749
+ return path.split(".").map((raw) => {
8750
+ const each = raw.endsWith("[]");
8751
+ const key = each ? raw.slice(0, -2) : raw;
8752
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8753
+ return {
8754
+ key,
8755
+ each
8756
+ };
8757
+ });
8758
+ }
8759
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8760
+ function secretArrayPrefixes(path) {
8761
+ const segments = parseSecretPath(path);
8762
+ const prefixes = [];
8763
+ const walked = [];
8764
+ for (const segment of segments) {
8765
+ walked.push(segment.key);
8766
+ if (segment.each) prefixes.push(walked.join("."));
8767
+ }
8768
+ return prefixes;
8769
+ }
8770
+ /**
8771
+ * The share-view scope kinds a share-link token can carry. The hub mints
8772
+ * and validates them (`ShareViewGrantSchema` builds its
8773
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8774
+ */
8775
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8776
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8777
+ var PeerBytesTicketSchema = object({
8778
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8779
+ url: string().min(1),
8780
+ /**
8781
+ * The HOST node this URL means something on — the hub or a named agent,
8782
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8783
+ * refuses `cross-node` by name when they differ, without dialling.
8784
+ */
8785
+ hostNodeId: string().min(1),
8786
+ expiresAtMs: number().int().nonnegative(),
8787
+ /**
8788
+ * What the producer DECLARED the body to be, when it knows — `null` when it
8789
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8790
+ * must be able to tell "the producer did not say" from "the body is empty".
8791
+ */
8792
+ declaredBytes: number().int().nonnegative().nullable()
8793
+ });
8720
8794
  function defOf(schema) {
8721
8795
  const zodBag = Reflect.get(schema, "_zod");
8722
8796
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -8785,42 +8859,6 @@ function collectZodSecretPaths(schema, prefix = "") {
8785
8859
  return [...new Set(paths)];
8786
8860
  }
8787
8861
  /**
8788
- * Secret fields of a record, addressed the way a Zod schema declares them
8789
- * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8790
- * (`auth.accessToken`, `headers[].value`).
8791
- *
8792
- * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8793
- * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8794
- * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8795
- * leaf names its element: `headers[X-Api].value`.
8796
- *
8797
- * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8798
- */
8799
- /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8800
- function parseSecretPath(path) {
8801
- if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8802
- return path.split(".").map((raw) => {
8803
- const each = raw.endsWith("[]");
8804
- const key = each ? raw.slice(0, -2) : raw;
8805
- if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8806
- return {
8807
- key,
8808
- each
8809
- };
8810
- });
8811
- }
8812
- /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8813
- function secretArrayPrefixes(path) {
8814
- const segments = parseSecretPath(path);
8815
- const prefixes = [];
8816
- const walked = [];
8817
- for (const segment of segments) {
8818
- walked.push(segment.key);
8819
- if (segment.each) prefixes.push(walked.join("."));
8820
- }
8821
- return prefixes;
8822
- }
8823
- /**
8824
8862
  * Record kinds (D881): the declaration an addon makes of one kind of fact, the
8825
8863
  * Zod row schema that describes it, and the helper that turns the two into the
8826
8864
  * JSON the engine stores.
@@ -9785,6 +9823,7 @@ var CAP_NAMES = Object.freeze([
9785
9823
  "humidity-sensor",
9786
9824
  "image",
9787
9825
  "image-settings",
9826
+ "integration-instances",
9788
9827
  "integrations",
9789
9828
  "intercom",
9790
9829
  "ir-transmitter",
@@ -11718,7 +11757,7 @@ object({
11718
11757
  params: record(string(), union([string(), number()])).optional()
11719
11758
  });
11720
11759
  //#endregion
11721
- //#region ../types/dist/composition-kKKOdFIG.mjs
11760
+ //#region ../types/dist/composition-BDUoQsw9.mjs
11722
11761
  /**
11723
11762
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11724
11763
  *
@@ -21355,6 +21394,7 @@ var SYSTEM_CAP_VOCABULARY = {
21355
21394
  "load-contribution": system(INTERNAL),
21356
21395
  "local-network": system(NETWORK),
21357
21396
  "log-channels": system(INTERNAL),
21397
+ "integration-instances": system(INTERNAL),
21358
21398
  "log-destination": system(INTERNAL),
21359
21399
  "login-method": system(IDENTITY),
21360
21400
  "mesh-network": system(NETWORK),
@@ -43655,7 +43695,7 @@ var TargetKindCapsSchema = object({
43655
43695
  * the union is large and not meant for runtime validation here; the exported
43656
43696
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
43657
43697
  */
43658
- var ConfigSchemaPassthrough$3 = unknown();
43698
+ var ConfigSchemaPassthrough$4 = unknown();
43659
43699
  var TargetKindSchema = object({
43660
43700
  kind: string(),
43661
43701
  label: string(),
@@ -43692,7 +43732,7 @@ var TargetKindSchema = object({
43692
43732
  * its raster path, which is the safe default for an unknown image.
43693
43733
  */
43694
43734
  iconMediaType: string().optional(),
43695
- configSchema: ConfigSchemaPassthrough$3,
43735
+ configSchema: ConfigSchemaPassthrough$4,
43696
43736
  /**
43697
43737
  * The config keys that decide WHERE this kind's secrets are sent (server,
43698
43738
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -55700,9 +55740,15 @@ var AvailableIntegrationTypeSchema = object({
55700
55740
  * branch on CAP — never on addon name. `device-adoption` integrations
55701
55741
  * (Home Assistant, …) route through the broker step (Approach A);
55702
55742
  * `device-provider` integrations (Reolink/ONVIF) keep the
55703
- * legacy config → discovery flow.
55743
+ * legacy config → discovery flow. `connection` is an addon that only holds a
55744
+ * connection as typed records (`integration-instances`, D985): no devices,
55745
+ * no discovery.
55704
55746
  */
55705
- kind: _enum(["device-adoption", "device-provider"]),
55747
+ kind: _enum([
55748
+ "device-adoption",
55749
+ "device-provider",
55750
+ "connection"
55751
+ ]),
55706
55752
  /**
55707
55753
  * For `device-adoption` addons: the broker kind to create/link
55708
55754
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55815,6 +55861,67 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55815
55861
  auth: "admin"
55816
55862
  });
55817
55863
  /**
55864
+ * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
55865
+ * (an account, a relay, a destination) as typed records, shown on the
55866
+ * Integrations page like every other integration (D985, D986).
55867
+ *
55868
+ * A collection of providers: each addon that holds connections registers one
55869
+ * and the hub aggregator (`integrations.*`) reaches a provider with the pinned
55870
+ * `addonId`. It is internal and has no route: the operator-facing surface is
55871
+ * the `integrations` cap, which keeps ONE list.
55872
+ *
55873
+ * Identity: the instance id IS the `integrations` registry id (`int_NNNN`,
55874
+ * immutable, already in the `/integrations/<id>` URL). A second operator-chosen
55875
+ * slug would be a second authority over one identity (D224). `name` is the
55876
+ * editable label.
55877
+ *
55878
+ * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55879
+ * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
55880
+ * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
55881
+ * clears it.
55882
+ */
55883
+ var ConfigSchemaPassthrough$3 = custom();
55884
+ var IntegrationInstanceIdSchema = string().min(1);
55885
+ var IntegrationInstanceDescriptionSchema = object({
55886
+ /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55887
+ instanceMode: _enum(["single", "multiple"]),
55888
+ /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55889
+ kind: string(),
55890
+ /** True when `testInstance` really probes the remote. */
55891
+ canTest: boolean()
55892
+ });
55893
+ var IntegrationInstanceStatusSchema = object({
55894
+ state: _enum([
55895
+ "ok",
55896
+ "off",
55897
+ "error",
55898
+ "unknown"
55899
+ ]),
55900
+ detail: string().optional()
55901
+ });
55902
+ var IntegrationInstanceSchema = object({
55903
+ id: IntegrationInstanceIdSchema,
55904
+ name: string(),
55905
+ /** The addon's own rule (an URL is present, a client id is present...). */
55906
+ configured: boolean(),
55907
+ /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55908
+ formValues: record(string(), unknown()),
55909
+ status: IntegrationInstanceStatusSchema.optional()
55910
+ });
55911
+ var IntegrationInstancePatchSchema = object({
55912
+ id: IntegrationInstanceIdSchema,
55913
+ name: string().min(1).optional(),
55914
+ /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55915
+ values: record(string(), unknown())
55916
+ });
55917
+ method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55918
+ id: IntegrationInstanceIdSchema,
55919
+ name: string().min(1)
55920
+ }), object({ id: string() }), { kind: "mutation" }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), { kind: "mutation" }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), { kind: "mutation" }), optionalMethod(object({
55921
+ id: IntegrationInstanceIdSchema.nullable(),
55922
+ values: record(string(), unknown())
55923
+ }), TestConnectionResultSchema$1, { kind: "mutation" });
55924
+ /**
55818
55925
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55819
55926
  * session (server-initiated offer pattern, identical to the main
55820
55927
  * video WebRTC flow) and the browser captures the mic; the server
@@ -85271,6 +85378,62 @@ Object.freeze({
85271
85378
  access: "create",
85272
85379
  role: "admin"
85273
85380
  },
85381
+ "integrationInstances.createInstance": {
85382
+ capName: "integration-instances",
85383
+ capScope: "system",
85384
+ addonId: null,
85385
+ access: "create",
85386
+ role: "admin"
85387
+ },
85388
+ "integrationInstances.deleteInstance": {
85389
+ capName: "integration-instances",
85390
+ capScope: "system",
85391
+ addonId: null,
85392
+ access: "delete",
85393
+ role: "admin"
85394
+ },
85395
+ "integrationInstances.describe": {
85396
+ capName: "integration-instances",
85397
+ capScope: "system",
85398
+ addonId: null,
85399
+ access: "view",
85400
+ role: "admin"
85401
+ },
85402
+ "integrationInstances.getInstance": {
85403
+ capName: "integration-instances",
85404
+ capScope: "system",
85405
+ addonId: null,
85406
+ access: "view",
85407
+ role: "admin"
85408
+ },
85409
+ "integrationInstances.instanceFormSchema": {
85410
+ capName: "integration-instances",
85411
+ capScope: "system",
85412
+ addonId: null,
85413
+ access: "view",
85414
+ role: "admin"
85415
+ },
85416
+ "integrationInstances.listInstances": {
85417
+ capName: "integration-instances",
85418
+ capScope: "system",
85419
+ addonId: null,
85420
+ access: "view",
85421
+ role: "admin"
85422
+ },
85423
+ "integrationInstances.testInstance": {
85424
+ capName: "integration-instances",
85425
+ capScope: "system",
85426
+ addonId: null,
85427
+ access: "create",
85428
+ role: "admin"
85429
+ },
85430
+ "integrationInstances.updateInstance": {
85431
+ capName: "integration-instances",
85432
+ capScope: "system",
85433
+ addonId: null,
85434
+ access: "create",
85435
+ role: "admin"
85436
+ },
85274
85437
  "integrations.create": {
85275
85438
  capName: "integrations",
85276
85439
  capScope: "system",
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_ha_broker_source = require("../ha-broker-source-Ba20mhmb.js");
27
+ const require_ha_broker_source = require("../ha-broker-source-CE3nHkyW.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -4950,18 +4950,29 @@ async function call(slice, capName, make) {
4950
4950
  *
4951
4951
  * Idempotent: a store without the key yields an empty plan, which writes nothing.
4952
4952
  */
4953
- /** The settings scope of this addon (`<addonId>:addon-settings`). */
4954
- var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4953
+ /** The addon id, the prefix of its rows in the canonical settings table. */
4954
+ var HA_EXPORT_ADDON_ID = "homeassistant-export";
4955
+ /**
4956
+ * Where the addon's global settings live: the canonical `addon-settings` table, one row per
4957
+ * key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
4958
+ * which exists only when something lazily declared it).
4959
+ */
4960
+ var ADDON_SETTINGS_TABLE = "addon-settings";
4955
4961
  var RETIRED_URL_KEYS = ["publicBaseUrl"];
4956
4962
  async function planDropPublicBaseUrl(ports) {
4957
4963
  const stored = await ports.readStored();
4958
4964
  const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4959
4965
  if (present.length === 0) return { edits: [] };
4966
+ const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
4960
4967
  return { edits: [{
4961
4968
  op: "deleteKeys",
4962
- scope: HA_EXPORT_SETTINGS_SCOPE,
4963
- keys: [...present],
4964
- precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4969
+ scope: ADDON_SETTINGS_TABLE,
4970
+ keys: present.map(rowId),
4971
+ precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
4972
+ addonId: HA_EXPORT_ADDON_ID,
4973
+ key,
4974
+ value: stored[key]
4975
+ }])))
4965
4976
  }] };
4966
4977
  }
4967
4978
  //#endregion
@@ -1,4 +1,4 @@
1
- import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-iLA_DsW8.mjs";
1
+ import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-D3FBA6uG.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -4915,18 +4915,29 @@ async function call(slice, capName, make) {
4915
4915
  *
4916
4916
  * Idempotent: a store without the key yields an empty plan, which writes nothing.
4917
4917
  */
4918
- /** The settings scope of this addon (`<addonId>:addon-settings`). */
4919
- var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4918
+ /** The addon id, the prefix of its rows in the canonical settings table. */
4919
+ var HA_EXPORT_ADDON_ID = "homeassistant-export";
4920
+ /**
4921
+ * Where the addon's global settings live: the canonical `addon-settings` table, one row per
4922
+ * key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
4923
+ * which exists only when something lazily declared it).
4924
+ */
4925
+ var ADDON_SETTINGS_TABLE = "addon-settings";
4920
4926
  var RETIRED_URL_KEYS = ["publicBaseUrl"];
4921
4927
  async function planDropPublicBaseUrl(ports) {
4922
4928
  const stored = await ports.readStored();
4923
4929
  const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4924
4930
  if (present.length === 0) return { edits: [] };
4931
+ const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
4925
4932
  return { edits: [{
4926
4933
  op: "deleteKeys",
4927
- scope: HA_EXPORT_SETTINGS_SCOPE,
4928
- keys: [...present],
4929
- precondition: preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4934
+ scope: ADDON_SETTINGS_TABLE,
4935
+ keys: present.map(rowId),
4936
+ precondition: preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
4937
+ addonId: HA_EXPORT_ADDON_ID,
4938
+ key,
4939
+ value: stored[key]
4940
+ }])))
4930
4941
  }] };
4931
4942
  }
4932
4943
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.648",
3
+ "version": "1.2.649",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -127,7 +127,7 @@
127
127
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
128
128
  },
129
129
  "requires": {
130
- "server": ">=1.2.733"
130
+ "server": ">=1.2.738"
131
131
  }
132
132
  },
133
133
  "files": [