@camstack/addon-matter-broker 0.2.159 → 0.2.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -5995,7 +5995,7 @@ var ZodIssueCode = {
5995
5995
  var ZodFirstPartyTypeKind;
5996
5996
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5997
5997
  //#endregion
5998
- //#region ../types/dist/sleep-DEZaLbiu.mjs
5998
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
5999
5999
  /**
6000
6000
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6001
6001
  * window to float samples (D455).
@@ -7624,6 +7624,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7624
7624
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7625
7625
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7626
7626
  /**
7627
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7628
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7629
+ * through `api` (guard `check-migration-ports-surface`).
7630
+ */
7631
+ function migrationSet(catalogue, bindPorts) {
7632
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7633
+ }
7634
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7635
+ function refuseMixed(form, kept, dropped) {
7636
+ 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`);
7637
+ }
7638
+ function catalogueOf(list) {
7639
+ const kept = list.filter((entry) => !("execute" in entry));
7640
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7641
+ return kept;
7642
+ }
7643
+ function boundOf(list) {
7644
+ const kept = list.filter((entry) => "execute" in entry);
7645
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7646
+ return kept;
7647
+ }
7648
+ /**
7627
7649
  * Base class for CamStack addons. Eliminates settings boilerplate:
7628
7650
  *
7629
7651
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7790,23 +7812,21 @@ var BaseAddon = class {
7790
7812
  await this.loadScopedSchemas();
7791
7813
  return normalized;
7792
7814
  }
7815
+ async runMigrations(first, second, third) {
7816
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7817
+ return this.runBound(boundOf(first), second);
7818
+ }
7793
7819
  /**
7794
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7795
- * engine. Call it at the point of the boot where the data must already have the new
7796
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7797
- * restored devices — with the catalogue bound to the addon's ports
7798
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7799
- *
7800
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7801
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7802
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7803
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7804
- *
7805
- * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7806
- * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7807
- * omitted applied ids "newer").
7820
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7821
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7822
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7808
7823
  */
7809
- async runMigrations(migrations, options) {
7824
+ async runMigrationSets(sets, options) {
7825
+ const api = this.ctx.migrationApi;
7826
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7827
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7828
+ }
7829
+ async runBound(migrations, options) {
7810
7830
  const api = this.ctx.api.migrations;
7811
7831
  const report = await runAddonMigrations({
7812
7832
  declareOwner: (input) => api.declareOwner.mutate(input),
@@ -13508,26 +13528,16 @@ var RecordIntegrationInstances = class {
13508
13528
  ...this.hooks.description,
13509
13529
  mintsId: true
13510
13530
  };
13511
- /**
13512
- * The record an id names. A caller holding the `integrations` registry id (the Integrations
13513
- * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13514
- * record id answers as it is. An id that names nothing comes back unchanged.
13515
- */
13516
- async resolveRecordId(id) {
13517
- if (await this.handle.get(id) !== null) return id;
13518
- const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13519
- return linked === void 0 ? id : linked.id;
13520
- }
13521
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13531
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13522
13532
  listInstances = async () => {
13523
13533
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13524
13534
  };
13525
13535
  getInstance = async ({ id }) => {
13526
- const record = await this.handle.get(await this.resolveRecordId(id));
13536
+ const record = await this.handle.get(id);
13527
13537
  return record === null ? null : this.viewOf(record);
13528
13538
  };
13529
13539
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13530
- createInstance = async ({ id, name, kind }) => {
13540
+ createInstance = async ({ id, name, kind, values }) => {
13531
13541
  const minted = id ?? this.hooks.mintId?.({
13532
13542
  name,
13533
13543
  ...kind !== void 0 ? { kind } : {}
@@ -13537,11 +13547,20 @@ var RecordIntegrationInstances = class {
13537
13547
  id: minted,
13538
13548
  data: this.hooks.initialBody(name, kind)
13539
13549
  }, { ifRev: 0 });
13550
+ const patch = this.patchOf(values ?? {});
13551
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13540
13552
  await this.hooks.onChanged?.(minted, "created");
13541
13553
  return { id: minted };
13542
13554
  };
13543
- updateInstance = async ({ id: askedId, name, values }) => {
13544
- const id = await this.resolveRecordId(askedId);
13555
+ updateInstance = async ({ id, name, values }) => {
13556
+ const patch = this.patchOf(values);
13557
+ if (name !== void 0) patch[this.nameField] = name;
13558
+ await this.handle.patch(id, patch);
13559
+ await this.hooks.onChanged?.(id, "updated");
13560
+ return { ok: true };
13561
+ };
13562
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13563
+ patchOf(values) {
13545
13564
  const patch = {};
13546
13565
  for (const [key, value] of Object.entries(values)) {
13547
13566
  if (value === void 0) continue;
@@ -13552,11 +13571,8 @@ var RecordIntegrationInstances = class {
13552
13571
  else if (isSecretClear(value)) patch[key] = null;
13553
13572
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13554
13573
  }
13555
- if (name !== void 0) patch[this.nameField] = name;
13556
- await this.handle.patch(id, patch);
13557
- await this.hooks.onChanged?.(id, "updated");
13558
- return { ok: true };
13559
- };
13574
+ return patch;
13575
+ }
13560
13576
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13561
13577
  setInstanceEnabled = async ({ id, enabled }) => {
13562
13578
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -13576,10 +13592,7 @@ var RecordIntegrationInstances = class {
13576
13592
  status: "unsupported",
13577
13593
  error: "This integration declares no connection test"
13578
13594
  };
13579
- return this.hooks.test(input.id === null ? input : {
13580
- ...input,
13581
- id: await this.resolveRecordId(input.id)
13582
- });
13595
+ return this.hooks.test(input);
13583
13596
  };
13584
13597
  /** The flat provider object to hand to `registerProvider`. */
13585
13598
  toProvider() {
@@ -13596,6 +13609,76 @@ var RecordIntegrationInstances = class {
13596
13609
  };
13597
13610
  }
13598
13611
  };
13612
+ /** No part holds the id a call named. */
13613
+ var UnknownInstanceError = class extends Error {
13614
+ instanceId;
13615
+ constructor(instanceId) {
13616
+ super(`Unknown instance '${instanceId}'`);
13617
+ this.instanceId = instanceId;
13618
+ this.name = "UnknownInstanceError";
13619
+ }
13620
+ };
13621
+ /** A create or a form named no kind, or a kind no part serves. */
13622
+ var UnknownInstanceKindError = class extends Error {
13623
+ kindId;
13624
+ constructor(kindId) {
13625
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13626
+ this.kindId = kindId;
13627
+ this.name = "UnknownInstanceKindError";
13628
+ }
13629
+ };
13630
+ function composeInstanceKinds(parts, base) {
13631
+ const byKind = (kindId) => {
13632
+ if (kindId === void 0) {
13633
+ const only = parts.length === 1 ? parts[0] : void 0;
13634
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13635
+ return only;
13636
+ }
13637
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13638
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13639
+ return part;
13640
+ };
13641
+ const holderOf = async (id) => {
13642
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13643
+ throw new UnknownInstanceError(id);
13644
+ };
13645
+ const tagged = (part, instance) => ({
13646
+ ...instance,
13647
+ kind: part.kind.id
13648
+ });
13649
+ return {
13650
+ describe: async () => ({
13651
+ ...base,
13652
+ kinds: parts.map((part) => part.kind)
13653
+ }),
13654
+ instanceFormSchema: async ({ id, kind }) => {
13655
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13656
+ },
13657
+ listInstances: async () => {
13658
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13659
+ },
13660
+ getInstance: async ({ id }) => {
13661
+ for (const part of parts) {
13662
+ const found = await part.provider.getInstance({ id });
13663
+ if (found !== null) return tagged(part, found);
13664
+ }
13665
+ return null;
13666
+ },
13667
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13668
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13669
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13670
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13671
+ testInstance: async (input) => {
13672
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13673
+ if (test === void 0) return {
13674
+ success: false,
13675
+ status: "inconclusive",
13676
+ error: "This kind cannot be tested"
13677
+ };
13678
+ return test(input);
13679
+ }
13680
+ };
13681
+ }
13599
13682
  var EncodeProfileSchema = object({
13600
13683
  video: object({
13601
13684
  codec: _enum([
@@ -15810,6 +15893,39 @@ var UnstampedEventMediaCountSchema = object({
15810
15893
  /** Sum across both, or `null` when either lane could not be counted. */
15811
15894
  total: number().int().nonnegative().nullable()
15812
15895
  }).nullable();
15896
+ /**
15897
+ * Where the stored event media sits, per storage location: how many media rows
15898
+ * and how many bytes carry each location id, how many carry none (legacy,
15899
+ * resolved to the default `eventMedia` location), and how many name a location
15900
+ * that was not asked about (a deleted or never-listed one).
15901
+ *
15902
+ * One covering-index aggregate per asked location plus the NULL one, and one
15903
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
15904
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
15905
+ * it is a deleted location only when every location id was asked.
15906
+ * `null` is "could not be measured", never zero.
15907
+ */
15908
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string$2().min(1)).max(50).readonly() });
15909
+ var MediaLocationCensusSchema = object({
15910
+ locations: array(object({
15911
+ locationId: string$2(),
15912
+ rows: number().int().nonnegative(),
15913
+ bytes: number().nonnegative()
15914
+ })).readonly(),
15915
+ unstamped: object({
15916
+ rows: number().int().nonnegative(),
15917
+ bytes: number().nonnegative()
15918
+ }),
15919
+ /** Rows stamped with a location id that was not in the asked list. */
15920
+ unlisted: object({
15921
+ rows: number().int().nonnegative(),
15922
+ bytes: number().nonnegative()
15923
+ }),
15924
+ total: object({
15925
+ rows: number().int().nonnegative(),
15926
+ bytes: number().nonnegative()
15927
+ })
15928
+ });
15813
15929
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string$2().min(1) });
15814
15930
  /** The independently selectable logical storage classes — every class
15815
15931
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -46846,52 +46962,7 @@ onBrightnessChanged: { data: object({
46846
46962
  minIntervalMs: 0
46847
46963
  } } }
46848
46964
  };
46849
- /**
46850
- * `broker` — unified pub/sub broker registry, system-scoped collection.
46851
- *
46852
- * The cap models any kind-tagged message broker the user wants to
46853
- * register and that other addons might consume. The first two kinds
46854
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
46855
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
46856
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
46857
- * the same surface.
46858
- *
46859
- * Why one cap, not one cap per kind:
46860
- * - The integrations page wants a single table of "every broker"
46861
- * across kinds; a unified cap drives it without join logic.
46862
- * - Generic admin operations (add / remove / test / status) are
46863
- * identical across kinds — duplicating them per cap was busywork.
46864
- * - The `network-access` cap precedent: same interface, many
46865
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
46866
- *
46867
- * What stays kind-specific:
46868
- * - `getBrokerConfig` payload (kind decides whether it returns
46869
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
46870
- * `Record<string, unknown>` at the wire; consumers narrow.
46871
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
46872
- * key) — also typed as `Record<string, unknown>` so each kind can
46873
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
46874
- * without rebreaking the cap signature.
46875
- * - `add` `settings` payload (different fields per kind, validated by
46876
- * the kind-specific provider).
46877
- *
46878
- * Bidirectionality:
46879
- * - `publish` is RPC — synchronous result (provider-defined).
46880
- * - `subscribe` returns a subscription handle; the actual message
46881
- * stream flows over the typed event-bus as `broker.message` events
46882
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
46883
- * event handler. Subscriptions persist across reconnects — the
46884
- * provider re-subscribes upstream after a transport drop.
46885
- *
46886
- * Why event-bus push (not a subscription RPC stream):
46887
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
46888
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
46889
- * drop is recoverable via `getState({key})`).
46890
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
46891
- * - Cross-process: events already route through `$event-bus`; no
46892
- * per-broker wire-shaping needed.
46893
- */
46894
- var BrokerStatusEnum = _enum([
46965
+ _enum([
46895
46966
  "connected",
46896
46967
  "disconnected",
46897
46968
  "connecting",
@@ -46899,141 +46970,12 @@ var BrokerStatusEnum = _enum([
46899
46970
  "unreachable",
46900
46971
  "error"
46901
46972
  ]);
46902
- var BrokerInfoSchema = object({
46903
- /** Stable broker id. Persisted; survives addon restarts. */
46904
- id: string$2(),
46905
- /** Addon id of the provider that OWNS this broker.
46906
- *
46907
- * The `broker` cap is a system-scoped collection: several addons
46908
- * register a `broker` provider, each owning a DISJOINT set of
46909
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
46910
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
46911
- * device belongs to one integration. The admin UI threads this id
46912
- * back as the `{ addonId }` system-collection selector on every
46913
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
46914
- * `testConnection`), so the call routes to the OWNING provider
46915
- * instead of defaulting to the first-registered one. */
46916
- addonId: string$2(),
46917
- /** Human-readable name (operator-chosen at add-time). */
46918
- name: string$2(),
46919
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
46920
- kind: string$2(),
46921
- status: BrokerStatusEnum,
46922
- /** Free-form provider-specific info (HA version, MQTT broker
46923
- * flavour, latency, mTLS active, …). */
46924
- info: record(string$2(), unknown()),
46925
- /** Ms epoch of the last connection probe / status update. */
46926
- lastCheckedAt: number().nullable(),
46927
- /** Last error message, when `status` indicates failure. */
46928
- error: string$2().nullable()
46929
- });
46930
46973
  var RegistryStatusSchema = object({
46931
46974
  brokerCount: number().int().nonnegative(),
46932
46975
  connectedCount: number().int().nonnegative()
46933
46976
  });
46934
- /**
46935
- * One entry per `broker` provider — which addon provides which broker
46936
- * kind(s). The unified create picker reads this to know, for each
46937
- * (addonId, kind) pair the operator can pick, where to route the
46938
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
46939
- * `{ addonId }` selector). A provider only advertises a kind here when
46940
- * it implements `getSettingsSchema` for that kind (so the picker never
46941
- * offers a kind whose creation form can't be rendered).
46942
- */
46943
- var BrokerProviderInfoSchema = object({
46944
- /** Addon id of the `broker` provider this entry describes. */
46945
- addonId: string$2(),
46946
- /** Broker kinds this provider can create, with a display label. */
46947
- kinds: array(object({
46948
- kind: string$2(),
46949
- label: string$2(),
46950
- /**
46951
- * Relative hub path to start an interactive account link (OAuth popup).
46952
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
46953
- */
46954
- interactiveConnectPath: string$2().min(1).optional(),
46955
- /** Relative hub path to disconnect the linked account (same popup pattern). */
46956
- interactiveDisconnectPath: string$2().min(1).optional(),
46957
- /**
46958
- * The kind signs in through a server-held interactive session instead of
46959
- * (or before) typed credentials. `basePath` is a hub-relative route set
46960
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
46961
- * contract: `POST {basePath}/start` (body: the form's settings) answers
46962
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
46963
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
46964
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
46965
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
46966
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
46967
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
46968
- * offers it as the default path; the fields the session fills are
46969
- * marked `filledBySignIn` in the kind's settings schema.
46970
- *
46971
- * `methods` lists the ways the session can sign in, recommended first:
46972
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
46973
- * settings) answering `{ sessionId, userCode, verificationUrl,
46974
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
46975
- * any device, and the same `status` / `commit` / `cancel` routes follow
46976
- * (status then carries `method`, `userCode`, `verificationUrl`,
46977
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
46978
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
46979
- * `['proxy']`.
46980
- */
46981
- signIn: object({
46982
- basePath: string$2().min(1),
46983
- methods: array(_enum(["device-code", "proxy"])).optional()
46984
- }).optional()
46985
- }))
46986
- });
46987
- var ListInputSchema = object({
46988
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
46989
- * only HA brokers. Omit to list every broker. */
46990
- kind: string$2().optional() });
46991
46977
  var GetInputSchema = object({ id: string$2() });
