@camstack/addon-provider-bosch 0.1.35 → 0.1.36

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.
@@ -1,5 +1,5 @@
1
- import { A as isSecretMarkerLike, D as customAction, F as number, H as DeviceType, I as object, M as array, N as boolean, O as defineCustomActions, P as literal, R as string, T as waterHeaterCapability, U as errMsg, W as EventCategory, f as deviceAdoptionCapability, j as _enum, k as isSecretMarker, l as climateControlCapability, n as BaseDeviceProvider, o as VendorLoginProxy, t as BaseDevice, u as connectionTestCapability } from "../dist-CkMPcvma.mjs";
2
- import { a as createSignInAttemptStore, i as openSignInProxy, n as cascadeRemoveDevicesForIntegration, o as loginFailureForAttempt, r as createSignInClaims, s as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-Bt3N9Zdk.mjs";
1
+ import { $ as errMsg, F as defineAddonMigrations, G as number, H as array, I as defineCustomActions, J as string, K as object, L as defineEntryKind, M as waterHeaterCapability, P as customAction, Q as DeviceType, R as entryBody, U as boolean, V as _enum, W as literal, _ as defaultIntegrationApiOver, a as BaseDevice, d as VendorLoginProxy, et as EventCategory, h as connectionTestCapability, i as testBoschInstance, l as RecordIntegrationInstances, m as climateControlCapability, n as boschRowsPorts, o as BaseDeviceProvider, r as keepBodyKeys, t as boschRowsMigration, v as deviceAdoptionCapability, w as integrationInstancesCapability } from "../bosch-instances-BIvOtuzj.mjs";
2
+ import { a as createSignInAttemptStore, i as openSignInProxy, n as cascadeRemoveDevicesForIntegration, o as loginFailureForAttempt, r as createSignInClaims, s as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-9ooJuNrN.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  //#region src/homecom/config.ts
5
5
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -41,11 +41,6 @@ var HOMECOM_GATEWAYS_PATH = "/pointt-api/api/v1/gateways/";
41
41
  function homeComApiBase(_region) {
42
42
  return HOMECOM_POINTT_API_ORIGIN;
43
43
  }
44
- var HOMECOM_SECRET_KEYS = [
45
- "username",
46
- "token",
47
- "refresh"
48
- ];
49
44
  var HIDDEN_UNTIL_ADVANCED = {
50
45
  field: "__hidden__",
51
46
  equals: "yes"
@@ -1262,37 +1257,23 @@ function beginHomeComProxiedSignIn(deps) {
1262
1257
  }
1263
1258
  //#endregion
1264
1259
  //#region src/homecom/homecom-token-persist.ts
1265
- function unreadable(value) {
1266
- return isSecretMarker(value) || isSecretMarkerLike(value);
1267
- }
1268
1260
  /**
1269
- * Merge `patch` into the integration's stored settings in ONE `setSettings` write, one at a
1270
- * time per integration. Refuses (and says so, without values) when a secret the write
1271
- * touches cannot be read back from the keychain: the hub refuses an overwrite of a secret the
1272
- * addon could not read, so the write would only fail.
1261
+ * Patch the integration's record, one write at a time per integration. Refuses (and says so,
1262
+ * without values) when the record's secrets cannot be read right now: the write would replace a
1263
+ * secret the addon cannot see.
1273
1264
  */
1274
1265
  function buildSerialPatcher(deps) {
1275
1266
  const tails = /* @__PURE__ */ new Map();
1276
1267
  return (integrationId, patch) => {
1277
1268
  const run = async () => {
1278
1269
  try {
1279
- const stored = await deps.getSettings(integrationId);
1280
- const locked = HOMECOM_SECRET_KEYS.filter((key) => unreadable(stored[key]));
1281
- if (locked.length > 0) {
1282
- deps.logger.warn(`HomeCom not persisting ${deps.what} — stored secrets are locked`, {
1283
- tags: { integrationId },
1284
- meta: { locked }
1285
- });
1270
+ const stored = await deps.read(integrationId);
1271
+ if (stored === null) return;
1272
+ if (stored.locked) {
1273
+ deps.logger.warn(`HomeCom not persisting ${deps.what} — stored secrets are locked`, { tags: { integrationId } });
1286
1274
  return;
1287
1275
  }
1288
- await deps.setSettings({
1289
- id: integrationId,
1290
- settings: {
1291
- ...stored,
1292
- ...patch
1293
- },
1294
- secretKeys: [...HOMECOM_SECRET_KEYS]
1295
- });
1276
+ await deps.patch(integrationId, patch);
1296
1277
  } catch (err) {
1297
1278
  deps.logger.warn(`HomeCom failed to persist ${deps.what}`, {
1298
1279
  tags: { integrationId },
@@ -1876,6 +1857,134 @@ function buildHomeComConnectionTestProvider(deps) {
1876
1857
  };
1877
1858
  }
1878
1859
  //#endregion
1860
+ //#region src/homecom/homecom-instances.ts
1861
+ /**
1862
+ * HomeCom integrations as `integration-instances` (planning#416, D985, D1007): one `connections`
1863
+ * record per account under the `integrations` registry id. The body IS the account (the account
1864
+ * email and the OAuth tokens in the keychain; brand, region, the chosen gateway and the
1865
+ * signed-in label in the open), so the form the instances page edits is the stored shape.
1866
+ */
1867
+ var homeComConnectionsEntryKind = defineEntryKind("connections", {
1868
+ title: "HomeCom accounts",
1869
+ schema: entryBody({
1870
+ username: string().optional().meta({ secret: true }),
1871
+ token: string().optional().meta({ secret: true }),
1872
+ refresh: string().optional().meta({ secret: true }),
1873
+ brand: HomeComBrandSchema.optional(),
1874
+ region: HomeComRegionSchema.optional(),
1875
+ gatewayId: string().optional(),
1876
+ accountLabel: string().optional(),
1877
+ gatewayCount: number().optional()
1878
+ }),
1879
+ writeTrigger: "an operator signs in, edits or removes a HomeCom account, or a connection stores a rotated token or its gateway"
1880
+ });
1881
+ /** Every kind this addon declares, keyed by its name. */
1882
+ var homeComRecordKinds = { connections: homeComConnectionsEntryKind };
1883
+ var LEGACY_KEYS = [
1884
+ "username",
1885
+ "token",
1886
+ "refresh",
1887
+ "brand",
1888
+ "region",
1889
+ "gatewayId",
1890
+ "accountLabel",
1891
+ "gatewayCount"
1892
+ ];
1893
+ var SECRET_KEYS = [
1894
+ "username",
1895
+ "token",
1896
+ "refresh"
1897
+ ];
1898
+ function text(value) {
1899
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1900
+ }
1901
+ /** The record body of one account, from its legacy rows (secrets resolved). */
1902
+ function homeComBodyFromRows(values, log, integration) {
1903
+ const dropped = Object.keys(values).filter((key) => !LEGACY_KEYS.includes(key));
1904
+ if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${dropped.join(", ")}`);
1905
+ const username = text(values["username"]);
1906
+ const token = text(values["token"]);
1907
+ const refresh = text(values["refresh"]);
1908
+ const brand = HomeComBrandSchema.safeParse(values["brand"]);
1909
+ const region = HomeComRegionSchema.safeParse(values["region"]);
1910
+ const gatewayId = text(values["gatewayId"])?.trim();
1911
+ const accountLabel = text(values["accountLabel"]);
1912
+ const count = values["gatewayCount"];
1913
+ return {
1914
+ name: integration.name,
1915
+ ...username !== void 0 ? { username } : {},
1916
+ ...token !== void 0 ? { token } : {},
1917
+ ...refresh !== void 0 ? { refresh } : {},
1918
+ ...brand.success ? { brand: brand.data } : {},
1919
+ ...region.success ? { region: region.data } : {},
1920
+ ...gatewayId !== void 0 && gatewayId.length > 0 ? { gatewayId } : {},
1921
+ ...accountLabel !== void 0 ? { accountLabel } : {},
1922
+ ...typeof count === "number" && Number.isFinite(count) ? { gatewayCount: count } : {}
1923
+ };
1924
+ }
1925
+ /** The whole `bosch-homecom` catalogue (D987): offered to the engine at `onInitialize`. */
1926
+ var HOMECOM_MIGRATIONS = [boschRowsMigration({
1927
+ addonId: "bosch-homecom",
1928
+ id: "bosch-homecom/2026-11-05-integration-rows-to-record",
1929
+ description: "Move every HomeCom account into a typed instance record keyed by its registry id, then clear the legacy integration settings rows and their keychain values",
1930
+ fixtureMark: "a legacy setting key the migration does not know",
1931
+ kind: homeComConnectionsEntryKind,
1932
+ bodyFrom: homeComBodyFromRows
1933
+ })];
1934
+ function homeComRowsPorts(deps) {
1935
+ return boschRowsPorts({
1936
+ ...deps,
1937
+ kind: homeComConnectionsEntryKind
1938
+ });
1939
+ }
1940
+ async function homeComStoredOf(instances, id) {
1941
+ const read = await instances.readResolved(id);
1942
+ if (read === null) return null;
1943
+ return {
1944
+ settings: { ...read.body },
1945
+ locked: read.secrets === "unavailable"
1946
+ };
1947
+ }
1948
+ /**
1949
+ * The live account of one integration, read from its record. null while it is not signed in; a
1950
+ * locked keychain throws so the caller keeps the client it has instead of reconfiguring it
1951
+ * without its tokens (D49).
1952
+ */
1953
+ async function homeComConnectionOf(instances, id) {
1954
+ const stored = await homeComStoredOf(instances, id);
1955
+ if (stored === null) return null;
1956
+ if (stored.locked) throw new Error("the keychain is locked, the saved HomeCom tokens cannot be read");
1957
+ return connectionFromSettings(stored.settings);
1958
+ }
1959
+ function buildHomeComInstances(records, deps) {
1960
+ const handle = records.kind(homeComConnectionsEntryKind);
1961
+ const instances = new RecordIntegrationInstances(records, homeComConnectionsEntryKind, {
1962
+ description: {
1963
+ role: "devices",
1964
+ instanceMode: "multiple",
1965
+ kind: "account",
1966
+ canTest: true
1967
+ },
1968
+ formSchema: () => buildHomeComFormSchema(),
1969
+ initialBody: (name) => ({ name }),
1970
+ configured: (body) => connectionFromSettings(body) !== null,
1971
+ onChanged: async (_id, change) => {
1972
+ if (change !== "deleted") await deps.onChanged();
1973
+ },
1974
+ test: (input) => testBoschInstance({
1975
+ ...input,
1976
+ tester: deps.tester,
1977
+ handle,
1978
+ secretKeys: SECRET_KEYS
1979
+ })
1980
+ });
1981
+ return {
1982
+ instances,
1983
+ handle,
1984
+ provider: keepBodyKeys(instances.toProvider(), new Set(Object.keys(homeComConnectionsEntryKind.schema.shape)))
1985
+ };
1986
+ }
1987
+ //#endregion
1879
1988
  //#region src/homecom/devices/homecom-climate-device.ts
1880
1989
  var homeComClimateSchema = object({
1881
1990
  nativeId: string().min(1),
@@ -2070,6 +2179,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2070
2179
  [DeviceType.WaterHeater]: HomeComWaterHeaterDevice
2071
2180
  };
2072
2181
  clients = null;
2182
+ instances = null;
2073
2183
  loginProxy = null;
2074
2184
  loginPlane = null;
2075
2185
  constructor() {
@@ -2077,10 +2187,23 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2077
2187
  }
2078
2188
  async onInitialize() {
2079
2189
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
2190
+ const records = this.ctx.records;
2191
+ if (records === void 0) throw new Error("bosch-homecom: ctx.records is required (hub-only addon)");
2192
+ await records.declare(homeComRecordKinds);
2193
+ await this.runMigrations(defineAddonMigrations(HOMECOM_MIGRATIONS, homeComRowsPorts({
2194
+ api: defaultIntegrationApiOver(this.ctx.api),
2195
+ records,
2196
+ warn: (message) => this.ctx.logger.warn(message)
2197
+ })));
2198
+ const built = buildHomeComInstances(records, {
2199
+ onChanged: () => this.reconcileIntegrations(),
2200
+ tester: this.buildConnectionTestProvider()
2201
+ });
2202
+ this.instances = built;
2080
2203
  await this.serveLoginProxy();
2081
2204
  const persistDeps = {
2082
- getSettings: (id) => this.ctx.api.integrations.getSettings.query({ id }),
2083
- setSettings: (input) => this.ctx.api.integrations.setSettings.mutate(input),
2205
+ read: (id) => homeComStoredOf(built.instances, id),
2206
+ patch: (id, patch) => built.handle.patch(id, patch),
2084
2207
  logger: this.ctx.logger
2085
2208
  };
2086
2209
  const persistTokens = buildHomeComTokenPersister(persistDeps);
@@ -2107,6 +2230,10 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2107
2230
  {
2108
2231
  capability: connectionTestCapability,
2109
2232
  provider: this.buildConnectionTestProvider()
2233
+ },
2234
+ {
2235
+ capability: integrationInstancesCapability,
2236
+ provider: built.provider
2110
2237
  }
2111
2238
  ],
2112
2239
  customActions: homeComActions,
@@ -2164,6 +2291,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2164
2291
  this.ctx.logger.warn("HomeCom shutdown error", { meta: { error: errMsg(err) } });
2165
2292
  }
2166
2293
  this.clients = null;
2294
+ this.instances = null;
2167
2295
  await super.onShutdown();
2168
2296
  }
2169
2297
  requireClients() {
@@ -2183,7 +2311,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2183
2311
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
2184
2312
  const surviving = /* @__PURE__ */ new Set();
2185
2313
  for (const integration of mine) try {
2186
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
2314
+ const connection = await homeComConnectionOf(this.requireInstances().instances, integration.id);
2187
2315
  if (!connection) continue;
2188
2316
  await reg.upsert(integration.id, integration.name, connection);
2189
2317
  surviving.add(integration.id);
@@ -2272,9 +2400,6 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2272
2400
  logger: this.ctx.logger
2273
2401
  });
2274
2402
  }
2275
- globalSettingsSchema() {
2276
- return buildHomeComFormSchema();
2277
- }
2278
2403
  async supportsManualCreation() {
2279
2404
  return false;
2280
2405
  }
@@ -2284,6 +2409,10 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2284
2409
  async onCreateDevice(_type, _config) {
2285
2410
  throw new Error("HomeCom devices are adopted from the cloud");
2286
2411
  }
2412
+ requireInstances() {
2413
+ if (!this.instances) throw new Error("HomeCom provider not initialised");
2414
+ return this.instances;
2415
+ }
2287
2416
  setIntegrationDevicesOnline(integrationId, online) {
2288
2417
  const reg = this.ctx.kernel.deviceRegistry;
2289
2418
  if (!reg) return;