@camstack/addon-provider-bosch 0.1.55 → 0.1.56

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 { $ as union, B as entryBody, F as RecordIntegrationInstances, G as _null, J as literal, K as array, L as customAction, P as waterHeaterCapability, Q as string, R as defineCustomActions, U as rowsDepsOver, W as _enum, X as object, Y as number, Z as record, a as BaseDevice, b as heatPumpControlCapability, f as climateControlCapability, h as deviceAdoptionCapability, i as testBoschInstance, it as EventCategory, k as registerPowerSwitch, l as SessionHealthTracker, n as boschRowsPorts, nt as DeviceType, o as BaseDeviceProvider, q as boolean, r as keepBodyKeys, rt as errMsg, t as boschRowsMigration, tt as DeviceRole, w as integrationInstancesCapability, x as heatPumpStatusCapability, y as gatewayDeviceName, z as defineEntryKind } from "../bosch-instances-g54r3Dus.mjs";
2
- import { a as loginFailureForAttempt, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, s as HOMECOM_REDIRECT_URI, t as setIntegrationDevicesOnline } from "../account-integration-offline-xi6muNeM.mjs";
1
+ import { $ as literal, A as registerPowerSwitch, B as defineCustomActions, F as waterHeaterCapability, H as entryBody, I as RecordIntegrationInstances, J as _enum, K as rowsDepsOver, L as SECRET_CLEAR, Q as custom, T as integrationPageHref, U as isSecretFieldView, V as defineEntryKind, X as array, Y as _null, Z as boolean, a as BaseDevice, b as heatPumpControlCapability, ct as errMsg, et as number, f as climateControlCapability, h as deviceAdoptionCapability, i as testBoschInstance, it as union, l as SessionHealthTracker, lt as EventCategory, n as boschRowsPorts, nt as record, o as BaseDeviceProvider, ot as DeviceRole, q as toSecretFieldView, r as keepBodyKeys, rt as string, st as DeviceType, t as boschRowsMigration, tt as object, w as integrationInstancesCapability, x as heatPumpStatusCapability, y as gatewayDeviceName, z as customAction } from "../bosch-instances-REcWLM14.mjs";
2
+ import { a as loginFailureForAttempt, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, s as HOMECOM_REDIRECT_URI, t as setIntegrationDevicesOnline } from "../account-integration-offline-fB8V7XNu.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync } from "node:fs";
@@ -457,6 +457,17 @@ async function exploreHomeComResources(input) {
457
457
  }
458
458
  //#endregion
459
459
  //#region src/homecom/custom-actions.ts