46992
- var AddInputSchema = object({
46993
- kind: string$2().min(1),
46994
- name: string$2().min(1),
46995
- /**
46996
- * ADOPT an existing id instead of minting a new one.
46997
- *
46998
- * Written the day this cost an outage. A broker's id is not a detail: HA
46999
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
47000
- * lost from config and re-added as `ha_001` leaves every one of its devices
47001
- * bound to a broker that no longer exists. Re-entering the password under the
47002
- * ORIGINAL id turns a multi-step device migration back into re-entering a
47003
- * password.
47004
- *
47005
- * A provider MUST refuse an id that is already in use — adopting a live
47006
- * broker's id would silently take it over.
47007
- */
47008
- id: string$2().min(1).optional(),
47009
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
47010
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
47011
- * branch on receipt — invalid shape rejects the add. */
47012
- settings: record(string$2(), unknown())
47013
- });
47014
- var AddResultSchema = object({ id: string$2() });
47015
- var RemoveInputSchema = object({ id: string$2() });
47016
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
47017
- ok: literal(true),
47018
- latencyMs: number().nonnegative()
47019
- }), object({
47020
- ok: literal(false),
47021
- error: string$2()
47022
- })]);
47023
46978
  var SettingsRecordSchema$1 = record(string$2(), unknown());
47024
- var SettingsSchemaInputSchema = object({ kind: string$2() });
47025
- var TestSettingsInputSchema = object({
47026
- kind: string$2(),
47027
- settings: SettingsRecordSchema$1
47028
- });
47029
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
47030
- ok: literal(true),
47031
- latencyMs: number().nonnegative().optional()
47032
- }).strict(), object({
47033
- ok: literal(false),
47034
- error: string$2()
47035
- })]);
47036
- var SettingsSchemaResultSchema = unknown().nullable();
47037
46979
  var PublishInputSchema = object({
47038
46980
  brokerId: string$2(),
47039
46981
  /** Kind-specific routing target.
@@ -47077,51 +47019,12 @@ var brokerCapability = {
47077
47019
  kind: "poll"
47078
47020
  },
47079
47021
  methods: {
47080
- list: method(ListInputSchema, array(BrokerInfoSchema)),
47081
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
47082
- /** Enumerate which addon provides which broker kind(s) for the
47083
- * unified create picker. The auto-mount fans this array across
47084
- * every registered `broker` provider (array-output method), so the
47085
- * picker sees every kind from every provider in one call. */
47086
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
47087
- add: method(AddInputSchema, AddResultSchema, {
47088
- kind: "mutation",
47089
- auth: "admin"
47090
- }),
47091
- remove: method(RemoveInputSchema, _void(), {
47092
- kind: "mutation",
47093
- auth: "admin"
47094
- }),
47095
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
47096
- kind: "mutation",
47097
- auth: "admin"
47098
- }),
47099
- /** Read the persisted settings record for a broker (kind-specific
47100
- * shape). Admin-only — settings may contain secrets. Returns `null`
47101
- * when the broker id is unknown to the provider (the collection
47102
- * fallback may route a foreign id to the first provider). */
47103
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47104
- /** Overwrite the persisted settings record. The kind-specific
47105
- * provider validates the shape and applies the change (reconnects
47106
- * if credentials changed). */
47107
- setSettings: method(object({
47108
- id: string$2(),
47109
- settings: SettingsRecordSchema$1
47110
- }), _void(), {
47111
- kind: "mutation",
47112
- auth: "admin"
47113
- }),
47114
47022
  /** Returns the kind-specific connection config the consumer needs
47115
47023
  * to open its own client (MQTT pattern: `{url, username, password,
47116
47024
  * clientIdPrefix}`). HA providers MAY return the auth envelope
47117
47025
  * but typical HA consumers use `publish` / `subscribe` instead.
47118
47026
  * Returns `null` when the broker id is unknown to the provider. */
