@camstack/addon-import-alexa 0.2.189 → 0.2.190

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
@@ -6044,7 +6044,7 @@ function number(params) {
6044
6044
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6045
6045
  }
6046
6046
  //#endregion
6047
- //#region ../types/dist/sleep-DEZaLbiu.mjs
6047
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
6048
6048
  /**
6049
6049
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6050
6050
  * window to float samples (D455).
@@ -7673,6 +7673,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7673
7673
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7674
7674
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7675
7675
  /**
7676
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7677
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7678
+ * through `api` (guard `check-migration-ports-surface`).
7679
+ */
7680
+ function migrationSet(catalogue, bindPorts) {
7681
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7682
+ }
7683
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7684
+ function refuseMixed(form, kept, dropped) {
7685
+ if (dropped > 0) throw new Error(`runMigrations(${form}): ${dropped} of ${kept + dropped} entries are of the other kind and would be dropped; pass one kind per call`);
7686
+ }
7687
+ function catalogueOf(list) {
7688
+ const kept = list.filter((entry) => !("execute" in entry));
7689
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7690
+ return kept;
7691
+ }
7692
+ function boundOf(list) {
7693
+ const kept = list.filter((entry) => "execute" in entry);
7694
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7695
+ return kept;
7696
+ }
7697
+ /**
7676
7698
  * Base class for CamStack addons. Eliminates settings boilerplate:
7677
7699
  *
7678
7700
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7839,23 +7861,21 @@ var BaseAddon = class {
7839
7861
  await this.loadScopedSchemas();
7840
7862
  return normalized;
7841
7863
  }
7864
+ async runMigrations(first, second, third) {
7865
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7866
+ return this.runBound(boundOf(first), second);
7867
+ }
7842
7868
  /**
7843
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7844
- * engine. Call it at the point of the boot where the data must already have the new
7845
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7846
- * restored devices — with the catalogue bound to the addon's ports
7847
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7848
- *
7849
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7850
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7851
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7852
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7853
- *
7854
- * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7855
- * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7856
- * omitted applied ids "newer").
7869
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7870
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7871
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7857
7872
  */
