@camstack/addon-import-alexa 0.2.189 → 0.2.191

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.mjs CHANGED
@@ -6042,7 +6042,7 @@ function number(params) {
6042
6042
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6043
6043
  }
6044
6044
  //#endregion
6045
- //#region ../types/dist/sleep-DEZaLbiu.mjs
6045
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
6046
6046
  /**
6047
6047
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6048
6048
  * window to float samples (D455).
@@ -7671,6 +7671,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7671
7671
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7672
7672
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7673
7673
  /**
7674
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7675
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7676
+ * through `api` (guard `check-migration-ports-surface`).
7677
+ */
7678
+ function migrationSet(catalogue, bindPorts) {
7679
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7680
+ }
7681
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7682
+ function refuseMixed(form, kept, dropped) {
7683
+ 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`);
7684
+ }
7685
+ function catalogueOf(list) {
7686
+ const kept = list.filter((entry) => !("execute" in entry));
7687
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7688
+ return kept;
7689
+ }
7690
+ function boundOf(list) {
7691
+ const kept = list.filter((entry) => "execute" in entry);
7692
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7693
+ return kept;
7694
+ }
7695
+ /**
7674
7696
  * Base class for CamStack addons. Eliminates settings boilerplate:
7675
7697
  *
7676
7698
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7837,23 +7859,21 @@ var BaseAddon = class {
7837
7859
  await this.loadScopedSchemas();
7838
7860
  return normalized;
7839
7861
  }
7862
+ async runMigrations(first, second, third) {
7863
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7864
+ return this.runBound(boundOf(first), second);
7865
+ }
7840
7866
  /**
7841
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7842
- * engine. Call it at the point of the boot where the data must already have the new
7843
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7844
- * restored devices — with the catalogue bound to the addon's ports
7845
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7846
- *
7847
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7848
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7849
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7850
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7851
- *
7852
- * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7853
- * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7854
- * omitted applied ids "newer").
7867
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7868
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7869
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7855
7870
  */
7856
- async runMigrations(migrations, options) {
7871
+ async runMigrationSets(sets, options) {
7872
+ const api = this.ctx.migrationApi;
7873
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7874
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7875
+ }
7876
+ async runBound(migrations, options) {
7857
7877
  const api = this.ctx.api.migrations;
7858
7878
  const report = await runAddonMigrations({
7859
7879
  declareOwner: (input) => api.declareOwner.mutate(input),
@@ -13555,6 +13575,39 @@ function buildCapturingReply(envelope) {
13555
13575
  return wrapper;
13556
13576
  }
13557
13577
  /**
13578
+ * The link primitive's declaration half (D1013 rule 5): what a link field names and how a kind
13579
+ * records it. Split from `instance-link.ts` so `RecordIntegrationInstances` can read the links of
13580
+ * its kind without importing the builders that import it.
13581
+ */
13582
+ /** The body field of a link: the id of the instance it names; the target rides in the field meta. */
13583
+ function linkField(target) {
13584
+ return string().meta({ link: target });
13585
+ }
13586
+ function linkTargetOf(meta) {
13587
+ if (typeof meta !== "object" || meta === null) return void 0;
13588
+ const link = Reflect.get(meta, "link");
13589
+ const parsed = LinkTargetSchema.safeParse(link);
13590
+ return parsed.success ? parsed.data : void 0;
13591
+ }
13592
+ var LinkTargetSchema = object({
13593
+ cap: string().min(1),
13594
+ kind: string().min(1).optional()
13595
+ });
13596
+ /** The links a kind declares, read from the meta of its body fields. */
13597
+ function linksOfKind(kind) {
13598
+ const schema = kind.schema;
13599
+ if (!(schema instanceof ZodObject)) return [];
13600
+ const links = [];
13601
+ for (const [field, fieldSchema] of Object.entries(schema.shape)) {
13602
+ const to = linkTargetOf(globalRegistry.get(fieldSchema));
13603
+ if (to !== void 0) links.push({
13604
+ field,
13605
+ to
13606
+ });
13607
+ }
13608
+ return links;
13609
+ }
13610
+ /**
13558
13611
  * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13559
13612
  * entry kind, so an addon that holds a connection serves it in ~30 lines.
13560
13613
  *
@@ -13675,30 +13728,29 @@ var RecordIntegrationInstances = class {
13675
13728
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13676
13729
  };
13677
13730
  }
13678
- describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13679
- ...this.hooks.description,
13680
- mintsId: true
13731
+ describe = async () => {
13732
+ const declared = linksOfKind(this.kind);
13733
+ const links = this.hooks.description.links !== void 0 || declared.length === 0 ? {} : { links: declared.map((link) => ({
13734
+ kind: this.kind.name,
13735
+ field: link.field,
13736
+ to: link.to
13737
+ })) };
13738
+ return {
13739
+ ...this.hooks.description,
13740
+ ...links,
13741
+ ...this.hooks.mintId === void 0 ? {} : { mintsId: true }
13742
+ };
13681
13743
  };
13682
- /**
13683
- * The record an id names. A caller holding the `integrations` registry id (the Integrations
13684
- * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13685
- * record id answers as it is. An id that names nothing comes back unchanged.
13686
- */
13687
- async resolveRecordId(id) {
13688
- if (await this.handle.get(id) !== null) return id;
13689
- const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13690
- return linked === void 0 ? id : linked.id;
13691
- }
13692
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13744
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13693
13745
  listInstances = async () => {
13694
13746
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13695
13747
  };
13696
13748
  getInstance = async ({ id }) => {
13697
- const record = await this.handle.get(await this.resolveRecordId(id));
13749
+ const record = await this.handle.get(id);
13698
13750
  return record === null ? null : this.viewOf(record);
13699
13751
  };
13700
13752
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13701
- createInstance = async ({ id, name, kind }) => {
13753
+ createInstance = async ({ id, name, kind, values }) => {
13702
13754
  const minted = id ?? this.hooks.mintId?.({
13703
13755
  name,
13704
13756
  ...kind !== void 0 ? { kind } : {}
@@ -13708,11 +13760,20 @@ var RecordIntegrationInstances = class {
13708
13760
  id: minted,
13709
13761
  data: this.hooks.initialBody(name, kind)
13710
13762
  }, { ifRev: 0 });
13763
+ const patch = this.patchOf(values ?? {});
13764
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13711
13765
  await this.hooks.onChanged?.(minted, "created");
13712
13766
  return { id: minted };
13713
13767
  };
13714
- updateInstance = async ({ id: askedId, name, values }) => {
13715
- const id = await this.resolveRecordId(askedId);
13768
+ updateInstance = async ({ id, name, values }) => {
13769
+ const patch = this.patchOf(values);
13770
+ if (name !== void 0) patch[this.nameField] = name;
13771
+ await this.handle.patch(id, patch);
13772
+ await this.hooks.onChanged?.(id, "updated");
13773
+ return { ok: true };
13774
+ };
13775
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13776
+ patchOf(values) {
13716
13777
  const patch = {};
13717
13778
  for (const [key, value] of Object.entries(values)) {
13718
13779
  if (value === void 0) continue;
@@ -13723,11 +13784,8 @@ var RecordIntegrationInstances = class {
13723
13784
  else if (isSecretClear(value)) patch[key] = null;
13724
13785
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13725
13786
  }
13726
- if (name !== void 0) patch[this.nameField] = name;
13727
- await this.handle.patch(id, patch);
13728
- await this.hooks.onChanged?.(id, "updated");
13729
- return { ok: true };
13730
- };
13787
+ return patch;
13788
+ }
13731
13789
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13732
13790
  setInstanceEnabled = async ({ id, enabled }) => {
13733
13791
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -13747,10 +13805,7 @@ var RecordIntegrationInstances = class {
13747
13805
  status: "unsupported",
13748
13806
  error: "This integration declares no connection test"
13749
13807
  };
13750
- return this.hooks.test(input.id === null ? input : {
13751
- ...input,
13752
- id: await this.resolveRecordId(input.id)
13753
- });
13808
+ return this.hooks.test(input);
13754
13809
  };
13755
13810
  /** The flat provider object to hand to `registerProvider`. */
13756
13811
  toProvider() {
@@ -13767,6 +13822,80 @@ var RecordIntegrationInstances = class {
13767
13822
  };
13768
13823
  }
13769
13824
  };
13825
+ /** No part holds the id a call named. */
13826
+ var UnknownInstanceError = class extends Error {
13827
+ instanceId;
13828
+ constructor(instanceId) {
13829
+ super(`Unknown instance '${instanceId}'`);
13830
+ this.instanceId = instanceId;
13831
+ this.name = "UnknownInstanceError";
13832
+ }
13833
+ };
13834
+ /** A create or a form named no kind, or a kind no part serves. */
13835
+ var UnknownInstanceKindError = class extends Error {
13836
+ kindId;
13837
+ constructor(kindId) {
13838
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13839
+ this.kindId = kindId;
13840
+ this.name = "UnknownInstanceKindError";
13841
+ }
13842
+ };
13843
+ function composeInstanceKinds(parts, base) {
13844
+ const byKind = (kindId) => {
13845
+ if (kindId === void 0) {
13846
+ const only = parts.length === 1 ? parts[0] : void 0;
13847
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13848
+ return only;
13849
+ }
13850
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13851
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13852
+ return part;
13853
+ };
13854
+ const holderOf = async (id) => {
13855
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13856
+ throw new UnknownInstanceError(id);
13857
+ };
13858
+ const tagged = (part, instance) => ({
13859
+ ...instance,
13860
+ kind: part.kind.id
13861
+ });
13862
+ return {
13863
+ describe: async () => {
13864
+ const links = (await Promise.all(parts.map(async (part) => (await part.provider.describe()).links ?? []))).flat();
13865
+ return {
13866
+ ...base,
13867
+ kinds: parts.map((part) => part.kind),
13868
+ ...links.length > 0 ? { links } : {}
13869
+ };
13870
+ },
13871
+ instanceFormSchema: async ({ id, kind }) => {
13872
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13873
+ },
13874
+ listInstances: async () => {
13875
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13876
+ },
13877
+ getInstance: async ({ id }) => {
13878
+ for (const part of parts) {
13879
+ const found = await part.provider.getInstance({ id });
13880
+ if (found !== null) return tagged(part, found);
13881
+ }
13882
+ return null;
13883
+ },
13884
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13885
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13886
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13887
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13888
+ testInstance: async (input) => {
13889
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13890
+ if (test === void 0) return {
13891
+ success: false,
13892
+ status: "inconclusive",
13893
+ error: "This kind cannot be tested"
13894
+ };
13895
+ return test(input);
13896
+ }
13897
+ };
13898
+ }
13770
13899
  var EncodeProfileSchema = object({
13771
13900
  video: object({
13772
13901
  codec: _enum([
@@ -16047,6 +16176,39 @@ var UnstampedEventMediaCountSchema = object({
16047
16176
  /** Sum across both, or `null` when either lane could not be counted. */
16048
16177
  total: number$1().int().nonnegative().nullable()
16049
16178
  }).nullable();
16179
+ /**
16180
+ * Where the stored event media sits, per storage location: how many media rows
16181
+ * and how many bytes carry each location id, how many carry none (legacy,
16182
+ * resolved to the default `eventMedia` location), and how many name a location
16183
+ * that was not asked about (a deleted or never-listed one).
16184
+ *
16185
+ * One covering-index aggregate per asked location plus the NULL one, and one
16186
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
16187
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
16188
+ * it is a deleted location only when every location id was asked.
16189
+ * `null` is "could not be measured", never zero.
16190
+ */
16191
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
16192
+ var MediaLocationCensusSchema = object({
16193
+ locations: array(object({
16194
+ locationId: string(),
16195
+ rows: number$1().int().nonnegative(),
16196
+ bytes: number$1().nonnegative()
16197
+ })).readonly(),
16198
+ unstamped: object({
16199
+ rows: number$1().int().nonnegative(),
16200
+ bytes: number$1().nonnegative()
16201
+ }),
16202
+ /** Rows stamped with a location id that was not in the asked list. */
16203
+ unlisted: object({
16204
+ rows: number$1().int().nonnegative(),
16205
+ bytes: number$1().nonnegative()
16206
+ }),
16207
+ total: object({
16208
+ rows: number$1().int().nonnegative(),
16209
+ bytes: number$1().nonnegative()
16210
+ })
16211
+ });
16050
16212
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
16051
16213
  /** The independently selectable logical storage classes — every class
16052
16214
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -47124,52 +47286,7 @@ onBrightnessChanged: { data: object({
47124
47286
  minIntervalMs: 0
47125
47287
  } } }
47126
47288
  };
47127
- /**
47128
- * `broker` — unified pub/sub broker registry, system-scoped collection.
47129
- *
47130
- * The cap models any kind-tagged message broker the user wants to
47131
- * register and that other addons might consume. The first two kinds
47132
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
47133
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
47134
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
47135
- * the same surface.
47136
- *
47137
- * Why one cap, not one cap per kind:
47138
- * - The integrations page wants a single table of "every broker"
47139
- * across kinds; a unified cap drives it without join logic.
47140
- * - Generic admin operations (add / remove / test / status) are
47141
- * identical across kinds — duplicating them per cap was busywork.
47142
- * - The `network-access` cap precedent: same interface, many
47143
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
47144
- *
47145
- * What stays kind-specific:
47146
- * - `getBrokerConfig` payload (kind decides whether it returns
47147
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
47148
- * `Record<string, unknown>` at the wire; consumers narrow.
47149
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
47150
- * key) — also typed as `Record<string, unknown>` so each kind can
47151
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
47152
- * without rebreaking the cap signature.
47153
- * - `add` `settings` payload (different fields per kind, validated by
47154
- * the kind-specific provider).
47155
- *
47156
- * Bidirectionality:
47157
- * - `publish` is RPC — synchronous result (provider-defined).
47158
- * - `subscribe` returns a subscription handle; the actual message
47159
- * stream flows over the typed event-bus as `broker.message` events
47160
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
47161
- * event handler. Subscriptions persist across reconnects — the
47162
- * provider re-subscribes upstream after a transport drop.
47163
- *
47164
- * Why event-bus push (not a subscription RPC stream):
47165
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
47166
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
47167
- * drop is recoverable via `getState({key})`).
47168
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
47169
- * - Cross-process: events already route through `$event-bus`; no
47170
- * per-broker wire-shaping needed.
47171
- */
47172
- var BrokerStatusEnum = _enum([
47289
+ _enum([
47173
47290
  "connected",
47174
47291
  "disconnected",
47175
47292
  "connecting",
@@ -47177,141 +47294,12 @@ var BrokerStatusEnum = _enum([
47177
47294
  "unreachable",
47178
47295
  "error"
47179
47296
  ]);
47180
- var BrokerInfoSchema = object({
47181
- /** Stable broker id. Persisted; survives addon restarts. */
47182
- id: string(),
47183
- /** Addon id of the provider that OWNS this broker.
47184
- *
47185
- * The `broker` cap is a system-scoped collection: several addons
47186
- * register a `broker` provider, each owning a DISJOINT set of
47187
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
47188
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
47189
- * device belongs to one integration. The admin UI threads this id
47190
- * back as the `{ addonId }` system-collection selector on every
47191
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
47192
- * `testConnection`), so the call routes to the OWNING provider
47193
- * instead of defaulting to the first-registered one. */
47194
- addonId: string(),
47195
- /** Human-readable name (operator-chosen at add-time). */
47196
- name: string(),
47197
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
47198
- kind: string(),
47199
- status: BrokerStatusEnum,
47200
- /** Free-form provider-specific info (HA version, MQTT broker
47201
- * flavour, latency, mTLS active, …). */
47202
- info: record(string(), unknown()),
47203
- /** Ms epoch of the last connection probe / status update. */
47204
- lastCheckedAt: number$1().nullable(),
47205
- /** Last error message, when `status` indicates failure. */
47206
- error: string().nullable()
47207
- });
47208
47297
  var RegistryStatusSchema = object({
47209
47298
  brokerCount: number$1().int().nonnegative(),
47210
47299
  connectedCount: number$1().int().nonnegative()
47211
47300
  });
47212
- /**
47213
- * One entry per `broker` provider — which addon provides which broker
47214
- * kind(s). The unified create picker reads this to know, for each
47215
- * (addonId, kind) pair the operator can pick, where to route the
47216
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
47217
- * `{ addonId }` selector). A provider only advertises a kind here when
47218
- * it implements `getSettingsSchema` for that kind (so the picker never
47219
- * offers a kind whose creation form can't be rendered).
47220
- */
47221
- var BrokerProviderInfoSchema = object({
47222
- /** Addon id of the `broker` provider this entry describes. */
47223
- addonId: string(),
47224
- /** Broker kinds this provider can create, with a display label. */
47225
- kinds: array(object({
47226
- kind: string(),
47227
- label: string(),
47228
- /**
47229
- * Relative hub path to start an interactive account link (OAuth popup).
47230
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
47231
- */
47232
- interactiveConnectPath: string().min(1).optional(),
47233
- /** Relative hub path to disconnect the linked account (same popup pattern). */
47234
- interactiveDisconnectPath: string().min(1).optional(),
47235
- /**
47236
- * The kind signs in through a server-held interactive session instead of
47237
- * (or before) typed credentials. `basePath` is a hub-relative route set
47238
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
47239
- * contract: `POST {basePath}/start` (body: the form's settings) answers
47240
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
47241
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
47242
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
47243
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
47244
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
47245
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
47246
- * offers it as the default path; the fields the session fills are
47247
- * marked `filledBySignIn` in the kind's settings schema.
47248
- *
47249
- * `methods` lists the ways the session can sign in, recommended first:
47250
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
47251
- * settings) answering `{ sessionId, userCode, verificationUrl,
47252
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
47253
- * any device, and the same `status` / `commit` / `cancel` routes follow
47254
- * (status then carries `method`, `userCode`, `verificationUrl`,
47255
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
47256
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
47257
- * `['proxy']`.
47258
- */
47259
- signIn: object({
47260
- basePath: string().min(1),
47261
- methods: array(_enum(["device-code", "proxy"])).optional()
47262
- }).optional()
47263
- }))
47264
- });
47265
- var ListInputSchema = object({
47266
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
47267
- * only HA brokers. Omit to list every broker. */
47268
- kind: string().optional() });
47269
47301
  var GetInputSchema = object({ id: string() });