47119
47027
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
47120
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
47121
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
47122
- kind: "mutation",
47123
- auth: "admin"
47124
- }),
47125
47028
  publish: method(PublishInputSchema, unknown(), {
47126
47029
  kind: "mutation",
47127
47030
  auth: "admin"
@@ -51968,7 +51871,14 @@ var DiscoveryCandidateSchema = object({
51968
51871
  /** `stableId` of the gateway candidate this device was found BEHIND. */
51969
51872
  parentStableId: string$2().optional(),
51970
51873
  /** `false` = shown for context but not pickable. Default true. */
51971
- selectable: boolean().optional()
51874
+ selectable: boolean().optional(),
51875
+ /**
51876
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
51877
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
51878
+ * admin asks for exactly these, on the creation form prefilled with
51879
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
51880
+ */
51881
+ needsParams: array(string$2()).optional()
51972
51882
  });
51973
51883
  /**
51974
51884
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -52095,8 +52005,14 @@ var deviceProviderCapability = {
52095
52005
  * provider does not support manual creation. Lets the Add-Device dialog
52096
52006
  * pick the right type instead of assuming Camera.
52097
52007
  */
52098
- getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
52099
- adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
52008
+ getManualCreationType: method(object({
52009
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52010
+ integrationId: string$2().optional() }), object({ deviceType: _enum(DeviceType).nullable() })),
52011
+ adoptDiscoveredDevice: method(object({
52012
+ candidate: DiscoveryCandidateSchema,
52013
+ /** The integration the operator adopts into; a provider with several instances needs it. */
52014
+ integrationId: string$2().optional()
52015
+ }), DeviceSummarySchema, {
52100
52016
  kind: "mutation",
52101
52017
  auth: "admin"
52102
52018
  }),
@@ -52107,10 +52023,16 @@ var deviceProviderCapability = {
52107
52023
  * devices of that type. The output is a `ConfigUISchema` — the
52108
52024
  * router type-asserts it at the boundary.
52109
52025
  */
52110
- getChildCreationSchema: method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema),
52026
+ getChildCreationSchema: method(object({
52027
+ type: _enum(DeviceType),
52028
+ /** The integration asked about; a provider whose integrations differ answers per one. */
52029
+ integrationId: string$2().optional()
52030
+ }), CreationSchemaOutputSchema),
52111
52031
  createDevice: method(object({
52112
52032
  type: _enum(DeviceType),
52113
- config: record(string$2(), unknown())
52033
+ config: record(string$2(), unknown()),
52034
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
52035
+ integrationId: string$2().optional()
52114
52036
  }), DeviceSummarySchema, {
52115
52037
  kind: "mutation",
52116
52038
  auth: "admin"
@@ -52835,7 +52757,10 @@ method(object({
52835
52757
  auth: "admin"
52836
52758
  }), method(object({
52837
52759
  addonId: string$2(),
52838
- type: _enum(DeviceType)
52760
+ type: _enum(DeviceType),
52761
+ /** The integration the form is for; a provider whose integrations differ (a hub
52762
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
52763
+ integrationId: string$2().optional()
52839
52764
  }), unknown().nullable()), method(object({
52840
52765
  addonId: string$2(),
52841
52766
  type: _enum(DeviceType),
@@ -52919,7 +52844,11 @@ method(object({
52919
52844
  kind: "mutation",
52920
52845
  auth: "admin",
52921
52846
  timeoutMs: 12e4
52922
- }), method(object({ addonId: string$2() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string$2() }), unknown(), { auth: "admin" }), method(object({
52847
+ }), method(object({
52848
+ addonId: string$2(),
52849
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
52850
+ integrationId: string$2().optional()
52851
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string$2() }), unknown(), { auth: "admin" }), method(object({
52923
52852
  deviceId: number(),
52924
52853
  key: string$2(),
52925
52854
  value: unknown()
@@ -56199,7 +56128,7 @@ var TestConnectionStatusEnum = _enum([
56199
56128
  "inconclusive",
56200
56129
  "unsupported"
56201
56130
  ]);
56202
- var TestConnectionResultSchema$1 = object({
56131
+ var TestConnectionResultSchema = object({
56203
56132
  /** True ONLY for `validated`. Never true for a test that did not run. */
56204
56133
  success: boolean(),
56205
56134
  error: string$2().optional(),
@@ -56244,7 +56173,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
56244
56173
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56245
56174
  addonId: string$2(),
56246
56175
  settings: record(string$2(), unknown())
56247
- }), TestConnectionResultSchema$1, {
56176
+ }), TestConnectionResultSchema, {
56248
56177
  kind: "mutation",
56249
56178
  auth: "admin"
56250
56179
  });
@@ -56271,6 +56200,14 @@ var ConfigSchemaPassthrough$3 = custom();
56271
56200
  var IntegrationInstanceIdSchema = string$2().min(1);
56272
56201
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56273
56202
  /**
56203
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
56204
+ * instance is an integration that brings devices (the registry owns its on/off switch);
56205
+ * `services`: a connection configured from the addon's own function page, never listed on
56206
+ * Integrations.
56207
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
56208
+ */
56209
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
56210
+ /**
56274
56211
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56275
56212
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56276
56213
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -56281,6 +56218,22 @@ var IntegrationInstanceKindSchema = object({
56281
56218
  /** Stable id of the kind, sent back as `createInstance.kind`. */
56282
56219
  id: string$2().min(1),
56283
56220
  label: string$2().min(1),
56221
+ /**
56222
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
56223
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
56224
+ * provider with more than one section must name it on every kind.
56225
+ */
56226
+ functionCap: string$2().min(1),
56227
+ /**
56228
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
56229
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
56230
+ * finds the instances whose kind names it; absent takes the kind `id`.
56231
+ */
56232
+ brokerKind: string$2().min(1).optional(),
56233
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
56234
+ role: IntegrationInstanceRoleSchema.optional(),
56235
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
56236
+ canTest: boolean().optional(),
56284
56237
  /** The kind signs in through a server-held interactive session. */
56285
56238
  signIn: object({
56286
56239
  basePath: string$2().min(1),
@@ -56293,11 +56246,7 @@ var IntegrationInstanceKindSchema = object({
56293
56246
  });
56294
56247
  var IntegrationInstanceDescriptionSchema = object({
56295
56248
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56296
- role: _enum([
56297
- "devices",
56298
- "services",
56299
- "both"
56300
- ]),
56249
+ role: IntegrationInstanceRoleSchema,
56301
56250
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56302
56251
  discovery: object({
56303
56252
  mode: _enum([
@@ -56372,7 +56321,11 @@ var integrationInstancesCapability = {
56372
56321
  methods: {
56373
56322
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56374
56323
  /** The form; `id: null` is the create form. */
56375
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56324
+ instanceFormSchema: method(object({
56325
+ id: IntegrationInstanceIdSchema.nullable(),
56326
+ /** The kind of the create form (`id: null`) of a provider with several. */
56327
+ kind: string$2().min(1).optional()
56328
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56376
56329
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56377
56330
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56378
56331
  /**
@@ -56382,7 +56335,9 @@ var integrationInstancesCapability = {
56382
56335
  createInstance: method(object({
56383
56336
  id: IntegrationInstanceIdSchema.optional(),
56384
56337
  name: string$2().min(1),
56385
- kind: string$2().min(1).optional()
56338
+ kind: string$2().min(1).optional(),
56339
+ /** Form values written with the record (a link picked in the create dialog). */
56340
+ values: record(string$2(), unknown()).optional()
56386
56341
  }), object({ id: string$2() }), {
56387
56342
  kind: "mutation",
56388
56343
  auth: "admin"
@@ -56409,8 +56364,10 @@ var integrationInstancesCapability = {
56409
56364
  }),
56410
56365
  testInstance: optionalMethod(object({
56411
56366
  id: IntegrationInstanceIdSchema.nullable(),
56412
- values: record(string$2(), unknown())
56413
- }), TestConnectionResultSchema$1, {
56367
+ values: record(string$2(), unknown()),
56368
+ /** The kind to test when `id` is null and the provider has several. */
56369
+ kind: string$2().min(1).optional()
56370
+ }), TestConnectionResultSchema, {
56414
56371
  kind: "mutation",
56415
56372
  auth: "admin"
56416
56373
  })
@@ -59167,6 +59124,380 @@ var SystemMetricsSchema = object({
59167
59124
  gpuMemoryPercent: number().optional()
59168
59125
  });
59169
59126
  method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string$2() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string$2() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
59127
+ /**
59128
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
59129
+ * config documents, mirrors. The third storage API next to `ctx.settings`
59130
+ * (operator intent, D870) and device-history (numbers over time, D850).
59131
+ *
59132
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
59133
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
59134
+ * that with the addon id closed over). It is deliberately NOT in any input
59135
+ * schema, so a session can never supply it, and a call with no stamp is refused
59136
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
59137
+ * addons go through the owner's capability.
59138
+ *
59139
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
59140
+ * never `[]`. Secret fields are never on the wire: a read shows the
59141
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
59142
+ * resolves itself, and a write may carry only a keychain marker bound to the
59143
+ * row's own ref.
59144
+ *
59145
+ * Internal: addons reach it through `ctx.records`, never directly.
59146
+ */
59147
+ var RecordIdSchema = string$2().min(1).max(256);
59148
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
59149
+ var RecordBodySchema = record(string$2(), unknown());
59150
+ var RecordInputSchema = object({
59151
+ id: RecordIdSchema,
59152
+ /** The device this row belongs to (D865 removal, per-device reads). */
59153
+ deviceId: number().int().optional(),
59154
+ /** The domain clock, ms: observed / created / opened. */
59155
+ at: number().int().optional(),
59156
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
59157
+ expiresAt: number().int().optional(),
59158
+ data: RecordBodySchema
59159
+ });
59160
+ var RecordViewSchema = object({
59161
+ id: string$2(),
59162
+ deviceId: number().int().nullable(),
59163
+ at: number().nullable(),
59164
+ expiresAt: number().nullable(),
59165
+ rev: number().int(),
59166
+ updatedAt: number(),
59167
+ data: RecordBodySchema
59168
+ });
59169
+ /** Declared index fields only. Anything else does not compile, by design. */
59170
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
59171
+ string$2(),
59172
+ number(),
59173
+ boolean(),
59174
+ object({ in: array(union([
59175
+ string$2(),
59176
+ number(),
59177
+ boolean()
59178
+ ])).min(1).max(RECORD_BATCH_MAX) }),
59179
+ object({
59180
+ gt: union([string$2(), number()]).optional(),
59181
+ gte: union([string$2(), number()]).optional(),
59182
+ lt: union([string$2(), number()]).optional(),
59183
+ lte: union([string$2(), number()]).optional()
59184
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
59185
+ ]));
59186
+ var KindShape = { kind: RecordKindNameSchema };
59187
+ var TimeWindowShape = {
59188
+ deviceId: number().int().optional(),
59189
+ atFrom: number().optional(),
59190
+ atTo: number().optional()
59191
+ };
59192
+ var RecordQueryInputSchema = object({
59193
+ ...KindShape,
59194
+ ...TimeWindowShape,
59195
+ where: RecordWhereSchema.optional(),
59196
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
59197
+ order: _enum(["at", "id"]).optional(),
59198
+ desc: boolean().optional(),
59199
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
59200
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
59201
+ cursor: string$2().min(1).optional()
59202
+ });
59203
+ var RecordQueryResultSchema = object({
59204
+ records: array(RecordViewSchema),
59205
+ nextCursor: string$2().nullable()
59206
+ });
59207
+ var RecordKindStateSchema = _enum([
59208
+ "created",
59209
+ "unchanged",
59210
+ "updated"
59211
+ ]);
59212
+ var DeclareKindResultSchema = object({
59213
+ kindId: number().int(),
59214
+ state: RecordKindStateSchema,
59215
+ createdIndexes: array(string$2())
59216
+ });
59217
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
59218
+ var SecretRefsList = array(string$2());
59219
+ /**
59220
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
59221
+ * All ops of one call share ONE database file and ONE `transactFamily`.
59222
+ */
59223
+ var RecordTransactOpSchema = discriminatedUnion("op", [
59224
+ object({
59225
+ op: literal("put"),
59226
+ kind: RecordKindNameSchema,
59227
+ record: RecordInputSchema,
59228
+ ifRev: number().int().min(0).optional()
59229
+ }),
59230
+ object({
59231
+ op: literal("patch"),
59232
+ kind: RecordKindNameSchema,
59233
+ id: RecordIdSchema,
59234
+ data: RecordBodySchema,
59235
+ at: number().int().nullable().optional(),
59236
+ expiresAt: number().int().nullable().optional(),
59237
+ ifRev: number().int().min(1).optional()
59238
+ }),
59239
+ object({
59240
+ op: literal("delete"),
59241
+ kind: RecordKindNameSchema,
59242
+ id: RecordIdSchema
59243
+ })
59244
+ ]);
59245
+ var RecordSecretRefsSchema = record(string$2(), record(string$2(), string$2()));
59246
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string$2(), DeclareKindResultSchema) }), {
59247
+ kind: "mutation",
59248
+ auth: "admin"
59249
+ }), method(object({
59250
+ ...KindShape,
59251
+ id: RecordIdSchema
59252
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
59253
+ ...KindShape,
59254
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59255
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
59256
+ ...KindShape,
59257
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59258
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
59259
+ ...KindShape,
59260
+ record: RecordInputSchema,
59261
+ /** Compare-and-set. `0` means "the row must not exist". */
59262
+ ifRev: number().int().min(0).optional()
59263
+ }), object({
59264
+ rev: number().int(),
59265
+ releasedRefs: SecretRefsList
59266
+ }), {
59267
+ kind: "mutation",
59268
+ auth: "admin"
59269
+ }), method(object({
59270
+ ...KindShape,
59271
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
59272
+ }), object({
59273
+ count: number().int(),
59274
+ releasedRefs: SecretRefsList
59275
+ }), {
59276
+ kind: "mutation",
59277
+ auth: "admin"
59278
+ }), method(object({
59279
+ ...KindShape,
59280
+ id: RecordIdSchema,
59281
+ data: RecordBodySchema,
59282
+ at: number().int().nullable().optional(),
59283
+ expiresAt: number().int().nullable().optional(),
59284
+ ifRev: number().int().min(1).optional()
59285
+ }), object({
59286
+ rev: number().int(),
59287
+ releasedRefs: SecretRefsList
59288
+ }), {
59289
+ kind: "mutation",
59290
+ auth: "admin"
59291
+ }), method(object({
59292
+ ...KindShape,
59293
+ id: RecordIdSchema
59294
+ }), object({
59295
+ deleted: boolean(),
59296
+ secretRefs: SecretRefsList
59297
+ }), {
59298
+ kind: "mutation",
59299
+ auth: "admin"
59300
+ }), method(object({
59301
+ ...KindShape,
59302
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
59303
+ }), object({
59304
+ deleted: number().int(),
59305
+ secretRefs: SecretRefsList
59306
+ }), {
59307
+ kind: "mutation",
59308
+ auth: "admin"
59309
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
59310
+ ...KindShape,
59311
+ ...TimeWindowShape,
59312
+ where: RecordWhereSchema.optional()
59313
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
59314
+ ...KindShape,
59315
+ deviceId: number().int().optional(),
59316
+ from: number(),
59317
+ to: number(),
59318
+ bucketMs: number().int().positive()
59319
+ }), object({ buckets: array(object({
59320
+ bucketStart: number(),
59321
+ count: number().int()
59322
+ })) }), { auth: "admin" }), method(object({
59323
+ ...KindShape,
59324
+ op: _enum([
59325
+ "sum",
59326
+ "min",
59327
+ "max",
59328
+ "count"
59329
+ ]),
59330
+ field: RecordFieldPathSchema.optional(),
59331
+ groupBy: RecordFieldPathSchema.optional(),
59332
+ where: RecordWhereSchema.optional()
59333
+ }), object({ rows: array(object({
59334
+ group: union([
59335
+ string$2(),
59336
+ number(),
59337
+ _null()
59338
+ ]),
59339
+ value: number().nullable()
59340
+ })) }), { auth: "admin" }), method(object({
59341
+ ...KindShape,
59342
+ deviceId: number().int().optional(),
59343
+ atBefore: number().optional(),
59344
+ where: RecordWhereSchema.optional()
59345
+ }), object({
59346
+ deleted: number().int(),
59347
+ secretRefs: SecretRefsList
59348
+ }), {
59349
+ kind: "mutation",
59350
+ auth: "admin"
59351
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
59352
+ results: array(object({
59353
+ rev: number().int().nullable(),
59354
+ deleted: boolean()
59355
+ })),
59356
+ releasedRefs: SecretRefsList,
59357
+ secretRefs: SecretRefsList
59358
+ }), {
59359
+ kind: "mutation",
59360
+ auth: "admin"
59361
+ }), method(object({
59362
+ deviceId: number().int(),
59363
+ kinds: array(RecordKindNameSchema).optional()
59364
+ }), object({
59365
+ deleted: record(string$2(), number().int()),
59366
+ secretRefs: SecretRefsList
59367
+ }), {
59368
+ kind: "mutation",
59369
+ auth: "admin"
59370
+ }), method(object(KindShape), object({ id: number().int() }), {
59371
+ kind: "mutation",
59372
+ auth: "admin"
59373
+ });
59374
+ /**
59375
+ * The wire shapes of the migration API, version 1 (planning#447).
59376
+ *
59377
+ * A migration is code that outlives the server it was written against: an addon at
59378
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
59379
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
59380
+ * is the contract of every method of it, one Zod pair per method.
59381
+ *
59382
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
59383
+ * field is never removed or made required, an enum never narrows, an output field is
59384
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
59385
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
59386
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
59387
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
59388
+ */
59389
+ var Rows = record(string$2(), unknown());
59390
+ var IntegrationId = object({ integrationId: string$2().min(1) });
59391
+ var RecordId = object({ id: string$2().min(1) });
59392
+ var RecordRowSchema = object({
59393
+ id: string$2(),
59394
+ deviceId: number().int().nullable(),
59395
+ at: number().nullable(),
59396
+ expiresAt: number().nullable(),
59397
+ rev: number().int(),
59398
+ updatedAt: number(),
59399
+ data: Rows
59400
+ });
59401
+ var RecordPutShape = {
59402
+ id: string$2().min(1),
59403
+ deviceId: number().int().optional(),
59404
+ at: number().int().optional(),
59405
+ expiresAt: number().int().optional(),
59406
+ data: Rows
59407
+ };
59408
+ var MigrationIntegrationSchema = object({
59409
+ id: string$2(),
59410
+ addonId: string$2(),
59411
+ name: string$2(),
59412
+ info: Rows,
59413
+ originStableId: string$2().optional()
59414
+ });
59415
+ var MigrationDeviceSchema = object({
59416
+ id: number().int(),
59417
+ integrationId: string$2().nullable().optional(),
59418
+ /** The owning device; null or absent for a root. */
59419
+ parentDeviceId: number().int().nullable().optional(),
59420
+ type: string$2().optional(),
59421
+ name: string$2().optional(),
59422
+ stableId: string$2().optional()
59423
+ });
59424
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
59425
+ var MigrationApiInfoSchema = object({
59426
+ version: number().int(),
59427
+ methods: array(string$2())
59428
+ });
59429
+ object({
59430
+ message: string$2(),
59431
+ meta: Rows.optional(),
59432
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
59433
+ tags: object({ deviceId: number().int().optional() }).optional()
59434
+ });
59435
+ object({
59436
+ owner: string$2().min(1),
59437
+ metas: array(MigrationMetaSchema)
59438
+ }), object({
59439
+ bootId: string$2(),
59440
+ snapshot: _enum([
59441
+ "none",
59442
+ "written",
59443
+ "refused"
59444
+ ]),
59445
+ steps: array(MigrationOwnerStepSchema),
59446
+ /** The migration API the engine serves; absent from a server that predates it. */
59447
+ migrationApi: MigrationApiInfoSchema.optional()
59448
+ }), object({ id: string$2().min(1) }), object({
59449
+ proceed: boolean(),
59450
+ state: union([literal("pending"), MigrationStatusSchema]),
59451
+ reason: string$2().nullable()
59452
+ }), object({
59453
+ id: string$2().min(1),
59454
+ result: MigrationOwnerResultSchema
59455
+ }), object({
59456
+ status: MigrationStatusSchema,
59457
+ error: string$2().nullable(),
59458
+ rowsChanged: number().int().nonnegative()
59459
+ }), object({
59460
+ rows: number().int().nonnegative(),
59461
+ secrets: number().int().nonnegative()
59462
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string$2().min(1)) }), object({
59463
+ present: boolean(),
59464
+ removed: Rows
59465
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
59466
+ ...RecordPutShape,
59467
+ ifRev: number().int().optional()
59468
+ }), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
59469
+ partial: Rows,
59470
+ ifRev: number().int().optional()
59471
+ }), object({ rev: number().int() }), object({ deleted: boolean() }), object({
59472
+ deviceId: number().int().optional(),
59473
+ atFrom: number().optional(),
59474
+ atTo: number().optional(),
59475
+ where: RecordWhereSchema.optional(),
59476
+ limit: number().int().min(1).optional(),
59477
+ cursor: string$2().min(1).optional(),
59478
+ resolveSecrets: boolean().optional()
59479
+ }), object({
59480
+ records: array(RecordRowSchema),
59481
+ nextCursor: string$2().nullable()
59482
+ }), object({
59483
+ deviceId: number().int().optional(),
59484
+ atBefore: number().optional(),
59485
+ where: RecordWhereSchema.optional()
59486
+ }), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string$2().min(1)) }), object({ removed: array(string$2()) }), object({ refs: array(string$2()) }), object({ values: record(string$2(), string$2().nullable()) }), object({
59487
+ ref: string$2().min(1),
59488
+ value: string$2()
59489
+ }), object({}), object({ refs: array(string$2()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string$2().min(1) }), MigrationIntegrationSchema.nullable(), object({
59490
+ addonId: string$2().min(1),
59491
+ integrationId: string$2().min(1).optional()
59492
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
59493
+ id: number().int(),
59494
+ integrationId: string$2().nullable(),
59495
+ /** Whether the device carries any stored config; its content is not offered. */
59496
+ hasConfig: boolean()
59497
+ }).nullable(), object({
59498
+ deviceId: number().int(),
59499
+ integrationId: string$2().min(1)
59500
+ }), object({}), object({}), object({ nodeId: string$2() }), object({}), object({});
59170
59501
  method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string$2().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string$2().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
59171
59502
  id: string$2().min(1),
59172
59503
  error: string$2().min(1)
@@ -59196,7 +59527,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59196
59527
  "written",
59197
59528
  "refused"
59198
59529
  ]),
