@camstack/addon-provider-homeassistant 1.2.651 → 1.2.653

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.653] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - Rebuilt against updated dependencies.
15
+
16
+ ## [1.2.652] - 2026-11-02
17
+
18
+ ### Changed
19
+
20
+ - Home Assistant can hand an ESPHome board's encryption key from its ESPHome Builder add-on to CamStack, over the existing access token (administrator only).
21
+ - pump-cycle is classified as not exported to Home Assistant
22
+
10
23
  ## [1.2.651] - 2026-11-02
11
24
 
12
25
  ### 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-D4_TtF2V.js");
2
+ const require_ha_broker_source = require("./ha-broker-source-DnSL-mbs.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
@@ -5500,6 +5500,10 @@ var HaIntegrationManager = class HaIntegrationManager {
5500
5500
  this.connection = null;
5501
5501
  this.setStatus("disconnected");
5502
5502
  }
5503
+ /** The address HA answers on; `null` for a supervisor-token broker, which has none of its own. */
5504
+ getBaseUrl() {
5505
+ return this.opts.auth.kind === "long-lived" ? this.opts.auth.baseUrl : null;
5506
+ }
5503
5507
  getInfo() {
5504
5508
  return {
5505
5509
  id: this.opts.id,
@@ -7773,6 +7777,179 @@ var HaBrokerStore = class HaBrokerStore {
7773
7777
  }
7774
7778
  };
7775
7779
  //#endregion
7780
+ //#region src/ha-board-key-source.ts
7781
+ /**
7782
+ * `board-key-source` for Home Assistant: reads an ESPHome board's API
7783
+ * encryption key from the ESPHome Builder add-on that runs inside HA.
7784
+ *
7785
+ * The path reuses what the broker already holds, the long-lived token on the
7786
+ * WebSocket, and opens no port and asks for no second credential:
7787
+ *
7788
+ * 1. `supervisor/api GET /discovery` -> the add-on that announced `esphome`
7789
+ * 2. `supervisor/api GET /addons/<slug>/info` -> its `ingress_url`
7790
+ * 3. `supervisor/api POST /ingress/session` (admin only) -> a session
7791
+ * 4. `GET <ha><ingress_url>devices`, cookie `ingress_session` -> the boards
7792
+ * 5. `GET <ha><ingress_url>json-config?configuration=<x>.yaml` -> the key
7793
+ *
7794
+ * **What crosses.** The builder's `json-config` body carries EVERY value the
7795
+ * YAML secrets file resolves, the Wi-Fi passphrase included. It is parsed in
7796
+ * memory straight into a schema that names only `api.encryption.key`; Zod
7797
+ * drops every other field at the parse, so the rest never reaches a variable,
7798
+ * a return value or a log line. No body is ever logged, only a reason.
7799
+ */
7800
+ var INGRESS_PREFIX = "/api/hassio_ingress/";
7801
+ var ESPHOME_SERVICE = "esphome";
7802
+ var DiscoverySchema = require_ha_broker_source.object({ discovery: require_ha_broker_source.array(require_ha_broker_source.object({
7803
+ addon: require_ha_broker_source.string(),
7804
+ service: require_ha_broker_source.string()
7805
+ })) });
7806
+ var AddonInfoSchema = require_ha_broker_source.object({ ingress_url: require_ha_broker_source.string().nullable().optional() });
7807
+ var SessionSchema = require_ha_broker_source.object({ session: require_ha_broker_source.string().min(1) });
7808
+ var DevicesSchema = require_ha_broker_source.object({ configured: require_ha_broker_source.array(require_ha_broker_source.object({
7809
+ name: require_ha_broker_source.string(),
7810
+ configuration: require_ha_broker_source.string(),
7811
+ address: require_ha_broker_source.string().nullable().optional()
7812
+ })) });
7813
+ /** The ONLY part of `json-config` that is read; every other field is dropped by the parse. */
7814
+ var KeyConfigSchema = require_ha_broker_source.object({ api: require_ha_broker_source.object({ encryption: require_ha_broker_source.object({ key: require_ha_broker_source.string().min(1) }).optional() }).optional() });
7815
+ var ADMIN_CODES = new Set([
7816
+ "unauthorized",
7817
+ "forbidden",
7818
+ "not_admin"
7819
+ ]);
7820
+ var SourceFailure = class extends Error {
7821
+ reason;
7822
+ detail;
7823
+ constructor(reason, detail) {
7824
+ super(reason);
7825
+ this.reason = reason;
7826
+ this.detail = detail;
7827
+ }
7828
+ };
7829
+ function errorCode(err) {
7830
+ if (typeof err !== "object" || err === null) return null;
7831
+ const code = Reflect.get(err, "code");
7832
+ return typeof code === "string" ? code : null;
7833
+ }
7834
+ var HaBoardKeySource = class {
7835
+ deps;
7836
+ constructor(deps) {
7837
+ this.deps = deps;
7838
+ }
7839
+ listSources() {
7840
+ return this.deps.brokers.list().map((b) => ({
7841
+ sourceId: b.id,
7842
+ name: b.name
7843
+ }));
7844
+ }
7845
+ async listBoards(sourceId) {
7846
+ try {
7847
+ const access = await this.openIngress(sourceId);
7848
+ const raw = await this.getJson(access, "devices");
7849
+ const parsed = DevicesSchema.safeParse(raw);
7850
+ if (!parsed.success) throw new SourceFailure("unreadable-config", "unexpected devices reply");
7851
+ return {
7852
+ ok: true,
7853
+ boards: parsed.data.configured.map((d) => ({
7854
+ configuration: d.configuration,
7855
+ name: d.name,
7856
+ address: d.address ?? null
7857
+ }))
7858
+ };
7859
+ } catch (err) {
7860
+ return this.fail(sourceId, "listBoards", err);
7861
+ }
7862
+ }
7863
+ async getBoardKey(sourceId, configuration) {
7864
+ try {
7865
+ const access = await this.openIngress(sourceId);
7866
+ const raw = await this.getJson(access, `json-config?configuration=${encodeURIComponent(configuration)}`, configuration);
7867
+ const parsed = KeyConfigSchema.safeParse(raw);
7868
+ if (!parsed.success) throw new SourceFailure("unreadable-config");
7869
+ const key = parsed.data.api?.encryption?.key;
7870
+ if (key === void 0) throw new SourceFailure("no-encryption-key");
7871
+ return {
7872
+ ok: true,
7873
+ key
7874
+ };
7875
+ } catch (err) {
7876
+ return this.fail(sourceId, "getBoardKey", err);
7877
+ }
7878
+ }
7879
+ fail(sourceId, op, err) {
7880
+ const failure = err instanceof SourceFailure ? err : new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
7881
+ this.deps.logger.warn(`board-key-source ${op} refused`, { meta: {
7882
+ sourceId,
7883
+ reason: failure.reason
7884
+ } });
7885
+ return failure.detail === void 0 ? {
7886
+ ok: false,
7887
+ reason: failure.reason
7888
+ } : {
7889
+ ok: false,
7890
+ reason: failure.reason,
7891
+ detail: failure.detail
7892
+ };
7893
+ }
7894
+ async supervisor(broker, endpoint, method) {
7895
+ try {
7896
+ return await broker.send({
7897
+ type: "supervisor/api",
7898
+ endpoint,
7899
+ method
7900
+ });
7901
+ } catch (err) {
7902
+ const code = errorCode(err);
7903
+ if (code !== null && ADMIN_CODES.has(code)) throw new SourceFailure("not-admin", "the Home Assistant token is not an administrator");
7904
+ throw new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
7905
+ }
7906
+ }
7907
+ async openIngress(sourceId) {
7908
+ const broker = this.deps.brokers.list().find((b) => b.id === sourceId);
7909
+ if (broker === void 0) throw new SourceFailure("unknown-source");
7910
+ if (broker.baseUrl === null) throw new SourceFailure("no-ingress-url", "this broker has no address of its own");
7911
+ const discovery = DiscoverySchema.safeParse(await this.supervisor(broker, "/discovery", "get"));
7912
+ const slug = discovery.success ? discovery.data.discovery.find((d) => d.service === ESPHOME_SERVICE)?.addon : void 0;
7913
+ if (slug === void 0) throw new SourceFailure("no-builder");
7914
+ const info = AddonInfoSchema.safeParse(await this.supervisor(broker, `/addons/${encodeURIComponent(slug)}/info`, "get"));
7915
+ const ingressUrl = info.success ? info.data.ingress_url : void 0;
7916
+ if (typeof ingressUrl !== "string" || !ingressUrl.startsWith(INGRESS_PREFIX)) throw new SourceFailure("no-ingress-url");
7917
+ const session = SessionSchema.safeParse(await this.supervisor(broker, "/ingress/session", "post"));
7918
+ if (!session.success) throw new SourceFailure("not-admin", "no ingress session was granted");
7919
+ return {
7920
+ root: `${broker.baseUrl.replace(/\/+$/, "")}${ingressUrl.endsWith("/") ? ingressUrl : `${ingressUrl}/`}`,
7921
+ cookie: `ingress_session=${session.data.session}`
7922
+ };
7923
+ }
7924
+ async getJson(access, relative, configuration) {
7925
+ let response;
7926
+ try {
7927
+ response = await this.deps.http(`${access.root}${relative}`, { headers: {
7928
+ Cookie: access.cookie,
7929
+ Accept: "application/json"
7930
+ } });
7931
+ } catch (err) {
7932
+ throw new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
7933
+ }
7934
+ if (response.status === 404 && configuration !== void 0) throw new SourceFailure("unknown-board", configuration);
7935
+ if (response.status === 401 || response.status === 403) throw new SourceFailure("not-admin", `the builder answered ${response.status}`);
7936
+ if (!response.ok) throw new SourceFailure("unreachable", `the builder answered ${response.status}`);
7937
+ try {
7938
+ return await response.json();
7939
+ } catch {
7940
+ throw new SourceFailure("unreadable-config");
7941
+ }
7942
+ }
7943
+ };
7944
+ /** The cap provider over a {@link HaBoardKeySource}. */
7945
+ function createHaBoardKeyProvider(source) {
7946
+ return {
7947
+ listSources: async () => source.listSources(),
7948
+ listBoards: (input) => source.listBoards(input.sourceId),
7949
+ getBoardKey: (input) => source.getBoardKey(input.sourceId, input.configuration)
7950
+ };
7951
+ }
7952
+ //#endregion
7776
7953
  //#region src/ha-calendar-source.ts
7777
7954
  function isCalendarEntity(entityId) {
7778
7955
  return entityId.startsWith("calendar.");
@@ -8488,6 +8665,10 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_broker_source.Bas
8488
8665
  capability: require_ha_broker_source.addonRoutesCapability,
8489
8666
  provider: createHaIconRouteProvider()
8490
8667
  },
8668
+ {
8669
+ capability: require_ha_broker_source.boardKeySourceCapability,
8670
+ provider: this.buildBoardKeyProvider()
8671
+ },
8491
8672
  {
8492
8673
  capability: require_ha_broker_source.calendarSourceCapability,
8493
8674
  provider: createHaCalendarSource({
@@ -8498,6 +8679,23 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_broker_source.Bas
8498
8679
  }
8499
8680
  ];
8500
8681
  }
8682
+ /** `board-key-source`: reads an ESPHome board's key from the builder beside HA, over each broker's own token. */
8683
+ buildBoardKeyProvider() {
8684
+ const registry = this.requireRegistry();
8685
+ return createHaBoardKeyProvider(new HaBoardKeySource({
8686
+ brokers: { list: () => registry.listManagers().map(({ brokerId, manager }) => ({
8687
+ id: brokerId,
8688
+ name: manager.getInfo().name,
8689
+ baseUrl: manager.getBaseUrl(),
8690
+ send: (message) => manager.sendMessage(message)
8691
+ })) },
8692
+ http: (url, init) => fetch(url, {
8693
+ headers: init.headers,
8694
+ signal: AbortSignal.timeout(15e3)
8695
+ }),
8696
+ logger: this.ctx.logger.child("board-key-source")
8697
+ }));
8698
+ }
8501
8699
  async buildNotificationOutputProvider() {
8502
8700
  const records = this.ctx.records;
8503
8701
  if (records === void 0) throw new Error("ha notification-output: ctx.records is required (hub-only addon)");
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-CCmZ9I6P.mjs";
1
+ import { $ as humidifierCapability, $t as boolean, A as brightnessCapability, At as switchCapability, Bt as weatherCapability, C as alarmPanelCapability, Ct as runningFromLawnMower, D as bestLocationMatch, Dt as scriptRunnerCapability, E as batteryCapability, Et as runningStateCapability, F as calendarSourceCapability, Ft as updateCapability, G as deviceAdminLinkCapability, H as contactCapability, Ht as RecordSecretUnavailableError, I as carbonMonoxideCapability, It as vacuumControlCapability, J as enumSensorCapability, Jt as entryBody, K as deviceAdoptionCapability, Kt as defineEntryKind, L as climateControlCapability, Lt as valveCapability, M as buildAddonRouteProvider, Mt as temperatureSensorCapability, N as buildDeviceAdminUrl, Nt as tryConvertDelta, O as binaryCapability, P as buttonCapability, Pt as tryConvertUnit, Q as gasCapability, Qt as array, R as colorCapability, Rt as vibrationCapability, S as airQualitySensorCapability, T as automationControlCapability, Tt as runningFromVacuumState, U as controlCapability, Ut as baseUnitOf, W as coverCapability, Wt as createEvent, X as fanControlCapability, Y as eventEmitterCapability, Z as floodCapability, Zt as _enum, _t as pressureSensorCapability, a as BaseDevice, an as string, at as mediaPlayerCapability, b as accessoriesCapability, bt as quantityFromUnit, cn as url, ct as notificationOutputCapability, dn as DeviceType, en as discriminatedUnion, et as humiditySensorCapability, fn as errMsg, ft as parseAdminBase, gt as presenceCapability, ht as prepareNotification, in as record, it as lockControlCapability, j as brokerCapability, jt as tamperCapability, k as boardKeySourceCapability, kt as smokeCapability, ln as DeviceFeature, lt as notifierCapability, nn as number, nt as ingestReading, o as BaseDeviceProvider, on as union, ot as motionCapability, p as EnumSensorDateTimeFormatSchema, pn as EventCategory, pt as powerMeterCapability, qt as defineRecordKinds, rn as object, rt as lawnMowerControlCapability, sn as unknown, st as normalizeUnit, t as HA_BROKER_KIND, tn as literal, tt as imageCapability, un as DeviceRole, ut as numericSensorCapability, v as RUNNING_STATE_COLD, w as ambientLightSensorCapability, wt as runningFromMediaPlayer, x as addonRoutesCapability, xt as resolveAttachmentUrls, y as TargetSchema, yt as publicEndpointReaderFrom, z as connectivityCapability, zt as waterHeaterCapability } from "./ha-broker-source-CKdH9XHn.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
@@ -5499,6 +5499,10 @@ var HaIntegrationManager = class HaIntegrationManager {
5499
5499
  this.connection = null;
5500
5500
  this.setStatus("disconnected");
5501
5501
  }
5502
+ /** The address HA answers on; `null` for a supervisor-token broker, which has none of its own. */
5503
+ getBaseUrl() {
5504
+ return this.opts.auth.kind === "long-lived" ? this.opts.auth.baseUrl : null;
5505
+ }
5502
5506
  getInfo() {
5503
5507
  return {
5504
5508
  id: this.opts.id,
@@ -7755,6 +7759,179 @@ var HaBrokerStore = class HaBrokerStore {
7755
7759
  }
7756
7760
  };
7757
7761
  //#endregion
7762
+ //#region src/ha-board-key-source.ts
7763
+ /**
7764
+ * `board-key-source` for Home Assistant: reads an ESPHome board's API
7765
+ * encryption key from the ESPHome Builder add-on that runs inside HA.
7766
+ *
7767
+ * The path reuses what the broker already holds, the long-lived token on the
7768
+ * WebSocket, and opens no port and asks for no second credential:
7769
+ *
7770
+ * 1. `supervisor/api GET /discovery` -> the add-on that announced `esphome`
7771
+ * 2. `supervisor/api GET /addons/<slug>/info` -> its `ingress_url`
7772
+ * 3. `supervisor/api POST /ingress/session` (admin only) -> a session
7773
+ * 4. `GET <ha><ingress_url>devices`, cookie `ingress_session` -> the boards
7774
+ * 5. `GET <ha><ingress_url>json-config?configuration=<x>.yaml` -> the key
7775
+ *
7776
+ * **What crosses.** The builder's `json-config` body carries EVERY value the
7777
+ * YAML secrets file resolves, the Wi-Fi passphrase included. It is parsed in
7778
+ * memory straight into a schema that names only `api.encryption.key`; Zod
7779
+ * drops every other field at the parse, so the rest never reaches a variable,
7780
+ * a return value or a log line. No body is ever logged, only a reason.
7781
+ */
7782
+ var INGRESS_PREFIX = "/api/hassio_ingress/";
7783
+ var ESPHOME_SERVICE = "esphome";
7784
+ var DiscoverySchema = object({ discovery: array(object({
7785
+ addon: string(),
7786
+ service: string()
7787
+ })) });
7788
+ var AddonInfoSchema = object({ ingress_url: string().nullable().optional() });
7789
+ var SessionSchema = object({ session: string().min(1) });
7790
+ var DevicesSchema = object({ configured: array(object({
7791
+ name: string(),
7792
+ configuration: string(),
7793
+ address: string().nullable().optional()
7794
+ })) });
7795
+ /** The ONLY part of `json-config` that is read; every other field is dropped by the parse. */
7796
+ var KeyConfigSchema = object({ api: object({ encryption: object({ key: string().min(1) }).optional() }).optional() });
7797
+ var ADMIN_CODES = new Set([
7798
+ "unauthorized",
7799
+ "forbidden",
7800
+ "not_admin"
7801
+ ]);
7802
+ var SourceFailure = class extends Error {
7803
+ reason;
7804
+ detail;
7805
+ constructor(reason, detail) {
7806
+ super(reason);
7807
+ this.reason = reason;
7808
+ this.detail = detail;
7809
+ }
7810
+ };
7811
+ function errorCode(err) {
7812
+ if (typeof err !== "object" || err === null) return null;
7813
+ const code = Reflect.get(err, "code");
7814
+ return typeof code === "string" ? code : null;
7815
+ }
7816
+ var HaBoardKeySource = class {
7817
+ deps;
7818
+ constructor(deps) {
7819
+ this.deps = deps;
7820
+ }
7821
+ listSources() {
7822
+ return this.deps.brokers.list().map((b) => ({
7823
+ sourceId: b.id,
7824
+ name: b.name
7825
+ }));
7826
+ }
7827
+ async listBoards(sourceId) {
7828
+ try {
7829
+ const access = await this.openIngress(sourceId);
7830
+ const raw = await this.getJson(access, "devices");
7831
+ const parsed = DevicesSchema.safeParse(raw);
7832
+ if (!parsed.success) throw new SourceFailure("unreadable-config", "unexpected devices reply");
7833
+ return {
7834
+ ok: true,
7835
+ boards: parsed.data.configured.map((d) => ({
7836
+ configuration: d.configuration,
7837
+ name: d.name,
7838
+ address: d.address ?? null
7839
+ }))
7840
+ };
7841
+ } catch (err) {
7842
+ return this.fail(sourceId, "listBoards", err);
7843
+ }
7844
+ }
7845
+ async getBoardKey(sourceId, configuration) {
7846
+ try {
7847
+ const access = await this.openIngress(sourceId);
7848
+ const raw = await this.getJson(access, `json-config?configuration=${encodeURIComponent(configuration)}`, configuration);
7849
+ const parsed = KeyConfigSchema.safeParse(raw);
7850
+ if (!parsed.success) throw new SourceFailure("unreadable-config");
7851
+ const key = parsed.data.api?.encryption?.key;
7852
+ if (key === void 0) throw new SourceFailure("no-encryption-key");
7853
+ return {
7854
+ ok: true,
7855
+ key
7856
+ };
7857
+ } catch (err) {
7858
+ return this.fail(sourceId, "getBoardKey", err);
7859
+ }
7860
+ }
7861
+ fail(sourceId, op, err) {
7862
+ const failure = err instanceof SourceFailure ? err : new SourceFailure("unreachable", errMsg(err));
7863
+ this.deps.logger.warn(`board-key-source ${op} refused`, { meta: {
7864
+ sourceId,
7865
+ reason: failure.reason
7866
+ } });
7867
+ return failure.detail === void 0 ? {
7868
+ ok: false,
7869
+ reason: failure.reason
7870
+ } : {
7871
+ ok: false,
7872
+ reason: failure.reason,
7873
+ detail: failure.detail
7874
+ };
7875
+ }
7876
+ async supervisor(broker, endpoint, method) {
7877
+ try {
7878
+ return await broker.send({
7879
+ type: "supervisor/api",
7880
+ endpoint,
7881
+ method
7882
+ });
7883
+ } catch (err) {
7884
+ const code = errorCode(err);
7885
+ if (code !== null && ADMIN_CODES.has(code)) throw new SourceFailure("not-admin", "the Home Assistant token is not an administrator");
7886
+ throw new SourceFailure("unreachable", errMsg(err));
7887
+ }
7888
+ }
7889
+ async openIngress(sourceId) {
7890
+ const broker = this.deps.brokers.list().find((b) => b.id === sourceId);
7891
+ if (broker === void 0) throw new SourceFailure("unknown-source");
7892
+ if (broker.baseUrl === null) throw new SourceFailure("no-ingress-url", "this broker has no address of its own");
7893
+ const discovery = DiscoverySchema.safeParse(await this.supervisor(broker, "/discovery", "get"));
7894
+ const slug = discovery.success ? discovery.data.discovery.find((d) => d.service === ESPHOME_SERVICE)?.addon : void 0;
7895
+ if (slug === void 0) throw new SourceFailure("no-builder");
7896
+ const info = AddonInfoSchema.safeParse(await this.supervisor(broker, `/addons/${encodeURIComponent(slug)}/info`, "get"));
7897
+ const ingressUrl = info.success ? info.data.ingress_url : void 0;
7898
+ if (typeof ingressUrl !== "string" || !ingressUrl.startsWith(INGRESS_PREFIX)) throw new SourceFailure("no-ingress-url");
7899
+ const session = SessionSchema.safeParse(await this.supervisor(broker, "/ingress/session", "post"));
7900
+ if (!session.success) throw new SourceFailure("not-admin", "no ingress session was granted");
7901
+ return {
7902
+ root: `${broker.baseUrl.replace(/\/+$/, "")}${ingressUrl.endsWith("/") ? ingressUrl : `${ingressUrl}/`}`,
7903
+ cookie: `ingress_session=${session.data.session}`
7904
+ };
7905
+ }
7906
+ async getJson(access, relative, configuration) {
7907
+ let response;
7908
+ try {
7909
+ response = await this.deps.http(`${access.root}${relative}`, { headers: {
7910
+ Cookie: access.cookie,
7911
+ Accept: "application/json"
7912
+ } });
7913
+ } catch (err) {
7914
+ throw new SourceFailure("unreachable", errMsg(err));
7915
+ }
7916
+ if (response.status === 404 && configuration !== void 0) throw new SourceFailure("unknown-board", configuration);
7917
+ if (response.status === 401 || response.status === 403) throw new SourceFailure("not-admin", `the builder answered ${response.status}`);
7918
+ if (!response.ok) throw new SourceFailure("unreachable", `the builder answered ${response.status}`);
7919
+ try {
7920
+ return await response.json();
7921
+ } catch {
7922
+ throw new SourceFailure("unreadable-config");
7923
+ }
7924
+ }
7925
+ };
7926
+ /** The cap provider over a {@link HaBoardKeySource}. */
7927
+ function createHaBoardKeyProvider(source) {
7928
+ return {
7929
+ listSources: async () => source.listSources(),
7930
+ listBoards: (input) => source.listBoards(input.sourceId),
7931
+ getBoardKey: (input) => source.getBoardKey(input.sourceId, input.configuration)
7932
+ };
7933
+ }
7934
+ //#endregion
7758
7935
  //#region src/ha-calendar-source.ts
7759
7936
  function isCalendarEntity(entityId) {
7760
7937
  return entityId.startsWith("calendar.");
@@ -8470,6 +8647,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8470
8647
  capability: addonRoutesCapability,
8471
8648
  provider: createHaIconRouteProvider()
8472
8649
  },
8650
+ {
8651
+ capability: boardKeySourceCapability,
8652
+ provider: this.buildBoardKeyProvider()
8653
+ },
8473
8654
  {
8474
8655
  capability: calendarSourceCapability,
8475
8656
  provider: createHaCalendarSource({
@@ -8480,6 +8661,23 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8480
8661
  }
8481
8662
  ];
8482
8663
  }
8664
+ /** `board-key-source`: reads an ESPHome board's key from the builder beside HA, over each broker's own token. */
8665
+ buildBoardKeyProvider() {
8666
+ const registry = this.requireRegistry();
8667
+ return createHaBoardKeyProvider(new HaBoardKeySource({
8668
+ brokers: { list: () => registry.listManagers().map(({ brokerId, manager }) => ({
8669
+ id: brokerId,
8670
+ name: manager.getInfo().name,
8671
+ baseUrl: manager.getBaseUrl(),
8672
+ send: (message) => manager.sendMessage(message)
8673
+ })) },
8674
+ http: (url, init) => fetch(url, {
8675
+ headers: init.headers,
8676
+ signal: AbortSignal.timeout(15e3)
8677
+ }),
8678
+ logger: this.ctx.logger.child("board-key-source")
8679
+ }));
8680
+ }
8483
8681
  async buildNotificationOutputProvider() {
8484
8682
  const records = this.ctx.records;
8485
8683
  if (records === void 0) throw new Error("ha notification-output: ctx.records is required (hub-only addon)");