47270
- var AddInputSchema = object({
47271
- kind: string().min(1),
47272
- name: string().min(1),
47273
- /**
47274
- * ADOPT an existing id instead of minting a new one.
47275
- *
47276
- * Written the day this cost an outage. A broker's id is not a detail: HA
47277
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
47278
- * lost from config and re-added as `ha_001` leaves every one of its devices
47279
- * bound to a broker that no longer exists. Re-entering the password under the
47280
- * ORIGINAL id turns a multi-step device migration back into re-entering a
47281
- * password.
47282
- *
47283
- * A provider MUST refuse an id that is already in use — adopting a live
47284
- * broker's id would silently take it over.
47285
- */
47286
- id: string().min(1).optional(),
47287
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
47288
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
47289
- * branch on receipt — invalid shape rejects the add. */
47290
- settings: record(string(), unknown())
47291
- });
47292
- var AddResultSchema = object({ id: string() });
47293
- var RemoveInputSchema = object({ id: string() });
47294
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
47295
- ok: literal(true),
47296
- latencyMs: number$1().nonnegative()
47297
- }), object({
47298
- ok: literal(false),
47299
- error: string()
47300
- })]);
47301
47302
  var SettingsRecordSchema$1 = record(string(), unknown());
47302
- var SettingsSchemaInputSchema = object({ kind: string() });
47303
- var TestSettingsInputSchema = object({
47304
- kind: string(),
47305
- settings: SettingsRecordSchema$1
47306
- });
47307
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
47308
- ok: literal(true),
47309
- latencyMs: number$1().nonnegative().optional()
47310
- }).strict(), object({
47311
- ok: literal(false),
47312
- error: string()
47313
- })]);
47314
- var SettingsSchemaResultSchema = unknown().nullable();
47315
47303
  var PublishInputSchema = object({
47316
47304
  brokerId: string(),
47317
47305
  /** Kind-specific routing target.
@@ -47355,51 +47343,12 @@ var brokerCapability = {
47355
47343
  kind: "poll"
47356
47344
  },
47357
47345
  methods: {
47358
- list: method(ListInputSchema, array(BrokerInfoSchema)),
47359
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
47360
- /** Enumerate which addon provides which broker kind(s) for the
47361
- * unified create picker. The auto-mount fans this array across
47362
- * every registered `broker` provider (array-output method), so the
47363
- * picker sees every kind from every provider in one call. */
47364
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
47365
- add: method(AddInputSchema, AddResultSchema, {
47366
- kind: "mutation",
47367
- auth: "admin"
47368
- }),
47369
- remove: method(RemoveInputSchema, _void(), {
47370
- kind: "mutation",
47371
- auth: "admin"
47372
- }),
47373
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
47374
- kind: "mutation",
47375
- auth: "admin"
47376
- }),
47377
- /** Read the persisted settings record for a broker (kind-specific
47378
- * shape). Admin-only — settings may contain secrets. Returns `null`
47379
- * when the broker id is unknown to the provider (the collection
47380
- * fallback may route a foreign id to the first provider). */
47381
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47382
- /** Overwrite the persisted settings record. The kind-specific
47383
- * provider validates the shape and applies the change (reconnects
47384
- * if credentials changed). */
47385
- setSettings: method(object({
47386
- id: string(),
47387
- settings: SettingsRecordSchema$1
47388
- }), _void(), {
47389
- kind: "mutation",
47390
- auth: "admin"
47391
- }),
47392
47346
  /** Returns the kind-specific connection config the consumer needs
47393
47347
  * to open its own client (MQTT pattern: `{url, username, password,
47394
47348
  * clientIdPrefix}`). HA providers MAY return the auth envelope
47395
47349
  * but typical HA consumers use `publish` / `subscribe` instead.
47396
47350
  * Returns `null` when the broker id is unknown to the provider. */