59199
- steps: array(MigrationOwnerStepSchema)
59530
+ steps: array(MigrationOwnerStepSchema),
59531
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
59532
+ migrationApi: MigrationApiInfoSchema.optional()
59200
59533
  }), {
59201
59534
  kind: "mutation",
59202
59535
  auth: "admin"
@@ -61863,7 +62196,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
61863
62196
  }), method(RelocateMediaInputSchema, object({ jobId: string$2() }), {
61864
62197
  kind: "mutation",
61865
62198
  auth: "admin"
61866
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
62199
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
62200
+ kind: "query",
62201
+ auth: "admin"
62202
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
61867
62203
  kind: "query",
61868
62204
  auth: "admin"
61869
62205
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -64518,7 +64854,7 @@ var BrokerStatusSchema$1 = _enum([
64518
64854
  "unreachable",
64519
64855
  "tls-error"
64520
64856
  ]);
64521
- var BrokerInfoSchema$1 = object({
64857
+ var BrokerInfoSchema = object({
64522
64858
  id: string$2(),
64523
64859
  name: string$2(),
64524
64860
  url: string$2(),
@@ -64581,7 +64917,7 @@ var StatusSchema = object({
64581
64917
  brokerCount: number(),
64582
64918
  embeddedRunning: boolean()
64583
64919
  });
64584
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
64920
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
64585
64921
  kind: "mutation",
64586
64922
  auth: "admin"
64587
64923
  }), method(IdInputSchema, _void(), {
@@ -71763,253 +72099,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
71763
72099
  auth: "admin"
71764
72100
  });
71765
72101
  /**
71766
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
71767
- * config documents, mirrors. The third storage API next to `ctx.settings`
71768
- * (operator intent, D870) and device-history (numbers over time, D850).
71769
- *
71770
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
71771
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
71772
- * that with the addon id closed over). It is deliberately NOT in any input
71773
- * schema, so a session can never supply it, and a call with no stamp is refused
71774
- * `records/no-caller`. A kind is readable and writable only by its owner; other
71775
- * addons go through the owner's capability.
71776
- *
71777
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
71778
- * never `[]`. Secret fields are never on the wire: a read shows the
71779
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
71780
- * resolves itself, and a write may carry only a keychain marker bound to the
71781
- * row's own ref.
71782
- *
71783
- * Internal: addons reach it through `ctx.records`, never directly.
71784
- */
71785
- var RecordIdSchema = string$2().min(1).max(256);
71786
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
71787
- var RecordBodySchema = record(string$2(), unknown());
71788
- var RecordInputSchema = object({
71789
- id: RecordIdSchema,
71790
- /** The device this row belongs to (D865 removal, per-device reads). */
71791
- deviceId: number().int().optional(),
71792
- /** The domain clock, ms: observed / created / opened. */
71793
- at: number().int().optional(),
71794
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
71795
- expiresAt: number().int().optional(),
71796
- data: RecordBodySchema
71797
- });
71798
- var RecordViewSchema = object({
71799
- id: string$2(),
71800
- deviceId: number().int().nullable(),
71801
- at: number().nullable(),
71802
- expiresAt: number().nullable(),
71803
- rev: number().int(),
71804
- updatedAt: number(),
71805
- data: RecordBodySchema
71806
- });
71807
- /** Declared index fields only. Anything else does not compile, by design. */
71808
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
71809
- string$2(),
71810
- number(),
71811
- boolean(),
71812
- object({ in: array(union([
71813
- string$2(),
71814
- number(),
71815
- boolean()
71816
- ])).min(1).max(RECORD_BATCH_MAX) }),
71817
- object({
71818
- gt: union([string$2(), number()]).optional(),
71819
- gte: union([string$2(), number()]).optional(),
71820
- lt: union([string$2(), number()]).optional(),
71821
- lte: union([string$2(), number()]).optional()
71822
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
71823
- ]));
71824
- var KindShape = { kind: RecordKindNameSchema };
71825
- var TimeWindowShape = {
71826
- deviceId: number().int().optional(),
71827
- atFrom: number().optional(),
71828
- atTo: number().optional()
71829
- };
71830
- var RecordQueryInputSchema = object({
71831
- ...KindShape,
71832
- ...TimeWindowShape,
71833
- where: RecordWhereSchema.optional(),
71834
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
71835
- order: _enum(["at", "id"]).optional(),
71836
- desc: boolean().optional(),
71837
- limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
71838
- /** Opaque keyset cursor from a previous answer. There is no offset. */
71839
- cursor: string$2().min(1).optional()
71840
- });
71841
- var RecordQueryResultSchema = object({
71842
- records: array(RecordViewSchema),
71843
- nextCursor: string$2().nullable()
71844
- });
71845
- var RecordKindStateSchema = _enum([
71846
- "created",
71847
- "unchanged",
71848
- "updated"
71849
- ]);
71850
- var DeclareKindResultSchema = object({
71851
- kindId: number().int(),
71852
- state: RecordKindStateSchema,
71853
- createdIndexes: array(string$2())
71854
- });
71855
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
71856
- var SecretRefsList = array(string$2());
71857
- /**
71858
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
71859
- * All ops of one call share ONE database file and ONE `transactFamily`.
71860
- */
71861
- var RecordTransactOpSchema = discriminatedUnion("op", [
71862
- object({
71863
- op: literal("put"),
71864
- kind: RecordKindNameSchema,
71865
- record: RecordInputSchema,
71866
- ifRev: number().int().min(0).optional()
71867
- }),
71868
- object({
71869
- op: literal("patch"),
71870
- kind: RecordKindNameSchema,
71871
- id: RecordIdSchema,
71872
- data: RecordBodySchema,
71873
- at: number().int().nullable().optional(),
71874
- expiresAt: number().int().nullable().optional(),
71875
- ifRev: number().int().min(1).optional()
71876
- }),
71877
- object({
71878
- op: literal("delete"),
71879
- kind: RecordKindNameSchema,
71880
- id: RecordIdSchema
71881
- })
71882
- ]);
71883
- var RecordSecretRefsSchema = record(string$2(), record(string$2(), string$2()));
71884
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string$2(), DeclareKindResultSchema) }), {
71885
- kind: "mutation",
71886
- auth: "admin"
71887
- }), method(object({
71888
- ...KindShape,
71889
- id: RecordIdSchema
71890
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
71891
- ...KindShape,
71892
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
71893
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
71894
- ...KindShape,
71895
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
71896
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
71897
- ...KindShape,
71898
- record: RecordInputSchema,
71899
- /** Compare-and-set. `0` means "the row must not exist". */
71900
- ifRev: number().int().min(0).optional()
71901
- }), object({
71902
- rev: number().int(),
71903
- releasedRefs: SecretRefsList
71904
- }), {
71905
- kind: "mutation",
71906
- auth: "admin"
71907
- }), method(object({
71908
- ...KindShape,
71909
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
71910
- }), object({
71911
- count: number().int(),
71912
- releasedRefs: SecretRefsList
71913
- }), {
71914
- kind: "mutation",
71915
- auth: "admin"
71916
- }), method(object({
71917
- ...KindShape,
71918
- id: RecordIdSchema,
71919
- data: RecordBodySchema,
71920
- at: number().int().nullable().optional(),
71921
- expiresAt: number().int().nullable().optional(),
71922
- ifRev: number().int().min(1).optional()
71923
- }), object({
71924
- rev: number().int(),
71925
- releasedRefs: SecretRefsList
71926
- }), {
71927
- kind: "mutation",
71928
- auth: "admin"
71929
- }), method(object({
71930
- ...KindShape,
71931
- id: RecordIdSchema
71932
- }), object({
71933
- deleted: boolean(),
71934
- secretRefs: SecretRefsList
71935
- }), {
71936
- kind: "mutation",
71937
- auth: "admin"
71938
- }), method(object({
71939
- ...KindShape,
71940
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
71941
- }), object({
71942
- deleted: number().int(),
71943
- secretRefs: SecretRefsList
71944
- }), {
71945
- kind: "mutation",
71946
- auth: "admin"
71947
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
71948
- ...KindShape,
71949
- ...TimeWindowShape,
71950
- where: RecordWhereSchema.optional()
71951
- }), object({ count: number().int() }), { auth: "admin" }), method(object({
71952
- ...KindShape,
71953
- deviceId: number().int().optional(),
71954
- from: number(),
71955
- to: number(),
71956
- bucketMs: number().int().positive()
71957
- }), object({ buckets: array(object({
71958
- bucketStart: number(),
71959
- count: number().int()
71960
- })) }), { auth: "admin" }), method(object({
71961
- ...KindShape,
71962
- op: _enum([
71963
- "sum",
71964
- "min",
71965
- "max",
71966
- "count"
71967
- ]),
71968
- field: RecordFieldPathSchema.optional(),
71969
- groupBy: RecordFieldPathSchema.optional(),
71970
- where: RecordWhereSchema.optional()
71971
- }), object({ rows: array(object({
71972
- group: union([
71973
- string$2(),
71974
- number(),
71975
- _null()
71976
- ]),
71977
- value: number().nullable()
71978
- })) }), { auth: "admin" }), method(object({
71979
- ...KindShape,
71980
- deviceId: number().int().optional(),
71981
- atBefore: number().optional(),
71982
- where: RecordWhereSchema.optional()
71983
- }), object({
71984
- deleted: number().int(),
71985
- secretRefs: SecretRefsList
71986
- }), {
71987
- kind: "mutation",
71988
- auth: "admin"
71989
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
71990
- results: array(object({
71991
- rev: number().int().nullable(),
71992
- deleted: boolean()
71993
- })),
71994
- releasedRefs: SecretRefsList,
71995
- secretRefs: SecretRefsList
71996
- }), {
71997
- kind: "mutation",
71998
- auth: "admin"
71999
- }), method(object({
72000
- deviceId: number().int(),
72001
- kinds: array(RecordKindNameSchema).optional()
72002
- }), object({
72003
- deleted: record(string$2(), number().int()),
72004
- secretRefs: SecretRefsList
72005
- }), {
72006
- kind: "mutation",
72007
- auth: "admin"
72008
- }), method(object(KindShape), object({ id: number().int() }), {
72009
- kind: "mutation",
72010
- auth: "admin"
72011
- });
72012
- /**
72013
72102
  * record-entries - the ONE admin surface for every list of user-created entries
72014
72103
  * (D881): notification targets, brokers, LLM profiles, anything an addon
72015
72104
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -81737,7 +81826,7 @@ var BaseDeviceProvider = class extends BaseAddon {
81737
81826
  * `deviceClasses` map (first registered type). `null` when manual creation is
81738
81827
  * unsupported. Lets the Add-Device dialog pick the right type per provider.
81739
81828
  */