7858
- async runMigrations(migrations, options) {
7873
+ async runMigrationSets(sets, options) {
7874
+ const api = this.ctx.migrationApi;
7875
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7876
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7877
+ }
7878
+ async runBound(migrations, options) {
7859
7879
  const api = this.ctx.api.migrations;
7860
7880
  const report = await runAddonMigrations({
7861
7881
  declareOwner: (input) => api.declareOwner.mutate(input),
@@ -13681,26 +13701,16 @@ var RecordIntegrationInstances = class {
13681
13701
  ...this.hooks.description,
13682
13702
  mintsId: true
13683
13703
  };
13684
- /**
13685
- * The record an id names. A caller holding the `integrations` registry id (the Integrations
13686
- * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13687
- * record id answers as it is. An id that names nothing comes back unchanged.
13688
- */
13689
- async resolveRecordId(id) {
13690
- if (await this.handle.get(id) !== null) return id;
13691
- const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13692
- return linked === void 0 ? id : linked.id;
13693
- }
13694
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13704
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13695
13705
  listInstances = async () => {
13696
13706
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13697
13707
  };
13698
13708
  getInstance = async ({ id }) => {
13699
- const record = await this.handle.get(await this.resolveRecordId(id));
13709
+ const record = await this.handle.get(id);
13700
13710
  return record === null ? null : this.viewOf(record);
13701
13711
  };
13702
13712
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13703
- createInstance = async ({ id, name, kind }) => {
13713
+ createInstance = async ({ id, name, kind, values }) => {
13704
13714
  const minted = id ?? this.hooks.mintId?.({
13705
13715
  name,
13706
13716
  ...kind !== void 0 ? { kind } : {}
@@ -13710,11 +13720,20 @@ var RecordIntegrationInstances = class {
13710
13720
  id: minted,
13711
13721
  data: this.hooks.initialBody(name, kind)
13712
13722
  }, { ifRev: 0 });
13723
+ const patch = this.patchOf(values ?? {});
13724
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13713
13725
  await this.hooks.onChanged?.(minted, "created");
13714
13726
  return { id: minted };
13715
13727
  };
13716
- updateInstance = async ({ id: askedId, name, values }) => {
13717
- const id = await this.resolveRecordId(askedId);
13728
+ updateInstance = async ({ id, name, values }) => {
13729
+ const patch = this.patchOf(values);
13730
+ if (name !== void 0) patch[this.nameField] = name;
13731
+ await this.handle.patch(id, patch);
13732
+ await this.hooks.onChanged?.(id, "updated");
13733
+ return { ok: true };
13734
+ };
13735
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13736
+ patchOf(values) {
13718
13737
  const patch = {};
13719
13738
  for (const [key, value] of Object.entries(values)) {
13720
13739
  if (value === void 0) continue;
@@ -13725,11 +13744,8 @@ var RecordIntegrationInstances = class {
13725
13744
  else if (isSecretClear(value)) patch[key] = null;
13726
13745
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13727
13746
  }
13728
- if (name !== void 0) patch[this.nameField] = name;
13729
- await this.handle.patch(id, patch);
13730
- await this.hooks.onChanged?.(id, "updated");
13731
- return { ok: true };
13732
- };
13747
+ return patch;
13748
+ }
13733
13749
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13734
13750
  setInstanceEnabled = async ({ id, enabled }) => {
13735
13751
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -13749,10 +13765,7 @@ var RecordIntegrationInstances = class {
13749
13765
  status: "unsupported",
13750
13766
  error: "This integration declares no connection test"
13751
13767
  };
13752
- return this.hooks.test(input.id === null ? input : {
13753
- ...input,
13754
- id: await this.resolveRecordId(input.id)
13755
- });
13768
+ return this.hooks.test(input);
13756
13769
  };
13757
13770
  /** The flat provider object to hand to `registerProvider`. */
13758
13771
  toProvider() {
@@ -13769,6 +13782,76 @@ var RecordIntegrationInstances = class {
13769
13782
  };
13770
13783
  }
13771
13784
  };
13785
+ /** No part holds the id a call named. */
13786
+ var UnknownInstanceError = class extends Error {
13787
+ instanceId;
13788
+ constructor(instanceId) {
13789
+ super(`Unknown instance '${instanceId}'`);
13790
+ this.instanceId = instanceId;
13791
+ this.name = "UnknownInstanceError";
13792
+ }
13793
+ };
13794
+ /** A create or a form named no kind, or a kind no part serves. */
13795
+ var UnknownInstanceKindError = class extends Error {
13796
+ kindId;
13797
+ constructor(kindId) {
13798
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13799
+ this.kindId = kindId;
13800
+ this.name = "UnknownInstanceKindError";
13801
+ }
13802
+ };
13803
+ function composeInstanceKinds(parts, base) {
13804
+ const byKind = (kindId) => {
13805
+ if (kindId === void 0) {
13806
+ const only = parts.length === 1 ? parts[0] : void 0;
13807
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13808
+ return only;
13809
+ }
13810
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13811
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13812
+ return part;
13813
+ };
13814
+ const holderOf = async (id) => {
13815
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13816
+ throw new UnknownInstanceError(id);
13817
+ };
13818
+ const tagged = (part, instance) => ({
13819
+ ...instance,
13820
+ kind: part.kind.id
13821
+ });
13822
+ return {
13823
+ describe: async () => ({
13824
+ ...base,
13825
+ kinds: parts.map((part) => part.kind)
13826
+ }),
13827
+ instanceFormSchema: async ({ id, kind }) => {
13828
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13829
+ },
13830
+ listInstances: async () => {
13831
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13832
+ },
13833
+ getInstance: async ({ id }) => {
13834
+ for (const part of parts) {
13835
+ const found = await part.provider.getInstance({ id });
13836
+ if (found !== null) return tagged(part, found);
13837
+ }
13838
+ return null;
13839
+ },
13840
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13841
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13842
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13843
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13844
+ testInstance: async (input) => {
13845
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13846
+ if (test === void 0) return {
13847
+ success: false,
13848
+ status: "inconclusive",
13849
+ error: "This kind cannot be tested"
13850
+ };
13851
+ return test(input);
13852
+ }
13853
+ };
13854
+ }
13772
13855
  var EncodeProfileSchema = object({
13773
13856
  video: object({
13774
13857
  codec: _enum([
@@ -16049,6 +16132,39 @@ var UnstampedEventMediaCountSchema = object({
16049
16132
  /** Sum across both, or `null` when either lane could not be counted. */
16050
16133
  total: number$1().int().nonnegative().nullable()
16051
16134
  }).nullable();
16135
+ /**
16136
+ * Where the stored event media sits, per storage location: how many media rows
16137
+ * and how many bytes carry each location id, how many carry none (legacy,
16138
+ * resolved to the default `eventMedia` location), and how many name a location
16139
+ * that was not asked about (a deleted or never-listed one).
16140
+ *
16141
+ * One covering-index aggregate per asked location plus the NULL one, and one
16142
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
16143
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
16144
+ * it is a deleted location only when every location id was asked.
16145
+ * `null` is "could not be measured", never zero.
16146
+ */
16147
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
16148
+ var MediaLocationCensusSchema = object({
16149
+ locations: array(object({
16150
+ locationId: string(),
16151
+ rows: number$1().int().nonnegative(),
16152
+ bytes: number$1().nonnegative()
16153
+ })).readonly(),
16154
+ unstamped: object({
16155
+ rows: number$1().int().nonnegative(),
16156
+ bytes: number$1().nonnegative()
16157
+ }),
16158
+ /** Rows stamped with a location id that was not in the asked list. */
16159
+ unlisted: object({
16160
+ rows: number$1().int().nonnegative(),
16161
+ bytes: number$1().nonnegative()
16162
+ }),
16163
+ total: object({
16164
+ rows: number$1().int().nonnegative(),
16165
+ bytes: number$1().nonnegative()
16166
+ })
16167
+ });
16052
16168
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
16053
16169
  /** The independently selectable logical storage classes — every class
16054
16170
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -47126,52 +47242,7 @@ onBrightnessChanged: { data: object({
47126
47242
  minIntervalMs: 0
47127
47243
  } } }
47128
47244
  };
47129
- /**
47130
- * `broker` — unified pub/sub broker registry, system-scoped collection.
47131
- *
47132
- * The cap models any kind-tagged message broker the user wants to
47133
- * register and that other addons might consume. The first two kinds
47134
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
47135
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
47136
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
47137
- * the same surface.
47138
- *
47139
- * Why one cap, not one cap per kind:
47140
- * - The integrations page wants a single table of "every broker"
47141
- * across kinds; a unified cap drives it without join logic.
47142
- * - Generic admin operations (add / remove / test / status) are
47143
- * identical across kinds — duplicating them per cap was busywork.
47144
- * - The `network-access` cap precedent: same interface, many
47145
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
47146
- *
47147
- * What stays kind-specific:
47148
- * - `getBrokerConfig` payload (kind decides whether it returns
47149
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
47150
- * `Record<string, unknown>` at the wire; consumers narrow.
47151
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
47152
- * key) — also typed as `Record<string, unknown>` so each kind can
47153
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
47154
- * without rebreaking the cap signature.
47155
- * - `add` `settings` payload (different fields per kind, validated by
47156
- * the kind-specific provider).
47157
- *
47158
- * Bidirectionality:
47159
- * - `publish` is RPC — synchronous result (provider-defined).
47160
- * - `subscribe` returns a subscription handle; the actual message
47161
- * stream flows over the typed event-bus as `broker.message` events
47162
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
47163
- * event handler. Subscriptions persist across reconnects — the
47164
- * provider re-subscribes upstream after a transport drop.
47165
- *
47166
- * Why event-bus push (not a subscription RPC stream):
47167
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
47168
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
47169
- * drop is recoverable via `getState({key})`).
47170
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
47171
- * - Cross-process: events already route through `$event-bus`; no
47172
- * per-broker wire-shaping needed.
47173
- */
47174
- var BrokerStatusEnum = _enum([
47245
+ _enum([
47175
47246
  "connected",
47176
47247
  "disconnected",
47177
47248
  "connecting",
@@ -47179,141 +47250,12 @@ var BrokerStatusEnum = _enum([
47179
47250
  "unreachable",
47180
47251
  "error"
47181
47252
  ]);
47182
- var BrokerInfoSchema = object({
47183
- /** Stable broker id. Persisted; survives addon restarts. */
47184
- id: string(),
47185
- /** Addon id of the provider that OWNS this broker.
47186
- *
47187
- * The `broker` cap is a system-scoped collection: several addons
47188
- * register a `broker` provider, each owning a DISJOINT set of
47189
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
47190
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
47191
- * device belongs to one integration. The admin UI threads this id
47192
- * back as the `{ addonId }` system-collection selector on every
47193
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
47194
- * `testConnection`), so the call routes to the OWNING provider
47195
- * instead of defaulting to the first-registered one. */
47196
- addonId: string(),
47197
- /** Human-readable name (operator-chosen at add-time). */
47198
- name: string(),
47199
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
47200
- kind: string(),
47201
- status: BrokerStatusEnum,
47202
- /** Free-form provider-specific info (HA version, MQTT broker
47203
- * flavour, latency, mTLS active, …). */
47204
- info: record(string(), unknown()),
47205
- /** Ms epoch of the last connection probe / status update. */
47206
- lastCheckedAt: number$1().nullable(),
47207
- /** Last error message, when `status` indicates failure. */
47208
- error: string().nullable()
47209
- });
47210
47253
  var RegistryStatusSchema = object({
47211
47254
  brokerCount: number$1().int().nonnegative(),
47212
47255
  connectedCount: number$1().int().nonnegative()
47213
47256
  });
47214
- /**
47215
- * One entry per `broker` provider — which addon provides which broker
47216
- * kind(s). The unified create picker reads this to know, for each
47217
- * (addonId, kind) pair the operator can pick, where to route the
47218
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
47219
- * `{ addonId }` selector). A provider only advertises a kind here when
47220
- * it implements `getSettingsSchema` for that kind (so the picker never
47221
- * offers a kind whose creation form can't be rendered).
47222
- */
47223
- var BrokerProviderInfoSchema = object({
47224
- /** Addon id of the `broker` provider this entry describes. */
47225
- addonId: string(),
47226
- /** Broker kinds this provider can create, with a display label. */
47227
- kinds: array(object({
47228
- kind: string(),
47229
- label: string(),
47230
- /**
47231
- * Relative hub path to start an interactive account link (OAuth popup).
47232
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
47233
- */
47234
- interactiveConnectPath: string().min(1).optional(),
47235
- /** Relative hub path to disconnect the linked account (same popup pattern). */
47236
- interactiveDisconnectPath: string().min(1).optional(),
47237
- /**
47238
- * The kind signs in through a server-held interactive session instead of
47239
- * (or before) typed credentials. `basePath` is a hub-relative route set
47240
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
47241
- * contract: `POST {basePath}/start` (body: the form's settings) answers
47242
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
47243
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
47244
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
47245
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
47246
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
47247
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
47248
- * offers it as the default path; the fields the session fills are
47249
- * marked `filledBySignIn` in the kind's settings schema.
47250
- *
47251
- * `methods` lists the ways the session can sign in, recommended first:
47252
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
47253
- * settings) answering `{ sessionId, userCode, verificationUrl,
47254
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
47255
- * any device, and the same `status` / `commit` / `cancel` routes follow
47256
- * (status then carries `method`, `userCode`, `verificationUrl`,
47257
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
47258
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
47259
- * `['proxy']`.
47260
- */
47261
- signIn: object({
47262
- basePath: string().min(1),
47263
- methods: array(_enum(["device-code", "proxy"])).optional()
47264
- }).optional()
47265
- }))
47266
- });
47267
- var ListInputSchema = object({
47268
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
47269
- * only HA brokers. Omit to list every broker. */
47270
- kind: string().optional() });
47271
47257
  var GetInputSchema = object({ id: string() });
47272
- var AddInputSchema = object({
47273
- kind: string().min(1),
47274
- name: string().min(1),
47275
- /**
47276
- * ADOPT an existing id instead of minting a new one.
47277
- *
47278
- * Written the day this cost an outage. A broker's id is not a detail: HA
47279
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
47280
- * lost from config and re-added as `ha_001` leaves every one of its devices
47281
- * bound to a broker that no longer exists. Re-entering the password under the
47282
- * ORIGINAL id turns a multi-step device migration back into re-entering a
47283
- * password.
47284
- *
47285
- * A provider MUST refuse an id that is already in use — adopting a live
47286
- * broker's id would silently take it over.
47287
- */
47288
- id: string().min(1).optional(),
47289
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
47290
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
47291
- * branch on receipt — invalid shape rejects the add. */
47292
- settings: record(string(), unknown())
47293
- });
47294
- var AddResultSchema = object({ id: string() });
47295
- var RemoveInputSchema = object({ id: string() });
47296
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
47297
- ok: literal(true),
47298
- latencyMs: number$1().nonnegative()
47299
- }), object({
47300
- ok: literal(false),
47301
- error: string()
47302
- })]);
47303
47258
  var SettingsRecordSchema$1 = record(string(), unknown());
47304
- var SettingsSchemaInputSchema = object({ kind: string() });
47305
- var TestSettingsInputSchema = object({
47306
- kind: string(),
47307
- settings: SettingsRecordSchema$1
47308
- });
47309
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
47310
- ok: literal(true),
47311
- latencyMs: number$1().nonnegative().optional()
47312
- }).strict(), object({
47313
- ok: literal(false),
47314
- error: string()
47315
- })]);
47316
- var SettingsSchemaResultSchema = unknown().nullable();
47317
47259
  var PublishInputSchema = object({
47318
47260
  brokerId: string(),
47319
47261
  /** Kind-specific routing target.
@@ -47357,51 +47299,12 @@ var brokerCapability = {
47357
47299
  kind: "poll"
47358
47300
  },
47359
47301
  methods: {
47360
- list: method(ListInputSchema, array(BrokerInfoSchema)),
47361
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
47362
- /** Enumerate which addon provides which broker kind(s) for the
47363
- * unified create picker. The auto-mount fans this array across
47364
- * every registered `broker` provider (array-output method), so the
47365
- * picker sees every kind from every provider in one call. */
47366
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
47367
- add: method(AddInputSchema, AddResultSchema, {
47368
- kind: "mutation",
47369
- auth: "admin"
47370
- }),
47371
- remove: method(RemoveInputSchema, _void(), {
47372
- kind: "mutation",
47373
- auth: "admin"
47374
- }),
47375
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
47376
- kind: "mutation",
47377
- auth: "admin"
47378
- }),
47379
- /** Read the persisted settings record for a broker (kind-specific
47380
- * shape). Admin-only — settings may contain secrets. Returns `null`
47381
- * when the broker id is unknown to the provider (the collection
47382
- * fallback may route a foreign id to the first provider). */
47383
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47384
- /** Overwrite the persisted settings record. The kind-specific
47385
- * provider validates the shape and applies the change (reconnects
47386
- * if credentials changed). */
47387
- setSettings: method(object({
47388
- id: string(),
47389
- settings: SettingsRecordSchema$1
47390
- }), _void(), {
47391
- kind: "mutation",
47392
- auth: "admin"
47393
- }),
47394
47302
  /** Returns the kind-specific connection config the consumer needs
47395
47303
  * to open its own client (MQTT pattern: `{url, username, password,
47396
47304
  * clientIdPrefix}`). HA providers MAY return the auth envelope
47397
47305
  * but typical HA consumers use `publish` / `subscribe` instead.
47398
47306
  * Returns `null` when the broker id is unknown to the provider. */
47399
47307
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47400
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
47401
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
47402
- kind: "mutation",
47403
- auth: "admin"
47404
- }),
47405
47308
  publish: method(PublishInputSchema, unknown(), {
47406
47309
  kind: "mutation",
47407
47310
  auth: "admin"
@@ -52248,7 +52151,14 @@ var DiscoveryCandidateSchema = object({
52248
52151
  /** `stableId` of the gateway candidate this device was found BEHIND. */
52249
52152
  parentStableId: string().optional(),
52250
52153
  /** `false` = shown for context but not pickable. Default true. */
52251
- selectable: boolean().optional()
52154
+ selectable: boolean().optional(),
52155
+ /**
52156
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
52157
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
52158
+ * admin asks for exactly these, on the creation form prefilled with
52159
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
52160
+ */
52161
+ needsParams: array(string()).optional()
52252
52162
  });
52253
52163
  /**
52254
52164
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -52375,8 +52285,14 @@ var deviceProviderCapability = {
52375
52285
  * provider does not support manual creation. Lets the Add-Device dialog
52376
52286
  * pick the right type instead of assuming Camera.
52377
52287
  */
52378
- getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
52379
- adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
52288
+ getManualCreationType: method(object({
52289
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52290
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })),
52291
+ adoptDiscoveredDevice: method(object({
52292
+ candidate: DiscoveryCandidateSchema,
52293
+ /** The integration the operator adopts into; a provider with several instances needs it. */
52294
+ integrationId: string().optional()
52295
+ }), DeviceSummarySchema, {
52380
52296
  kind: "mutation",
52381
52297
  auth: "admin"
52382
52298
  }),
@@ -52387,10 +52303,16 @@ var deviceProviderCapability = {
52387
52303
  * devices of that type. The output is a `ConfigUISchema` — the
52388
52304
  * router type-asserts it at the boundary.
52389
52305
  */
52390
- getChildCreationSchema: method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema),
52306
+ getChildCreationSchema: method(object({
52307
+ type: _enum(DeviceType),
52308
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52309
+ integrationId: string().optional()
52310
+ }), CreationSchemaOutputSchema),
52391
52311
  createDevice: method(object({
52392
52312
  type: _enum(DeviceType),
52393
- config: record(string(), unknown())
52313
+ config: record(string(), unknown()),
52314
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
52315
+ integrationId: string().optional()
52394
52316
  }), DeviceSummarySchema, {
52395
52317
  kind: "mutation",
52396
52318
  auth: "admin"
@@ -53115,7 +53037,10 @@ method(object({
53115
53037
  auth: "admin"
53116
53038
  }), method(object({
53117
53039
  addonId: string(),
53118
- type: _enum(DeviceType)
53040
+ type: _enum(DeviceType),
53041
+ /** The integration the form is for; a provider whose integrations differ (a hub
53042
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
53043
+ integrationId: string().optional()
53119
53044
  }), unknown().nullable()), method(object({
53120
53045
  addonId: string(),
53121
53046
  type: _enum(DeviceType),
@@ -53199,7 +53124,11 @@ method(object({
53199
53124
  kind: "mutation",
53200
53125
  auth: "admin",
53201
53126
  timeoutMs: 12e4
53202
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
53127
+ }), method(object({
53128
+ addonId: string(),
53129
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
53130
+ integrationId: string().optional()
53131
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
53203
53132
  deviceId: number$1(),
53204
53133
  key: string(),
53205
53134
  value: unknown()
@@ -56479,7 +56408,7 @@ var TestConnectionStatusEnum = _enum([
56479
56408
  "inconclusive",
56480
56409
  "unsupported"
56481
56410
  ]);
56482
- var TestConnectionResultSchema$1 = object({
56411
+ var TestConnectionResultSchema = object({
56483
56412
  /** True ONLY for `validated`. Never true for a test that did not run. */
56484
56413
  success: boolean(),
56485
56414
  error: string().optional(),
@@ -56524,7 +56453,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56524
56453
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56525
56454
  addonId: string(),
56526
56455
  settings: record(string(), unknown())
56527
- }), TestConnectionResultSchema$1, {
56456
+ }), TestConnectionResultSchema, {
56528
56457
  kind: "mutation",
56529
56458
  auth: "admin"
56530
56459
  });
@@ -56551,6 +56480,14 @@ var ConfigSchemaPassthrough$3 = custom();
56551
56480
  var IntegrationInstanceIdSchema = string().min(1);
56552
56481
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56553
56482
  /**
56483
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
56484
+ * instance is an integration that brings devices (the registry owns its on/off switch);
56485
+ * `services`: a connection configured from the addon's own function page, never listed on
56486
+ * Integrations.
56487
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
56488
+ */
56489
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
56490
+ /**
56554
56491
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56555
56492
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56556
56493
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -56561,6 +56498,22 @@ var IntegrationInstanceKindSchema = object({
56561
56498
  /** Stable id of the kind, sent back as `createInstance.kind`. */
56562
56499
  id: string().min(1),
56563
56500
  label: string().min(1),
56501
+ /**
56502
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
56503
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
56504
+ * provider with more than one section must name it on every kind.
56505
+ */
56506
+ functionCap: string().min(1),
56507
+ /**
56508
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
56509
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
56510
+ * finds the instances whose kind names it; absent takes the kind `id`.
56511
+ */
56512
+ brokerKind: string().min(1).optional(),
56513
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
56514
+ role: IntegrationInstanceRoleSchema.optional(),
56515
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
56516
+ canTest: boolean().optional(),
56564
56517
  /** The kind signs in through a server-held interactive session. */
56565
56518
  signIn: object({
56566
56519
  basePath: string().min(1),
@@ -56573,11 +56526,7 @@ var IntegrationInstanceKindSchema = object({
56573
56526
  });
56574
56527
  var IntegrationInstanceDescriptionSchema = object({
56575
56528
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56576
- role: _enum([
56577
- "devices",
56578
- "services",
56579
- "both"
56580
- ]),
56529
+ role: IntegrationInstanceRoleSchema,
56581
56530
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56582
56531
  discovery: object({
56583
56532
  mode: _enum([
@@ -56652,7 +56601,11 @@ var integrationInstancesCapability = {
56652
56601
  methods: {
56653
56602
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56654
56603
  /** The form; `id: null` is the create form. */
56655
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56604
+ instanceFormSchema: method(object({
56605
+ id: IntegrationInstanceIdSchema.nullable(),
56606
+ /** The kind of the create form (`id: null`) of a provider with several. */
56607
+ kind: string().min(1).optional()
56608
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56656
56609
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56657
56610
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56658
56611
  /**
@@ -56662,7 +56615,9 @@ var integrationInstancesCapability = {
56662
56615
  createInstance: method(object({
56663
56616
  id: IntegrationInstanceIdSchema.optional(),
56664
56617
  name: string().min(1),
56665
- kind: string().min(1).optional()
56618
+ kind: string().min(1).optional(),
56619
+ /** Form values written with the record (a link picked in the create dialog). */
56620
+ values: record(string(), unknown()).optional()
56666
56621
  }), object({ id: string() }), {
56667
56622
  kind: "mutation",
56668
56623
  auth: "admin"
@@ -56689,8 +56644,10 @@ var integrationInstancesCapability = {
56689
56644
  }),
56690
56645
  testInstance: optionalMethod(object({
56691
56646
  id: IntegrationInstanceIdSchema.nullable(),
56692
- values: record(string(), unknown())
56693
- }), TestConnectionResultSchema$1, {
56647
+ values: record(string(), unknown()),
56648
+ /** The kind to test when `id` is null and the provider has several. */
56649
+ kind: string().min(1).optional()
56650
+ }), TestConnectionResultSchema, {
56694
56651
  kind: "mutation",
56695
56652
  auth: "admin"
56696
56653
  })
@@ -59447,6 +59404,380 @@ var SystemMetricsSchema = object({
59447
59404
  gpuMemoryPercent: number$1().optional()
59448
59405
  });
59449
59406
  method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number$1().nullable()), method(object({ pids: array(number$1()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
59407
+ /**
59408
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
59409
+ * config documents, mirrors. The third storage API next to `ctx.settings`
59410
+ * (operator intent, D870) and device-history (numbers over time, D850).
59411
+ *
59412
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
59413
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
59414
+ * that with the addon id closed over). It is deliberately NOT in any input
59415
+ * schema, so a session can never supply it, and a call with no stamp is refused
59416
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
59417
+ * addons go through the owner's capability.
59418
+ *
59419
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
59420
+ * never `[]`. Secret fields are never on the wire: a read shows the
59421
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
59422
+ * resolves itself, and a write may carry only a keychain marker bound to the
59423
+ * row's own ref.
59424
+ *
59425
+ * Internal: addons reach it through `ctx.records`, never directly.
59426
+ */
59427
+ var RecordIdSchema = string().min(1).max(256);
59428
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
59429
+ var RecordBodySchema = record(string(), unknown());
59430
+ var RecordInputSchema = object({
59431
+ id: RecordIdSchema,
59432
+ /** The device this row belongs to (D865 removal, per-device reads). */
59433
+ deviceId: number$1().int().optional(),
59434
+ /** The domain clock, ms: observed / created / opened. */
59435
+ at: number$1().int().optional(),
59436
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
59437
+ expiresAt: number$1().int().optional(),
59438
+ data: RecordBodySchema
59439
+ });
59440
+ var RecordViewSchema = object({
59441
+ id: string(),
59442
+ deviceId: number$1().int().nullable(),
59443
+ at: number$1().nullable(),
59444
+ expiresAt: number$1().nullable(),
59445
+ rev: number$1().int(),
59446
+ updatedAt: number$1(),
59447
+ data: RecordBodySchema
59448
+ });
59449
+ /** Declared index fields only. Anything else does not compile, by design. */
59450
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
59451
+ string(),
59452
+ number$1(),
59453
+ boolean(),
59454
+ object({ in: array(union([
59455
+ string(),
59456
+ number$1(),
59457
+ boolean()
59458
+ ])).min(1).max(RECORD_BATCH_MAX) }),
59459
+ object({
59460
+ gt: union([string(), number$1()]).optional(),
59461
+ gte: union([string(), number$1()]).optional(),
59462
+ lt: union([string(), number$1()]).optional(),
59463
+ lte: union([string(), number$1()]).optional()
59464
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
59465
+ ]));
59466
+ var KindShape = { kind: RecordKindNameSchema };
59467
+ var TimeWindowShape = {
59468
+ deviceId: number$1().int().optional(),
59469
+ atFrom: number$1().optional(),
59470
+ atTo: number$1().optional()
59471
+ };
59472
+ var RecordQueryInputSchema = object({
59473
+ ...KindShape,
59474
+ ...TimeWindowShape,
59475
+ where: RecordWhereSchema.optional(),
59476
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
59477
+ order: _enum(["at", "id"]).optional(),
59478
+ desc: boolean().optional(),
59479
+ limit: number$1().int().min(1).max(RECORD_QUERY_MAX).default(100),
59480
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
59481
+ cursor: string().min(1).optional()
59482
+ });
59483
+ var RecordQueryResultSchema = object({
59484
+ records: array(RecordViewSchema),
59485
+ nextCursor: string().nullable()
59486
+ });
59487
+ var RecordKindStateSchema = _enum([
59488
+ "created",
59489
+ "unchanged",
59490
+ "updated"
59491
+ ]);
59492
+ var DeclareKindResultSchema = object({
59493
+ kindId: number$1().int(),
59494
+ state: RecordKindStateSchema,
59495
+ createdIndexes: array(string())
59496
+ });
59497
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
59498
+ var SecretRefsList = array(string());
59499
+ /**
59500
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
59501
+ * All ops of one call share ONE database file and ONE `transactFamily`.
59502
+ */
59503
+ var RecordTransactOpSchema = discriminatedUnion("op", [
59504
+ object({
59505
+ op: literal("put"),
59506
+ kind: RecordKindNameSchema,
59507
+ record: RecordInputSchema,
59508
+ ifRev: number$1().int().min(0).optional()
59509
+ }),
59510
+ object({
59511
+ op: literal("patch"),
59512
+ kind: RecordKindNameSchema,
59513
+ id: RecordIdSchema,
59514
+ data: RecordBodySchema,
59515
+ at: number$1().int().nullable().optional(),
59516
+ expiresAt: number$1().int().nullable().optional(),
59517
+ ifRev: number$1().int().min(1).optional()
59518
+ }),
59519
+ object({
59520
+ op: literal("delete"),
59521
+ kind: RecordKindNameSchema,
59522
+ id: RecordIdSchema
59523
+ })
59524
+ ]);
59525
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
59526
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
59527
+ kind: "mutation",
59528
+ auth: "admin"
59529
+ }), method(object({
59530
+ ...KindShape,
59531
+ id: RecordIdSchema
59532
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
59533
+ ...KindShape,
59534
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59535
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
59536
+ ...KindShape,
59537
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59538
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
59539
+ ...KindShape,
59540
+ record: RecordInputSchema,
59541
+ /** Compare-and-set. `0` means "the row must not exist". */
59542
+ ifRev: number$1().int().min(0).optional()
59543
+ }), object({
59544
+ rev: number$1().int(),
59545
+ releasedRefs: SecretRefsList
59546
+ }), {
59547
+ kind: "mutation",
59548
+ auth: "admin"
59549
+ }), method(object({
59550
+ ...KindShape,
59551
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
59552
+ }), object({
59553
+ count: number$1().int(),
59554
+ releasedRefs: SecretRefsList
59555
+ }), {
59556
+ kind: "mutation",
59557
+ auth: "admin"
59558
+ }), method(object({
59559
+ ...KindShape,
59560
+ id: RecordIdSchema,
59561
+ data: RecordBodySchema,
59562
+ at: number$1().int().nullable().optional(),
59563
+ expiresAt: number$1().int().nullable().optional(),
59564
+ ifRev: number$1().int().min(1).optional()
59565
+ }), object({
59566
+ rev: number$1().int(),
59567
+ releasedRefs: SecretRefsList
59568
+ }), {
59569
+ kind: "mutation",
59570
+ auth: "admin"
59571
+ }), method(object({
59572
+ ...KindShape,
59573
+ id: RecordIdSchema
59574
+ }), object({
59575
+ deleted: boolean(),
59576
+ secretRefs: SecretRefsList
59577
+ }), {
59578
+ kind: "mutation",
59579
+ auth: "admin"
59580
+ }), method(object({
59581
+ ...KindShape,
59582
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59583
+ }), object({
59584
+ deleted: number$1().int(),
59585
+ secretRefs: SecretRefsList
59586
+ }), {
59587
+ kind: "mutation",
59588
+ auth: "admin"
59589
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
59590
+ ...KindShape,
59591
+ ...TimeWindowShape,
59592
+ where: RecordWhereSchema.optional()
59593
+ }), object({ count: number$1().int() }), { auth: "admin" }), method(object({
59594
+ ...KindShape,
59595
+ deviceId: number$1().int().optional(),
59596
+ from: number$1(),
59597
+ to: number$1(),
59598
+ bucketMs: number$1().int().positive()
59599
+ }), object({ buckets: array(object({
59600
+ bucketStart: number$1(),
59601
+ count: number$1().int()
59602
+ })) }), { auth: "admin" }), method(object({
59603
+ ...KindShape,
59604
+ op: _enum([
59605
+ "sum",
59606
+ "min",
59607
+ "max",
59608
+ "count"
59609
+ ]),
59610
+ field: RecordFieldPathSchema.optional(),
59611
+ groupBy: RecordFieldPathSchema.optional(),
59612
+ where: RecordWhereSchema.optional()
59613
+ }), object({ rows: array(object({
59614
+ group: union([
59615
+ string(),
59616
+ number$1(),
59617
+ _null()
59618
+ ]),
59619
+ value: number$1().nullable()
59620
+ })) }), { auth: "admin" }), method(object({
59621
+ ...KindShape,
59622
+ deviceId: number$1().int().optional(),
59623
+ atBefore: number$1().optional(),
59624
+ where: RecordWhereSchema.optional()
59625
+ }), object({
59626
+ deleted: number$1().int(),
59627
+ secretRefs: SecretRefsList
59628
+ }), {
59629
+ kind: "mutation",
59630
+ auth: "admin"
59631
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
59632
+ results: array(object({
59633
+ rev: number$1().int().nullable(),
59634
+ deleted: boolean()
59635
+ })),
59636
+ releasedRefs: SecretRefsList,
59637
+ secretRefs: SecretRefsList
59638
+ }), {
59639
+ kind: "mutation",
59640
+ auth: "admin"
59641
+ }), method(object({
59642
+ deviceId: number$1().int(),
59643
+ kinds: array(RecordKindNameSchema).optional()
59644
+ }), object({
59645
+ deleted: record(string(), number$1().int()),
59646
+ secretRefs: SecretRefsList
59647
+ }), {
59648
+ kind: "mutation",
59649
+ auth: "admin"
59650
+ }), method(object(KindShape), object({ id: number$1().int() }), {
59651
+ kind: "mutation",
59652
+ auth: "admin"
59653
+ });
59654
+ /**
59655
+ * The wire shapes of the migration API, version 1 (planning#447).
59656
+ *
59657
+ * A migration is code that outlives the server it was written against: an addon at
59658
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
59659
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
59660
+ * is the contract of every method of it, one Zod pair per method.
59661
+ *
59662
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
59663
+ * field is never removed or made required, an enum never narrows, an output field is
59664
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
59665
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
59666
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
59667
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
59668
+ */
59669
+ var Rows = record(string(), unknown());
59670
+ var IntegrationId = object({ integrationId: string().min(1) });
59671
+ var RecordId = object({ id: string().min(1) });
59672
+ var RecordRowSchema = object({
59673
+ id: string(),
59674
+ deviceId: number$1().int().nullable(),
59675
+ at: number$1().nullable(),
59676
+ expiresAt: number$1().nullable(),
59677
+ rev: number$1().int(),
59678
+ updatedAt: number$1(),
59679
+ data: Rows
59680
+ });
59681
+ var RecordPutShape = {
59682
+ id: string().min(1),
59683
+ deviceId: number$1().int().optional(),
59684
+ at: number$1().int().optional(),
59685
+ expiresAt: number$1().int().optional(),
59686
+ data: Rows
59687
+ };
59688
+ var MigrationIntegrationSchema = object({
59689
+ id: string(),
59690
+ addonId: string(),
59691
+ name: string(),
59692
+ info: Rows,
59693
+ originStableId: string().optional()
59694
+ });
59695
+ var MigrationDeviceSchema = object({
59696
+ id: number$1().int(),
59697
+ integrationId: string().nullable().optional(),
59698
+ /** The owning device; null or absent for a root. */
59699
+ parentDeviceId: number$1().int().nullable().optional(),
59700
+ type: string().optional(),
59701
+ name: string().optional(),
59702
+ stableId: string().optional()
59703
+ });
59704
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
59705
+ var MigrationApiInfoSchema = object({
59706
+ version: number$1().int(),
59707
+ methods: array(string())
59708
+ });
59709
+ object({
59710
+ message: string(),
59711
+ meta: Rows.optional(),
59712
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
59713
+ tags: object({ deviceId: number$1().int().optional() }).optional()
59714
+ });
59715
+ object({
59716
+ owner: string().min(1),
59717
+ metas: array(MigrationMetaSchema)
59718
+ }), object({
59719
+ bootId: string(),
59720
+ snapshot: _enum([
59721
+ "none",
59722
+ "written",
59723
+ "refused"
59724
+ ]),
59725
+ steps: array(MigrationOwnerStepSchema),
59726
+ /** The migration API the engine serves; absent from a server that predates it. */
59727
+ migrationApi: MigrationApiInfoSchema.optional()
59728
+ }), object({ id: string().min(1) }), object({
59729
+ proceed: boolean(),
59730
+ state: union([literal("pending"), MigrationStatusSchema]),
59731
+ reason: string().nullable()
59732
+ }), object({
59733
+ id: string().min(1),
59734
+ result: MigrationOwnerResultSchema
59735
+ }), object({
59736
+ status: MigrationStatusSchema,
59737
+ error: string().nullable(),
59738
+ rowsChanged: number$1().int().nonnegative()
59739
+ }), object({
59740
+ rows: number$1().int().nonnegative(),
59741
+ secrets: number$1().int().nonnegative()
59742
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
59743
+ present: boolean(),
59744
+ removed: Rows
59745
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
59746
+ ...RecordPutShape,
59747
+ ifRev: number$1().int().optional()
59748
+ }), object({ rev: number$1().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number$1().int().nonnegative() }), RecordId.extend({
59749
+ partial: Rows,
59750
+ ifRev: number$1().int().optional()
59751
+ }), object({ rev: number$1().int() }), object({ deleted: boolean() }), object({
59752
+ deviceId: number$1().int().optional(),
59753
+ atFrom: number$1().optional(),
59754
+ atTo: number$1().optional(),
59755
+ where: RecordWhereSchema.optional(),
59756
+ limit: number$1().int().min(1).optional(),
59757
+ cursor: string().min(1).optional(),
59758
+ resolveSecrets: boolean().optional()
59759
+ }), object({
59760
+ records: array(RecordRowSchema),
59761
+ nextCursor: string().nullable()
59762
+ }), object({
59763
+ deviceId: number$1().int().optional(),
59764
+ atBefore: number$1().optional(),
59765
+ where: RecordWhereSchema.optional()
59766
+ }), object({ count: number$1().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
59767
+ ref: string().min(1),
59768
+ value: string()
59769
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number$1().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
59770
+ addonId: string().min(1),
59771
+ integrationId: string().min(1).optional()
59772
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number$1().int() }), object({
59773
+ id: number$1().int(),
59774
+ integrationId: string().nullable(),
59775
+ /** Whether the device carries any stored config; its content is not offered. */
59776
+ hasConfig: boolean()
59777
+ }).nullable(), object({
59778
+ deviceId: number$1().int(),
59779
+ integrationId: string().min(1)
59780
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
59450
59781
  method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
59451
59782
  id: string().min(1),
59452
59783
  error: string().min(1)
@@ -59476,7 +59807,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59476
59807
  "written",
59477
59808
  "refused"
59478
59809
  ]),
59479
- steps: array(MigrationOwnerStepSchema)
59810
+ steps: array(MigrationOwnerStepSchema),
59811
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
59812
+ migrationApi: MigrationApiInfoSchema.optional()
59480
59813
  }), {
59481
59814
  kind: "mutation",
59482
59815
  auth: "admin"
@@ -62143,7 +62476,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
62143
62476
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
62144
62477
  kind: "mutation",
62145
62478
  auth: "admin"
62146
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
62479
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
62480
+ kind: "query",
62481
+ auth: "admin"
62482
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
62147
62483
  kind: "query",
62148
62484
  auth: "admin"
62149
62485
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -64798,7 +65134,7 @@ var BrokerStatusSchema$1 = _enum([
64798
65134
  "unreachable",
64799
65135
  "tls-error"
64800
65136
  ]);
64801
- var BrokerInfoSchema$1 = object({
65137
+ var BrokerInfoSchema = object({
64802
65138
  id: string(),
64803
65139
  name: string(),
64804
65140
  url: string(),
@@ -64861,7 +65197,7 @@ var StatusSchema = object({
64861
65197
  brokerCount: number$1(),
64862
65198
  embeddedRunning: boolean()
64863
65199
  });
64864
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
65200
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
64865
65201
  kind: "mutation",
64866
65202
  auth: "admin"
64867
65203
  }), method(IdInputSchema, _void(), {
@@ -72043,253 +72379,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
72043
72379
  auth: "admin"
72044
72380
  });
72045
72381
  /**
72046
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
72047
- * config documents, mirrors. The third storage API next to `ctx.settings`
72048
- * (operator intent, D870) and device-history (numbers over time, D850).
72049
- *
72050
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
72051
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
72052
- * that with the addon id closed over). It is deliberately NOT in any input
72053
- * schema, so a session can never supply it, and a call with no stamp is refused
72054
- * `records/no-caller`. A kind is readable and writable only by its owner; other
72055
- * addons go through the owner's capability.
72056
- *
72057
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
72058
- * never `[]`. Secret fields are never on the wire: a read shows the
72059
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
72060
- * resolves itself, and a write may carry only a keychain marker bound to the
72061
- * row's own ref.
72062
- *
72063
- * Internal: addons reach it through `ctx.records`, never directly.
72064
- */
72065
- var RecordIdSchema = string().min(1).max(256);
72066
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
72067
- var RecordBodySchema = record(string(), unknown());
72068
- var RecordInputSchema = object({
72069
- id: RecordIdSchema,
72070
- /** The device this row belongs to (D865 removal, per-device reads). */
72071
- deviceId: number$1().int().optional(),
72072
- /** The domain clock, ms: observed / created / opened. */
72073
- at: number$1().int().optional(),
72074
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
72075
- expiresAt: number$1().int().optional(),
72076
- data: RecordBodySchema
72077
- });
72078
- var RecordViewSchema = object({
72079
- id: string(),
72080
- deviceId: number$1().int().nullable(),
72081
- at: number$1().nullable(),
72082
- expiresAt: number$1().nullable(),
72083
- rev: number$1().int(),
72084
- updatedAt: number$1(),
72085
- data: RecordBodySchema
72086
- });
72087
- /** Declared index fields only. Anything else does not compile, by design. */
72088
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
72089
- string(),
72090
- number$1(),
72091
- boolean(),
72092
- object({ in: array(union([
72093
- string(),
72094
- number$1(),
72095
- boolean()
72096
- ])).min(1).max(RECORD_BATCH_MAX) }),
72097
- object({
72098
- gt: union([string(), number$1()]).optional(),
72099
- gte: union([string(), number$1()]).optional(),
72100
- lt: union([string(), number$1()]).optional(),
72101
- lte: union([string(), number$1()]).optional()
72102
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
72103
- ]));
72104
- var KindShape = { kind: RecordKindNameSchema };
72105
- var TimeWindowShape = {
72106
- deviceId: number$1().int().optional(),
72107
- atFrom: number$1().optional(),
72108
- atTo: number$1().optional()
72109
- };
72110
- var RecordQueryInputSchema = object({
72111
- ...KindShape,
72112
- ...TimeWindowShape,
72113
- where: RecordWhereSchema.optional(),
72114
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
72115
- order: _enum(["at", "id"]).optional(),
72116
- desc: boolean().optional(),
72117
- limit: number$1().int().min(1).max(RECORD_QUERY_MAX).default(100),
72118
- /** Opaque keyset cursor from a previous answer. There is no offset. */
72119
- cursor: string().min(1).optional()
72120
- });
72121
- var RecordQueryResultSchema = object({
72122
- records: array(RecordViewSchema),
72123
- nextCursor: string().nullable()
72124
- });
72125
- var RecordKindStateSchema = _enum([
72126
- "created",
72127
- "unchanged",
72128
- "updated"
72129
- ]);
72130
- var DeclareKindResultSchema = object({
72131
- kindId: number$1().int(),
72132
- state: RecordKindStateSchema,
72133
- createdIndexes: array(string())
72134
- });
72135
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
72136
- var SecretRefsList = array(string());
72137
- /**
72138
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
72139
- * All ops of one call share ONE database file and ONE `transactFamily`.
72140
- */
72141
- var RecordTransactOpSchema = discriminatedUnion("op", [
72142
- object({
72143
- op: literal("put"),
72144
- kind: RecordKindNameSchema,
72145
- record: RecordInputSchema,
72146
- ifRev: number$1().int().min(0).optional()
72147
- }),
72148
- object({
72149
- op: literal("patch"),
72150
- kind: RecordKindNameSchema,
72151
- id: RecordIdSchema,
72152
- data: RecordBodySchema,
72153
- at: number$1().int().nullable().optional(),
72154
- expiresAt: number$1().int().nullable().optional(),
72155
- ifRev: number$1().int().min(1).optional()
72156
- }),
72157
- object({
72158
- op: literal("delete"),
72159
- kind: RecordKindNameSchema,
72160
- id: RecordIdSchema
72161
- })
72162
- ]);
72163
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
72164
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
72165
- kind: "mutation",
72166
- auth: "admin"
72167
- }), method(object({
72168
- ...KindShape,
72169
- id: RecordIdSchema
72170
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
72171
- ...KindShape,
72172
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72173
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
72174
- ...KindShape,
72175
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72176
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
72177
- ...KindShape,
72178
- record: RecordInputSchema,
72179
- /** Compare-and-set. `0` means "the row must not exist". */
72180
- ifRev: number$1().int().min(0).optional()
72181
- }), object({
72182
- rev: number$1().int(),
72183
- releasedRefs: SecretRefsList
72184
- }), {
72185
- kind: "mutation",
72186
- auth: "admin"
72187
- }), method(object({
72188
- ...KindShape,
72189
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
72190
- }), object({
72191
- count: number$1().int(),
72192
- releasedRefs: SecretRefsList
72193
- }), {
72194
- kind: "mutation",
72195
- auth: "admin"
72196
- }), method(object({
72197
- ...KindShape,
72198
- id: RecordIdSchema,
72199
- data: RecordBodySchema,
72200
- at: number$1().int().nullable().optional(),
72201
- expiresAt: number$1().int().nullable().optional(),
72202
- ifRev: number$1().int().min(1).optional()
72203
- }), object({
72204
- rev: number$1().int(),
72205
- releasedRefs: SecretRefsList
72206
- }), {
72207
- kind: "mutation",
72208
- auth: "admin"
72209
- }), method(object({
72210
- ...KindShape,
72211
- id: RecordIdSchema
72212
- }), object({
72213
- deleted: boolean(),
72214
- secretRefs: SecretRefsList
72215
- }), {
72216
- kind: "mutation",
72217
- auth: "admin"
72218
- }), method(object({
72219
- ...KindShape,
72220
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72221
- }), object({
72222
- deleted: number$1().int(),
72223
- secretRefs: SecretRefsList
72224
- }), {
72225
- kind: "mutation",
72226
- auth: "admin"
72227
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
72228
- ...KindShape,
72229
- ...TimeWindowShape,
72230
- where: RecordWhereSchema.optional()
72231
- }), object({ count: number$1().int() }), { auth: "admin" }), method(object({
72232
- ...KindShape,
72233
- deviceId: number$1().int().optional(),
72234
- from: number$1(),
72235
- to: number$1(),
72236
- bucketMs: number$1().int().positive()
72237
- }), object({ buckets: array(object({
72238
- bucketStart: number$1(),
72239
- count: number$1().int()
72240
- })) }), { auth: "admin" }), method(object({
72241
- ...KindShape,
72242
- op: _enum([
72243
- "sum",
72244
- "min",
72245
- "max",
72246
- "count"
72247
- ]),
72248
- field: RecordFieldPathSchema.optional(),
72249
- groupBy: RecordFieldPathSchema.optional(),
72250
- where: RecordWhereSchema.optional()
72251
- }), object({ rows: array(object({
72252
- group: union([
72253
- string(),
72254
- number$1(),
72255
- _null()
72256
- ]),
72257
- value: number$1().nullable()
72258
- })) }), { auth: "admin" }), method(object({
72259
- ...KindShape,
72260
- deviceId: number$1().int().optional(),
72261
- atBefore: number$1().optional(),
72262
- where: RecordWhereSchema.optional()
72263
- }), object({
72264
- deleted: number$1().int(),
72265
- secretRefs: SecretRefsList
72266
- }), {
72267
- kind: "mutation",
72268
- auth: "admin"
72269
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
72270
- results: array(object({
72271
- rev: number$1().int().nullable(),
72272
- deleted: boolean()
72273
- })),
72274
- releasedRefs: SecretRefsList,
72275
- secretRefs: SecretRefsList
72276
- }), {
72277
- kind: "mutation",
72278
- auth: "admin"
72279
- }), method(object({
72280
- deviceId: number$1().int(),
72281
- kinds: array(RecordKindNameSchema).optional()
72282
- }), object({
72283
- deleted: record(string(), number$1().int()),
72284
- secretRefs: SecretRefsList
72285
- }), {
72286
- kind: "mutation",
72287
- auth: "admin"
72288
- }), method(object(KindShape), object({ id: number$1().int() }), {
72289
- kind: "mutation",
72290
- auth: "admin"
72291
- });
72292
- /**
72293
72382
  * record-entries - the ONE admin surface for every list of user-created entries
72294
72383
  * (D881): notification targets, brokers, LLM profiles, anything an addon
72295
72384
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -82017,7 +82106,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82017
82106
  * `deviceClasses` map (first registered type). `null` when manual creation is
82018
82107
  * unsupported. Lets the Add-Device dialog pick the right type per provider.
82019
82108
  */
82020
- async getManualCreationType() {
82109
+ async getManualCreationType(_input) {
82021
82110
  if (!await this.supportsManualCreation()) return { deviceType: null };
82022
82111
  return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
82023
82112
  }
@@ -82028,7 +82117,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82028
82117
  return true;
82029
82118
  }
82030
82119
  async getChildCreationSchema(input) {
82031
- return this.onGetCreationSchema(input.type);
82120
+ return this.onGetCreationSchema(input.type, { integrationId: input.integrationId });
82032
82121
  }
82033
82122
  /**
82034
82123
  * Default kernel-orchestrated `createDevice` implementation. The
@@ -82040,7 +82129,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82040
82129
  * `deviceClasses` instead.
82041
82130
  */
82042
82131
  async createDevice(input) {
82043
- const spec = await this.onCreateDevice(input.type, input.config);
82132
+ const spec = await this.onCreateDevice(input.type, input.config, { ...input.integrationId !== void 0 ? { integrationId: input.integrationId } : {} });
82044
82133
  const Class = this.deviceClasses[spec.meta.type];
82045
82134
  if (!Class) throw new Error(`${this.providerName} provider: no device class registered for type "${spec.meta.type}" — add it to the deviceClasses map`);
82046
82135
  const stableId = this.generateStableId(spec.meta.type, spec.config);
@@ -83449,20 +83538,6 @@ Object.freeze({
83449
83538
  access: "create",
83450
83539
  role: "viewer"
83451
83540
  },
83452
- "broker.add": {
83453
- capName: "broker",
83454
- capScope: "system",
83455
- addonId: null,
83456
- access: "create",
83457
- role: "admin"
83458
- },
83459
- "broker.get": {
83460
- capName: "broker",
83461
- capScope: "system",
83462
- addonId: null,
83463
- access: "view",
83464
- role: "admin"
83465
- },
83466
83541
  "broker.getBrokerConfig": {
83467
83542
  capName: "broker",
83468
83543
  capScope: "system",
@@ -83470,20 +83545,6 @@ Object.freeze({
83470
83545
  access: "view",
83471
83546
  role: "admin"
83472
83547
  },
83473
- "broker.getSettings": {
83474
- capName: "broker",
83475
- capScope: "system",
83476
- addonId: null,
83477
- access: "view",
83478
- role: "admin"
83479
- },
83480
- "broker.getSettingsSchema": {
83481
- capName: "broker",
83482
- capScope: "system",
83483
- addonId: null,
83484
- access: "view",
83485
- role: "admin"
83486
- },
83487
83548
  "broker.getState": {
83488
83549
  capName: "broker",
83489
83550
  capScope: "system",
@@ -83498,20 +83559,6 @@ Object.freeze({
83498
83559
  access: "view",
83499
83560
  role: "admin"
83500
83561
  },
83501
- "broker.list": {
83502
- capName: "broker",
83503
- capScope: "system",
83504
- addonId: null,
83505
- access: "view",
83506
- role: "admin"
83507
- },
83508
- "broker.listProviders": {
83509
- capName: "broker",
83510
- capScope: "system",
83511
- addonId: null,
83512
- access: "view",
83513
- role: "admin"
83514
- },
83515
83562
  "broker.publish": {
83516
83563
  capName: "broker",
83517
83564
  capScope: "system",
@@ -83519,20 +83566,6 @@ Object.freeze({
83519
83566
  access: "create",
83520
83567
  role: "admin"
83521
83568
  },
83522
- "broker.remove": {
83523
- capName: "broker",
83524
- capScope: "system",
83525
- addonId: null,
83526
- access: "delete",
83527
- role: "admin"
83528
- },
83529
- "broker.setSettings": {
83530
- capName: "broker",
83531
- capScope: "system",
83532
- addonId: null,
83533
- access: "create",
83534
- role: "admin"
83535
- },
83536
83569
  "broker.subscribe": {
83537
83570
  capName: "broker",
83538
83571
  capScope: "system",
@@ -83540,20 +83573,6 @@ Object.freeze({
83540
83573
  access: "create",
83541
83574
  role: "admin"
83542
83575
  },
83543
- "broker.testConnection": {
83544
- capName: "broker",
83545
- capScope: "system",
83546
- addonId: null,
83547
- access: "create",
83548
- role: "admin"
83549
- },
83550
- "broker.testSettings": {
83551
- capName: "broker",
83552
- capScope: "system",
83553
- addonId: null,
83554
- access: "create",
83555
- role: "admin"
83556
- },
83557
83576
  "broker.unsubscribe": {
83558
83577
  capName: "broker",
83559
83578
  capScope: "system",
@@ -88517,6 +88536,13 @@ Object.freeze({
88517
88536
  access: "view",
88518
88537
  role: "viewer"
88519
88538
  },
88539
+ "pipelineAnalytics.getMediaLocationCensus": {
88540
+ capName: "pipeline-analytics",
88541
+ capScope: "device",
88542
+ addonId: null,
88543
+ access: "view",
88544
+ role: "admin"
88545
+ },
88520
88546
  "pipelineAnalytics.getMediaReclaimStatus": {
88521
88547
  capName: "pipeline-analytics",
88522
88548
  capScope: "device",
@@ -97767,6 +97793,15 @@ function rewriteLocation(location, currentUpstream, route) {
97767
97793
  /** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
97768
97794
  var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
97769
97795
  /**
97796
+ * Subresource Integrity pins the bytes of a script or stylesheet. The proxy rewrites vendor
97797
+ * URLs inside those bytes (and re-serves them from another origin), so the hash can no longer
97798
+ * match and the browser refuses the file: the page renders with no script at all. The hub
97799
+ * already drops the CSP that would have enforced the same pin; the attribute goes with it.
97800
+ */
97801
+ function stripIntegrity(html) {
97802
+ return html.replace(/\s+integrity\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
97803
+ }
97804
+ /**
97770
97805
  * Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
97771
97806
  * (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
97772
97807
  * HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
@@ -97782,6 +97817,7 @@ function rewriteBody(body, kind, currentUpstream, route) {
97782
97817
  const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
97783
97818
  out = out.replace(escaped, escapedBase);
97784
97819
  }
97820
+ if (kind === "html") out = stripIntegrity(out);
97785
97821
  const current = `${route.basePath}/${currentUpstream.host}/`;
97786
97822
  const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
97787
97823
  if (kind === "html") {
@@ -98406,6 +98442,179 @@ object({
98406
98442
  delay: number$1().nonnegative().optional(),
98407
98443
  commands: record(string(), unknown())
98408
98444
  });
98445
+ var BrokerInUseError = class extends Error {
98446
+ brokerId;
98447
+ users;
98448
+ name = "BrokerInUseError";
98449
+ constructor(brokerId, users) {
98450
+ super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
98451
+ this.brokerId = brokerId;
98452
+ this.users = users;
98453
+ }
98454
+ };
98455
+ /** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
98456
+ async function assertBrokerUnused(brokerId, finders) {
98457
+ const users = [];
98458
+ for (const finder of finders) users.push(...await finder.find(brokerId));
98459
+ if (users.length > 0) throw new BrokerInUseError(brokerId, users);
98460
+ }
98461
+ /**
98462
+ * An account provider whose `deleteInstance` is refused while anything still uses the account
98463
+ * (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
98464
+ * delete the instances page calls, not on a separate broker cap.
98465
+ */
98466
+ function guardBrokerDelete(base, users) {
98467
+ return {
98468
+ ...base,
98469
+ deleteInstance: async (input) => {
98470
+ await assertBrokerUnused(input.id, users());
98471
+ return base.deleteInstance(input);
98472
+ }
98473
+ };
98474
+ }
98475
+ /**
98476
+ * The generic binding of a service to a broker (D1012). A service (an integration today; a
98477
+ * notifier target, a calendar source or an exporter later) names the broker it runs through by id
98478
+ * and keeps only its OWN link settings: never a copy of the broker's host, sign-in or secrets,
98479
+ * which stay with the broker and are edited on the Brokers page.
98480
+ *
98481
+ * One primitive serves every consumer:
98482
+ * - {@link declareBrokerLinkKind}: the record kind, body `{ name, brokerId, ...linkFields }`;
98483
+ * - {@link buildBrokerLinkForm}: the form, a `broker-select` picker plus the service's link
98484
+ * sections, which may be DYNAMIC (a list read from the chosen broker, like HA calendars);
98485
+ * - {@link buildBrokerLinkInstances}: the `integration-instances` provider of an integration;
98486
+ * - {@link resolveBrokerId} / {@link brokerLinkUserFinder}: which broker a record names and which
98487
+ * records name a broker, so a delete of a broker still in use can name its users generically.
98488
+ */
98489
+ /** The record kind an integration is stored under. */
98490
+ var BROKER_LINK_KIND_NAME = "connections";
98491
+ /** What every broker-linked record carries; `brokerId` is always present, `''` is a row whose broker is not picked yet; link fields of the service come on top. */
98492
+ var BrokerLinkBodySchema = entryBody({ brokerId: string() });
98493
+ function declareBrokerLinkKind(spec) {
98494
+ return defineEntryKind(BROKER_LINK_KIND_NAME, {
98495
+ title: spec.title,
98496
+ schema: BrokerLinkBodySchema.extend(spec.linkFields ?? {}),
98497
+ writeTrigger: spec.writeTrigger
98498
+ });
98499
+ }
98500
+ /** The form: a picker over the brokers of `brokerKind`, then the service's link sections. */
98501
+ async function buildBrokerLinkForm(spec, brokerId) {
98502
+ const dynamic = brokerId !== void 0 && spec.dynamicSections !== void 0 ? await spec.dynamicSections(brokerId) : [];
98503
+ return { sections: [
98504
+ {
98505
+ id: "broker-link",
98506
+ title: spec.brokerLabel,
98507
+ description: `Each integration links to a ${spec.brokerLabel} connection (Connectivity → Brokers). The connection and its sign-in are edited there.`,
98508
+ columns: 1,
98509
+ fields: [{
98510
+ type: "broker-select",
98511
+ key: "brokerId",
98512
+ label: spec.brokerLabel,
98513
+ brokerKind: spec.brokerKind
98514
+ }]
98515
+ },
98516
+ ...spec.linkSections ?? [],
98517
+ ...dynamic
98518
+ ] };
98519
+ }
98520
+ /** The broker a record names; undefined while it names none. */
98521
+ function resolveBrokerId(record) {
98522
+ return record.brokerId === void 0 || record.brokerId === "" ? void 0 : record.brokerId;
98523
+ }
98524
+ /** The broker an integration links to, from its record. */
98525
+ async function brokerIdOf(instances, integrationId) {
98526
+ const body = await instances.getResolved(integrationId);
98527
+ return body === null ? void 0 : resolveBrokerId(body);
98528
+ }
98529
+ /** Every record of the kind that names `brokerId`, as users of that broker. */
98530
+ function brokerLinkUserFinder(instances, userKind = "integration") {
98531
+ return { find: async (brokerId) => (await instances.listResolved()).filter((entry) => resolveBrokerId(entry.body) === brokerId).map((entry) => ({
98532
+ kind: userKind,
98533
+ id: entry.id,
98534
+ name: entry.body.name
98535
+ })) };
98536
+ }
98537
+ /** The `integration-instances` provider of an addon whose integrations link to a broker. */
98538
+ function buildBrokerLinkInstances(records, spec) {
98539
+ const instances = new RecordIntegrationInstances(records, spec.kind, {
98540
+ description: {
98541
+ role: "devices",
98542
+ instanceMode: "multiple",
98543
+ kind: "connection",
98544
+ canTest: false
98545
+ },
98546
+ formSchema: async (id) => buildBrokerLinkForm(spec, id === null ? void 0 : await brokerIdOf(instances, id)),
98547
+ initialBody: (name) => spec.kind.schema.parse({
98548
+ name,
98549
+ brokerId: ""
98550
+ }),
98551
+ configured: (body) => resolveBrokerId(body) !== void 0,
98552
+ ...spec.onChanged !== void 0 ? { onChanged: spec.onChanged } : {}
98553
+ });
98554
+ return instances;
98555
+ }
98556
+ var BROKER_ROWS_MAX = 1e3;
98557
+ var STRAY_REGISTRY_ID = /^int_\d+$/;
98558
+ async function moveBrokerLinksToConnections(ports) {
98559
+ const brokers = await ports.listBrokers();
98560
+ const strays = brokers.filter((b) => STRAY_REGISTRY_ID.test(b.id));
98561
+ if (strays.length > 0) ports.warn(`${strays.length} broker row(s) are keyed like a registry integration and are left as they are: ${strays.map((s) => s.id).join(", ")}`);
98562
+ let moved = 0;
98563
+ for (const broker of brokers) {
98564
+ const integrationId = broker.integrationId;
98565
+ if (integrationId === void 0 || integrationId === "") continue;
98566
+ const existing = await ports.readConnection(integrationId);
98567
+ if (existing === null) await ports.putConnection(integrationId, {
98568
+ name: broker.name,
98569
+ brokerId: broker.id
98570
+ });
98571
+ else if (existing.brokerId !== broker.id) {
98572
+ ports.warn(`integration '${integrationId}' already names broker '${existing.brokerId ?? "none"}', not '${broker.id}'; the link stays on the broker`);
98573
+ continue;
98574
+ }
98575
+ if ((await ports.readConnection(integrationId))?.brokerId !== broker.id) throw new Error(`integration '${integrationId}': the link did not read back - broker '${broker.id}' keeps it`);
98576
+ await ports.clearBrokerLink(broker.id);
98577
+ moved += 1;
98578
+ }
98579
+ return {
98580
+ outcome: moved === 0 ? "already-done" : "done",
98581
+ summary: {
98582
+ brokers: brokers.length,
98583
+ moved,
98584
+ strayIntegrationRows: strays.length
98585
+ }
98586
+ };
98587
+ }
98588
+ /** The ports over the two record kinds of the addon. */
98589
+ function brokerLinkPortsOver(deps) {
98590
+ return {
98591
+ warn: deps.warn,
98592
+ listBrokers: async () => {
98593
+ return (await deps.brokers.query({ limit: BROKER_ROWS_MAX })).records.map((row) => {
98594
+ const name = row.data["name"];
98595
+ const link = row.data["integrationId"];
98596
+ return {
98597
+ id: row.id,
98598
+ name: typeof name === "string" && name.length > 0 ? name : row.id,
98599
+ integrationId: typeof link === "string" ? link : void 0
98600
+ };
98601
+ });
98602
+ },
98603
+ readConnection: async (id) => {
98604
+ const row = await deps.connections.get(id);
98605
+ return row === null ? null : { brokerId: row.data.brokerId };
98606
+ },
98607
+ putConnection: async (id, body) => {
98608
+ await deps.connections.put({
98609
+ id,
98610
+ data: body
98611
+ }, { ifRev: 0 });
98612
+ },
98613
+ clearBrokerLink: async (brokerId) => {
98614
+ await deps.brokers.patch(brokerId, { integrationId: null });
98615
+ }
98616
+ };
98617
+ }
98409
98618
  //#endregion
98410
98619
  //#region src/config.ts
98411
98620
  /**
@@ -98451,7 +98660,6 @@ var AlexaBrokerEntrySchema = object({
98451
98660
  id: string().min(1),
98452
98661
  name: string().min(1),
98453
98662
  connection: alexaConnectionSchema,
98454
- integrationId: string().optional(),
98455
98663
  /** Absent means on; the instances page switches a connection off without deleting it. */
98456
98664
  enabled: boolean().optional()
98457
98665
  });
@@ -142697,7 +142905,6 @@ var AlexaBrokerRegistry = class {
142697
142905
  #onStatesPolled;
142698
142906
  #makeManager;
142699
142907
  #managers = /* @__PURE__ */ new Map();
142700
- #integrationToBroker = /* @__PURE__ */ new Map();
142701
142908
  #nextId = 1;
142702
142909
  constructor(logger, deps = {}) {
142703
142910
  this.#logger = logger;
@@ -142748,7 +142955,6 @@ var AlexaBrokerRegistry = class {
142748
142955
  }
142749
142956
  }));
142750
142957
  this.#managers.clear();
142751
- this.#integrationToBroker.clear();
142752
142958
  }
142753
142959
  setOnBrokerConnected(cb) {
142754
142960
  this.#onBrokerConnected = cb;
@@ -142771,17 +142977,14 @@ var AlexaBrokerRegistry = class {
142771
142977
  const entry = {
142772
142978
  id,
142773
142979
  name,
142774
- connection,
142775
- ...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
142980
+ connection
142776
142981
  };
142777
142982
  await this.#startManager(entry);
142778
- if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
142779
142983
  return entry;
142780
142984
  }
142781
142985
  async removeEntry(id) {
142782
142986
  const mgr = this.#managers.get(id);
142783
142987
  if (!mgr) return;
142784
- for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
142785
142988
  this.#managers.delete(id);
142786
142989
  try {
142787
142990
  await mgr.stop();
@@ -142807,9 +143010,6 @@ var AlexaBrokerRegistry = class {
142807
143010
  if (!mgr) throw new Error(`AlexaBrokerRegistry: unknown broker id "${brokerId}"`);
142808
143011
  await mgr.sendCommand(command);
142809
143012
  }
142810
- linkIntegration(integrationId, brokerId) {
142811
- this.#integrationToBroker.set(integrationId, brokerId);
142812
- }
142813
143013
  list() {
142814
143014
  return Array.from(this.#managers.values()).map((m) => m.getInfo());
142815
143015
  }
@@ -142828,10 +143028,7 @@ var AlexaBrokerRegistry = class {
142828
143028
  return count;
142829
143029
  }
142830
143030
  async #startManager(entry) {
142831
- if (this.#managers.has(entry.id)) {
142832
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
142833
- return;
142834
- }
143031
+ if (this.#managers.has(entry.id)) return;
142835
143032
  const mgr = this.#makeManager({
142836
143033
  id: entry.id,
142837
143034
  name: entry.name,
@@ -142844,7 +143041,6 @@ var AlexaBrokerRegistry = class {
142844
143041
  onStatesPolled: (id, values) => this.#onStatesPolled(id, values)
142845
143042
  });
142846
143043
  this.#managers.set(entry.id, mgr);
142847
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
142848
143044
  await mgr.start();
142849
143045
  }
142850
143046
  /** The next free broker id, for a record minted before its manager exists. */
@@ -142908,40 +143104,15 @@ async function removeSessionFile(dataDir, brokerId) {
142908
143104
  await node_fs_promises.rm(sessionPath(dataDir, brokerId), { force: true });
142909
143105
  }
142910
143106
  //#endregion
142911
- //#region src/speak/constants.ts
142912
- /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
142913
- var ALEXA_SPEAK_ADDON_ID = "import-alexa";
142914
- var ALEXA_SPEAK_KIND = "alexa-echo";
142915
- /** The kind of the Amazon-account broker the Echoes are read from. */
142916
- var ALEXA_BROKER_KIND = "alexa";
142917
- var DEVICE_REFRESH_MS = 5 * 6e4;
142918
- /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
142919
- var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
142920
- /** How often an adopted Echo's player state is read (a push channel is a later phase). */
142921
- var ECHO_POLL_MS = 3e4;
142922
- /** One more read after a command, once Amazon has had time to apply it. */
142923
- var ECHO_REREAD_MS = 1500;
142924
- /** After a 429 the account is left alone for this long, doubling up to the cap. */
142925
- var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
142926
- var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
142927
- /**
142928
- * A command (volume, play, Do Not Disturb) that has not started within this, or not
142929
- * finished, is answered with an error instead of running minutes late.
142930
- */
142931
- var ECHO_COMMAND_DEADLINE_MS = 12e3;
142932
- /** A poll read is stale after this; it is dropped from the queue rather than sent late. */
142933
- var ECHO_READ_DEADLINE_MS = 25e3;
142934
- //#endregion
142935
143107
  //#region src/alexa-broker-provider.ts
142936
143108
  /**
142937
143109
  * Construct the `broker` cap provider for the Alexa import addon. Pure builder.
142938
- * Mirrors `buildEcowittBrokerProvider`. The cookie + refresh token are secrets:
142939
- * redacted on settings reads, preserved on an empty round-trip. State sync is
142940
- * poll-based (no pub/sub) — `publish`/`subscribe`/`getState` are no-ops.
143110
+ * The bus half only (D1013): creating, listing, editing and removing an account is
143111
+ * `integration-instances`. The cookie + refresh token are secrets, redacted on the config
143112
+ * read. State sync is poll-based (no pub/sub) — `publish`/`subscribe`/`getState` are no-ops.
142941
143113
  */
142942
143114
  function buildAlexaBrokerProvider(deps) {
142943
- const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
142944
- const owns = (id) => getBrokers().some((b) => b.id === id);
143115
+ const { registry, getBrokers } = deps;
142945
143116
  const redact = (entry) => ({
142946
143117
  ...entry.connection,
142947
143118
  cookie: "",
@@ -142949,108 +143120,10 @@ function buildAlexaBrokerProvider(deps) {
142949
143120
  macDms: ""
142950
143121
  });
142951
143122
  return {
142952
- list: async ({ kind }) => {
142953
- if (kind && kind !== "alexa") return [];
142954
- return registry.list().map((info) => ({
142955
- ...info,
142956
- addonId: ownerAddonId
142957
- }));
142958
- },
142959
- get: async ({ id }) => {
142960
- const info = registry.get(id);
142961
- return info ? {
142962
- ...info,
142963
- addonId: ownerAddonId
142964
- } : null;
142965
- },
142966
- listProviders: async () => [{
142967
- addonId: ownerAddonId,
142968
- kinds: [{
142969
- kind: ALEXA_BROKER_KIND,
142970
- label: "Alexa",
142971
- signIn: {
142972
- basePath: "/addon/import-alexa/login",
142973
- methods: ["device-code", "proxy"]
142974
- }
142975
- }]
142976
- }],
142977
- add: async ({ kind, name, settings }) => {
142978
- if (kind !== "alexa") throw new Error(`import-alexa: only kind '${ALEXA_BROKER_KIND}' is handled here (got '${kind}')`);
142979
- const entry = await registry.createEntry(name, settingsToAlexaConnection(settings));
142980
- await persistBrokers([...getBrokers(), entry]);
142981
- return { id: entry.id };
142982
- },
142983
- remove: async ({ id }) => {
142984
- if (!owns(id)) return;
142985
- await registry.removeEntry(id);
142986
- await cascadeRemoveDevices(id).catch((err) => {
142987
- logger.warn("alexa: broker cascade-remove threw", {
142988
- tags: { brokerId: id },
142989
- meta: { error: errMsg(err) }
142990
- });
142991
- });
142992
- await persistBrokers(getBrokers().filter((b) => b.id !== id));
142993
- },
142994
- testConnection: async ({ id }) => {
142995
- if (!owns(id)) return {
142996
- ok: false,
142997
- error: "unknown broker"
142998
- };
142999
- const info = registry.get(id);
143000
- if (!info) return {
143001
- ok: false,
143002
- error: "unknown broker"
143003
- };
143004
- if (info.status === "connected") return {
143005
- ok: true,
143006
- latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
143007
- };
143008
- return {
143009
- ok: false,
143010
- error: info.error ?? `status: ${info.status}`
143011
- };
143012
- },
143013
- getSettings: async ({ id }) => {
143014
- const entry = getBrokers().find((b) => b.id === id);
143015
- return entry ? redact(entry) : null;
143016
- },
143017
- setSettings: async ({ id, settings }) => {
143018
- if (!owns(id)) return;
143019
- const existing = getBrokers().find((b) => b.id === id);
143020
- if (!existing) return;
143021
- const parsed = settingsToAlexaConnection(settings);
143022
- const merged = {
143023
- ...parsed,
143024
- cookie: parsed.cookie.length > 0 ? parsed.cookie : existing.connection.cookie,
143025
- refreshToken: parsed.refreshToken.length > 0 ? parsed.refreshToken : existing.connection.refreshToken,
143026
- macDms: parsed.macDms.length > 0 ? parsed.macDms : existing.connection.macDms
143027
- };
143028
- const updated = await registry.updateEntry(id, merged);
143029
- await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
143030
- },
143031
143123
  getBrokerConfig: async ({ id }) => {
143032
143124
  const entry = getBrokers().find((b) => b.id === id);
143033
143125
  return entry ? redact(entry) : null;
143034
143126
  },
143035
- getSettingsSchema: async ({ kind }) => {
143036
- if (kind !== "alexa") return null;
143037
- return buildConnectionFormSchema();
143038
- },
143039
- testSettings: async ({ kind, settings }) => {
143040
- if (kind !== "alexa") return {
143041
- ok: false,
143042
- error: `unsupported kind: ${kind}`
143043
- };
143044
- try {
143045
- settingsToAlexaConnection(settings);
143046
- return { ok: true };
143047
- } catch (err) {
143048
- return {
143049
- ok: false,
143050
- error: errMsg(err)
143051
- };
143052
- }
143053
- },
143054
143127
  publish: async () => null,
143055
143128
  subscribe: async () => ({ subscriptionId: "" }),
143056
143129
  unsubscribe: async () => void 0,
@@ -143376,7 +143449,7 @@ function buildAlexaAdoptionProvider(deps) {
143376
143449
  //#region src/alexa-broker-device-cascade.ts
143377
143450
  /**
143378
143451
  * Remove every adopted Alexa device whose persisted config carries
143379
- * `{ system: 'alexa', brokerId }`. Used on broker.remove and integration delete.
143452
+ * `{ system: 'alexa', brokerId }`. Used when an account instance is deleted and when an integration is deleted.
143380
143453
  * Alexa appliances are flat (standalone) devices, so this removes top-level
143381
143454
  * devices directly. Best-effort per device; returns the count removed. Mirrors
143382
143455
  * the Ecowitt cascade helper.
@@ -143964,6 +144037,30 @@ function withListedVolume(status, listed) {
143964
144037
  };
143965
144038
  }
143966
144039
  //#endregion
144040
+ //#region src/speak/constants.ts
144041
+ /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
144042
+ var ALEXA_SPEAK_ADDON_ID = "import-alexa";
144043
+ var ALEXA_SPEAK_KIND = "alexa-echo";
144044
+ /** The kind of the Amazon-account broker the Echoes are read from. */
144045
+ var ALEXA_BROKER_KIND = "alexa";
144046
+ var DEVICE_REFRESH_MS = 5 * 6e4;
144047
+ /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
144048
+ var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
144049
+ /** How often an adopted Echo's player state is read (a push channel is a later phase). */
144050
+ var ECHO_POLL_MS = 3e4;
144051
+ /** One more read after a command, once Amazon has had time to apply it. */
144052
+ var ECHO_REREAD_MS = 1500;
144053
+ /** After a 429 the account is left alone for this long, doubling up to the cap. */
144054
+ var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
144055
+ var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
144056
+ /**
144057
+ * A command (volume, play, Do Not Disturb) that has not started within this, or not
144058
+ * finished, is answered with an error instead of running minutes late.
144059
+ */
144060
+ var ECHO_COMMAND_DEADLINE_MS = 12e3;
144061
+ /** A poll read is stale after this; it is dropped from the queue rather than sent late. */
144062
+ var ECHO_READ_DEADLINE_MS = 25e3;
144063
+ //#endregion
143967
144064
  //#region src/speak/worker-protocol.ts
143968
144065
  /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
143969
144066
  var EchoDeviceSchema = object({
@@ -144340,6 +144437,7 @@ var alexaBrokersEntryKind = defineEntryKind("brokers", {
144340
144437
  title: "Alexa import brokers",
144341
144438
  schema: entryBody({
144342
144439
  ...alexaConnectionSchema.shape,
144440
+ /** Retired (D1012): the link lives on the integration's `connections` record. Only the migration reads it. */
144343
144441
  integrationId: string().optional(),
144344
144442
  enabled: boolean().optional()
144345
144443
  }),
@@ -144351,20 +144449,27 @@ var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
144351
144449
  schema: entryBody({ session: string().default("").meta({ secret: true }) }),
144352
144450
  writeTrigger: "an Echo worker signs in as an Alexa account broker"
144353
144451
  });
144452
+ /** An Alexa integration: the link to the Alexa account (broker) it imports devices from. */
144453
+ var alexaConnectionsEntryKind = declareBrokerLinkKind({
144454
+ title: "Alexa integrations",
144455
+ writeTrigger: "an operator links an Alexa integration to an Alexa account"
144456
+ });
144354
144457
  /** Every kind this addon declares, keyed by its name: `records.declare` refuses any other key. */
144355
144458
  var alexaRecordKinds = {
144356
144459
  brokers: alexaBrokersEntryKind,
144460
+ connections: alexaConnectionsEntryKind,
144357
144461
  "speak-sessions": alexaSpeakSessionsEntryKind
144358
144462
  };
144359
144463
  //#endregion
144360
- //#region src/alexa-instances.ts
144464
+ //#region src/alexa-account-instances.ts
144361
144465
  /**
144362
- * The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
144363
- * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
144466
+ * The Alexa accounts as `integration-instances` (planning#408 P3, D1013): one instance per `brokers`
144467
+ * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves; the `account` kind of
144468
+ * the composed provider (the integrations are the `integration` kind, `alexa-instances.ts`). The body is flat, so
144364
144469
  * the form the instances page edits is the stored shape. Signing in (device code or the Amazon
144365
144470
  * login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
144366
144471
  */
144367
- var ALEXA_INSTANCE_KIND = "alexa";
144472
+ var ALEXA_ACCOUNT_KIND = "account";
144368
144473
  var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
144369
144474
  function isAlexaBodyConfigured(body) {
144370
144475
  return body.cookie.length > 0 || body.refreshToken.length > 0;
@@ -144392,21 +144497,13 @@ function statusOf(info, configured) {
144392
144497
  ...info.error !== null ? { detail: info.error } : {}
144393
144498
  };
144394
144499
  }
144395
- function buildAlexaInstances(records, deps) {
144500
+ function buildAlexaAccountInstances(records, deps) {
144396
144501
  return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
144397
144502
  description: {
144398
- role: "both",
144503
+ role: "services",
144399
144504
  instanceMode: "multiple",
144400
144505
  kind: "account",
144401
144506
  canTest: true,
144402
- kinds: [{
144403
- id: ALEXA_INSTANCE_KIND,
144404
- label: "Alexa",
144405
- signIn: {
144406
- basePath: ALEXA_SIGN_IN_PATH,
144407
- methods: ["device-code", "proxy"]
144408
- }
144409
- }],
144410
144507
  allowRestoreId: true
144411
144508
  },
144412
144509
  formSchema: () => buildConnectionFormSchema(),
@@ -144419,7 +144516,7 @@ function buildAlexaInstances(records, deps) {
144419
144516
  pollIntervalMs: 6e4
144420
144517
  }),
144421
144518
  mintId: () => deps.mintId(),
144422
- kindOf: () => ALEXA_INSTANCE_KIND,
144519
+ kindOf: () => ALEXA_ACCOUNT_KIND,
144423
144520
  health: (body, id) => {
144424
144521
  const info = deps.infoOf(id);
144425
144522
  return {
@@ -144448,6 +144545,21 @@ function buildAlexaInstances(records, deps) {
144448
144545
  });
144449
144546
  }
144450
144547
  //#endregion
144548
+ //#region src/alexa-instances.ts
144549
+ /**
144550
+ * The Alexa integrations as `integration-instances` (D1012): one `connections` record per
144551
+ * integration, keyed by the registry id, whose body is the link to an Alexa account (a `brokers`
144552
+ * record, edited on the Brokers page). The account's sign-in and secrets are never copied here.
144553
+ */
144554
+ function buildAlexaInstances(records) {
144555
+ return buildBrokerLinkInstances(records, {
144556
+ kind: alexaConnectionsEntryKind,
144557
+ brokerKind: ALEXA_BROKER_KIND,
144558
+ brokerLabel: "Alexa account",
144559
+ onChanged: () => void 0
144560
+ });
144561
+ }
144562
+ //#endregion
144451
144563
  //#region src/alexa-broker-store.ts
144452
144564
  function rowToEntry(id, data) {
144453
144565
  const parsed = AlexaBrokerEntrySchema.safeParse({
@@ -144460,7 +144572,6 @@ function rowToEntry(id, data) {
144460
144572
  macDms: data.macDms,
144461
144573
  pollIntervalMs: data.pollIntervalMs
144462
144574
  },
144463
- integrationId: data.integrationId,
144464
144575
  enabled: data.enabled
144465
144576
  });
144466
144577
  return parsed.success ? parsed.data : null;
@@ -144497,11 +144608,10 @@ var AlexaBrokerStore = class AlexaBrokerStore {
144497
144608
  const nextIds = new Set(entries.map((e) => e.id));
144498
144609
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
144499
144610
  for (const entry of entries) {
144500
- const { id, name, connection, integrationId, enabled } = entry;
144611
+ const { id, name, connection, enabled } = entry;
144501
144612
  const body = {
144502
144613
  name,
144503
144614
  ...connection,
144504
- ...integrationId === void 0 ? {} : { integrationId },
144505
144615
  ...enabled === void 0 ? {} : { enabled }
144506
144616
  };
144507
144617
  const parsed = alexaBrokersEntryKind.schema.safeParse(body);
@@ -146084,6 +146194,21 @@ async function flattenBrokerRecords(ports) {
146084
146194
  });
146085
146195
  }
146086
146196
  //#endregion
146197
+ //#region src/migrations/broker-links-to-connections.ts
146198
+ var BROKER_LINKS_TO_CONNECTIONS_ID = "import-alexa/2026-11-05-broker-links-to-connections";
146199
+ /** The broker records and the integration (`connections`) records of this addon. */
146200
+ function alexaBrokerLinkPorts(records, warn) {
146201
+ return brokerLinkPortsOver({
146202
+ brokers: records.kind({
146203
+ name: alexaBrokersEntryKind.name,
146204
+ declaration: alexaBrokersEntryKind.declaration,
146205
+ schema: record(string(), unknown())
146206
+ }),
146207
+ connections: records.kind(alexaConnectionsEntryKind),
146208
+ warn
146209
+ });
146210
+ }
146211
+ //#endregion
146087
146212
  //#region src/migrations/strip-echo-target-volume-and-mode.ts
146088
146213
  var RETIRED_KEYS = ["volume", "mode"];
146089
146214
  function isRecord(value) {
@@ -146166,8 +146291,30 @@ var ADDON_MIGRATIONS = [
146166
146291
  fixtureMark: "the name of the broker and the integration it already names"
146167
146292
  },
146168
146293
  procedure: moveAlexaIntegrationRows
146294
+ },
146295
+ {
146296
+ meta: {
146297
+ id: BROKER_LINKS_TO_CONNECTIONS_ID,
146298
+ owner: "import-alexa",
146299
+ level: "addon-state",
146300
+ seq: 4,
146301
+ dependsOn: [INTEGRATION_ROWS_TO_RECORD_ID],
146302
+ store: "settings",
146303
+ scope: "cluster",
146304
+ onFailure: "block-owner",
146305
+ bodyKind: "procedure",
146306
+ description: "Write each integration a connections record naming its Alexa account (keyed by the integration id), then clear the integration link on the account; stray registry-keyed account rows are counted and logged, never deleted",
146307
+ fixtureMark: "the name and the extra field of the account that keeps its record"
146308
+ },
146309
+ procedure: moveBrokerLinksToConnections
146169
146310
  }
146170
146311
  ];
146312
+ /** The migrations that rewrite the broker records: they run BEFORE anything reads or persists one. */
146313
+ var BROKER_RECORD_MIGRATION_IDS = [
146314
+ FLATTEN_BROKER_RECORDS_ID,
146315
+ INTEGRATION_ROWS_TO_RECORD_ID,
146316
+ BROKER_LINKS_TO_CONNECTIONS_ID
146317
+ ];
146171
146318
  //#endregion
146172
146319
  //#region src/speak/icon-routes.ts
146173
146320
  var ALEXA_ICON_SVG = "<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#00CAFF\"><title>Amazon Alexa</title><path d=\"M12 0C5.37 0 0 5.37 0 12C0 18.09 4.53 23.11 10.4 23.9V21.5A1.59 1.59 0 0 0 9.32 19.97A8.41 8.41 0 0 1 3.6 11.8A8.37 8.37 0 0 1 12.09 3.6A8.4 8.4 0 0 1 20.4 12.31L20.39 12.38A8.68 8.68 0 0 1 20.36 12.76C20.36 12.83 20.35 12.9 20.34 12.96C20.34 13.04 20.33 13.12 20.32 13.19L20.3 13.29C19.27 20.07 10.45 23.87 10.4 23.9C10.92 23.97 11.46 24 12 24C18.63 24 24 18.63 24 12S18.63 0 12 0Z\"/></svg>";
@@ -146266,6 +146413,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146266
146413
  loginProxy = null;
146267
146414
  brokerStore = null;
146268
146415
  instances = null;
146416
+ accountInstances = null;
146269
146417
  activeBrokers = [];
146270
146418
  speak = null;
146271
146419
  constructor() {
@@ -146277,12 +146425,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146277
146425
  if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
146278
146426
  await records.declare(alexaRecordKinds);
146279
146427
  this.brokerStore = AlexaBrokerStore.of(records);
146280
- this.instances = buildAlexaInstances(records, {
146428
+ this.instances = buildAlexaInstances(records);
146429
+ this.accountInstances = buildAlexaAccountInstances(records, {
146281
146430
  mintId: () => this.requireRegistry().allocateId(),
146282
146431
  infoOf: (id) => this.registry?.get(id) ?? null,
146283
146432
  onChanged: (id) => this.syncBrokerRuntime(id)
146284
146433
  });
146285
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
146434
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: BROKER_RECORD_MIGRATION_IDS });
146286
146435
  this.activeBrokers = await this.loadPersistedBrokers();
146287
146436
  await this.adoptSessionFiles();
146288
146437
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
@@ -146317,15 +146466,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146317
146466
  });
146318
146467
  this.deviceCodeLogin = new AlexaDeviceCodeManager({ logger: this.ctx.logger });
146319
146468
  this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
146320
- await this.reconcileIntegrationsToBrokers();
146321
- this.subscribeIntegrationLifecycle();
146322
146469
  const loginRoutes = [...buildAlexaLoginRoutes({
146323
146470
  logger: this.ctx.logger,
146324
146471
  proxyLogin: this.proxyLogin,
146325
146472
  deviceCode: this.deviceCodeLogin,
146326
146473
  commitCookie: (input) => this.commitCapturedCookie(input)
146327
146474
  }), ...speakIconRoutes()];
146328
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
146475
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => !BROKER_RECORD_MIGRATION_IDS.includes(id)) });
146329
146476
  const speak = new AlexaSpeakController({
146330
146477
  logger: this.ctx.logger.child("speak"),
146331
146478
  readConfig: () => this.config,
@@ -146357,7 +146504,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146357
146504
  },
146358
146505
  {
146359
146506
  capability: integrationInstancesCapability,
146360
- provider: this.requireInstances().toProvider()
146507
+ provider: this.buildInstancesProvider()
146361
146508
  },
146362
146509
  {
146363
146510
  capability: deviceAdoptionCapability,
@@ -146382,6 +146529,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146382
146529
  await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
146383
146530
  },
146384
146531
  ...flattenPortsOver(records.kind(alexaBrokersEntryKind)),
146532
+ ...alexaBrokerLinkPorts(records, (message) => this.ctx.logger.warn(message)),
146385
146533
  ...alexaIntegrationRowsPorts({
146386
146534
  api: defaultIntegrationApiOver(this.ctx.api),
146387
146535
  records,
@@ -146390,10 +146538,42 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146390
146538
  })
146391
146539
  };
146392
146540
  }
146541
+ /** One provider over the two record kinds: the account (broker) and the integrations using it. */
146542
+ buildInstancesProvider() {
146543
+ if (this.accountInstances === null) throw new Error("import-alexa: account instances not ready");
146544
+ return composeInstanceKinds([{
146545
+ kind: {
146546
+ id: "account",
146547
+ label: "Alexa account",
146548
+ functionCap: "broker",
146549
+ brokerKind: ALEXA_BROKER_KIND,
146550
+ role: "services",
146551
+ signIn: {
146552
+ basePath: ALEXA_SIGN_IN_PATH,
146553
+ methods: ["device-code", "proxy"]
146554
+ }
146555
+ },
146556
+ provider: guardBrokerDelete(this.accountInstances.toProvider(), () => this.brokerUsers())
146557
+ }, {
146558
+ kind: {
146559
+ id: "integration",
146560
+ label: "Alexa",
146561
+ functionCap: "device-provider",
146562
+ role: "devices"
146563
+ },
146564
+ provider: this.requireInstances().toProvider()
146565
+ }], {
146566
+ role: "devices",
146567
+ instanceMode: "multiple",
146568
+ kind: "account",
146569
+ canTest: true,
146570
+ allowRestoreId: true
146571
+ });
146572
+ }
146393
146573
  /**
146394
- * Bring the runtime in line with one record after the instances page changed it: a removed
146395
- * record stops its manager and cascades its devices, an off record only stops the manager, a
146396
- * live one re-applies its connection or starts under the record's own id.
146574
+ * Bring the runtime in line with one account record after the instances page changed it: a
146575
+ * removed record stops its manager and cascades its devices, an off record only stops the
146576
+ * manager, a live one re-applies its connection or starts under the record's own id.
146397
146577
  */
146398
146578
  async syncBrokerRuntime(id) {
146399
146579
  const reg = this.requireRegistry();
@@ -146414,10 +146594,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146414
146594
  try {
146415
146595
  if (reg.get(id) !== null) {
146416
146596
  if (!sameConnection(entry.connection, reg.getConnection(id))) await reg.updateEntry(id, entry.connection);
146417
- } else await reg.createEntry(entry.name, entry.connection, {
146418
- id,
146419
- ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
146420
- });
146597
+ } else await reg.createEntry(entry.name, entry.connection, { id });
146421
146598
  } catch (err) {
146422
146599
  this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
146423
146600
  brokerId: id,
@@ -146636,12 +146813,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146636
146813
  }
146637
146814
  buildBrokerProvider() {
146638
146815
  return buildAlexaBrokerProvider({
146639
- ownerAddonId: this.ctx.id,
146640
146816
  registry: this.requireRegistry(),
146641
- getBrokers: () => this.activeBrokers,
146642
- persistBrokers: (brokers) => this.persistBrokerEntries(brokers),
146643
- cascadeRemoveDevices: (brokerId) => this.cascadeRemoveDevicesForBroker(brokerId),
146644
- logger: this.ctx.logger
146817
+ getBrokers: () => this.activeBrokers
146645
146818
  });
146646
146819
  }
146647
146820
  /**
@@ -146678,6 +146851,14 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146678
146851
  } });
146679
146852
  return { brokerId: entry.id };
146680
146853
  }
146854
+ /** Who still points at an Alexa account: its integrations and the Echo targets that speak through it. */
146855
+ brokerUsers() {
146856
+ return [brokerLinkUserFinder(this.requireInstances()), { find: async (brokerId) => readEchoTargets(this.config).filter((target) => target.brokerId === brokerId).map((target) => ({
146857
+ kind: "notification-target",
146858
+ id: target.id,
146859
+ name: target.name
146860
+ })) }];
146861
+ }
146681
146862
  async cascadeRemoveDevicesForBroker(brokerId) {
146682
146863
  const reg = this.ctx.kernel.deviceRegistry;
146683
146864
  const devices = this.ctx.kernel.devices;
@@ -146690,42 +146871,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146690
146871
  logger: this.ctx.logger
146691
146872
  });
146692
146873
  }
146693
- async reconcileIntegrationsToBrokers() {
146694
- try {
146695
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
146696
- const surviving = new Set(mine.map((i) => i.id));
146697
- for (const broker of this.activeBrokers) {
146698
- const integrationId = broker.integrationId;
146699
- if (integrationId === void 0 || !surviving.has(integrationId)) continue;
146700
- this.requireRegistry().linkIntegration(integrationId, broker.id);
146701
- }
146702
- const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
146703
- if (toRemove.length === 0) return;
146704
- for (const id of toRemove) try {
146705
- await this.requireRegistry().removeEntry(id);
146706
- await this.cascadeRemoveDevicesForBroker(id);
146707
- } catch (err) {
146708
- this.ctx.logger.warn("Alexa integration→broker: broker cleanup failed", { meta: {
146709
- brokerId: id,
146710
- error: errMsg(err)
146711
- } });
146712
- }
146713
- const nextBrokers = this.activeBrokers.filter((b) => !toRemove.includes(b.id));
146714
- await this.persistBrokerEntries(nextBrokers);
146715
- } catch (err) {
146716
- this.ctx.logger.warn("Alexa integration→broker reconcile failed", { meta: { error: errMsg(err) } });
146717
- }
146718
- }
146719
- subscribeIntegrationLifecycle() {
146720
- const handler = (event) => {
146721
- const addonId = event.data["addonId"];
146722
- if (typeof addonId === "string" && addonId !== this.ctx.id) return;
146723
- this.reconcileIntegrationsToBrokers();
146724
- };
146725
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
146726
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
146727
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
146728
- }
146729
146874
  buildAdoptionProvider() {
146730
146875
  return buildAlexaAdoptionProvider({
146731
146876
  registry: {
@@ -146739,9 +146884,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146739
146884
  },
146740
146885
  logger: this.ctx.logger,
146741
146886
  getBrokerIdForIntegration: async (id) => {
146742
- const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
146743
- if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
146744
- return broker.id;
146887
+ const brokerId = await brokerIdOf(this.requireInstances(), id);
146888
+ if (brokerId === void 0) throw new Error(`integration ${id} has no broker`);
146889
+ return brokerId;
146745
146890
  },
146746
146891
  listAdoptedAlexa: async () => {
146747
146892
  const reg = this.ctx.kernel.deviceRegistry;