47397
47351
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47398
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
47399
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
47400
- kind: "mutation",
47401
- auth: "admin"
47402
- }),
47403
47352
  publish: method(PublishInputSchema, unknown(), {
47404
47353
  kind: "mutation",
47405
47354
  auth: "admin"
@@ -52246,7 +52195,14 @@ var DiscoveryCandidateSchema = object({
52246
52195
  /** `stableId` of the gateway candidate this device was found BEHIND. */
52247
52196
  parentStableId: string().optional(),
52248
52197
  /** `false` = shown for context but not pickable. Default true. */
52249
- selectable: boolean().optional()
52198
+ selectable: boolean().optional(),
52199
+ /**
52200
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
52201
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
52202
+ * admin asks for exactly these, on the creation form prefilled with
52203
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
52204
+ */
52205
+ needsParams: array(string()).optional()
52250
52206
  });
52251
52207
  /**
52252
52208
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -52373,8 +52329,14 @@ var deviceProviderCapability = {
52373
52329
  * provider does not support manual creation. Lets the Add-Device dialog
52374
52330
  * pick the right type instead of assuming Camera.
52375
52331
  */
52376
- getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
52377
- adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
52332
+ getManualCreationType: method(object({
52333
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52334
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })),
52335
+ adoptDiscoveredDevice: method(object({
52336
+ candidate: DiscoveryCandidateSchema,
52337
+ /** The integration the operator adopts into; a provider with several instances needs it. */
52338
+ integrationId: string().optional()
52339
+ }), DeviceSummarySchema, {
52378
52340
  kind: "mutation",
52379
52341
  auth: "admin"
52380
52342
  }),
@@ -52385,10 +52347,16 @@ var deviceProviderCapability = {
52385
52347
  * devices of that type. The output is a `ConfigUISchema` — the
52386
52348
  * router type-asserts it at the boundary.
52387
52349
  */
52388
- getChildCreationSchema: method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema),
52350
+ getChildCreationSchema: method(object({
52351
+ type: _enum(DeviceType),
52352
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52353
+ integrationId: string().optional()
52354
+ }), CreationSchemaOutputSchema),
52389
52355
  createDevice: method(object({
52390
52356
  type: _enum(DeviceType),
52391
- config: record(string(), unknown())
52357
+ config: record(string(), unknown()),
52358
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
52359
+ integrationId: string().optional()
52392
52360
  }), DeviceSummarySchema, {
52393
52361
  kind: "mutation",
52394
52362
  auth: "admin"
@@ -53113,7 +53081,10 @@ method(object({
53113
53081
  auth: "admin"
53114
53082
  }), method(object({
53115
53083
  addonId: string(),
53116
- type: _enum(DeviceType)
53084
+ type: _enum(DeviceType),
53085
+ /** The integration the form is for; a provider whose integrations differ (a hub
53086
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
53087
+ integrationId: string().optional()
53117
53088
  }), unknown().nullable()), method(object({
53118
53089
  addonId: string(),
53119
53090
  type: _enum(DeviceType),
@@ -53197,7 +53168,11 @@ method(object({
53197
53168
  kind: "mutation",
53198
53169
  auth: "admin",
53199
53170
  timeoutMs: 12e4
53200
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
53171
+ }), method(object({
53172
+ addonId: string(),
53173
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
53174
+ integrationId: string().optional()
53175
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
53201
53176
  deviceId: number$1(),
53202
53177
  key: string(),
53203
53178
  value: unknown()
@@ -56477,7 +56452,7 @@ var TestConnectionStatusEnum = _enum([
56477
56452
  "inconclusive",
56478
56453
  "unsupported"
56479
56454
  ]);
56480
- var TestConnectionResultSchema$1 = object({
56455
+ var TestConnectionResultSchema = object({
56481
56456
  /** True ONLY for `validated`. Never true for a test that did not run. */
56482
56457
  success: boolean(),
56483
56458
  error: string().optional(),
@@ -56522,7 +56497,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56522
56497
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56523
56498
  addonId: string(),
56524
56499
  settings: record(string(), unknown())
56525
- }), TestConnectionResultSchema$1, {
56500
+ }), TestConnectionResultSchema, {
56526
56501
  kind: "mutation",
56527
56502
  auth: "admin"
56528
56503
  });
@@ -56549,6 +56524,14 @@ var ConfigSchemaPassthrough$3 = custom();
56549
56524
  var IntegrationInstanceIdSchema = string().min(1);
56550
56525
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56551
56526
  /**
56527
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
56528
+ * instance is an integration that brings devices (the registry owns its on/off switch);
56529
+ * `services`: a connection configured from the addon's own function page, never listed on
56530
+ * Integrations.
56531
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
56532
+ */
56533
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
56534
+ /**
56552
56535
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56553
56536
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56554
56537
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -56559,6 +56542,22 @@ var IntegrationInstanceKindSchema = object({
56559
56542
  /** Stable id of the kind, sent back as `createInstance.kind`. */
56560
56543
  id: string().min(1),
56561
56544
  label: string().min(1),
56545
+ /**
56546
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
56547
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
56548
+ * provider with more than one section must name it on every kind.
56549
+ */
56550
+ functionCap: string().min(1),
56551
+ /**
56552
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
56553
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
56554
+ * finds the instances whose kind names it; absent takes the kind `id`.
56555
+ */
56556
+ brokerKind: string().min(1).optional(),
56557
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
56558
+ role: IntegrationInstanceRoleSchema.optional(),
56559
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
56560
+ canTest: boolean().optional(),
56562
56561
  /** The kind signs in through a server-held interactive session. */
56563
56562
  signIn: object({
56564
56563
  basePath: string().min(1),
@@ -56569,22 +56568,38 @@ var IntegrationInstanceKindSchema = object({
56569
56568
  /** Relative hub path to disconnect that account. */
56570
56569
  disconnectPath: string().min(1).optional()
56571
56570
  });
56571
+ /**
56572
+ * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
56573
+ * scans the network before any instance exists (the wizard offers the candidates); `account`: the
56574
+ * devices appear after the instance signs in. `scopedToInstance`: a discovery run belongs to ONE
56575
+ * instance and its candidates are claimed by it.
56576
+ */
56577
+ var IntegrationInstanceDiscoverySchema = object({
56578
+ mode: _enum([
56579
+ "none",
56580
+ "lan",
56581
+ "account"
56582
+ ]),
56583
+ scopedToInstance: boolean()
56584
+ });
56585
+ /**
56586
+ * One link a kind of this provider declares (D1013 rule 5): the body `field` of an instance of
56587
+ * `kind` holds the id of an instance of the function cap `to.cap` (of kind `to.kind`, when set).
56588
+ * The hub indexes these so the target's delete is refused while a link names it.
56589
+ */
56590
+ var IntegrationInstanceLinkSchema = object({
56591
+ kind: string().min(1),
56592
+ field: string().min(1),
56593
+ to: object({
56594
+ cap: string().min(1),
56595
+ kind: string().min(1).optional()
56596
+ })
56597
+ });
56572
56598
  var IntegrationInstanceDescriptionSchema = object({
56573
56599
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56574
- role: _enum([
56575
- "devices",
56576
- "services",
56577
- "both"
56578
- ]),
56600
+ role: IntegrationInstanceRoleSchema,
56579
56601
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56580
- discovery: object({
56581
- mode: _enum([
56582
- "none",
56583
- "lan",
56584
- "account"
56585
- ]),
56586
- scopedToInstance: boolean()
56587
- }).optional(),
56602
+ discovery: IntegrationInstanceDiscoverySchema.optional(),
56588
56603
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56589
56604
  instanceMode: IntegrationInstanceModeSchema,
56590
56605
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56605,7 +56620,9 @@ var IntegrationInstanceDescriptionSchema = object({
56605
56620
  * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56606
56621
  * send an id (the UI then mints an opaque one).
56607
56622
  */
56608
- mintsId: boolean().optional()
56623
+ mintsId: boolean().optional(),
56624
+ /** The links to other instances the provider's kinds declare; absent means none. */
56625
+ links: array(IntegrationInstanceLinkSchema).optional()
56609
56626
  });
56610
56627
  var IntegrationInstanceStatusSchema = object({
56611
56628
  state: _enum([
@@ -56650,7 +56667,11 @@ var integrationInstancesCapability = {
56650
56667
  methods: {
56651
56668
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56652
56669
  /** The form; `id: null` is the create form. */
56653
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56670
+ instanceFormSchema: method(object({
56671
+ id: IntegrationInstanceIdSchema.nullable(),
56672
+ /** The kind of the create form (`id: null`) of a provider with several. */
56673
+ kind: string().min(1).optional()
56674
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56654
56675
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56655
56676
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56656
56677
  /**
@@ -56660,7 +56681,9 @@ var integrationInstancesCapability = {
56660
56681
  createInstance: method(object({
56661
56682
  id: IntegrationInstanceIdSchema.optional(),
56662
56683
  name: string().min(1),
56663
- kind: string().min(1).optional()
56684
+ kind: string().min(1).optional(),
56685
+ /** Form values written with the record (a link picked in the create dialog). */
56686
+ values: record(string(), unknown()).optional()
56664
56687
  }), object({ id: string() }), {
56665
56688
  kind: "mutation",
56666
56689
  auth: "admin"
@@ -56687,8 +56710,10 @@ var integrationInstancesCapability = {
56687
56710
  }),
56688
56711
  testInstance: optionalMethod(object({
56689
56712
  id: IntegrationInstanceIdSchema.nullable(),
56690
- values: record(string(), unknown())
56691
- }), TestConnectionResultSchema$1, {
56713
+ values: record(string(), unknown()),
56714
+ /** The kind to test when `id` is null and the provider has several. */
56715
+ kind: string().min(1).optional()
56716
+ }), TestConnectionResultSchema, {
56692
56717
  kind: "mutation",
56693
56718
  auth: "admin"
56694
56719
  })
@@ -59445,6 +59470,380 @@ var SystemMetricsSchema = object({
59445
59470
  gpuMemoryPercent: number$1().optional()
59446
59471
  });
59447
59472
  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);
59473
+ /**
59474
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
59475
+ * config documents, mirrors. The third storage API next to `ctx.settings`
59476
+ * (operator intent, D870) and device-history (numbers over time, D850).
59477
+ *
59478
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
59479
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
59480
+ * that with the addon id closed over). It is deliberately NOT in any input
59481
+ * schema, so a session can never supply it, and a call with no stamp is refused
59482
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
59483
+ * addons go through the owner's capability.
59484
+ *
59485
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
59486
+ * never `[]`. Secret fields are never on the wire: a read shows the
59487
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
59488
+ * resolves itself, and a write may carry only a keychain marker bound to the
59489
+ * row's own ref.
59490
+ *
59491
+ * Internal: addons reach it through `ctx.records`, never directly.
59492
+ */
59493
+ var RecordIdSchema = string().min(1).max(256);
59494
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
59495
+ var RecordBodySchema = record(string(), unknown());
59496
+ var RecordInputSchema = object({
59497
+ id: RecordIdSchema,
59498
+ /** The device this row belongs to (D865 removal, per-device reads). */
59499
+ deviceId: number$1().int().optional(),
59500
+ /** The domain clock, ms: observed / created / opened. */
59501
+ at: number$1().int().optional(),
59502
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
59503
+ expiresAt: number$1().int().optional(),
59504
+ data: RecordBodySchema
59505
+ });
59506
+ var RecordViewSchema = object({
59507
+ id: string(),
59508
+ deviceId: number$1().int().nullable(),
59509
+ at: number$1().nullable(),
59510
+ expiresAt: number$1().nullable(),
59511
+ rev: number$1().int(),
59512
+ updatedAt: number$1(),
59513
+ data: RecordBodySchema
59514
+ });
59515
+ /** Declared index fields only. Anything else does not compile, by design. */
59516
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
59517
+ string(),
59518
+ number$1(),
59519
+ boolean(),
59520
+ object({ in: array(union([
59521
+ string(),
59522
+ number$1(),
59523
+ boolean()
59524
+ ])).min(1).max(RECORD_BATCH_MAX) }),
59525
+ object({
59526
+ gt: union([string(), number$1()]).optional(),
59527
+ gte: union([string(), number$1()]).optional(),
59528
+ lt: union([string(), number$1()]).optional(),
59529
+ lte: union([string(), number$1()]).optional()
59530
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
59531
+ ]));
59532
+ var KindShape = { kind: RecordKindNameSchema };
59533
+ var TimeWindowShape = {
59534
+ deviceId: number$1().int().optional(),
59535
+ atFrom: number$1().optional(),
59536
+ atTo: number$1().optional()
59537
+ };
59538
+ var RecordQueryInputSchema = object({
59539
+ ...KindShape,
59540
+ ...TimeWindowShape,
59541
+ where: RecordWhereSchema.optional(),
59542
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
59543
+ order: _enum(["at", "id"]).optional(),
59544
+ desc: boolean().optional(),
59545
+ limit: number$1().int().min(1).max(RECORD_QUERY_MAX).default(100),
59546
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
59547
+ cursor: string().min(1).optional()
59548
+ });
59549
+ var RecordQueryResultSchema = object({
59550
+ records: array(RecordViewSchema),
59551
+ nextCursor: string().nullable()
59552
+ });
59553
+ var RecordKindStateSchema = _enum([
59554
+ "created",
59555
+ "unchanged",
59556
+ "updated"
59557
+ ]);
59558
+ var DeclareKindResultSchema = object({
59559
+ kindId: number$1().int(),
59560
+ state: RecordKindStateSchema,
59561
+ createdIndexes: array(string())
59562
+ });
59563
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
59564
+ var SecretRefsList = array(string());
59565
+ /**
59566
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
59567
+ * All ops of one call share ONE database file and ONE `transactFamily`.
59568
+ */
59569
+ var RecordTransactOpSchema = discriminatedUnion("op", [
59570
+ object({
59571
+ op: literal("put"),
59572
+ kind: RecordKindNameSchema,
59573
+ record: RecordInputSchema,
59574
+ ifRev: number$1().int().min(0).optional()
59575
+ }),
59576
+ object({
59577
+ op: literal("patch"),
59578
+ kind: RecordKindNameSchema,
59579
+ id: RecordIdSchema,
59580
+ data: RecordBodySchema,
59581
+ at: number$1().int().nullable().optional(),
59582
+ expiresAt: number$1().int().nullable().optional(),
59583
+ ifRev: number$1().int().min(1).optional()
59584
+ }),
59585
+ object({
59586
+ op: literal("delete"),
59587
+ kind: RecordKindNameSchema,
59588
+ id: RecordIdSchema
59589
+ })
59590
+ ]);
59591
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
59592
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
59593
+ kind: "mutation",
59594
+ auth: "admin"
59595
+ }), method(object({
59596
+ ...KindShape,
59597
+ id: RecordIdSchema
59598
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
59599
+ ...KindShape,
59600
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59601
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
59602
+ ...KindShape,
59603
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59604
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
59605
+ ...KindShape,
59606
+ record: RecordInputSchema,
59607
+ /** Compare-and-set. `0` means "the row must not exist". */
59608
+ ifRev: number$1().int().min(0).optional()
59609
+ }), object({
59610
+ rev: number$1().int(),
59611
+ releasedRefs: SecretRefsList
59612
+ }), {
59613
+ kind: "mutation",
59614
+ auth: "admin"
59615
+ }), method(object({
59616
+ ...KindShape,
59617
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
59618
+ }), object({
59619
+ count: number$1().int(),
59620
+ releasedRefs: SecretRefsList
59621
+ }), {
59622
+ kind: "mutation",
59623
+ auth: "admin"
59624
+ }), method(object({
59625
+ ...KindShape,
59626
+ id: RecordIdSchema,
59627
+ data: RecordBodySchema,
59628
+ at: number$1().int().nullable().optional(),
59629
+ expiresAt: number$1().int().nullable().optional(),
59630
+ ifRev: number$1().int().min(1).optional()
59631
+ }), object({
59632
+ rev: number$1().int(),
59633
+ releasedRefs: SecretRefsList
59634
+ }), {
59635
+ kind: "mutation",
59636
+ auth: "admin"
59637
+ }), method(object({
59638
+ ...KindShape,
59639
+ id: RecordIdSchema
59640
+ }), object({
59641
+ deleted: boolean(),
59642
+ secretRefs: SecretRefsList
59643
+ }), {
59644
+ kind: "mutation",
59645
+ auth: "admin"
59646
+ }), method(object({
59647
+ ...KindShape,
59648
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59649
+ }), object({
59650
+ deleted: number$1().int(),
59651
+ secretRefs: SecretRefsList
59652
+ }), {
59653
+ kind: "mutation",
59654
+ auth: "admin"
59655
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
59656
+ ...KindShape,
59657
+ ...TimeWindowShape,
59658
+ where: RecordWhereSchema.optional()
59659
+ }), object({ count: number$1().int() }), { auth: "admin" }), method(object({
59660
+ ...KindShape,
59661
+ deviceId: number$1().int().optional(),
59662
+ from: number$1(),
59663
+ to: number$1(),
59664
+ bucketMs: number$1().int().positive()
59665
+ }), object({ buckets: array(object({
59666
+ bucketStart: number$1(),
59667
+ count: number$1().int()
59668
+ })) }), { auth: "admin" }), method(object({
59669
+ ...KindShape,
59670
+ op: _enum([
59671
+ "sum",
59672
+ "min",
59673
+ "max",
59674
+ "count"
59675
+ ]),
59676
+ field: RecordFieldPathSchema.optional(),
59677
+ groupBy: RecordFieldPathSchema.optional(),
59678
+ where: RecordWhereSchema.optional()
59679
+ }), object({ rows: array(object({
59680
+ group: union([
59681
+ string(),
59682
+ number$1(),
59683
+ _null()
59684
+ ]),
59685
+ value: number$1().nullable()
59686
+ })) }), { auth: "admin" }), method(object({
59687
+ ...KindShape,
59688
+ deviceId: number$1().int().optional(),
59689
+ atBefore: number$1().optional(),
59690
+ where: RecordWhereSchema.optional()
59691
+ }), object({
59692
+ deleted: number$1().int(),
59693
+ secretRefs: SecretRefsList
59694
+ }), {
59695
+ kind: "mutation",
59696
+ auth: "admin"
59697
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
59698
+ results: array(object({
59699
+ rev: number$1().int().nullable(),
59700
+ deleted: boolean()
59701
+ })),
59702
+ releasedRefs: SecretRefsList,
59703
+ secretRefs: SecretRefsList
59704
+ }), {
59705
+ kind: "mutation",
59706
+ auth: "admin"
59707
+ }), method(object({
59708
+ deviceId: number$1().int(),
59709
+ kinds: array(RecordKindNameSchema).optional()
59710
+ }), object({
59711
+ deleted: record(string(), number$1().int()),
59712
+ secretRefs: SecretRefsList
59713
+ }), {
59714
+ kind: "mutation",
59715
+ auth: "admin"
59716
+ }), method(object(KindShape), object({ id: number$1().int() }), {
59717
+ kind: "mutation",
59718
+ auth: "admin"
59719
+ });
59720
+ /**
59721
+ * The wire shapes of the migration API, version 1 (planning#447).
59722
+ *
59723
+ * A migration is code that outlives the server it was written against: an addon at
59724
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
59725
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
59726
+ * is the contract of every method of it, one Zod pair per method.
59727
+ *
59728
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
59729
+ * field is never removed or made required, an enum never narrows, an output field is
59730
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
59731
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
59732
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
59733
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
59734
+ */
59735
+ var Rows = record(string(), unknown());
59736
+ var IntegrationId = object({ integrationId: string().min(1) });
59737
+ var RecordId = object({ id: string().min(1) });
59738
+ var RecordRowSchema = object({
59739
+ id: string(),
59740
+ deviceId: number$1().int().nullable(),
59741
+ at: number$1().nullable(),
59742
+ expiresAt: number$1().nullable(),
59743
+ rev: number$1().int(),
59744
+ updatedAt: number$1(),
59745
+ data: Rows
59746
+ });
59747
+ var RecordPutShape = {
59748
+ id: string().min(1),
59749
+ deviceId: number$1().int().optional(),
59750
+ at: number$1().int().optional(),
59751
+ expiresAt: number$1().int().optional(),
59752
+ data: Rows
59753
+ };
59754
+ var MigrationIntegrationSchema = object({
59755
+ id: string(),
59756
+ addonId: string(),
59757
+ name: string(),
59758
+ info: Rows,
59759
+ originStableId: string().optional()
59760
+ });
59761
+ var MigrationDeviceSchema = object({
59762
+ id: number$1().int(),
59763
+ integrationId: string().nullable().optional(),
59764
+ /** The owning device; null or absent for a root. */
59765
+ parentDeviceId: number$1().int().nullable().optional(),
59766
+ type: string().optional(),
59767
+ name: string().optional(),
59768
+ stableId: string().optional()
59769
+ });
59770
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
59771
+ var MigrationApiInfoSchema = object({
59772
+ version: number$1().int(),
59773
+ methods: array(string())
59774
+ });
59775
+ object({
59776
+ message: string(),
59777
+ meta: Rows.optional(),
59778
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
59779
+ tags: object({ deviceId: number$1().int().optional() }).optional()
59780
+ });
59781
+ object({
59782
+ owner: string().min(1),
59783
+ metas: array(MigrationMetaSchema)
59784
+ }), object({
59785
+ bootId: string(),
59786
+ snapshot: _enum([
59787
+ "none",
59788
+ "written",
59789
+ "refused"
59790
+ ]),
59791
+ steps: array(MigrationOwnerStepSchema),
59792
+ /** The migration API the engine serves; absent from a server that predates it. */
59793
+ migrationApi: MigrationApiInfoSchema.optional()
59794
+ }), object({ id: string().min(1) }), object({
59795
+ proceed: boolean(),
59796
+ state: union([literal("pending"), MigrationStatusSchema]),
59797
+ reason: string().nullable()
59798
+ }), object({
59799
+ id: string().min(1),
59800
+ result: MigrationOwnerResultSchema
59801
+ }), object({
59802
+ status: MigrationStatusSchema,
59803
+ error: string().nullable(),
59804
+ rowsChanged: number$1().int().nonnegative()
59805
+ }), object({
59806
+ rows: number$1().int().nonnegative(),
59807
+ secrets: number$1().int().nonnegative()
59808
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
59809
+ present: boolean(),
59810
+ removed: Rows
59811
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
59812
+ ...RecordPutShape,
59813
+ ifRev: number$1().int().optional()
59814
+ }), object({ rev: number$1().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number$1().int().nonnegative() }), RecordId.extend({
59815
+ partial: Rows,
59816
+ ifRev: number$1().int().optional()
59817
+ }), object({ rev: number$1().int() }), object({ deleted: boolean() }), object({
59818
+ deviceId: number$1().int().optional(),
59819
+ atFrom: number$1().optional(),
59820
+ atTo: number$1().optional(),
59821
+ where: RecordWhereSchema.optional(),
59822
+ limit: number$1().int().min(1).optional(),
59823
+ cursor: string().min(1).optional(),
59824
+ resolveSecrets: boolean().optional()
59825
+ }), object({
59826
+ records: array(RecordRowSchema),
59827
+ nextCursor: string().nullable()
59828
+ }), object({
59829
+ deviceId: number$1().int().optional(),
59830
+ atBefore: number$1().optional(),
59831
+ where: RecordWhereSchema.optional()
59832
+ }), 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({
59833
+ ref: string().min(1),
59834
+ value: string()
59835
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number$1().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
59836
+ addonId: string().min(1),
59837
+ integrationId: string().min(1).optional()
59838
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number$1().int() }), object({
59839
+ id: number$1().int(),
59840
+ integrationId: string().nullable(),
59841
+ /** Whether the device carries any stored config; its content is not offered. */
59842
+ hasConfig: boolean()
59843
+ }).nullable(), object({
59844
+ deviceId: number$1().int(),
59845
+ integrationId: string().min(1)
59846
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
59448
59847
  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({
59449
59848
  id: string().min(1),
59450
59849
  error: string().min(1)
@@ -59474,7 +59873,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59474
59873
  "written",
59475
59874
  "refused"
59476
59875
  ]),
59477
- steps: array(MigrationOwnerStepSchema)
59876
+ steps: array(MigrationOwnerStepSchema),
59877
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
59878
+ migrationApi: MigrationApiInfoSchema.optional()
59478
59879
  }), {
59479
59880
  kind: "mutation",
59480
59881
  auth: "admin"
@@ -62141,7 +62542,10 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
62141
62542
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
62142
62543
  kind: "mutation",
62143
62544
  auth: "admin"
62144
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
62545
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
62546
+ kind: "query",
62547
+ auth: "admin"
62548
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
62145
62549
  kind: "query",
62146
62550
  auth: "admin"
62147
62551
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -64796,7 +65200,7 @@ var BrokerStatusSchema$1 = _enum([
64796
65200
  "unreachable",
64797
65201
  "tls-error"
64798
65202
  ]);
64799
- var BrokerInfoSchema$1 = object({
65203
+ var BrokerInfoSchema = object({
64800
65204
  id: string(),
64801
65205
  name: string(),
64802
65206
  url: string(),
@@ -64859,7 +65263,7 @@ var StatusSchema = object({
64859
65263
  brokerCount: number$1(),
64860
65264
  embeddedRunning: boolean()
64861
65265
  });
64862
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
65266
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
64863
65267
  kind: "mutation",
64864
65268
  auth: "admin"
64865
65269
  }), method(IdInputSchema, _void(), {
@@ -72041,253 +72445,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
72041
72445
  auth: "admin"
72042
72446
  });
72043
72447
  /**
72044
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
72045
- * config documents, mirrors. The third storage API next to `ctx.settings`
72046
- * (operator intent, D870) and device-history (numbers over time, D850).
72047
- *
72048
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
72049
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
72050
- * that with the addon id closed over). It is deliberately NOT in any input
72051
- * schema, so a session can never supply it, and a call with no stamp is refused
72052
- * `records/no-caller`. A kind is readable and writable only by its owner; other
72053
- * addons go through the owner's capability.
72054
- *
72055
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
72056
- * never `[]`. Secret fields are never on the wire: a read shows the
72057
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
72058
- * resolves itself, and a write may carry only a keychain marker bound to the
72059
- * row's own ref.
72060
- *
72061
- * Internal: addons reach it through `ctx.records`, never directly.
72062
- */
72063
- var RecordIdSchema = string().min(1).max(256);
72064
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
72065
- var RecordBodySchema = record(string(), unknown());
72066
- var RecordInputSchema = object({
72067
- id: RecordIdSchema,
72068
- /** The device this row belongs to (D865 removal, per-device reads). */
72069
- deviceId: number$1().int().optional(),
72070
- /** The domain clock, ms: observed / created / opened. */
72071
- at: number$1().int().optional(),
72072
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
72073
- expiresAt: number$1().int().optional(),
72074
- data: RecordBodySchema
72075
- });
72076
- var RecordViewSchema = object({
72077
- id: string(),
72078
- deviceId: number$1().int().nullable(),
72079
- at: number$1().nullable(),
72080
- expiresAt: number$1().nullable(),
72081
- rev: number$1().int(),
72082
- updatedAt: number$1(),
72083
- data: RecordBodySchema
72084
- });
72085
- /** Declared index fields only. Anything else does not compile, by design. */
72086
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
72087
- string(),
72088
- number$1(),
72089
- boolean(),
72090
- object({ in: array(union([
72091
- string(),
72092
- number$1(),
72093
- boolean()
72094
- ])).min(1).max(RECORD_BATCH_MAX) }),
72095
- object({
72096
- gt: union([string(), number$1()]).optional(),
72097
- gte: union([string(), number$1()]).optional(),
72098
- lt: union([string(), number$1()]).optional(),
72099
- lte: union([string(), number$1()]).optional()
72100
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
72101
- ]));
72102
- var KindShape = { kind: RecordKindNameSchema };
72103
- var TimeWindowShape = {
72104
- deviceId: number$1().int().optional(),
72105
- atFrom: number$1().optional(),
72106
- atTo: number$1().optional()
72107
- };
72108
- var RecordQueryInputSchema = object({
72109
- ...KindShape,
72110
- ...TimeWindowShape,
72111
- where: RecordWhereSchema.optional(),
72112
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
72113
- order: _enum(["at", "id"]).optional(),
72114
- desc: boolean().optional(),
72115
- limit: number$1().int().min(1).max(RECORD_QUERY_MAX).default(100),
72116
- /** Opaque keyset cursor from a previous answer. There is no offset. */
72117
- cursor: string().min(1).optional()
72118
- });
72119
- var RecordQueryResultSchema = object({
72120
- records: array(RecordViewSchema),
72121
- nextCursor: string().nullable()
72122
- });
72123
- var RecordKindStateSchema = _enum([
72124
- "created",
72125
- "unchanged",
72126
- "updated"
72127
- ]);
72128
- var DeclareKindResultSchema = object({
72129
- kindId: number$1().int(),
72130
- state: RecordKindStateSchema,
72131
- createdIndexes: array(string())
72132
- });
72133
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
72134
- var SecretRefsList = array(string());
72135
- /**
72136
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
72137
- * All ops of one call share ONE database file and ONE `transactFamily`.
72138
- */
72139
- var RecordTransactOpSchema = discriminatedUnion("op", [
72140
- object({
72141
- op: literal("put"),
72142
- kind: RecordKindNameSchema,
72143
- record: RecordInputSchema,
72144
- ifRev: number$1().int().min(0).optional()
72145
- }),
72146
- object({
72147
- op: literal("patch"),
72148
- kind: RecordKindNameSchema,
72149
- id: RecordIdSchema,
72150
- data: RecordBodySchema,
72151
- at: number$1().int().nullable().optional(),
72152
- expiresAt: number$1().int().nullable().optional(),
72153
- ifRev: number$1().int().min(1).optional()
72154
- }),
72155
- object({
72156
- op: literal("delete"),
72157
- kind: RecordKindNameSchema,
72158
- id: RecordIdSchema
72159
- })
72160
- ]);
72161
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
72162
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
72163
- kind: "mutation",
72164
- auth: "admin"
72165
- }), method(object({
72166
- ...KindShape,
72167
- id: RecordIdSchema
72168
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
72169
- ...KindShape,
72170
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72171
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
72172
- ...KindShape,
72173
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72174
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
72175
- ...KindShape,
72176
- record: RecordInputSchema,
72177
- /** Compare-and-set. `0` means "the row must not exist". */
72178
- ifRev: number$1().int().min(0).optional()
72179
- }), object({
72180
- rev: number$1().int(),
72181
- releasedRefs: SecretRefsList
72182
- }), {
72183
- kind: "mutation",
72184
- auth: "admin"
72185
- }), method(object({
72186
- ...KindShape,
72187
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
72188
- }), object({
72189
- count: number$1().int(),
72190
- releasedRefs: SecretRefsList
72191
- }), {
72192
- kind: "mutation",
72193
- auth: "admin"
72194
- }), method(object({
72195
- ...KindShape,
72196
- id: RecordIdSchema,
72197
- data: RecordBodySchema,
72198
- at: number$1().int().nullable().optional(),
72199
- expiresAt: number$1().int().nullable().optional(),
72200
- ifRev: number$1().int().min(1).optional()
72201
- }), object({
72202
- rev: number$1().int(),
72203
- releasedRefs: SecretRefsList
72204
- }), {
72205
- kind: "mutation",
72206
- auth: "admin"
72207
- }), method(object({
72208
- ...KindShape,
72209
- id: RecordIdSchema
72210
- }), object({
72211
- deleted: boolean(),
72212
- secretRefs: SecretRefsList
72213
- }), {
72214
- kind: "mutation",
72215
- auth: "admin"
72216
- }), method(object({
72217
- ...KindShape,
72218
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
72219
- }), object({
72220
- deleted: number$1().int(),
72221
- secretRefs: SecretRefsList
72222
- }), {
72223
- kind: "mutation",
72224
- auth: "admin"
72225
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
72226
- ...KindShape,
72227
- ...TimeWindowShape,
72228
- where: RecordWhereSchema.optional()
72229
- }), object({ count: number$1().int() }), { auth: "admin" }), method(object({
72230
- ...KindShape,
72231
- deviceId: number$1().int().optional(),
72232
- from: number$1(),
72233
- to: number$1(),
72234
- bucketMs: number$1().int().positive()
72235
- }), object({ buckets: array(object({
72236
- bucketStart: number$1(),
72237
- count: number$1().int()
72238
- })) }), { auth: "admin" }), method(object({
72239
- ...KindShape,
72240
- op: _enum([
72241
- "sum",
72242
- "min",
72243
- "max",
72244
- "count"
72245
- ]),
72246
- field: RecordFieldPathSchema.optional(),
72247
- groupBy: RecordFieldPathSchema.optional(),
72248
- where: RecordWhereSchema.optional()
72249
- }), object({ rows: array(object({
72250
- group: union([
72251
- string(),
72252
- number$1(),
72253
- _null()
72254
- ]),
72255
- value: number$1().nullable()
72256
- })) }), { auth: "admin" }), method(object({
72257
- ...KindShape,
72258
- deviceId: number$1().int().optional(),
72259
- atBefore: number$1().optional(),
72260
- where: RecordWhereSchema.optional()
72261
- }), object({
72262
- deleted: number$1().int(),
72263
- secretRefs: SecretRefsList
72264
- }), {
72265
- kind: "mutation",
72266
- auth: "admin"
72267
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
72268
- results: array(object({
72269
- rev: number$1().int().nullable(),
72270
- deleted: boolean()
72271
- })),
72272
- releasedRefs: SecretRefsList,
72273
- secretRefs: SecretRefsList
72274
- }), {
72275
- kind: "mutation",
72276
- auth: "admin"
72277
- }), method(object({
72278
- deviceId: number$1().int(),
72279
- kinds: array(RecordKindNameSchema).optional()
72280
- }), object({
72281
- deleted: record(string(), number$1().int()),
72282
- secretRefs: SecretRefsList
72283
- }), {
72284
- kind: "mutation",
72285
- auth: "admin"
72286
- }), method(object(KindShape), object({ id: number$1().int() }), {
72287
- kind: "mutation",
72288
- auth: "admin"
72289
- });
72290
- /**
72291
72448
  * record-entries - the ONE admin surface for every list of user-created entries
72292
72449
  * (D881): notification targets, brokers, LLM profiles, anything an addon
72293
72450
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -82015,7 +82172,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82015
82172
  * `deviceClasses` map (first registered type). `null` when manual creation is
82016
82173
  * unsupported. Lets the Add-Device dialog pick the right type per provider.
82017
82174
  */
82018
- async getManualCreationType() {
82175
+ async getManualCreationType(_input) {
82019
82176
  if (!await this.supportsManualCreation()) return { deviceType: null };
82020
82177
  return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
82021
82178
  }
@@ -82026,7 +82183,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82026
82183
  return true;
82027
82184
  }
82028
82185
  async getChildCreationSchema(input) {
82029
- return this.onGetCreationSchema(input.type);
82186
+ return this.onGetCreationSchema(input.type, { integrationId: input.integrationId });
82030
82187
  }
82031
82188
  /**
82032
82189
  * Default kernel-orchestrated `createDevice` implementation. The
@@ -82038,7 +82195,7 @@ var BaseDeviceProvider = class extends BaseAddon {
82038
82195
  * `deviceClasses` instead.
82039
82196
  */
82040
82197
  async createDevice(input) {
82041
- const spec = await this.onCreateDevice(input.type, input.config);
82198
+ const spec = await this.onCreateDevice(input.type, input.config, { ...input.integrationId !== void 0 ? { integrationId: input.integrationId } : {} });
82042
82199
  const Class = this.deviceClasses[spec.meta.type];
82043
82200
  if (!Class) throw new Error(`${this.providerName} provider: no device class registered for type "${spec.meta.type}" — add it to the deviceClasses map`);
82044
82201
  const stableId = this.generateStableId(spec.meta.type, spec.config);
@@ -83447,20 +83604,6 @@ Object.freeze({
83447
83604
  access: "create",
83448
83605
  role: "viewer"
83449
83606
  },
83450
- "broker.add": {
83451
- capName: "broker",
83452
- capScope: "system",
83453
- addonId: null,
83454
- access: "create",
83455
- role: "admin"
83456
- },
83457
- "broker.get": {
83458
- capName: "broker",
83459
- capScope: "system",
83460
- addonId: null,
83461
- access: "view",
83462
- role: "admin"
83463
- },
83464
83607
  "broker.getBrokerConfig": {
83465
83608
  capName: "broker",
83466
83609
  capScope: "system",
@@ -83468,20 +83611,6 @@ Object.freeze({
83468
83611
  access: "view",
83469
83612
  role: "admin"
83470
83613
  },
83471
- "broker.getSettings": {
83472
- capName: "broker",
83473
- capScope: "system",
83474
- addonId: null,
83475
- access: "view",
83476
- role: "admin"
83477
- },
83478
- "broker.getSettingsSchema": {
83479
- capName: "broker",
83480
- capScope: "system",
83481
- addonId: null,
83482
- access: "view",
83483
- role: "admin"
83484
- },
83485
83614
  "broker.getState": {
83486
83615
  capName: "broker",
83487
83616
  capScope: "system",
@@ -83496,20 +83625,6 @@ Object.freeze({
83496
83625
  access: "view",
83497
83626
  role: "admin"
83498
83627
  },
83499
- "broker.list": {
83500
- capName: "broker",
83501
- capScope: "system",
83502
- addonId: null,
83503
- access: "view",
83504
- role: "admin"
83505
- },
83506
- "broker.listProviders": {
83507
- capName: "broker",
83508
- capScope: "system",
83509
- addonId: null,
83510
- access: "view",
83511
- role: "admin"
83512
- },
83513
83628
  "broker.publish": {
83514
83629
  capName: "broker",
83515
83630
  capScope: "system",
@@ -83517,20 +83632,6 @@ Object.freeze({
83517
83632
  access: "create",
83518
83633
  role: "admin"
83519
83634
  },
83520
- "broker.remove": {
83521
- capName: "broker",
83522
- capScope: "system",
83523
- addonId: null,
83524
- access: "delete",
83525
- role: "admin"
83526
- },
83527
- "broker.setSettings": {
83528
- capName: "broker",
83529
- capScope: "system",
83530
- addonId: null,
83531
- access: "create",
83532
- role: "admin"
83533
- },
83534
83635
  "broker.subscribe": {
83535
83636
  capName: "broker",
83536
83637
  capScope: "system",
@@ -83538,20 +83639,6 @@ Object.freeze({
83538
83639
  access: "create",
83539
83640
  role: "admin"
83540
83641
  },
83541
- "broker.testConnection": {
83542
- capName: "broker",
83543
- capScope: "system",
83544
- addonId: null,
83545
- access: "create",
83546
- role: "admin"
83547
- },
83548
- "broker.testSettings": {
83549
- capName: "broker",
83550
- capScope: "system",
83551
- addonId: null,
83552
- access: "create",
83553
- role: "admin"
83554
- },
83555
83642
  "broker.unsubscribe": {
83556
83643
  capName: "broker",
83557
83644
  capScope: "system",
@@ -88515,6 +88602,13 @@ Object.freeze({
88515
88602
  access: "view",
88516
88603
  role: "viewer"
88517
88604
  },
88605
+ "pipelineAnalytics.getMediaLocationCensus": {
88606
+ capName: "pipeline-analytics",
88607
+ capScope: "device",
88608
+ addonId: null,
88609
+ access: "view",
88610
+ role: "admin"
88611
+ },
88518
88612
  "pipelineAnalytics.getMediaReclaimStatus": {
88519
88613
  capName: "pipeline-analytics",
88520
88614
  capScope: "device",
@@ -97765,6 +97859,15 @@ function rewriteLocation(location, currentUpstream, route) {
97765
97859
  /** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
97766
97860
  var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
97767
97861
  /**
97862
+ * Subresource Integrity pins the bytes of a script or stylesheet. The proxy rewrites vendor
97863
+ * URLs inside those bytes (and re-serves them from another origin), so the hash can no longer
97864
+ * match and the browser refuses the file: the page renders with no script at all. The hub
97865
+ * already drops the CSP that would have enforced the same pin; the attribute goes with it.
97866
+ */
97867
+ function stripIntegrity(html) {
97868
+ return html.replace(/\s+integrity\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
97869
+ }
97870
+ /**
97768
97871
  * Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
97769
97872
  * (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
97770
97873
  * HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
@@ -97780,6 +97883,7 @@ function rewriteBody(body, kind, currentUpstream, route) {
97780
97883
  const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
97781
97884
  out = out.replace(escaped, escapedBase);
97782
97885
  }
97886
+ if (kind === "html") out = stripIntegrity(out);
97783
97887
  const current = `${route.basePath}/${currentUpstream.host}/`;
97784
97888
  const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
97785
97889
  if (kind === "html") {
@@ -98404,6 +98508,232 @@ object({
98404
98508
  delay: number$1().nonnegative().optional(),
98405
98509
  commands: record(string(), unknown())
98406
98510
  });
98511
+ var InstanceInUseError = class extends Error {
98512
+ instanceId;
98513
+ users;
98514
+ name = "InstanceInUseError";
98515
+ constructor(instanceId, users) {
98516
+ super(`instance '${instanceId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
98517
+ this.instanceId = instanceId;
98518
+ this.users = users;
98519
+ }
98520
+ };
98521
+ /** Throw {@link InstanceInUseError} when any finder names a user of `instanceId`. */
98522
+ async function assertInstanceUnused(instanceId, finders) {
98523
+ const users = [];
98524
+ for (const finder of finders) users.push(...await finder.find(instanceId));
98525
+ if (users.length > 0) throw new InstanceInUseError(instanceId, users);
98526
+ }
98527
+ /** The `instance-select` field of a link, keyed like the body field it fills. */
98528
+ function linkFieldDescriptor(key, target, labels) {
98529
+ return {
98530
+ type: "instance-select",
98531
+ key,
98532
+ label: labels.label,
98533
+ ...labels.description !== void 0 ? { description: labels.description } : {},
98534
+ required: labels.required ?? true,
98535
+ ...labels.noneLabel !== void 0 ? { noneLabel: labels.noneLabel } : {},
98536
+ functionCap: target.cap,
98537
+ ...target.kind !== void 0 ? { kind: target.kind } : {}
98538
+ };
98539
+ }
98540
+ /** @deprecated use {@link InstanceInUseError}; this one only adds the old `brokerId` name. */
98541
+ var BrokerInUseError = class extends InstanceInUseError {
98542
+ get brokerId() {
98543
+ return this.instanceId;
98544
+ }
98545
+ };
98546
+ /** @deprecated use {@link assertInstanceUnused}. */
98547
+ async function assertBrokerUnused(brokerId, finders) {
98548
+ try {
98549
+ await assertInstanceUnused(brokerId, finders);
98550
+ } catch (err) {
98551
+ if (err instanceof InstanceInUseError) throw new BrokerInUseError(err.instanceId, err.users);
98552
+ throw err;
98553
+ }
98554
+ }
98555
+ /** @deprecated use `guardInstanceDelete`. */
98556
+ function guardBrokerDelete(base, users) {
98557
+ return {
98558
+ ...base,
98559
+ deleteInstance: async (input) => {
98560
+ await assertBrokerUnused(input.id, users());
98561
+ return base.deleteInstance(input);
98562
+ }
98563
+ };
98564
+ }
98565
+ /**
98566
+ * The generic binding of a service to a broker (D1012). A service (an integration today; a
98567
+ * notifier target, a calendar source or an exporter later) names the broker it runs through by id
98568
+ * and keeps only its OWN link settings: never a copy of the broker's host, sign-in or secrets,
98569
+ * which stay with the broker and are edited on the Brokers page.
98570
+ *
98571
+ * One primitive serves every consumer:
98572
+ * - {@link declareBrokerLinkKind}: the record kind, body `{ name, brokerId, ...linkFields }`;
98573
+ * - {@link buildBrokerLinkForm}: the form, a `broker-select` picker plus the service's link
98574
+ * sections, which may be DYNAMIC (a list read from the chosen broker, like HA calendars);
98575
+ * - {@link buildBrokerLinkInstances}: the `integration-instances` provider of an integration;
98576
+ * - {@link resolveBrokerId} / {@link brokerLinkUserFinder}: which broker a record names and which
98577
+ * records name a broker, so a delete of a broker still in use can name its users generically.
98578
+ */
98579
+ /** The record kind an integration is stored under. */
98580
+ var BROKER_LINK_KIND_NAME = "connections";
98581
+ /** 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. */
98582
+ var BrokerLinkBodySchema = entryBody({ brokerId: string() });
98583
+ function declareBrokerLinkKind(spec) {
98584
+ return defineEntryKind(BROKER_LINK_KIND_NAME, {
98585
+ title: spec.title,
98586
+ schema: BrokerLinkBodySchema.extend(spec.linkFields ?? {}),
98587
+ writeTrigger: spec.writeTrigger
98588
+ });
98589
+ }
98590
+ /** The form: a picker over the brokers of `brokerKind`, then the service's link sections. */
98591
+ async function buildBrokerLinkForm(spec, brokerId) {
98592
+ const dynamic = brokerId !== void 0 && spec.dynamicSections !== void 0 ? await spec.dynamicSections(brokerId) : [];
98593
+ return { sections: [
98594
+ {
98595
+ id: "broker-link",
98596
+ title: spec.brokerLabel,
98597
+ description: `Each integration links to a ${spec.brokerLabel} connection (Connectivity → Brokers). The connection and its sign-in are edited there.`,
98598
+ columns: 1,
98599
+ fields: [{
98600
+ type: "broker-select",
98601
+ key: "brokerId",
98602
+ label: spec.brokerLabel,
98603
+ brokerKind: spec.brokerKind
98604
+ }]
98605
+ },
98606
+ ...spec.linkSections ?? [],
98607
+ ...dynamic
98608
+ ] };
98609
+ }
98610
+ /** The broker a record names; undefined while it names none. */
98611
+ function resolveBrokerId(record) {
98612
+ return record.brokerId === void 0 || record.brokerId === "" ? void 0 : record.brokerId;
98613
+ }
98614
+ /** The broker an integration links to, from its record. */
98615
+ async function brokerIdOf(instances, integrationId) {
98616
+ const body = await instances.getResolved(integrationId);
98617
+ return body === null ? void 0 : resolveBrokerId(body);
98618
+ }
98619
+ /** Every record of the kind that names `brokerId`, as users of that broker. */
98620
+ function brokerLinkUserFinder(instances, userKind = "integration") {
98621
+ return { find: async (brokerId) => (await instances.listResolved()).filter((entry) => resolveBrokerId(entry.body) === brokerId).map((entry) => ({
98622
+ kind: userKind,
98623
+ id: entry.id,
98624
+ name: entry.body.name
98625
+ })) };
98626
+ }
98627
+ /** The `integration-instances` provider of an addon whose integrations link to a broker. */
98628
+ function buildBrokerLinkInstances(records, spec) {
98629
+ const instances = new RecordIntegrationInstances(records, spec.kind, {
98630
+ description: {
98631
+ role: "devices",
98632
+ instanceMode: "multiple",
98633
+ kind: "connection",
98634
+ canTest: false
98635
+ },
98636
+ formSchema: async (id) => buildBrokerLinkForm(spec, id === null ? void 0 : await brokerIdOf(instances, id)),
98637
+ initialBody: (name) => spec.kind.schema.parse({
98638
+ name,
98639
+ brokerId: ""
98640
+ }),
98641
+ configured: (body) => resolveBrokerId(body) !== void 0,
98642
+ ...spec.onChanged !== void 0 ? { onChanged: spec.onChanged } : {}
98643
+ });
98644
+ return instances;
98645
+ }
98646
+ var BROKER_ROWS_MAX = 1e3;
98647
+ var STRAY_REGISTRY_ID = /^int_\d+$/;
98648
+ async function moveBrokerLinksToConnections(ports) {
98649
+ const brokers = await ports.listBrokers();
98650
+ const strays = brokers.filter((b) => STRAY_REGISTRY_ID.test(b.id));
98651
+ 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(", ")}`);
98652
+ let moved = 0;
98653
+ for (const broker of brokers) {
98654
+ const integrationId = broker.integrationId;
98655
+ if (integrationId === void 0 || integrationId === "") continue;
98656
+ const existing = await ports.readConnection(integrationId);
98657
+ if (existing === null) await ports.putConnection(integrationId, {
98658
+ name: broker.name,
98659
+ brokerId: broker.id
98660
+ });
98661
+ else if (existing.brokerId !== broker.id) {
98662
+ ports.warn(`integration '${integrationId}' already names broker '${existing.brokerId ?? "none"}', not '${broker.id}'; the link stays on the broker`);
98663
+ continue;
98664
+ }
98665
+ if ((await ports.readConnection(integrationId))?.brokerId !== broker.id) throw new Error(`integration '${integrationId}': the link did not read back - broker '${broker.id}' keeps it`);
98666
+ await ports.clearBrokerLink(broker.id);
98667
+ moved += 1;
98668
+ }
98669
+ return {
98670
+ outcome: moved === 0 ? "already-done" : "done",
98671
+ summary: {
98672
+ brokers: brokers.length,
98673
+ moved,
98674
+ strayIntegrationRows: strays.length
98675
+ }
98676
+ };
98677
+ }
98678
+ /** The ports over the two record kinds of the addon. */
98679
+ function brokerLinkPortsOver(deps) {
98680
+ return {
98681
+ warn: deps.warn,
98682
+ listBrokers: async () => {
98683
+ return (await deps.brokers.query({ limit: BROKER_ROWS_MAX })).records.map((row) => {
98684
+ const name = row.data["name"];
98685
+ const link = row.data["integrationId"];
98686
+ return {
98687
+ id: row.id,
98688
+ name: typeof name === "string" && name.length > 0 ? name : row.id,
98689
+ integrationId: typeof link === "string" ? link : void 0
98690
+ };
98691
+ });
98692
+ },
98693
+ readConnection: async (id) => {
98694
+ const row = await deps.connections.get(id);
98695
+ return row === null ? null : { brokerId: row.data.brokerId };
98696
+ },
98697
+ putConnection: async (id, body) => {
98698
+ await deps.connections.put({
98699
+ id,
98700
+ data: body
98701
+ }, { ifRev: 0 });
98702
+ },
98703
+ clearBrokerLink: async (brokerId) => {
98704
+ await deps.brokers.patch(brokerId, { integrationId: null });
98705
+ }
98706
+ };
98707
+ }
98708
+ //#endregion
98709
+ //#region src/speak/constants.ts
98710
+ /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
98711
+ var ALEXA_SPEAK_ADDON_ID = "import-alexa";
98712
+ var ALEXA_SPEAK_KIND = "alexa-echo";
98713
+ /** The kind of the Amazon-account broker the Echoes are read from. */
98714
+ var ALEXA_BROKER_KIND = "alexa";
98715
+ /** The Amazon account an Echo target speaks through. */
98716
+ var ALEXA_ACCOUNT_LINK = {
98717
+ cap: "broker",
98718
+ kind: ALEXA_BROKER_KIND
98719
+ };
98720
+ var DEVICE_REFRESH_MS = 5 * 6e4;
98721
+ /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
98722
+ var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
98723
+ /** How often an adopted Echo's player state is read (a push channel is a later phase). */
98724
+ var ECHO_POLL_MS = 3e4;
98725
+ /** One more read after a command, once Amazon has had time to apply it. */
98726
+ var ECHO_REREAD_MS = 1500;
98727
+ /** After a 429 the account is left alone for this long, doubling up to the cap. */
98728
+ var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
98729
+ var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
98730
+ /**
98731
+ * A command (volume, play, Do Not Disturb) that has not started within this, or not
98732
+ * finished, is answered with an error instead of running minutes late.
98733
+ */
98734
+ var ECHO_COMMAND_DEADLINE_MS = 12e3;
98735
+ /** A poll read is stale after this; it is dropped from the queue rather than sent late. */
98736
+ var ECHO_READ_DEADLINE_MS = 25e3;
98407
98737
  //#endregion
98408
98738
  //#region src/config.ts
98409
98739
  /**
@@ -98449,7 +98779,6 @@ var AlexaBrokerEntrySchema = object({
98449
98779
  id: string().min(1),
98450
98780
  name: string().min(1),
98451
98781
  connection: alexaConnectionSchema,
98452
- integrationId: string().optional(),
98453
98782
  /** Absent means on; the instances page switches a connection off without deleting it. */
98454
98783
  enabled: boolean().optional()
98455
98784
  });
@@ -98462,7 +98791,7 @@ var alexaEchoTargetSchema = object({
98462
98791
  name: string(),
98463
98792
  enabled: boolean(),
98464
98793
  /** The broker (Amazon account) the Echo belongs to; '' until one is chosen. */
98465
- brokerId: string().default(""),
98794
+ brokerId: linkField(ALEXA_ACCOUNT_LINK).default(""),
98466
98795
  echoId: string(),
98467
98796
  respectDnd: boolean()
98468
98797
  });
@@ -142695,7 +143024,6 @@ var AlexaBrokerRegistry = class {
142695
143024
  #onStatesPolled;
142696
143025
  #makeManager;
142697
143026
  #managers = /* @__PURE__ */ new Map();
142698
- #integrationToBroker = /* @__PURE__ */ new Map();
142699
143027
  #nextId = 1;
142700
143028
  constructor(logger, deps = {}) {
142701
143029
  this.#logger = logger;
@@ -142746,7 +143074,6 @@ var AlexaBrokerRegistry = class {
142746
143074
  }
142747
143075
  }));
142748
143076
  this.#managers.clear();
142749
- this.#integrationToBroker.clear();
142750
143077
  }
142751
143078
  setOnBrokerConnected(cb) {
142752
143079
  this.#onBrokerConnected = cb;
@@ -142769,17 +143096,14 @@ var AlexaBrokerRegistry = class {
142769
143096
  const entry = {
142770
143097
  id,
142771
143098
  name,
142772
- connection,
142773
- ...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
143099
+ connection
142774
143100
  };
142775
143101
  await this.#startManager(entry);
142776
- if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
142777
143102
  return entry;
142778
143103
  }
142779
143104
  async removeEntry(id) {
142780
143105
  const mgr = this.#managers.get(id);
142781
143106
  if (!mgr) return;
142782
- for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
142783
143107
  this.#managers.delete(id);
142784
143108
  try {
142785
143109
  await mgr.stop();
@@ -142805,9 +143129,6 @@ var AlexaBrokerRegistry = class {
142805
143129
  if (!mgr) throw new Error(`AlexaBrokerRegistry: unknown broker id "${brokerId}"`);
142806
143130
  await mgr.sendCommand(command);
142807
143131
  }
142808
- linkIntegration(integrationId, brokerId) {
142809
- this.#integrationToBroker.set(integrationId, brokerId);
142810
- }
142811
143132
  list() {
142812
143133
  return Array.from(this.#managers.values()).map((m) => m.getInfo());
142813
143134
  }
@@ -142826,10 +143147,7 @@ var AlexaBrokerRegistry = class {
142826
143147
  return count;
142827
143148
  }
142828
143149
  async #startManager(entry) {
142829
- if (this.#managers.has(entry.id)) {
142830
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
142831
- return;
142832
- }
143150
+ if (this.#managers.has(entry.id)) return;
142833
143151
  const mgr = this.#makeManager({
142834
143152
  id: entry.id,
142835
143153
  name: entry.name,
@@ -142842,7 +143160,6 @@ var AlexaBrokerRegistry = class {
142842
143160
  onStatesPolled: (id, values) => this.#onStatesPolled(id, values)
142843
143161
  });
142844
143162
  this.#managers.set(entry.id, mgr);
142845
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
142846
143163
  await mgr.start();
142847
143164
  }
142848
143165
  /** The next free broker id, for a record minted before its manager exists. */
@@ -142906,40 +143223,15 @@ async function removeSessionFile(dataDir, brokerId) {
142906
143223
  await fs$1.rm(sessionPath(dataDir, brokerId), { force: true });
142907
143224
  }
142908
143225
  //#endregion
142909
- //#region src/speak/constants.ts
142910
- /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
142911
- var ALEXA_SPEAK_ADDON_ID = "import-alexa";
142912
- var ALEXA_SPEAK_KIND = "alexa-echo";
142913
- /** The kind of the Amazon-account broker the Echoes are read from. */
142914
- var ALEXA_BROKER_KIND = "alexa";
142915
- var DEVICE_REFRESH_MS = 5 * 6e4;
142916
- /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
142917
- var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
142918
- /** How often an adopted Echo's player state is read (a push channel is a later phase). */
142919
- var ECHO_POLL_MS = 3e4;
142920
- /** One more read after a command, once Amazon has had time to apply it. */
142921
- var ECHO_REREAD_MS = 1500;
142922
- /** After a 429 the account is left alone for this long, doubling up to the cap. */
142923
- var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
142924
- var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
142925
- /**
142926
- * A command (volume, play, Do Not Disturb) that has not started within this, or not
142927
- * finished, is answered with an error instead of running minutes late.
142928
- */
142929
- var ECHO_COMMAND_DEADLINE_MS = 12e3;
142930
- /** A poll read is stale after this; it is dropped from the queue rather than sent late. */
142931
- var ECHO_READ_DEADLINE_MS = 25e3;
142932
- //#endregion
142933
143226
  //#region src/alexa-broker-provider.ts
142934
143227
  /**
142935
143228
  * Construct the `broker` cap provider for the Alexa import addon. Pure builder.
142936
- * Mirrors `buildEcowittBrokerProvider`. The cookie + refresh token are secrets:
142937
- * redacted on settings reads, preserved on an empty round-trip. State sync is
142938
- * poll-based (no pub/sub) — `publish`/`subscribe`/`getState` are no-ops.
143229
+ * The bus half only (D1013): creating, listing, editing and removing an account is
143230
+ * `integration-instances`. The cookie + refresh token are secrets, redacted on the config
143231
+ * read. State sync is poll-based (no pub/sub) — `publish`/`subscribe`/`getState` are no-ops.
142939
143232
  */
142940
143233
  function buildAlexaBrokerProvider(deps) {
142941
- const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
142942
- const owns = (id) => getBrokers().some((b) => b.id === id);
143234
+ const { registry, getBrokers } = deps;
142943
143235
  const redact = (entry) => ({
142944
143236
  ...entry.connection,
142945
143237
  cookie: "",
@@ -142947,108 +143239,10 @@ function buildAlexaBrokerProvider(deps) {
142947
143239
  macDms: ""
142948
143240
  });
142949
143241
  return {
142950
- list: async ({ kind }) => {
142951
- if (kind && kind !== "alexa") return [];
142952
- return registry.list().map((info) => ({
142953
- ...info,
142954
- addonId: ownerAddonId
142955
- }));
142956
- },
142957
- get: async ({ id }) => {
142958
- const info = registry.get(id);
142959
- return info ? {
142960
- ...info,
142961
- addonId: ownerAddonId
142962
- } : null;
142963
- },
142964
- listProviders: async () => [{
142965
- addonId: ownerAddonId,
142966
- kinds: [{
142967
- kind: ALEXA_BROKER_KIND,
142968
- label: "Alexa",
142969
- signIn: {
142970
- basePath: "/addon/import-alexa/login",
142971
- methods: ["device-code", "proxy"]
142972
- }
142973
- }]
142974
- }],
142975
- add: async ({ kind, name, settings }) => {
142976
- if (kind !== "alexa") throw new Error(`import-alexa: only kind '${ALEXA_BROKER_KIND}' is handled here (got '${kind}')`);
142977
- const entry = await registry.createEntry(name, settingsToAlexaConnection(settings));
142978
- await persistBrokers([...getBrokers(), entry]);
142979
- return { id: entry.id };
142980
- },
142981
- remove: async ({ id }) => {
142982
- if (!owns(id)) return;
142983
- await registry.removeEntry(id);
142984
- await cascadeRemoveDevices(id).catch((err) => {
142985
- logger.warn("alexa: broker cascade-remove threw", {
142986
- tags: { brokerId: id },
142987
- meta: { error: errMsg(err) }
142988
- });
142989
- });
142990
- await persistBrokers(getBrokers().filter((b) => b.id !== id));
142991
- },
142992
- testConnection: async ({ id }) => {
142993
- if (!owns(id)) return {
142994
- ok: false,
142995
- error: "unknown broker"
142996
- };
142997
- const info = registry.get(id);
142998
- if (!info) return {
142999
- ok: false,
143000
- error: "unknown broker"
143001
- };
143002
- if (info.status === "connected") return {
143003
- ok: true,
143004
- latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
143005
- };
143006
- return {
143007
- ok: false,
143008
- error: info.error ?? `status: ${info.status}`
143009
- };
143010
- },
143011
- getSettings: async ({ id }) => {
143012
- const entry = getBrokers().find((b) => b.id === id);
143013
- return entry ? redact(entry) : null;
143014
- },
143015
- setSettings: async ({ id, settings }) => {
143016
- if (!owns(id)) return;
143017
- const existing = getBrokers().find((b) => b.id === id);
143018
- if (!existing) return;
143019
- const parsed = settingsToAlexaConnection(settings);
143020
- const merged = {
143021
- ...parsed,
143022
- cookie: parsed.cookie.length > 0 ? parsed.cookie : existing.connection.cookie,
143023
- refreshToken: parsed.refreshToken.length > 0 ? parsed.refreshToken : existing.connection.refreshToken,
143024
- macDms: parsed.macDms.length > 0 ? parsed.macDms : existing.connection.macDms
143025
- };
143026
- const updated = await registry.updateEntry(id, merged);
143027
- await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
143028
- },
143029
143242
  getBrokerConfig: async ({ id }) => {
143030
143243
  const entry = getBrokers().find((b) => b.id === id);
143031
143244
  return entry ? redact(entry) : null;
143032
143245
  },
143033
- getSettingsSchema: async ({ kind }) => {
143034
- if (kind !== "alexa") return null;
143035
- return buildConnectionFormSchema();
143036
- },
143037
- testSettings: async ({ kind, settings }) => {
143038
- if (kind !== "alexa") return {
143039
- ok: false,
143040
- error: `unsupported kind: ${kind}`
143041
- };
143042
- try {
143043
- settingsToAlexaConnection(settings);
143044
- return { ok: true };
143045
- } catch (err) {
143046
- return {
143047
- ok: false,
143048
- error: errMsg(err)
143049
- };
143050
- }
143051
- },
143052
143246
  publish: async () => null,
143053
143247
  subscribe: async () => ({ subscriptionId: "" }),
143054
143248
  unsubscribe: async () => void 0,
@@ -143374,7 +143568,7 @@ function buildAlexaAdoptionProvider(deps) {
143374
143568
  //#region src/alexa-broker-device-cascade.ts
143375
143569
  /**
143376
143570
  * Remove every adopted Alexa device whose persisted config carries
143377
- * `{ system: 'alexa', brokerId }`. Used on broker.remove and integration delete.
143571
+ * `{ system: 'alexa', brokerId }`. Used when an account instance is deleted and when an integration is deleted.
143378
143572
  * Alexa appliances are flat (standalone) devices, so this removes top-level
143379
143573
  * devices directly. Best-effort per device; returns the count removed. Mirrors
143380
143574
  * the Ecowitt cascade helper.
@@ -144338,6 +144532,7 @@ var alexaBrokersEntryKind = defineEntryKind("brokers", {
144338
144532
  title: "Alexa import brokers",
144339
144533
  schema: entryBody({
144340
144534
  ...alexaConnectionSchema.shape,
144535
+ /** Retired (D1012): the link lives on the integration's `connections` record. Only the migration reads it. */
144341
144536
  integrationId: string().optional(),
144342
144537
  enabled: boolean().optional()
144343
144538
  }),
@@ -144349,20 +144544,27 @@ var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
144349
144544
  schema: entryBody({ session: string().default("").meta({ secret: true }) }),
144350
144545
  writeTrigger: "an Echo worker signs in as an Alexa account broker"
144351
144546
  });
144547
+ /** An Alexa integration: the link to the Alexa account (broker) it imports devices from. */
144548
+ var alexaConnectionsEntryKind = declareBrokerLinkKind({
144549
+ title: "Alexa integrations",
144550
+ writeTrigger: "an operator links an Alexa integration to an Alexa account"
144551
+ });
144352
144552
  /** Every kind this addon declares, keyed by its name: `records.declare` refuses any other key. */
144353
144553
  var alexaRecordKinds = {
144354
144554
  brokers: alexaBrokersEntryKind,
144555
+ connections: alexaConnectionsEntryKind,
144355
144556
  "speak-sessions": alexaSpeakSessionsEntryKind
144356
144557
  };
144357
144558
  //#endregion
144358
- //#region src/alexa-instances.ts
144559
+ //#region src/alexa-account-instances.ts
144359
144560
  /**
144360
- * The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
144361
- * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
144561
+ * The Alexa accounts as `integration-instances` (planning#408 P3, D1013): one instance per `brokers`
144562
+ * record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves; the `account` kind of
144563
+ * the composed provider (the integrations are the `integration` kind, `alexa-instances.ts`). The body is flat, so
144362
144564
  * the form the instances page edits is the stored shape. Signing in (device code or the Amazon
144363
144565
  * login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
144364
144566
  */
144365
- var ALEXA_INSTANCE_KIND = "alexa";
144567
+ var ALEXA_ACCOUNT_KIND = "account";
144366
144568
  var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
144367
144569
  function isAlexaBodyConfigured(body) {
144368
144570
  return body.cookie.length > 0 || body.refreshToken.length > 0;
@@ -144390,21 +144592,13 @@ function statusOf(info, configured) {
144390
144592
  ...info.error !== null ? { detail: info.error } : {}
144391
144593
  };
144392
144594
  }
144393
- function buildAlexaInstances(records, deps) {
144595
+ function buildAlexaAccountInstances(records, deps) {
144394
144596
  return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
144395
144597
  description: {
144396
- role: "both",
144598
+ role: "services",
144397
144599
  instanceMode: "multiple",
144398
144600
  kind: "account",
144399
144601
  canTest: true,
144400
- kinds: [{
144401
- id: ALEXA_INSTANCE_KIND,
144402
- label: "Alexa",
144403
- signIn: {
144404
- basePath: ALEXA_SIGN_IN_PATH,
144405
- methods: ["device-code", "proxy"]
144406
- }
144407
- }],
144408
144602
  allowRestoreId: true
144409
144603
  },
144410
144604
  formSchema: () => buildConnectionFormSchema(),
@@ -144417,7 +144611,7 @@ function buildAlexaInstances(records, deps) {
144417
144611
  pollIntervalMs: 6e4
144418
144612
  }),
144419
144613
  mintId: () => deps.mintId(),
144420
- kindOf: () => ALEXA_INSTANCE_KIND,
144614
+ kindOf: () => ALEXA_ACCOUNT_KIND,
144421
144615
  health: (body, id) => {
144422
144616
  const info = deps.infoOf(id);
144423
144617
  return {
@@ -144446,6 +144640,21 @@ function buildAlexaInstances(records, deps) {
144446
144640
  });
144447
144641
  }
144448
144642
  //#endregion
144643
+ //#region src/alexa-instances.ts
144644
+ /**
144645
+ * The Alexa integrations as `integration-instances` (D1012): one `connections` record per
144646
+ * integration, keyed by the registry id, whose body is the link to an Alexa account (a `brokers`
144647
+ * record, edited on the Brokers page). The account's sign-in and secrets are never copied here.
144648
+ */
144649
+ function buildAlexaInstances(records) {
144650
+ return buildBrokerLinkInstances(records, {
144651
+ kind: alexaConnectionsEntryKind,
144652
+ brokerKind: ALEXA_BROKER_KIND,
144653
+ brokerLabel: "Alexa account",
144654
+ onChanged: () => void 0
144655
+ });
144656
+ }
144657
+ //#endregion
144449
144658
  //#region src/alexa-broker-store.ts
144450
144659
  function rowToEntry(id, data) {
144451
144660
  const parsed = AlexaBrokerEntrySchema.safeParse({
@@ -144458,7 +144667,6 @@ function rowToEntry(id, data) {
144458
144667
  macDms: data.macDms,
144459
144668
  pollIntervalMs: data.pollIntervalMs
144460
144669
  },
144461
- integrationId: data.integrationId,
144462
144670
  enabled: data.enabled
144463
144671
  });
144464
144672
  return parsed.success ? parsed.data : null;
@@ -144495,11 +144703,10 @@ var AlexaBrokerStore = class AlexaBrokerStore {
144495
144703
  const nextIds = new Set(entries.map((e) => e.id));
144496
144704
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
144497
144705
  for (const entry of entries) {
144498
- const { id, name, connection, integrationId, enabled } = entry;
144706
+ const { id, name, connection, enabled } = entry;
144499
144707
  const body = {
144500
144708
  name,
144501
144709
  ...connection,
144502
- ...integrationId === void 0 ? {} : { integrationId },
144503
144710
  ...enabled === void 0 ? {} : { enabled }
144504
144711
  };
144505
144712
  const parsed = alexaBrokersEntryKind.schema.safeParse(body);
@@ -145026,6 +145233,13 @@ function speechFromNotification(notification) {
145026
145233
  }
145027
145234
  //#endregion
145028
145235
  //#region src/speak/provider.ts
145236
+ /**
145237
+ * `notification-output` provider for Echo devices. Text is the rule template
145238
+ * as plain speech. A burst collapses or waits; it is not queued. A transport
145239
+ * failure is `success: false` so the notification outbox retries. A collapse,
145240
+ * a rate limit, or Do Not Disturb is a recorded skip (`success: true`) so the
145241
+ * outbox does not speak the same sentence again later.
145242
+ */
145029
145243
  function quietLogger$1() {
145030
145244
  const logger = {
145031
145245
  debug() {},
@@ -145081,11 +145295,8 @@ function configSchema() {
145081
145295
  title: "Echo",
145082
145296
  titleKey: "import-alexa:echoTarget.s_echo.title",
145083
145297
  fields: [...[{
145084
- type: "broker-select",
145085
- key: "brokerId",
145086
- label: "Amazon account",
145087
- labelKey: "import-alexa:echoTarget.s_echo.brokerId.label",
145088
- brokerKind: ALEXA_BROKER_KIND
145298
+ ...linkFieldDescriptor("brokerId", ALEXA_ACCOUNT_LINK, { label: "Amazon account" }),
145299
+ labelKey: "import-alexa:echoTarget.s_echo.brokerId.label"
145089
145300
  }, {
145090
145301
  type: "select",
145091
145302
  key: "echoId",
@@ -146082,6 +146293,21 @@ async function flattenBrokerRecords(ports) {
146082
146293
  });
146083
146294
  }
146084
146295
  //#endregion
146296
+ //#region src/migrations/broker-links-to-connections.ts
146297
+ var BROKER_LINKS_TO_CONNECTIONS_ID = "import-alexa/2026-11-05-broker-links-to-connections";
146298
+ /** The broker records and the integration (`connections`) records of this addon. */
146299
+ function alexaBrokerLinkPorts(records, warn) {
146300
+ return brokerLinkPortsOver({
146301
+ brokers: records.kind({
146302
+ name: alexaBrokersEntryKind.name,
146303
+ declaration: alexaBrokersEntryKind.declaration,
146304
+ schema: record(string(), unknown())
146305
+ }),
146306
+ connections: records.kind(alexaConnectionsEntryKind),
146307
+ warn
146308
+ });
146309
+ }
146310
+ //#endregion
146085
146311
  //#region src/migrations/strip-echo-target-volume-and-mode.ts
146086
146312
  var RETIRED_KEYS = ["volume", "mode"];
146087
146313
  function isRecord(value) {
@@ -146164,8 +146390,30 @@ var ADDON_MIGRATIONS = [
146164
146390
  fixtureMark: "the name of the broker and the integration it already names"
146165
146391
  },
146166
146392
  procedure: moveAlexaIntegrationRows
146393
+ },
146394
+ {
146395
+ meta: {
146396
+ id: BROKER_LINKS_TO_CONNECTIONS_ID,
146397
+ owner: "import-alexa",
146398
+ level: "addon-state",
146399
+ seq: 4,
146400
+ dependsOn: [INTEGRATION_ROWS_TO_RECORD_ID],
146401
+ store: "settings",
146402
+ scope: "cluster",
146403
+ onFailure: "block-owner",
146404
+ bodyKind: "procedure",
146405
+ 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",
146406
+ fixtureMark: "the name and the extra field of the account that keeps its record"
146407
+ },
146408
+ procedure: moveBrokerLinksToConnections
146167
146409
  }
146168
146410
  ];
146411
+ /** The migrations that rewrite the broker records: they run BEFORE anything reads or persists one. */
146412
+ var BROKER_RECORD_MIGRATION_IDS = [
146413
+ FLATTEN_BROKER_RECORDS_ID,
146414
+ INTEGRATION_ROWS_TO_RECORD_ID,
146415
+ BROKER_LINKS_TO_CONNECTIONS_ID
146416
+ ];
146169
146417
  //#endregion
146170
146418
  //#region src/speak/icon-routes.ts
146171
146419
  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>";
@@ -146264,6 +146512,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146264
146512
  loginProxy = null;
146265
146513
  brokerStore = null;
146266
146514
  instances = null;
146515
+ accountInstances = null;
146267
146516
  activeBrokers = [];
146268
146517
  speak = null;
146269
146518
  constructor() {
@@ -146275,12 +146524,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146275
146524
  if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
146276
146525
  await records.declare(alexaRecordKinds);
146277
146526
  this.brokerStore = AlexaBrokerStore.of(records);
146278
- this.instances = buildAlexaInstances(records, {
146527
+ this.instances = buildAlexaInstances(records);
146528
+ this.accountInstances = buildAlexaAccountInstances(records, {
146279
146529
  mintId: () => this.requireRegistry().allocateId(),
146280
146530
  infoOf: (id) => this.registry?.get(id) ?? null,
146281
146531
  onChanged: (id) => this.syncBrokerRuntime(id)
146282
146532
  });
146283
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
146533
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: BROKER_RECORD_MIGRATION_IDS });
146284
146534
  this.activeBrokers = await this.loadPersistedBrokers();
146285
146535
  await this.adoptSessionFiles();
146286
146536
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
@@ -146315,15 +146565,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146315
146565
  });
146316
146566
  this.deviceCodeLogin = new AlexaDeviceCodeManager({ logger: this.ctx.logger });
146317
146567
  this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
146318
- await this.reconcileIntegrationsToBrokers();
146319
- this.subscribeIntegrationLifecycle();
146320
146568
  const loginRoutes = [...buildAlexaLoginRoutes({
146321
146569
  logger: this.ctx.logger,
146322
146570
  proxyLogin: this.proxyLogin,
146323
146571
  deviceCode: this.deviceCodeLogin,
146324
146572
  commitCookie: (input) => this.commitCapturedCookie(input)
146325
146573
  }), ...speakIconRoutes()];
146326
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
146574
+ 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)) });
146327
146575
  const speak = new AlexaSpeakController({
146328
146576
  logger: this.ctx.logger.child("speak"),
146329
146577
  readConfig: () => this.config,
@@ -146355,7 +146603,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146355
146603
  },
146356
146604
  {
146357
146605
  capability: integrationInstancesCapability,
146358
- provider: this.requireInstances().toProvider()
146606
+ provider: this.buildInstancesProvider()
146359
146607
  },
146360
146608
  {
146361
146609
  capability: deviceAdoptionCapability,
@@ -146380,6 +146628,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146380
146628
  await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
146381
146629
  },
146382
146630
  ...flattenPortsOver(records.kind(alexaBrokersEntryKind)),
146631
+ ...alexaBrokerLinkPorts(records, (message) => this.ctx.logger.warn(message)),
146383
146632
  ...alexaIntegrationRowsPorts({
146384
146633
  api: defaultIntegrationApiOver(this.ctx.api),
146385
146634
  records,
@@ -146388,10 +146637,42 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146388
146637
  })
146389
146638
  };
146390
146639
  }
146640
+ /** One provider over the two record kinds: the account (broker) and the integrations using it. */
146641
+ buildInstancesProvider() {
146642
+ if (this.accountInstances === null) throw new Error("import-alexa: account instances not ready");
146643
+ return composeInstanceKinds([{
146644
+ kind: {
146645
+ id: "account",
146646
+ label: "Alexa account",
146647
+ functionCap: "broker",
146648
+ brokerKind: ALEXA_BROKER_KIND,
146649
+ role: "services",
146650
+ signIn: {
146651
+ basePath: ALEXA_SIGN_IN_PATH,
146652
+ methods: ["device-code", "proxy"]
146653
+ }
146654
+ },
146655
+ provider: guardBrokerDelete(this.accountInstances.toProvider(), () => this.brokerUsers())
146656
+ }, {
146657
+ kind: {
146658
+ id: "integration",
146659
+ label: "Alexa",
146660
+ functionCap: "device-provider",
146661
+ role: "devices"
146662
+ },
146663
+ provider: this.requireInstances().toProvider()
146664
+ }], {
146665
+ role: "devices",
146666
+ instanceMode: "multiple",
146667
+ kind: "account",
146668
+ canTest: true,
146669
+ allowRestoreId: true
146670
+ });
146671
+ }
146391
146672
  /**
146392
- * Bring the runtime in line with one record after the instances page changed it: a removed
146393
- * record stops its manager and cascades its devices, an off record only stops the manager, a
146394
- * live one re-applies its connection or starts under the record's own id.
146673
+ * Bring the runtime in line with one account record after the instances page changed it: a
146674
+ * removed record stops its manager and cascades its devices, an off record only stops the
146675
+ * manager, a live one re-applies its connection or starts under the record's own id.
146395
146676
  */
146396
146677
  async syncBrokerRuntime(id) {
146397
146678
  const reg = this.requireRegistry();
@@ -146412,10 +146693,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146412
146693
  try {
146413
146694
  if (reg.get(id) !== null) {
146414
146695
  if (!sameConnection(entry.connection, reg.getConnection(id))) await reg.updateEntry(id, entry.connection);
146415
- } else await reg.createEntry(entry.name, entry.connection, {
146416
- id,
146417
- ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
146418
- });
146696
+ } else await reg.createEntry(entry.name, entry.connection, { id });
146419
146697
  } catch (err) {
146420
146698
  this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
146421
146699
  brokerId: id,
@@ -146634,12 +146912,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146634
146912
  }
146635
146913
  buildBrokerProvider() {
146636
146914
  return buildAlexaBrokerProvider({
146637
- ownerAddonId: this.ctx.id,
146638
146915
  registry: this.requireRegistry(),
146639
- getBrokers: () => this.activeBrokers,
146640
- persistBrokers: (brokers) => this.persistBrokerEntries(brokers),
146641
- cascadeRemoveDevices: (brokerId) => this.cascadeRemoveDevicesForBroker(brokerId),
146642
- logger: this.ctx.logger
146916
+ getBrokers: () => this.activeBrokers
146643
146917
  });
146644
146918
  }
146645
146919
  /**
@@ -146676,6 +146950,14 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146676
146950
  } });
146677
146951
  return { brokerId: entry.id };
146678
146952
  }
146953
+ /** Who still points at an Alexa account: its integrations and the Echo targets that speak through it. */
146954
+ brokerUsers() {
146955
+ return [brokerLinkUserFinder(this.requireInstances()), { find: async (brokerId) => readEchoTargets(this.config).filter((target) => target.brokerId === brokerId).map((target) => ({
146956
+ kind: "notification-target",
146957
+ id: target.id,
146958
+ name: target.name
146959
+ })) }];
146960
+ }
146679
146961
  async cascadeRemoveDevicesForBroker(brokerId) {
146680
146962
  const reg = this.ctx.kernel.deviceRegistry;
146681
146963
  const devices = this.ctx.kernel.devices;
@@ -146688,42 +146970,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146688
146970
  logger: this.ctx.logger
146689
146971
  });
146690
146972
  }
146691
- async reconcileIntegrationsToBrokers() {
146692
- try {
146693
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
146694
- const surviving = new Set(mine.map((i) => i.id));
146695
- for (const broker of this.activeBrokers) {
146696
- const integrationId = broker.integrationId;
146697
- if (integrationId === void 0 || !surviving.has(integrationId)) continue;
146698
- this.requireRegistry().linkIntegration(integrationId, broker.id);
146699
- }
146700
- const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
146701
- if (toRemove.length === 0) return;
146702
- for (const id of toRemove) try {
146703
- await this.requireRegistry().removeEntry(id);
146704
- await this.cascadeRemoveDevicesForBroker(id);
146705
- } catch (err) {
146706
- this.ctx.logger.warn("Alexa integration→broker: broker cleanup failed", { meta: {
146707
- brokerId: id,
146708
- error: errMsg(err)
146709
- } });
146710
- }
146711
- const nextBrokers = this.activeBrokers.filter((b) => !toRemove.includes(b.id));
146712
- await this.persistBrokerEntries(nextBrokers);
146713
- } catch (err) {
146714
- this.ctx.logger.warn("Alexa integration→broker reconcile failed", { meta: { error: errMsg(err) } });
146715
- }
146716
- }
146717
- subscribeIntegrationLifecycle() {
146718
- const handler = (event) => {
146719
- const addonId = event.data["addonId"];
146720
- if (typeof addonId === "string" && addonId !== this.ctx.id) return;
146721
- this.reconcileIntegrationsToBrokers();
146722
- };
146723
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
146724
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
146725
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
146726
- }
146727
146973
  buildAdoptionProvider() {
146728
146974
  return buildAlexaAdoptionProvider({
146729
146975
  registry: {
@@ -146737,9 +146983,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146737
146983
  },
146738
146984
  logger: this.ctx.logger,
146739
146985
  getBrokerIdForIntegration: async (id) => {
146740
- const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
146741
- if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
146742
- return broker.id;
146986
+ const brokerId = await brokerIdOf(this.requireInstances(), id);
146987
+ if (brokerId === void 0) throw new Error(`integration ${id} has no broker`);
146988
+ return brokerId;
146743
146989
  },
146744
146990
  listAdoptedAlexa: async () => {
146745
146991
  const reg = this.ctx.kernel.deviceRegistry;