81740
- async getManualCreationType() {
81829
+ async getManualCreationType(_input) {
81741
81830
  if (!await this.supportsManualCreation()) return { deviceType: null };
81742
81831
  return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
81743
81832
  }
@@ -81748,7 +81837,7 @@ var BaseDeviceProvider = class extends BaseAddon {
81748
81837
  return true;
81749
81838
  }
81750
81839
  async getChildCreationSchema(input) {
81751
- return this.onGetCreationSchema(input.type);
81840
+ return this.onGetCreationSchema(input.type, { integrationId: input.integrationId });
81752
81841
  }
81753
81842
  /**
81754
81843
  * Default kernel-orchestrated `createDevice` implementation. The
@@ -81760,7 +81849,7 @@ var BaseDeviceProvider = class extends BaseAddon {
81760
81849
  * `deviceClasses` instead.
81761
81850
  */
81762
81851
  async createDevice(input) {
81763
- const spec = await this.onCreateDevice(input.type, input.config);
81852
+ const spec = await this.onCreateDevice(input.type, input.config, { ...input.integrationId !== void 0 ? { integrationId: input.integrationId } : {} });
81764
81853
  const Class = this.deviceClasses[spec.meta.type];
81765
81854
  if (!Class) throw new Error(`${this.providerName} provider: no device class registered for type "${spec.meta.type}" — add it to the deviceClasses map`);
81766
81855
  const stableId = this.generateStableId(spec.meta.type, spec.config);
@@ -83169,20 +83258,6 @@ Object.freeze({
83169
83258
  access: "create",
83170
83259
  role: "viewer"
83171
83260
  },
83172
- "broker.add": {
83173
- capName: "broker",
83174
- capScope: "system",
83175
- addonId: null,
83176
- access: "create",
83177
- role: "admin"
83178
- },
83179
- "broker.get": {
83180
- capName: "broker",
83181
- capScope: "system",
83182
- addonId: null,
83183
- access: "view",
83184
- role: "admin"
83185
- },
83186
83261
  "broker.getBrokerConfig": {
83187
83262
  capName: "broker",
83188
83263
  capScope: "system",
@@ -83190,20 +83265,6 @@ Object.freeze({
83190
83265
  access: "view",
83191
83266
  role: "admin"
83192
83267
  },
83193
- "broker.getSettings": {
83194
- capName: "broker",
83195
- capScope: "system",
83196
- addonId: null,
83197
- access: "view",
83198
- role: "admin"
83199
- },
83200
- "broker.getSettingsSchema": {
83201
- capName: "broker",
83202
- capScope: "system",
83203
- addonId: null,
83204
- access: "view",
83205
- role: "admin"
83206
- },
83207
83268
  "broker.getState": {
83208
83269
  capName: "broker",
83209
83270
  capScope: "system",
@@ -83218,20 +83279,6 @@ Object.freeze({
83218
83279
  access: "view",
83219
83280
  role: "admin"
83220
83281
  },
83221
- "broker.list": {
83222
- capName: "broker",
83223
- capScope: "system",
83224
- addonId: null,
83225
- access: "view",
83226
- role: "admin"
83227
- },
83228
- "broker.listProviders": {
83229
- capName: "broker",
83230
- capScope: "system",
83231
- addonId: null,
83232
- access: "view",
83233
- role: "admin"
83234
- },
83235
83282
  "broker.publish": {
83236
83283
  capName: "broker",
83237
83284
  capScope: "system",
@@ -83239,20 +83286,6 @@ Object.freeze({
83239
83286
  access: "create",
83240
83287
  role: "admin"
83241
83288
  },
83242
- "broker.remove": {
83243
- capName: "broker",
83244
- capScope: "system",
83245
- addonId: null,
83246
- access: "delete",
83247
- role: "admin"
83248
- },
83249
- "broker.setSettings": {
83250
- capName: "broker",
83251
- capScope: "system",
83252
- addonId: null,
83253
- access: "create",
83254
- role: "admin"
83255
- },
83256
83289
  "broker.subscribe": {
83257
83290
  capName: "broker",
83258
83291
  capScope: "system",
@@ -83260,20 +83293,6 @@ Object.freeze({
83260
83293
  access: "create",
83261
83294
  role: "admin"
83262
83295
  },
83263
- "broker.testConnection": {
83264
- capName: "broker",
83265
- capScope: "system",
83266
- addonId: null,
83267
- access: "create",
83268
- role: "admin"
83269
- },
83270
- "broker.testSettings": {
83271
- capName: "broker",
83272
- capScope: "system",
83273
- addonId: null,
83274
- access: "create",
83275
- role: "admin"
83276
- },
83277
83296
  "broker.unsubscribe": {
83278
83297
  capName: "broker",
83279
83298
  capScope: "system",
@@ -88237,6 +88256,13 @@ Object.freeze({
88237
88256
  access: "view",
88238
88257
  role: "viewer"
88239
88258
  },
88259
+ "pipelineAnalytics.getMediaLocationCensus": {
88260
+ capName: "pipeline-analytics",
88261
+ capScope: "device",
88262
+ addonId: null,
88263
+ access: "view",
88264
+ role: "admin"
88265
+ },
88240
88266
  "pipelineAnalytics.getMediaReclaimStatus": {
88241
88267
  capName: "pipeline-analytics",
88242
88268
  capScope: "device",
@@ -97178,6 +97204,179 @@ object({
97178
97204
  delay: number().nonnegative().optional(),
97179
97205
  commands: record(string$2(), unknown())
97180
97206
  });
97207
+ var BrokerInUseError = class extends Error {
97208
+ brokerId;
97209
+ users;
97210
+ name = "BrokerInUseError";
97211
+ constructor(brokerId, users) {
97212
+ super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
97213
+ this.brokerId = brokerId;
97214
+ this.users = users;
97215
+ }
97216
+ };
97217
+ /** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
97218
+ async function assertBrokerUnused(brokerId, finders) {
97219
+ const users = [];
97220
+ for (const finder of finders) users.push(...await finder.find(brokerId));
97221
+ if (users.length > 0) throw new BrokerInUseError(brokerId, users);
97222
+ }
97223
+ /**
97224
+ * An account provider whose `deleteInstance` is refused while anything still uses the account
97225
+ * (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
97226
+ * delete the instances page calls, not on a separate broker cap.
97227
+ */
97228
+ function guardBrokerDelete(base, users) {
97229
+ return {
97230
+ ...base,
97231
+ deleteInstance: async (input) => {
97232
+ await assertBrokerUnused(input.id, users());
97233
+ return base.deleteInstance(input);
97234
+ }
97235
+ };
97236
+ }
97237
+ /**
97238
+ * The generic binding of a service to a broker (D1012). A service (an integration today; a
97239
+ * notifier target, a calendar source or an exporter later) names the broker it runs through by id
97240
+ * and keeps only its OWN link settings: never a copy of the broker's host, sign-in or secrets,
97241
+ * which stay with the broker and are edited on the Brokers page.
97242
+ *
97243
+ * One primitive serves every consumer:
97244
+ * - {@link declareBrokerLinkKind}: the record kind, body `{ name, brokerId, ...linkFields }`;
97245
+ * - {@link buildBrokerLinkForm}: the form, a `broker-select` picker plus the service's link
97246
+ * sections, which may be DYNAMIC (a list read from the chosen broker, like HA calendars);
97247
+ * - {@link buildBrokerLinkInstances}: the `integration-instances` provider of an integration;
97248
+ * - {@link resolveBrokerId} / {@link brokerLinkUserFinder}: which broker a record names and which
97249
+ * records name a broker, so a delete of a broker still in use can name its users generically.
97250
+ */
97251
+ /** The record kind an integration is stored under. */
97252
+ var BROKER_LINK_KIND_NAME = "connections";
97253
+ /** 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. */
97254
+ var BrokerLinkBodySchema = entryBody({ brokerId: string$2() });
97255
+ function declareBrokerLinkKind(spec) {
97256
+ return defineEntryKind(BROKER_LINK_KIND_NAME, {
97257
+ title: spec.title,
97258
+ schema: BrokerLinkBodySchema.extend(spec.linkFields ?? {}),
97259
+ writeTrigger: spec.writeTrigger
97260
+ });
97261
+ }
97262
+ /** The form: a picker over the brokers of `brokerKind`, then the service's link sections. */
97263
+ async function buildBrokerLinkForm(spec, brokerId) {
97264
+ const dynamic = brokerId !== void 0 && spec.dynamicSections !== void 0 ? await spec.dynamicSections(brokerId) : [];
97265
+ return { sections: [
97266
+ {
97267
+ id: "broker-link",
97268
+ title: spec.brokerLabel,
97269
+ description: `Each integration links to a ${spec.brokerLabel} connection (Connectivity → Brokers). The connection and its sign-in are edited there.`,
97270
+ columns: 1,
97271
+ fields: [{
97272
+ type: "broker-select",
97273
+ key: "brokerId",
97274
+ label: spec.brokerLabel,
97275
+ brokerKind: spec.brokerKind
97276
+ }]
97277
+ },
97278
+ ...spec.linkSections ?? [],
97279
+ ...dynamic
97280
+ ] };
97281
+ }
97282
+ /** The broker a record names; undefined while it names none. */
97283
+ function resolveBrokerId(record) {
97284
+ return record.brokerId === void 0 || record.brokerId === "" ? void 0 : record.brokerId;
97285
+ }
97286
+ /** The broker an integration links to, from its record. */
97287
+ async function brokerIdOf(instances, integrationId) {
97288
+ const body = await instances.getResolved(integrationId);
97289
+ return body === null ? void 0 : resolveBrokerId(body);
97290
+ }
97291
+ /** Every record of the kind that names `brokerId`, as users of that broker. */
97292
+ function brokerLinkUserFinder(instances, userKind = "integration") {
97293
+ return { find: async (brokerId) => (await instances.listResolved()).filter((entry) => resolveBrokerId(entry.body) === brokerId).map((entry) => ({
97294
+ kind: userKind,
97295
+ id: entry.id,
97296
+ name: entry.body.name
97297
+ })) };
97298
+ }
97299
+ /** The `integration-instances` provider of an addon whose integrations link to a broker. */
97300
+ function buildBrokerLinkInstances(records, spec) {
97301
+ const instances = new RecordIntegrationInstances(records, spec.kind, {
97302
+ description: {
97303
+ role: "devices",
97304
+ instanceMode: "multiple",
97305
+ kind: "connection",
97306
+ canTest: false
97307
+ },
97308
+ formSchema: async (id) => buildBrokerLinkForm(spec, id === null ? void 0 : await brokerIdOf(instances, id)),
97309
+ initialBody: (name) => spec.kind.schema.parse({
97310
+ name,
97311
+ brokerId: ""
97312
+ }),
97313
+ configured: (body) => resolveBrokerId(body) !== void 0,
97314
+ ...spec.onChanged !== void 0 ? { onChanged: spec.onChanged } : {}
97315
+ });
97316
+ return instances;
97317
+ }
97318
+ var BROKER_ROWS_MAX = 1e3;
97319
+ var STRAY_REGISTRY_ID = /^int_\d+$/;
97320
+ async function moveBrokerLinksToConnections(ports) {
97321
+ const brokers = await ports.listBrokers();
97322
+ const strays = brokers.filter((b) => STRAY_REGISTRY_ID.test(b.id));
97323
+ 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(", ")}`);
97324
+ let moved = 0;
97325
+ for (const broker of brokers) {
97326
+ const integrationId = broker.integrationId;
97327
+ if (integrationId === void 0 || integrationId === "") continue;
97328
+ const existing = await ports.readConnection(integrationId);
97329
+ if (existing === null) await ports.putConnection(integrationId, {
97330
+ name: broker.name,
97331
+ brokerId: broker.id
97332
+ });
97333
+ else if (existing.brokerId !== broker.id) {
97334
+ ports.warn(`integration '${integrationId}' already names broker '${existing.brokerId ?? "none"}', not '${broker.id}'; the link stays on the broker`);
97335
+ continue;
97336
+ }
97337
+ if ((await ports.readConnection(integrationId))?.brokerId !== broker.id) throw new Error(`integration '${integrationId}': the link did not read back - broker '${broker.id}' keeps it`);
97338
+ await ports.clearBrokerLink(broker.id);
97339
+ moved += 1;
97340
+ }
97341
+ return {
97342
+ outcome: moved === 0 ? "already-done" : "done",
97343
+ summary: {
97344
+ brokers: brokers.length,
97345
+ moved,
97346
+ strayIntegrationRows: strays.length
97347
+ }
97348
+ };
97349
+ }
97350
+ /** The ports over the two record kinds of the addon. */
97351
+ function brokerLinkPortsOver(deps) {
97352
+ return {
97353
+ warn: deps.warn,
97354
+ listBrokers: async () => {
97355
+ return (await deps.brokers.query({ limit: BROKER_ROWS_MAX })).records.map((row) => {
97356
+ const name = row.data["name"];
97357
+ const link = row.data["integrationId"];
97358
+ return {
97359
+ id: row.id,
97360
+ name: typeof name === "string" && name.length > 0 ? name : row.id,
97361
+ integrationId: typeof link === "string" ? link : void 0
97362
+ };
97363
+ });
97364
+ },
97365
+ readConnection: async (id) => {
97366
+ const row = await deps.connections.get(id);
97367
+ return row === null ? null : { brokerId: row.data.brokerId };
97368
+ },
97369
+ putConnection: async (id, body) => {
97370
+ await deps.connections.put({
97371
+ id,
97372
+ data: body
97373
+ }, { ifRev: 0 });
97374
+ },
97375
+ clearBrokerLink: async (brokerId) => {
97376
+ await deps.brokers.patch(brokerId, { integrationId: null });
97377
+ }
97378
+ };
97379
+ }
97181
97380
  //#endregion
97182
97381
  //#region src/config.ts
97183
97382
  /**
@@ -97204,7 +97403,6 @@ var MatterBrokerEntrySchema = object({
97204
97403
  id: string$2().min(1),
97205
97404
  name: string$2().min(1),
97206
97405
  connection: matterConnectionSchema,
97207
- integrationId: string$2().optional(),
97208
97406
  /** Absent means on; the instances page switches a fabric off without deleting it. */
97209
97407
  enabled: boolean().optional()
97210
97408
  });
@@ -97231,14 +97429,6 @@ function publicMatterConnection(connection) {
97231
97429
  threadDataset: ""
97232
97430
  };
97233
97431
  }
97234
- /** Keep a stored secret when the form sent back the redacted empty string. */
97235
- function mergeMatterSecrets(existing, incoming) {
97236
- return {
97237
- ...incoming,
97238
- wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
97239
- threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
97240
- };
97241
- }
97242
97432
  /** Hand-written connection form for the broker/integration creation UI. */
97243
97433
  function buildConnectionFormSchema() {
97244
97434
  return { sections: [{
@@ -207622,7 +207812,6 @@ var MatterBrokerRegistry = class {
207622
207812
  #onAttributeReport;
207623
207813
  #makeManager;
207624
207814
  #managers = /* @__PURE__ */ new Map();
207625
- #integrationToBroker = /* @__PURE__ */ new Map();
207626
207815
  #nextId = 1;
207627
207816
  constructor(logger, deps) {
207628
207817
  this.#logger = logger;
@@ -207671,7 +207860,6 @@ var MatterBrokerRegistry = class {
207671
207860
  }
207672
207861
  }));
207673
207862
  this.#managers.clear();
207674
- this.#integrationToBroker.clear();
207675
207863
  }
207676
207864
  setOnBrokerConnected(cb) {
207677
207865
  this.#onBrokerConnected = cb;
@@ -207691,17 +207879,14 @@ var MatterBrokerRegistry = class {
207691
207879
  const entry = {
207692
207880
  id,
207693
207881
  name,
207694
- connection,
207695
- ...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
207882
+ connection
207696
207883
  };
207697
207884
  await this.#startManager(entry);
207698
- if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
207699
207885
  return entry;
207700
207886
  }
207701
207887
  async removeEntry(id) {
207702
207888
  const mgr = this.#managers.get(id);
207703
207889
  if (!mgr) return;
207704
- for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
207705
207890
  this.#managers.delete(id);
207706
207891
  try {
207707
207892
  await mgr.stop();
@@ -207742,9 +207927,6 @@ var MatterBrokerRegistry = class {
207742
207927
  if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
207743
207928
  await mgr.invokeCommand(invocation);
207744
207929
  }
207745
- linkIntegration(integrationId, brokerId) {
207746
- this.#integrationToBroker.set(integrationId, brokerId);
207747
- }
207748
207930
  list() {
207749
207931
  return Array.from(this.#managers.values()).map((m) => m.getInfo());
207750
207932
  }
@@ -207763,10 +207945,7 @@ var MatterBrokerRegistry = class {
207763
207945
  return count;
207764
207946
  }
207765
207947
  async #startManager(entry) {
207766
- if (this.#managers.has(entry.id)) {
207767
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
207768
- return;
207769
- }
207948
+ if (this.#managers.has(entry.id)) return;
207770
207949
  const mgr = this.#makeManager({
207771
207950
  id: entry.id,
207772
207951
  name: entry.name,
@@ -207779,7 +207958,6 @@ var MatterBrokerRegistry = class {
207779
207958
  onAttributeReport: (id, report) => this.#onAttributeReport(id, report)
207780
207959
  });
207781
207960
  this.#managers.set(entry.id, mgr);
207782
- if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
207783
207961
  await mgr.start();
207784
207962
  }
207785
207963
  /** The next free broker id, for a record minted before its manager exists. */
@@ -207829,7 +208007,7 @@ function parsePairingCode(raw) {
207829
208007
  normalized: digits
207830
208008
  };
207831
208009
  }
207832
- /** True when a code parses cleanly (no throw). Convenience for `testSettings`. */
208010
+ /** True when a code parses cleanly (no throw). Convenience for validating the pasted code. */
207833
208011
  function isValidPairingCode(raw) {
207834
208012
  try {
207835
208013
  parsePairingCode(raw);
@@ -207840,113 +208018,22 @@ function isValidPairingCode(raw) {
207840
208018
  }
207841
208019
  //#endregion
207842
208020
  //#region src/matter-broker-provider.ts
207843
- var MATTER_KIND = "matter";
207844
208021
  /**
207845
- * Construct the `broker` cap provider for the Matter addon. Pure builder.
207846
- * Mirrors `buildEcowittBrokerProvider`. The defining Matter operation —
207847
- * commissioning — is surfaced through `publish` with
207848
- * `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
207849
- * `threadDataset` and `wifiPassword` are redacted on settings reads and
207850
- * preserved on an empty round-trip. Fabric keys stay in matter.js storage.
208022
+ * Construct the `broker` cap provider for the Matter addon: the bus only (D1013). The fabrics
208023
+ * themselves are the `account` kind of the addon's `integration-instances` provider. The defining
208024
+ * Matter operation, commissioning, is surfaced through `publish` with
208025
+ * `target: { action: 'commissionNode' }` and `payload: { pairingCode }`. `threadDataset` and
208026
+ * `wifiPassword` are redacted on config reads. Fabric keys stay in matter.js storage.
207851
208027
  */
207852
208028
  function buildMatterBrokerProvider(deps) {
207853
- const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
208029
+ const { registry, getBrokers } = deps;
207854
208030
  const owns = (id) => getBrokers().some((b) => b.id === id);
207855
208031
  const redact = (entry) => publicMatterConnection(entry.connection);
207856
208032
  return {
207857
- list: async ({ kind }) => {
207858
- if (kind && kind !== MATTER_KIND) return [];
207859
- return registry.list().map((info) => ({
207860
- ...info,
207861
- addonId: ownerAddonId
207862
- }));
207863
- },
207864
- get: async ({ id }) => {
207865
- const info = registry.get(id);
207866
- return info ? {
207867
- ...info,
207868
- addonId: ownerAddonId
207869
- } : null;
207870
- },
207871
- listProviders: async () => [{
207872
- addonId: ownerAddonId,
207873
- kinds: [{
207874
- kind: MATTER_KIND,
207875
- label: "Matter"
207876
- }]
207877
- }],
207878
- add: async ({ kind, name, settings }) => {
207879
- if (kind !== MATTER_KIND) throw new Error(`matter-broker: only kind '${MATTER_KIND}' is handled here (got '${kind}')`);
207880
- const entry = await registry.createEntry(name, settingsToMatterConnection(settings));
207881
- await persistBrokers([...getBrokers(), entry]);
207882
- return { id: entry.id };
207883
- },
207884
- remove: async ({ id }) => {
207885
- if (!owns(id)) return;
207886
- await registry.removeEntry(id);
207887
- await cascadeRemoveDevices(id).catch((err) => {
207888
- logger.warn("matter: broker cascade-remove threw", {
207889
- tags: { brokerId: id },
207890
- meta: { error: errMsg(err) }
207891
- });
207892
- });
207893
- await persistBrokers(getBrokers().filter((b) => b.id !== id));
207894
- },
207895
- testConnection: async ({ id }) => {
207896
- if (!owns(id)) return {
207897
- ok: false,
207898
- error: "unknown broker"
207899
- };
207900
- const info = registry.get(id);
207901
- if (!info) return {
207902
- ok: false,
207903
- error: "unknown broker"
207904
- };
207905
- if (info.status === "connected") return {
207906
- ok: true,
207907
- latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
207908
- };
207909
- return {
207910
- ok: false,
207911
- error: info.error ?? `status: ${info.status}`
207912
- };
207913
- },
207914
- getSettings: async ({ id }) => {
207915
- const entry = getBrokers().find((b) => b.id === id);
207916
- return entry ? redact(entry) : null;
207917
- },
207918
- setSettings: async ({ id, settings }) => {
207919
- if (!owns(id)) return;
207920
- const existing = getBrokers().find((b) => b.id === id);
207921
- if (!existing) return;
207922
- const parsed = settingsToMatterConnection(settings);
207923
- const merged = mergeMatterSecrets(existing.connection, parsed);
207924
- const updated = await registry.updateEntry(id, merged);
207925
- await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
207926
- },
207927
208033
  getBrokerConfig: async ({ id }) => {
207928
208034
  const entry = getBrokers().find((b) => b.id === id);
207929
208035
  return entry ? redact(entry) : null;
207930
208036
  },
207931
- getSettingsSchema: async ({ kind }) => {
207932
- if (kind !== MATTER_KIND) return null;
207933
- return buildConnectionFormSchema();
207934
- },
207935
- testSettings: async ({ kind, settings }) => {
207936
- if (kind !== MATTER_KIND) return {
207937
- ok: false,
207938
- error: `unsupported kind: ${kind}`
207939
- };
207940
- try {
207941
- settingsToMatterConnection(settings);
207942
- return { ok: true };
207943
- } catch (err) {
207944
- return {
207945
- ok: false,
207946
- error: errMsg(err)
207947
- };
207948
- }
207949
- },
207950
208037
  /**
207951
208038
  * `publish` is the commissioning + control action channel.
207952
208039
  * `target: { action: 'commissionNode' }`, `payload: { pairingCode }` →
@@ -208166,8 +208253,8 @@ function buildMatterAdoptionProvider(deps) {
208166
208253
  //#region src/matter-broker-device-cascade.ts
208167
208254
  /**
208168
208255
  * Remove every adopted Matter PARENT device (children cascade via the kernel)
208169
- * whose persisted config carries `{ system: 'matter', brokerId }`. Used on
208170
- * broker.remove and integration delete. Best-effort per device; returns the
208256
+ * whose persisted config carries `{ system: 'matter', brokerId }`. Used when a
208257
+ * fabric instance is deleted and when an integration is deleted. Best-effort per device; returns the
208171
208258
  * count removed. Mirrors the Ecowitt cascade helper.
208172
208259
  */
208173
208260
  async function cascadeRemoveDevicesForBroker(input) {
@@ -208807,20 +208894,26 @@ var matterBrokersEntryKind = defineEntryKind("brokers", {
208807
208894
  title: "Matter brokers",
208808
208895
  schema: entryBody({
208809
208896
  ...matterConnectionSchema.shape,
208897
+ /** Retired (D1012): the link lives on the integration's `connections` record. Only the migration reads it. */
208810
208898
  integrationId: string$2().optional(),
208811
208899
  enabled: boolean().optional()
208812
208900
  }),
208813
208901
  writeTrigger: "an operator adds, edits or removes a Matter fabric broker",
208814
208902
  retiredSecretPaths: ["connection.threadDataset", "connection.wifiPassword"]
208815
208903
  });
208904
+ /** A Matter integration: the link to the fabric (broker) it imports devices from. */
208905
+ var matterConnectionsEntryKind = declareBrokerLinkKind({
208906
+ title: "Matter integrations",
208907
+ writeTrigger: "an operator links a Matter integration to a Matter fabric"
208908
+ });
208816
208909
  //#endregion
208817
- //#region src/matter-instances.ts
208910
+ //#region src/matter-account-instances.ts
208818
208911
  /**
208819
- * The Matter fabrics as `integration-instances` (planning#408 P3): one instance per `brokers`
208912
+ * The Matter fabrics as `integration-instances` (planning#408 P3, D1013; the `account` kind of the composed provider, the integrations are `matter-instances.ts`): one instance per `brokers`
208820
208913
  * record, over the SAME records and ids (`matter_NNN`) the `broker` cap serves. The body is flat.
208821
208914
  * A fabric needs no sign-in; commissioning a device stays on the adoption flow, behind the fabric.
208822
208915
  */
208823
- var MATTER_INSTANCE_KIND = "matter";
208916
+ var MATTER_ACCOUNT_KIND = "account";
208824
208917
  function statusOf(info) {
208825
208918
  if (info === null) return {
208826
208919
  state: "unknown",
@@ -208840,17 +208933,13 @@ function statusOf(info) {
208840
208933
  ...info.error !== null ? { detail: info.error } : {}
208841
208934
  };
208842
208935
  }
208843
- function buildMatterInstances(records, deps) {
208936
+ function buildMatterAccountInstances(records, deps) {
208844
208937
  return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208845
208938
  description: {
208846
- role: "both",
208939
+ role: "services",
208847
208940
  instanceMode: "multiple",
208848
208941
  kind: "connection",
208849
208942
  canTest: true,
208850
- kinds: [{
208851
- id: MATTER_INSTANCE_KIND,
208852
- label: "Matter"
208853
- }],
208854
208943
  allowRestoreId: true
208855
208944
  },
208856
208945
  formSchema: () => buildConnectionFormSchema(),
@@ -208862,7 +208951,7 @@ function buildMatterInstances(records, deps) {
208862
208951
  reconcileIntervalMs: 3e5
208863
208952
  }),
208864
208953
  mintId: () => deps.mintId(),
208865
- kindOf: () => MATTER_INSTANCE_KIND,
208954
+ kindOf: () => MATTER_ACCOUNT_KIND,
208866
208955
  health: (_body, id) => {
208867
208956
  const info = deps.infoOf(id);
208868
208957
  return {
@@ -208891,6 +208980,22 @@ function buildMatterInstances(records, deps) {
208891
208980
  });
208892
208981
  }
208893
208982
  //#endregion
208983
+ //#region src/matter-instances.ts
208984
+ /**
208985
+ * The Matter integrations as `integration-instances` (D1012): one `connections` record per
208986
+ * integration, keyed by the registry id, whose body is the link to a Matter fabric (a `brokers`
208987
+ * record, edited on the Brokers page).
208988
+ */
208989
+ var MATTER_BROKER_KIND = "matter";
208990
+ function buildMatterInstances(records) {
208991
+ return buildBrokerLinkInstances(records, {
208992
+ kind: matterConnectionsEntryKind,
208993
+ brokerKind: MATTER_BROKER_KIND,
208994
+ brokerLabel: "Matter fabric",
208995
+ onChanged: () => void 0
208996
+ });
208997
+ }
208998
+ //#endregion
208894
208999
  //#region src/migrations/flatten-broker-records.ts
208895
209000
  /**
208896
209001
  * The Matter broker records keep their fabric settings flat (planning#408 P3): the nested
@@ -208914,6 +209019,21 @@ async function flattenBrokerRecords(ports) {
208914
209019
  });
208915
209020
  }
208916
209021
  //#endregion
209022
+ //#region src/migrations/broker-links-to-connections.ts
209023
+ var BROKER_LINKS_TO_CONNECTIONS_ID = "matter-broker/2026-11-05-broker-links-to-connections";
209024
+ /** The broker records and the integration (`connections`) records of this addon. */
209025
+ function matterBrokerLinkPorts(records, warn) {
209026
+ return brokerLinkPortsOver({
209027
+ brokers: records.kind({
209028
+ name: matterBrokersEntryKind.name,
209029
+ declaration: matterBrokersEntryKind.declaration,
209030
+ schema: record(string$2(), unknown())
209031
+ }),
209032
+ connections: records.kind(matterConnectionsEntryKind),
209033
+ warn
209034
+ });
209035
+ }
209036
+ //#endregion
208917
209037
  //#region src/migrations/index.ts
208918
209038
  /** The `matter-broker` catalogue (D987): offered to the engine when the addon initialises. */
208919
209039
  var ADDON_MIGRATIONS = [{
@@ -208931,6 +209051,21 @@ var ADDON_MIGRATIONS = [{
208931
209051
  fixtureMark: "the integration link and the Wi-Fi SSID of each fabric"
208932
209052
  },
208933
209053
  procedure: flattenBrokerRecords
209054
+ }, {
209055
+ meta: {
209056
+ id: BROKER_LINKS_TO_CONNECTIONS_ID,
209057
+ owner: "matter-broker",
209058
+ level: "addon-state",
209059
+ seq: 2,
209060
+ dependsOn: [FLATTEN_BROKER_RECORDS_ID],
209061
+ store: "settings",
209062
+ scope: "cluster",
209063
+ onFailure: "block-owner",
209064
+ bodyKind: "procedure",
209065
+ description: "Write each integration a connections record naming its Matter fabric (keyed by the integration id), then clear the integration link on the fabric; stray registry-keyed fabric rows are counted and logged, never deleted",
209066
+ fixtureMark: "the name and the Wi-Fi SSID of the fabric that keeps its record"
209067
+ },
209068
+ procedure: moveBrokerLinksToConnections
208934
209069
  }];
208935
209070
  //#endregion
208936
209071
  //#region src/matter-broker-store.ts
@@ -208944,7 +209079,6 @@ function rowToEntry(id, data) {
208944
209079
  wifiPassword: data.wifiPassword,
208945
209080
  reconcileIntervalMs: data.reconcileIntervalMs
208946
209081
  },
208947
- integrationId: data.integrationId,
208948
209082
  enabled: data.enabled
208949
209083
  });
208950
209084
  return parsed.success ? parsed.data : null;
@@ -208981,11 +209115,10 @@ var MatterBrokerStore = class MatterBrokerStore {
208981
209115
  const nextIds = new Set(entries.map((e) => e.id));
208982
209116
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
208983
209117
  for (const entry of entries) {
208984
- const { id, name, connection, integrationId, enabled } = entry;
209118
+ const { id, name, connection, enabled } = entry;
208985
209119
  const body = {
208986
209120
  name,
208987
209121
  ...connection,
208988
- ...integrationId === void 0 ? {} : { integrationId },
208989
209122
  ...enabled === void 0 ? {} : { enabled }
208990
209123
  };
208991
209124
  const parsed = matterBrokersEntryKind.schema.safeParse(body);
@@ -209023,6 +209156,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209023
209156
  registry = null;
209024
209157
  brokerStore = null;
209025
209158
  instances = null;
209159
+ accountInstances = null;
209026
209160
  activeBrokers = [];
209027
209161
  constructor() {
209028
209162
  super({ ...DEFAULTS });
@@ -209031,15 +209165,22 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209031
209165
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
209032
209166
  const records = this.ctx.records;
209033
209167
  if (records === void 0) throw new Error("matter-broker: ctx.records is required (hub-only addon)");
209034
- await records.declare({ brokers: matterBrokersEntryKind });
209168
+ await records.declare({
209169
+ brokers: matterBrokersEntryKind,
209170
+ connections: matterConnectionsEntryKind
209171
+ });
209035
209172
  this.brokerStore = MatterBrokerStore.of(records);
209036
- this.instances = buildMatterInstances(records, {
209173
+ this.instances = buildMatterInstances(records);
209174
+ this.accountInstances = buildMatterAccountInstances(records, {
209037
209175
  mintId: () => this.requireRegistry().allocateId(),
209038
209176
  infoOf: (id) => this.registry?.get(id) ?? null,
209039
209177
  onChanged: (id) => this.syncBrokerRuntime(id)
209040
209178
  });
209041
209179
  if (this.ctx.secrets === void 0) throw new Error("matter-broker: ctx.secrets is required");
209042
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, flattenPortsOver(records.kind(matterBrokersEntryKind))));
209180
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
209181
+ ...flattenPortsOver(records.kind(matterBrokersEntryKind)),
209182
+ ...matterBrokerLinkPorts(records, (message) => this.ctx.logger.warn(message))
209183
+ }));
209043
209184
  this.activeBrokers = await this.loadPersistedBrokers();
209044
209185
  this.registry = new MatterBrokerRegistry(this.ctx.logger, {
209045
209186
  dataDir: this.ctx.dataDir,
@@ -209060,8 +209201,6 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209060
209201
  matterCommands.setDispatcher((brokerId, invocation) => this.requireRegistry().invokeCommand(brokerId, invocation));
209061
209202
  await this.registry.restore(this.activeBrokers);
209062
209203
  this.ctx.logger.info("Matter: broker addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
209063
- await this.reconcileIntegrationsToBrokers();
209064
- this.subscribeIntegrationLifecycle();
209065
209204
  return [
209066
209205
  ...regs,
209067
209206
  {
@@ -209070,7 +209209,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209070
209209
  },
209071
209210
  {
209072
209211
  capability: integrationInstancesCapability,
209073
- provider: this.requireInstances().toProvider()
209212
+ provider: this.buildInstancesProvider()
209074
209213
  },
209075
209214
  {
209076
209215
  capability: deviceAdoptionCapability,
@@ -209089,9 +209228,37 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209089
209228
  if (this.instances === null) throw new Error("matter-broker: instances not ready");
209090
209229
  return this.instances;
209091
209230
  }
209231
+ /** One provider over the two record kinds: the fabric (broker) and the integrations using it. */
209232
+ buildInstancesProvider() {
209233
+ if (this.accountInstances === null) throw new Error("matter-broker: fabric instances not ready");
209234
+ return composeInstanceKinds([{
209235
+ kind: {
209236
+ id: "account",
209237
+ label: "Matter fabric",
209238
+ functionCap: "broker",
209239
+ brokerKind: "matter",
209240
+ role: "services"
209241
+ },
209242
+ provider: guardBrokerDelete(this.accountInstances.toProvider(), () => [brokerLinkUserFinder(this.requireInstances())])
209243
+ }, {
209244
+ kind: {
209245
+ id: "integration",
209246
+ label: "Matter",
209247
+ functionCap: "device-provider",
209248
+ role: "devices"
209249
+ },
209250
+ provider: this.requireInstances().toProvider()
209251
+ }], {
209252
+ role: "devices",
209253
+ instanceMode: "multiple",
209254
+ kind: "account",
209255
+ canTest: true,
209256
+ allowRestoreId: true
209257
+ });
209258
+ }
209092
209259
  /**
209093
- * Bring the runtime in line with one record after the instances page changed it: a removed
209094
- * record stops its controller and cascades its devices, an off record only stops the
209260
+ * Bring the runtime in line with one fabric record after the instances page changed it: a
209261
+ * removed record stops its controller and cascades its devices, an off record only stops the
209095
209262
  * controller, a live one re-applies its settings or starts under the record's own id.
209096
209263
  */
209097
209264
  async syncBrokerRuntime(id) {
@@ -209109,10 +209276,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209109
209276
  }
209110
209277
  try {
209111
209278
  if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
209112
- else await reg.createEntry(entry.name, entry.connection, {
209113
- id,
209114
- ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
209115
- });
209279
+ else await reg.createEntry(entry.name, entry.connection, { id });
209116
209280
  } catch (err) {
209117
209281
  this.ctx.logger.warn("matter instance: could not start the fabric", { meta: {
209118
209282
  brokerId: id,
@@ -209239,12 +209403,8 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209239
209403
  }
209240
209404
  buildBrokerProvider() {
209241
209405
  return buildMatterBrokerProvider({
209242
- ownerAddonId: this.ctx.id,
209243
209406
  registry: this.requireRegistry(),
209244
- getBrokers: () => this.activeBrokers,
209245
- persistBrokers: (brokers) => this.persistBrokerEntries(brokers),
209246
- cascadeRemoveDevices: (brokerId) => this.cascadeRemoveDevicesForBroker(brokerId),
209247
- logger: this.ctx.logger
209407
+ getBrokers: () => this.activeBrokers
209248
209408
  });
209249
209409
  }
209250
209410
  async cascadeRemoveDevicesForBroker(brokerId) {
@@ -209259,42 +209419,6 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209259
209419
  logger: this.ctx.logger
209260
209420
  });
209261
209421
  }
209262
- async reconcileIntegrationsToBrokers() {
209263
- try {
209264
- const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
209265
- const surviving = new Set(mine.map((i) => i.id));
209266
- for (const broker of this.activeBrokers) {
209267
- const integrationId = broker.integrationId;
209268
- if (integrationId === void 0 || !surviving.has(integrationId)) continue;
209269
- this.requireRegistry().linkIntegration(integrationId, broker.id);
209270
- }
209271
- const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
209272
- if (toRemove.length === 0) return;
209273
- for (const id of toRemove) try {
209274
- await this.requireRegistry().removeEntry(id);
209275
- await this.cascadeRemoveDevicesForBroker(id);
209276
- } catch (err) {
209277
- this.ctx.logger.warn("Matter integration→broker: broker cleanup failed", { meta: {
209278
- brokerId: id,
209279
- error: errMsg(err)
209280
- } });
209281
- }
209282
- const nextBrokers = this.activeBrokers.filter((b) => !toRemove.includes(b.id));
209283
- await this.persistBrokerEntries(nextBrokers);
209284
- } catch (err) {
209285
- this.ctx.logger.warn("Matter integration→broker reconcile failed", { meta: { error: errMsg(err) } });
209286
- }
209287
- }
209288
- subscribeIntegrationLifecycle() {
209289
- const handler = (event) => {
209290
- const addonId = event.data["addonId"];
209291
- if (typeof addonId === "string" && addonId !== this.ctx.id) return;
209292
- this.reconcileIntegrationsToBrokers();
209293
- };
209294
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
209295
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
209296
- this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
209297
- }
209298
209422
  buildAdoptionProvider() {
209299
209423
  return buildMatterAdoptionProvider({
209300
209424
  registry: {
@@ -209307,9 +209431,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209307
209431
  },
209308
209432
  logger: this.ctx.logger,
209309
209433
  getBrokerIdForIntegration: async (id) => {
209310
- const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
209311
- if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
209312
- return broker.id;
209434
+ const brokerId = await brokerIdOf(this.requireInstances(), id);
209435
+ if (brokerId === void 0) throw new Error(`integration ${id} has no broker`);
209436
+ return brokerId;
209313
209437
  },
209314
209438
  listAdoptedMatter: async () => {
209315
209439
  const reg = this.ctx.kernel.deviceRegistry;