460
+ /** A stored secret as the form sees it (`SecretFieldView`): set or not, never the value. */
461
+ var secretView = custom(isSecretFieldView, "a stored secret view");
462
+ /**
463
+ * A sign-in secret in the result: in clear only for a form that has no record yet (the wizard
464
+ * stores it on create); for an existing instance it is already stored and comes back as a view.
465
+ */
466
+ var signInSecret = union([
467
+ string().min(1),
468
+ secretView,
469
+ _null()
470
+ ]).meta({ secret: true });
460
471
  var gatewayChoice = object({
461
472
  value: string(),
462
473
  label: string(),
@@ -495,11 +506,17 @@ var homeComActions = defineCustomActions({
495
506
  }),
496
507
  finishSignIn: customAction(object({
497
508
  attemptId: string().min(1),
498
- callback: string().min(1)
509
+ callback: string().min(1),
510
+ /** An existing instance: the new sign-in is stored on it and applied at once. */
511
+ integrationId: string().min(1).optional()
499
512
  }), object({
500
- token: string().min(1).meta({ secret: true }),
501
- refresh: string().min(1).meta({ secret: true }),
502
- username: string().meta({ secret: true }),
513
+ token: signInSecret,
514
+ refresh: signInSecret,
515
+ username: union([
516
+ string(),
517
+ secretView,
518
+ _null()
519
+ ]).meta({ secret: true }),
503
520
  accountLabel: string(),
504
521
  gatewayCount: number(),
505
522
  summary: string(),
@@ -511,13 +528,18 @@ var homeComActions = defineCustomActions({
511
528
  kind: "mutation",
512
529
  auth: "admin"
513
530
  }),
514
- signOut: customAction(object({}), object({
515
- signedOut: literal(true),
516
- summary: string()
517
- }), {
518
- kind: "mutation",
519
- auth: "admin"
520
- }),
531
+ signOut: customAction(
532
+ /** An existing instance: its stored sign-in is cleared and its connection stopped. */
533
+ object({ integrationId: string().min(1).optional() }),
534
+ object({
535
+ signedOut: literal(true),
536
+ summary: string()
537
+ }),
538
+ {
539
+ kind: "mutation",
540
+ auth: "admin"
541
+ }
542
+ ),
521
543
  /** Read-only walk of the gateway's resource tree, for diagnosing what it serves. */
522
544
  exploreResources: customAction(exploreResourcesInput, exploreResourcesOutput, {
523
545
  kind: "query",
@@ -525,6 +547,104 @@ var homeComActions = defineCustomActions({
525
547
  })
526
548
  });
527
549
  //#endregion
550
+ //#region src/homecom/homecom-account.ts
551
+ /**
552
+ * Sign in / sign out of an EXISTING HomeCom instance, written through the instance record — the
553
+ * owner of the tokens (keychain-backed secret fields). The record's change hook reconciles the
554
+ * integration: new tokens are applied to the running manager, cleared ones stop it.
555
+ */
556
+ /** Forget the account: the three secrets are cleared in the keychain, the account labels reset. */
557
+ async function signOutHomeComInstance(integrationId, instances) {
558
+ await instances.updateInstance({
559
+ id: integrationId,
560
+ values: {
561
+ token: SECRET_CLEAR,
562
+ refresh: SECRET_CLEAR,
563
+ username: SECRET_CLEAR,
564
+ accountLabel: "",
565
+ gatewayCount: 0
566
+ }
567
+ });
568
+ }
569
+ /**
570
+ * Store a finished sign-in on the instance. The gateway (and its brand / region) is written only
571
+ * when the sign-in settled on one: a choice still open keeps the stored gateway.
572
+ */
573
+ async function storeHomeComSignIn(integrationId, finished, instances) {
574
+ await instances.updateInstance({
575
+ id: integrationId,
576
+ values: {
577
+ token: finished.token,
578
+ refresh: finished.refresh,
579
+ username: finished.username,
580
+ accountLabel: finished.accountLabel,
581
+ gatewayCount: finished.gatewayCount,
582
+ ...finished.gatewayId !== void 0 ? { gatewayId: finished.gatewayId } : {},
583
+ ...finished.brand !== void 0 ? { brand: finished.brand } : {},
584
+ ...finished.region !== void 0 ? { region: finished.region } : {}
585
+ }
586
+ });
587
+ }
588
+ /**
589
+ * The answer to a sign-in on an existing instance: the tokens are on the record now, so the form
590
+ * gets the record's "set" views (as `hydrateSchema` hands them out) in their place — a Save that
591
+ * follows keeps them, and the values never travel to the browser.
592
+ */
593
+ async function storedSignInResult(integrationId, finished, handle) {
594
+ const data = (await handle.get(integrationId))?.data ?? {};
595
+ const { token: _token, refresh: _refresh, username: _username, ...open } = finished;
596
+ return {
597
+ ...open,
598
+ token: toSecretFieldView(data["token"]),
599
+ refresh: toSecretFieldView(data["refresh"]),
600
+ username: toSecretFieldView(data["username"])
601
+ };
602
+ }
603
+ //#endregion
604
+ //#region src/homecom/homecom-reconcile.ts
605
+ /**
606
+ * Bring the running HomeCom managers in line with the enabled integrations:
607
+ *
608
+ * - signed in -> upsert (a changed connection is applied to the running manager);
609
+ * - signed out -> the manager stops and the devices go offline; the integration still
610
+ * exists, so its devices and its session alert are KEPT;
611
+ * - unreadable -> left as it is (a locked keychain is not a sign out, nor a deletion);
612
+ * - gone / disabled -> the manager stops and the caller drops what belonged to it.
613
+ *
614
+ * "Gone" is judged over EVERY id the addon still holds something for — a running manager, an
615
+ * instance record, a device — not only the running managers: an integration that was signed out
616
+ * or unreadable has no manager, and its devices and alert must still go when it is deleted.
617
+ */
618
+ async function reconcileHomeComIntegrations(deps) {
619
+ const enabled = await deps.listEnabled();
620
+ const present = new Set(enabled.map((integration) => integration.id));
621
+ for (const integration of enabled) await reconcileOne(deps, integration);
622
+ const vanished = [...new Set([...deps.registry.list(), ...await deps.knownIds()])].filter((id) => !present.has(id));
623
+ await deps.registry.retain(present);
624
+ for (const integrationId of vanished) await deps.onVanished(integrationId);
625
+ }
626
+ async function reconcileOne(deps, integration) {
627
+ const tags = { integrationId: integration.id };
628
+ try {
629
+ const connection = await deps.connectionOf(integration.id);
630
+ deps.onNamed(integration.id, integration.name);
631
+ if (connection === null) {
632
+ if (deps.registry.has(integration.id)) {
633
+ await deps.registry.remove(integration.id);
634
+ deps.logger.info("HomeCom integration has no sign-in — stopped, devices offline", { tags });
635
+ }
636
+ deps.onSignedOut(integration.id);
637
+ return;
638
+ }
639
+ await deps.registry.upsert(integration.id, integration.name, connection);
640
+ } catch (err) {
641
+ deps.logger.warn("HomeCom reconcile failed for integration", {
642
+ tags,
643
+ meta: { error: errMsg(err) }
644
+ });
645
+ }
646
+ }
647
+ //#endregion
528
648
  //#region src/homecom/homecom-session-alert.ts
529
649
  /** The standard session-lost alert for HomeCom; "Sign in again" opens the integration page. */
530
650
  function createHomeComSessionAlert(deps) {
@@ -533,7 +653,9 @@ function createHomeComSessionAlert(deps) {
533
653
  providerName: "Bosch HomeCom",
534
654
  logger: deps.logger,
535
655
  getAlerts: deps.getAlerts,
536
- debounceMs: deps.debounceMs ?? 0
656
+ debounceMs: deps.debounceMs ?? 0,
657
+ addonId: deps.addonId,
658
+ severity: "critical"
537
659
  });
538
660
  return {
539
661
  authFailed: (integrationId) => tracker.lost(integrationId, {
@@ -541,7 +663,7 @@ function createHomeComSessionAlert(deps) {
541
663
  reason: "token-expired",
542
664
  action: {
543
665
  label: "Sign in again",
544
- href: `/integrations/${encodeURIComponent(integrationId)}`,
666
+ href: integrationPageHref(deps.addonId, integrationId),
545
667
  navigation: "app"
546
668
  }
547
669
  }),
@@ -3737,7 +3859,8 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
3737
3859
  const sessionAlert = createHomeComSessionAlert({
3738
3860
  logger: this.ctx.logger,
3739
3861
  getAlerts: () => this.ctx.api.alerts,
3740
- nameOf: (id) => this.integrationNames.get(id) ?? id
3862
+ nameOf: (id) => this.integrationNames.get(id) ?? id,
3863
+ addonId: this.addonId
3741
3864
  });
3742
3865
  this.sessionAlert = sessionAlert;
3743
3866
  this.clients = new HomeComClientRegistry(this.ctx.logger, {
@@ -3778,15 +3901,30 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
3778
3901
  ...publishHomeComSignIn(homeComSignInAttempts, Date.now()),
3779
3902
  summary: "Open the sign-in page, sign in, then paste the address the browser stops on."
3780
3903
  }),
3781
- finishSignIn: async (input) => finishHomeComSignIn({
3782
- attemptId: input.attemptId,
3783
- callback: input.callback,
3784
- nowMs: Date.now()
3785
- }, homeComSignInAttempts),
3786
- signOut: async () => ({
3787
- signedOut: true,
3788
- summary: "Signed out."
3789
- }),
3904
+ finishSignIn: async (input) => {
3905
+ const finished = await finishHomeComSignIn({
3906
+ attemptId: input.attemptId,
3907
+ callback: input.callback,
3908
+ nowMs: Date.now()
3909
+ }, homeComSignInAttempts);
3910
+ const integrationId = input.integrationId;
3911
+ if (integrationId === void 0) return finished;
3912
+ await this.storeSignIn(integrationId, () => storeHomeComSignIn(integrationId, finished, built.instances));
3913
+ return storedSignInResult(integrationId, finished, built.handle);
3914
+ },
3915
+ signOut: async (input) => {
3916
+ const integrationId = input.integrationId;
3917
+ if (integrationId === void 0) return {
3918
+ signedOut: true,
3919
+ summary: "Signed out."
3920
+ };
3921
+ await this.storeSignIn(integrationId, () => signOutHomeComInstance(integrationId, built.instances));
3922
+ this.ctx.logger.info("HomeCom signed out — stored sign-in cleared", { tags: { integrationId } });
3923
+ return {
3924
+ signedOut: true,
3925
+ summary: "Signed out. The devices stay offline until you sign in again."
3926
+ };
3927
+ },
3790
3928
  exploreResources: explore
3791
3929
  }
3792
3930
  };
@@ -3831,43 +3969,60 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
3831
3969
  if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("homecom generateStableId: nativeId required");
3832
3970
  return homeComStableId(integrationId, nativeId);
3833
3971
  }
3972
+ /** Write a sign-in change on an instance that exists; a missing one is refused by name. */
3973
+ async storeSignIn(integrationId, write) {
3974
+ if (await homeComStoredOf(this.requireInstances().instances, integrationId) === null) throw new Error(`HomeCom integration ${integrationId} does not exist`);
3975
+ await write();
3976
+ }
3834
3977
  async reconcileIntegrations() {
3835
3978
  const reg = this.requireClients();
3836
3979
  try {
3837
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
3838
- const surviving = /* @__PURE__ */ new Set();
3839
- for (const integration of mine) try {
3840
- const connection = await homeComConnectionOf(this.requireInstances().instances, integration.id);
3841
- if (!connection) continue;
3842
- this.integrationNames.set(integration.id, integration.name);
3843
- await reg.upsert(integration.id, integration.name, connection);
3844
- surviving.add(integration.id);
3845
- } catch (err) {
3846
- this.ctx.logger.warn("HomeCom reconcile failed for integration", { meta: {
3847
- integrationId: integration.id,
3848
- error: errMsg(err)
3849
- } });
3850
- }
3851
- const vanished = reg.list().filter((id) => !surviving.has(id));
3852
- await reg.retain(surviving);
3853
- for (const integrationId of vanished) {
3854
- this.integrationNames.delete(integrationId);
3855
- this.sessionAlert?.removed(integrationId);
3856
- }
3857
- const deviceReg = this.ctx.kernel.deviceRegistry;
3858
- const deviceApi = this.ctx.kernel.devices;
3859
- if (deviceReg && deviceApi) for (const integrationId of vanished) await cascadeRemoveDevicesForIntegration({
3860
- reg: deviceReg,
3861
- devices: deviceApi,
3862
- addonId: this.addonId,
3863
- system: "homecom",
3864
- integrationId,
3980
+ await reconcileHomeComIntegrations({
3981
+ listEnabled: async () => {
3982
+ return (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
3983
+ },
3984
+ knownIds: () => this.knownIntegrationIds(),
3985
+ connectionOf: (id) => homeComConnectionOf(this.requireInstances().instances, id),
3986
+ registry: reg,
3987
+ onNamed: (id, name) => this.integrationNames.set(id, name),
3988
+ onSignedOut: (id) => this.setIntegrationDevicesOnline(id, false),
3989
+ onVanished: (id) => this.dropIntegration(id),
3865
3990
  logger: this.ctx.logger
3866
3991
  });
3867
3992
  } catch (err) {
3868
3993
  this.ctx.logger.warn("HomeCom integration reconcile failed", { meta: { error: errMsg(err) } });
3869
3994
  }
3870
3995
  }
3996
+ /** Every integration this addon holds a record or a top-level device for. */
3997
+ async knownIntegrationIds() {
3998
+ const ids = new Set(this.integrationNames.keys());
3999
+ for (const row of await this.requireInstances().instances.listResolved()) ids.add(row.id);
4000
+ const reg = this.ctx.kernel.deviceRegistry;
4001
+ const devices = this.ctx.kernel.devices;
4002
+ if (reg && devices) for (const d of reg.getAllForAddon(this.addonId)) {
4003
+ if (d.parentDeviceId !== null) continue;
4004
+ const cfg = await devices.loadConfig(d.id).catch(() => ({}));
4005
+ const brokerId = cfg["brokerId"];
4006
+ if (cfg["system"] === "homecom" && typeof brokerId === "string") ids.add(brokerId);
4007
+ }
4008
+ return [...ids];
4009
+ }
4010
+ /** The integration is gone: its alert and its devices go with it. */
4011
+ async dropIntegration(integrationId) {
4012
+ this.integrationNames.delete(integrationId);
4013
+ this.sessionAlert?.removed(integrationId);
4014
+ const deviceReg = this.ctx.kernel.deviceRegistry;
4015
+ const deviceApi = this.ctx.kernel.devices;
4016
+ if (!deviceReg || !deviceApi) return;
4017
+ await cascadeRemoveDevicesForIntegration({
4018
+ reg: deviceReg,
4019
+ devices: deviceApi,
4020
+ addonId: this.addonId,
4021
+ system: "homecom",
4022
+ integrationId,
4023
+ logger: this.ctx.logger
4024
+ });
4025
+ }
3871
4026
  subscribeIntegrationLifecycle() {
3872
4027
  const handler = (event) => {
3873
4028
  const addonId = event.data["addonId"];
package/dist/shc/addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region \0rolldown/runtime.js
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  //#endregion
5
- const require_bosch_instances = require("../bosch-instances-DaWS-s0M.js");
5
+ const require_bosch_instances = require("../bosch-instances-BnuV5On6.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_https = require("node:https");
8
8
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { B as entryBody, F as RecordIntegrationInstances, J as literal, K as array, L as customAction, M as switchCapability, N as temperatureSensorCapability, Q as string, R as defineCustomActions, S as heatingValveCapability, U as rowsDepsOver, W as _enum, X as object, Y as number, a as BaseDevice, f as climateControlCapability, h as deviceAdoptionCapability, i as testBoschInstance, it as EventCategory, k as registerPowerSwitch, m as defaultIntegrationName, n as boschRowsPorts, nt as DeviceType, o as BaseDeviceProvider, r as keepBodyKeys, rt as errMsg, t as boschRowsMigration, tt as DeviceRole, u as batteryCapability, w as integrationInstancesCapability, y as gatewayDeviceName, z as defineEntryKind } from "../bosch-instances-g54r3Dus.mjs";
1
+ import { $ as literal, A as registerPowerSwitch, B as defineCustomActions, H as entryBody, I as RecordIntegrationInstances, J as _enum, K as rowsDepsOver, N as switchCapability, P as temperatureSensorCapability, S as heatingValveCapability, V as defineEntryKind, X as array, a as BaseDevice, ct as errMsg, et as number, f as climateControlCapability, h as deviceAdoptionCapability, i as testBoschInstance, lt as EventCategory, m as defaultIntegrationName, n as boschRowsPorts, o as BaseDeviceProvider, ot as DeviceRole, r as keepBodyKeys, rt as string, st as DeviceType, t as boschRowsMigration, tt as object, u as batteryCapability, w as integrationInstancesCapability, y as gatewayDeviceName, z as customAction } from "../bosch-instances-REcWLM14.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { request } from "node:https";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-bosch",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -142,7 +142,7 @@
142
142
  }
143
143
  ],
144
144
  "requires": {
145
- "server": ">=1.2.879"
145
+ "server": ">=1.2.882"
146
146
  }
147
147
  },
148
148
  "files": [