@camstack/addon-provider-homeassistant 1.2.18 → 1.2.20

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/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-DuKJzmrJ.js");
2
+ const require_dist = require("./dist-Bz5ENMVc.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-device-source.ts
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as tamperCapability, A as eventEmitterCapability, B as motionCapability, C as connectivityCapability, Ct as url, D as deviceAdoptionCapability, E as coverCapability, F as humiditySensorCapability, H as notificationOutputCapability, I as imageCapability, J as presenceCapability, K as powerMeterCapability, L as lawnMowerControlCapability, M as floodCapability, N as gasCapability, P as humidifierCapability, Q as switchCapability, R as lockControlCapability, S as colorCapability, St as unknown, T as controlCapability, U as notifierCapability, V as normalizeUnit, W as numericSensorCapability, X as scriptRunnerCapability, Y as pressureSensorCapability, Z as smokeCapability, _ as brokerCapability, _t as number, a as EnumSensorDateTimeFormatSchema, at as waterHeaterCapability, b as carbonMonoxideCapability, bt as string, c as addonRoutesCapability, d as ambientLightSensorCapability, dt as DeviceType, et as temperatureSensorCapability, f as automationControlCapability, ft as createEvent, g as brightnessCapability, gt as literal, h as binaryCapability, ht as discriminatedUnion, it as vibrationCapability, j as fanControlCapability, k as enumSensorCapability, l as airQualitySensorCapability, lt as DeviceFeature, m as bestLocationMatch, mt as array, n as BaseDeviceProvider, nt as vacuumControlCapability, o as TargetSchema, ot as weatherCapability, p as batteryCapability, pt as _enum, q as prepareNotification, rt as valveCapability, s as accessoriesCapability, st as errMsg, t as BaseDevice, tt as updateCapability, u as alarmPanelCapability, ut as DeviceRole, v as buildAddonRouteProvider, vt as object, w as contactCapability, wt as EventCategory, x as climateControlCapability, xt as union, y as buttonCapability, yt as record, z as mediaPlayerCapability } from "./dist-UfT_fD3-.mjs";
1
+ import { $ as tamperCapability, A as eventEmitterCapability, B as motionCapability, C as connectivityCapability, Ct as url, D as deviceAdoptionCapability, E as coverCapability, F as humiditySensorCapability, H as notificationOutputCapability, I as imageCapability, J as presenceCapability, K as powerMeterCapability, L as lawnMowerControlCapability, M as floodCapability, N as gasCapability, P as humidifierCapability, Q as switchCapability, R as lockControlCapability, S as colorCapability, St as unknown, T as controlCapability, U as notifierCapability, V as normalizeUnit, W as numericSensorCapability, X as scriptRunnerCapability, Y as pressureSensorCapability, Z as smokeCapability, _ as brokerCapability, _t as number, a as EnumSensorDateTimeFormatSchema, at as waterHeaterCapability, b as carbonMonoxideCapability, bt as string, c as addonRoutesCapability, d as ambientLightSensorCapability, dt as DeviceType, et as temperatureSensorCapability, f as automationControlCapability, ft as createEvent, g as brightnessCapability, gt as literal, h as binaryCapability, ht as discriminatedUnion, it as vibrationCapability, j as fanControlCapability, k as enumSensorCapability, l as airQualitySensorCapability, lt as DeviceFeature, m as bestLocationMatch, mt as array, n as BaseDeviceProvider, nt as vacuumControlCapability, o as TargetSchema, ot as weatherCapability, p as batteryCapability, pt as _enum, q as prepareNotification, rt as valveCapability, s as accessoriesCapability, st as errMsg, t as BaseDevice, tt as updateCapability, u as alarmPanelCapability, ut as DeviceRole, v as buildAddonRouteProvider, vt as object, w as contactCapability, wt as EventCategory, x as climateControlCapability, xt as union, y as buttonCapability, yt as record, z as mediaPlayerCapability } from "./dist-BSQpoIVg.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-device-source.ts
@@ -15351,21 +15351,32 @@ object({
15351
15351
  * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15352
15352
  * `/api/oauth2/integrations` are built from this collection alone.
15353
15353
  *
15354
- * **Scopes.** `requestedScopes` is baked into every token this integration is
15355
- * ever issued and the operator consents to it once. Derive it from the tRPC
15356
- * paths the client calls **with that token**, against `METHOD_ACCESS_MAP`, and
15357
- * prefer a narrow `capability:` scope to a `category:` one unless the client
15358
- * genuinely needs a whole family. A category scope grants every future member
15354
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15355
+ * NEEDS to function.** Not a blast radius, not a conservative
15356
+ * under-declaration, not a description of some other path the addon happens to
15357
+ * have. Derive it from what the client actually calls **with this token** —
15358
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15359
+ * addon HTTP route it posts to — and write the call that justifies each entry
15360
+ * next to it. Two integrations once used this field to mean two different
15361
+ * things; the operator ruled there is one meaning, and any third integration
15362
+ * inherits it (2026-08-09).
15363
+ *
15364
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15365
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15366
+ * checks an integration token's grant before letting it reach an
15367
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15368
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15369
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15370
+ * honestly rather than weakening a check to make a route pass.
15371
+ *
15372
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15373
+ * genuinely needs a whole family; a category scope grants every future member
15359
15374
  * of that category too. `category:system [create]` has been rejected once and
15360
15375
  * should stay rejected: it hands `addons.installPackage` to an integration.
15361
15376
  *
15362
- * What it does NOT cover: calls the ADDON makes over `ctx.api`, which run as
15363
- * the addon and are not scope-checked. Alexa's descriptor is narrower than
15364
- * Home Assistant's for exactly that reason its Lambda posts directives and
15365
- * the addon does the work, while the Home Assistant component calls tRPC
15366
- * directly with the token. So `requestedScopes` describes the blast radius of
15367
- * the GRANT, not the reach of the integration; do not widen one to describe the
15368
- * other.
15377
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15378
+ * scope-checked, so they are not what this field describes but reaching the
15379
+ * addon's route in the first place IS, and that is the entry to declare.
15369
15380
  *
15370
15381
  * **The boot window.** An addon registers its provider after its runner forks
15371
15382
  * and initialises, so between hub start and that moment this collection is
@@ -15406,7 +15417,30 @@ var OauthIntegrationDescriptorSchema = object({
15406
15417
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15407
15418
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15408
15419
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15409
- hubUrl: string().optional()
15420
+ hubUrl: string().optional(),
15421
+ /**
15422
+ * How long a REFRESH token issued for this integration lives — seconds, or
15423
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
15424
+ * 30-day default, which is what every link used before this field existed.
15425
+ *
15426
+ * Declared here for the same reason `requestedScopes` is: the integration
15427
+ * knows what it needs. Amazon's account linking and a Home Assistant config
15428
+ * entry are both meant to survive indefinitely, and re-linking is a manual
15429
+ * user action, so a 30-day expiry silently unlinks a working integration.
15430
+ *
15431
+ * **The security posture, stated so it is owned deliberately.** A refresh
15432
+ * token that never expires is permanent access if it leaks. What bounds it is
15433
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
15434
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
15435
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
15436
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
15437
+ * revoking a link takes effect there only after its remaining hour. That hour
15438
+ * is why the access TTL is not configurable.
15439
+ *
15440
+ * The value is baked into the authorization code at `/authorize` and travels
15441
+ * on the tokens, so editing this field changes FUTURE links only.
15442
+ */
15443
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15410
15444
  });
15411
15445
  var oauthIntegrationCapability = {
15412
15446
  name: "oauth-integration",
@@ -18549,11 +18583,29 @@ var SsoBridgeClaimsSchema = object({
18549
18583
  codeChallenge: string().optional(),
18550
18584
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
18551
18585
  * tokens so the verify path can check the session is not revoked. */
18552
- sessionId: string().optional()
18586
+ sessionId: string().optional(),
18587
+ /**
18588
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
18589
+ * Baked into the code at `/authorize` from the integration's descriptor and
18590
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
18591
+ * on the token rather than being re-read from the descriptor on purpose:
18592
+ * editing a descriptor must not retroactively extend or shorten a link the
18593
+ * operator already consented to.
18594
+ */
18595
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
18553
18596
  });
18554
18597
  method(object({
18555
18598
  claims: SsoBridgeClaimsSchema,
18556
- ttlSec: number().int().positive().optional()
18599
+ /**
18600
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
18601
+ *
18602
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
18603
+ * this field already means "the 5-minute SSO hand-off default", and
18604
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
18605
+ * went through the numeric path would fail at mint time and break
18606
+ * linking rather than produce an eternal token.
18607
+ */
18608
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
18557
18609
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
18558
18610
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
18559
18611
  providerId: string().min(1),
@@ -25834,7 +25886,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25834
25886
  hubUrl: string(),
25835
25887
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
25836
25888
  * that carries one can ONLY be exchanged with the matching verifier. */
25837
- codeChallenge: string().optional()
25889
+ codeChallenge: string().optional(),
25890
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
25891
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
25892
+ * code so the link carries its own lifetime; omit for the 30-day
25893
+ * default. */
25894
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
25838
25895
  }), object({ code: string() }), {
25839
25896
  kind: "mutation",
25840
25897
  access: "create"
@@ -15351,21 +15351,32 @@ object({
15351
15351
  * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15352
15352
  * `/api/oauth2/integrations` are built from this collection alone.
15353
15353
  *
15354
- * **Scopes.** `requestedScopes` is baked into every token this integration is
15355
- * ever issued and the operator consents to it once. Derive it from the tRPC
15356
- * paths the client calls **with that token**, against `METHOD_ACCESS_MAP`, and
15357
- * prefer a narrow `capability:` scope to a `category:` one unless the client
15358
- * genuinely needs a whole family. A category scope grants every future member
15354
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15355
+ * NEEDS to function.** Not a blast radius, not a conservative
15356
+ * under-declaration, not a description of some other path the addon happens to
15357
+ * have. Derive it from what the client actually calls **with this token** —
15358
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15359
+ * addon HTTP route it posts to — and write the call that justifies each entry
15360
+ * next to it. Two integrations once used this field to mean two different
15361
+ * things; the operator ruled there is one meaning, and any third integration
15362
+ * inherits it (2026-08-09).
15363
+ *
15364
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15365
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15366
+ * checks an integration token's grant before letting it reach an
15367
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15368
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15369
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15370
+ * honestly rather than weakening a check to make a route pass.
15371
+ *
15372
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15373
+ * genuinely needs a whole family; a category scope grants every future member
15359
15374
  * of that category too. `category:system [create]` has been rejected once and
15360
15375
  * should stay rejected: it hands `addons.installPackage` to an integration.
15361
15376
  *
15362
- * What it does NOT cover: calls the ADDON makes over `ctx.api`, which run as
15363
- * the addon and are not scope-checked. Alexa's descriptor is narrower than
15364
- * Home Assistant's for exactly that reason its Lambda posts directives and
15365
- * the addon does the work, while the Home Assistant component calls tRPC
15366
- * directly with the token. So `requestedScopes` describes the blast radius of
15367
- * the GRANT, not the reach of the integration; do not widen one to describe the
15368
- * other.
15377
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15378
+ * scope-checked, so they are not what this field describes but reaching the
15379
+ * addon's route in the first place IS, and that is the entry to declare.
15369
15380
  *
15370
15381
  * **The boot window.** An addon registers its provider after its runner forks
15371
15382
  * and initialises, so between hub start and that moment this collection is
@@ -15406,7 +15417,30 @@ var OauthIntegrationDescriptorSchema = object({
15406
15417
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15407
15418
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15408
15419
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15409
- hubUrl: string().optional()
15420
+ hubUrl: string().optional(),
15421
+ /**
15422
+ * How long a REFRESH token issued for this integration lives — seconds, or
15423
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
15424
+ * 30-day default, which is what every link used before this field existed.
15425
+ *
15426
+ * Declared here for the same reason `requestedScopes` is: the integration
15427
+ * knows what it needs. Amazon's account linking and a Home Assistant config
15428
+ * entry are both meant to survive indefinitely, and re-linking is a manual
15429
+ * user action, so a 30-day expiry silently unlinks a working integration.
15430
+ *
15431
+ * **The security posture, stated so it is owned deliberately.** A refresh
15432
+ * token that never expires is permanent access if it leaks. What bounds it is
15433
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
15434
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
15435
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
15436
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
15437
+ * revoking a link takes effect there only after its remaining hour. That hour
15438
+ * is why the access TTL is not configurable.
15439
+ *
15440
+ * The value is baked into the authorization code at `/authorize` and travels
15441
+ * on the tokens, so editing this field changes FUTURE links only.
15442
+ */
15443
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15410
15444
  });
15411
15445
  var oauthIntegrationCapability = {
15412
15446
  name: "oauth-integration",
@@ -18549,11 +18583,29 @@ var SsoBridgeClaimsSchema = object({
18549
18583
  codeChallenge: string().optional(),
18550
18584
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
18551
18585
  * tokens so the verify path can check the session is not revoked. */
18552
- sessionId: string().optional()
18586
+ sessionId: string().optional(),
18587
+ /**
18588
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
18589
+ * Baked into the code at `/authorize` from the integration's descriptor and
18590
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
18591
+ * on the token rather than being re-read from the descriptor on purpose:
18592
+ * editing a descriptor must not retroactively extend or shorten a link the
18593
+ * operator already consented to.
18594
+ */
18595
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
18553
18596
  });
18554
18597
  method(object({
18555
18598
  claims: SsoBridgeClaimsSchema,
18556
- ttlSec: number().int().positive().optional()
18599
+ /**
18600
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
18601
+ *
18602
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
18603
+ * this field already means "the 5-minute SSO hand-off default", and
18604
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
18605
+ * went through the numeric path would fail at mint time and break
18606
+ * linking rather than produce an eternal token.
18607
+ */
18608
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
18557
18609
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
18558
18610
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
18559
18611
  providerId: string().min(1),
@@ -25834,7 +25886,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25834
25886
  hubUrl: string(),
25835
25887
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
25836
25888
  * that carries one can ONLY be exchanged with the matching verifier. */
25837
- codeChallenge: string().optional()
25889
+ codeChallenge: string().optional(),
25890
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
25891
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
25892
+ * code so the link carries its own lifetime; omit for the 30-day
25893
+ * default. */
25894
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
25838
25895
  }), object({ code: string() }), {
25839
25896
  kind: "mutation",
25840
25897
  access: "create"