@camstack/addon-mqtt-broker 1.2.150 → 1.2.152

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.
@@ -6021,7 +6021,7 @@ var ZodIssueCode = {
6021
6021
  var ZodFirstPartyTypeKind;
6022
6022
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6023
6023
  //#endregion
6024
- //#region ../types/dist/sleep-BaWhXZFm.mjs
6024
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
6025
6025
  /**
6026
6026
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6027
6027
  * window to float samples (D455).
@@ -6621,7 +6621,7 @@ var NOT_RUN = {
6621
6621
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6622
6622
  * because the hub (and this addon's boot) must not hang on a migration.
6623
6623
  */
6624
- async function runAddonMigrations(host, migrations, logger) {
6624
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6625
6625
  const first = migrations[0];
6626
6626
  if (first === void 0) return NOT_RUN;
6627
6627
  const owner = first.meta.owner;
@@ -6650,7 +6650,9 @@ async function runAddonMigrations(host, migrations, logger) {
6650
6650
  else if (status === "blocked") blocked.push(id);
6651
6651
  else skipped.push(id);
6652
6652
  };
6653
+ const selected = options.only === void 0 ? null : new Set(options.only);
6653
6654
  for (const step of declaration.steps) {
6655
+ if (selected !== null && !selected.has(step.id)) continue;
6654
6656
  const bound = byId.get(step.id);
6655
6657
  if (bound === void 0 || step.action === "skip") {
6656
6658
  skipped.push(step.id);
@@ -6828,6 +6830,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6828
6830
  }
6829
6831
  return out;
6830
6832
  }
6833
+ /** Every scope, in the order the guard and the docs list them. */
6834
+ var SCOPED_SETTINGS_SCOPES = [
6835
+ "cluster",
6836
+ "device-defaults",
6837
+ "node"
6838
+ ];
6831
6839
  /**
6832
6840
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6833
6841
  *
@@ -7642,6 +7650,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7642
7650
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7643
7651
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7644
7652
  /**
7653
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7654
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7655
+ * through `api` (guard `check-migration-ports-surface`).
7656
+ */
7657
+ function migrationSet(catalogue, bindPorts) {
7658
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7659
+ }
7660
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7661
+ function refuseMixed(form, kept, dropped) {
7662
+ 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`);
7663
+ }
7664
+ function catalogueOf(list) {
7665
+ const kept = list.filter((entry) => !("execute" in entry));
7666
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7667
+ return kept;
7668
+ }
7669
+ function boundOf(list) {
7670
+ const kept = list.filter((entry) => "execute" in entry);
7671
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7672
+ return kept;
7673
+ }
7674
+ /**
7645
7675
  * Base class for CamStack addons. Eliminates settings boilerplate:
7646
7676
  *
7647
7677
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7719,6 +7749,13 @@ var BaseAddon = class {
7719
7749
  */
7720
7750
  secretStatePaths = [];
7721
7751
  _addonSecretTree = null;
7752
+ /**
7753
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7754
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7755
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7756
+ */
7757
+ _scopedByCap = /* @__PURE__ */ new Map();
7758
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7722
7759
  _deviceSecretTree = null;
7723
7760
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7724
7761
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7788,6 +7825,7 @@ var BaseAddon = class {
7788
7825
  this._ctx = context;
7789
7826
  this._secretSettings = null;
7790
7827
  }
7828
+ await this.loadScopedSchemas();
7791
7829
  await this.resolveConfig();
7792
7830
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7793
7831
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7797,27 +7835,30 @@ var BaseAddon = class {
7797
7835
  const normalized = normalizeAddonInitResult(result);
7798
7836
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7799
7837
  this._registeredCapNames = providers.map((p) => p.capability.name);
7838
+ await this.loadScopedSchemas();
7800
7839
  return normalized;
7801
7840
  }
7841
+ async runMigrations(first, second, third) {
7842
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7843
+ return this.runBound(boundOf(first), second);
7844
+ }
7802
7845
  /**
7803
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7804
- * engine. Call it at the point of the boot where the data must already have the new
7805
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7806
- * restored devices — with the catalogue bound to the addon's ports
7807
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7808
- *
7809
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7810
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7811
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7812
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7846
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7847
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7848
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7813
7849
  */
7814
- async runMigrations(migrations) {
7850
+ async runMigrationSets(sets, options) {
7851
+ const api = this.ctx.migrationApi;
7852
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7853
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7854
+ }
7855
+ async runBound(migrations, options) {
7815
7856
  const api = this.ctx.api.migrations;
7816
7857
  const report = await runAddonMigrations({
7817
7858
  declareOwner: (input) => api.declareOwner.mutate(input),
7818
7859
  startMigration: (input) => api.startMigration.mutate(input),
7819
7860
  finishMigration: (input) => api.finishMigration.mutate(input)
7820
- }, migrations, this.ctx.logger);
7861
+ }, migrations, this.ctx.logger, options);
7821
7862
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7822
7863
  return report;
7823
7864
  }
@@ -7881,6 +7922,16 @@ var BaseAddon = class {
7881
7922
  schema(schema) {
7882
7923
  return schema;
7883
7924
  }
7925
+ /**
7926
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7927
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7928
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7929
+ * must already know which keys are per node. Names the providers register at init are added
7930
+ * once known.
7931
+ */
7932
+ scopedSettingsCapabilities() {
7933
+ return [];
7934
+ }
7884
7935
  /** Override to provide global-level settings UI schema. */
7885
7936
  globalSettingsSchema(_cap) {
7886
7937
  return null;
@@ -7924,7 +7975,8 @@ var BaseAddon = class {
7924
7975
  return [];
7925
7976
  }
7926
7977
  async getGlobalSettings(overlay, cap, nodeId) {
7927
- const schema = this.globalSettingsSchema(cap);
7978
+ await this.loadScopedSchemas(cap);
7979
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7928
7980
  if (!schema) return { sections: [] };
7929
7981
  const projected = await this.readRawProjectedStore(nodeId, cap);
7930
7982
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7959,10 +8011,9 @@ var BaseAddon = class {
7959
8011
  *
7960
8012
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7961
8013
  * needs its connection fields, the default integration the addon's own sections. It is the
7962
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7963
- * unchanged (`integrations.setSettings` for an account integration).
8014
+ * SHAPE of the form only — the values live on the instance record.
7964
8015
  */
7965
- async integrationSettingsSchema(_instance) {
8016
+ async integrationFormSchema(_instance) {
7966
8017
  return null;
7967
8018
  }
7968
8019
  /**
@@ -8008,7 +8059,7 @@ var BaseAddon = class {
8008
8059
  * per-instance schema when it declares one, else its global schema (what they always showed).
8009
8060
  */
8010
8061
  async getIntegrationFormSchema(instance, nodeId) {
8011
- const scoped = await this.integrationSettingsSchema(instance);
8062
+ const scoped = await this.integrationFormSchema(instance);
8012
8063
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8013
8064
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8014
8065
  }
@@ -8056,6 +8107,7 @@ var BaseAddon = class {
8056
8107
  * resolved blob back is a no-op for every unchanged secret.
8057
8108
  */
8058
8109
  async resolveGlobalStore(nodeId, cap) {
8110
+ await this.loadScopedSchemas(cap);
8059
8111
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8060
8112
  return this.projectForNode(raw, nodeId, cap);
8061
8113
  }
@@ -8066,6 +8118,7 @@ var BaseAddon = class {
8066
8118
  }
8067
8119
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8068
8120
  async readRawProjectedStore(nodeId, cap) {
8121
+ await this.loadScopedSchemas(cap);
8069
8122
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8070
8123
  return this.projectForNode(raw, nodeId, cap);
8071
8124
  }
@@ -8168,6 +8221,7 @@ var BaseAddon = class {
8168
8221
  await this.applyGlobalPatch(patch, nodeId);
8169
8222
  }
8170
8223
  async applyGlobalPatch(barePatch, nodeId) {
8224
+ await this.loadScopedSchemas();
8171
8225
  const keys = this.perNodeKeys();
8172
8226
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8173
8227
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8181,7 +8235,8 @@ var BaseAddon = class {
8181
8235
  await this.resolveConfig();
8182
8236
  await this.onConfigChanged();
8183
8237
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8184
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8238
+ const legacy = this.globalSettingsSchema();
8239
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8185
8240
  }
8186
8241
  /**
8187
8242
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8189,14 +8244,67 @@ var BaseAddon = class {
8189
8244
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8190
8245
  * settings API behaves exactly like the legacy node-agnostic one.
8191
8246
  */
8192
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8247
+ /**
8248
+ * Load the scoped schemas of every capability this addon declares (the override and the
8249
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8250
+ * derived from the previous set. Idempotent.
8251
+ */
8252
+ async loadScopedSchemas(extraCap) {
8253
+ const caps = new Set([
8254
+ ...this.scopedSettingsCapabilities(),
8255
+ ...this._registeredCapNames,
8256
+ ...extraCap === void 0 ? [] : [extraCap]
8257
+ ]);
8258
+ let added = false;
8259
+ for (const cap of caps) {
8260
+ if (this._scopedByCap.has(cap)) continue;
8261
+ const loaded = [];
8262
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8263
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8264
+ if (schema !== null) loaded.push({
8265
+ scope,
8266
+ schema
8267
+ });
8268
+ }
8269
+ this._scopedByCap.set(cap, loaded);
8270
+ added = true;
8271
+ }
8272
+ if (!added) return;
8273
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8274
+ this._addonSecretTree = null;
8275
+ }
8276
+ /**
8277
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8278
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8279
+ */
8280
+ scopedFormSchema(cap) {
8281
+ const byId = /* @__PURE__ */ new Map();
8282
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8283
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8284
+ }
8285
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8286
+ loadedScopedSchemas(cap) {
8287
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8288
+ return [...this._scopedByCap.values()].flat();
8289
+ }
8290
+ /**
8291
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8292
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8293
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8294
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8295
+ * Call {@link loadScopedSchemas} first (the async seams do).
8296
+ */
8193
8297
  perNodeKeys(cap) {
8194
8298
  const cacheKey = cap ?? "";
8195
- const cached = this._perNodeKeysCache.get(cacheKey);
8299
+ const cached = this._nodeKeysByCap.get(cacheKey);
8196
8300
  if (cached) return cached;
8197
- const schema = this.globalSettingsSchema(cap);
8198
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8199
- this._perNodeKeysCache.set(cacheKey, keys);
8301
+ const legacy = this.globalSettingsSchema(cap);
8302
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8303
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8304
+ if (scope !== "node") continue;
8305
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8306
+ }
8307
+ this._nodeKeysByCap.set(cacheKey, keys);
8200
8308
  return keys;
8201
8309
  }
8202
8310
  /**
@@ -8206,10 +8314,9 @@ var BaseAddon = class {
8206
8314
  * time to return its response before the addon is torn down and
8207
8315
  * re-initialised by `AddonRegistryService.restartAddon`.
8208
8316
  */
8209
- maybeAutoRestart(patchKeys, schema) {
8210
- if (!schema) return;
8317
+ maybeAutoRestart(patchKeys, schemas) {
8211
8318
  const restartKeys = /* @__PURE__ */ new Set();
8212
- for (const section of schema.sections) for (const field of section.fields) {
8319
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8213
8320
  if (field.type === "separator" || field.type === "info") continue;
8214
8321
  if (field.requiresRestart) restartKeys.add(field.key);
8215
8322
  }
@@ -8405,12 +8512,14 @@ var BaseAddon = class {
8405
8512
  * here, in the addon's own process, so the hub keeps no copy of it.
8406
8513
  */
8407
8514
  async getDeclaredSettingsKeys() {
8515
+ await this.loadScopedSchemas();
8408
8516
  return { keys: [...this.declaredKeySet()].toSorted() };
8409
8517
  }
8410
8518
  declaredKeySet() {
8411
8519
  const keys = new Set(Object.keys(this.defaults));
8412
8520
  const schema = this.globalSettingsSchema();
8413
8521
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8522
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8414
8523
  for (const key of this._declaredStateKeys) keys.add(key);
8415
8524
  for (const path of this.secretStatePaths) {
8416
8525
  const root = path.split(/[.[]/, 1)[0];
@@ -8506,7 +8615,11 @@ var BaseAddon = class {
8506
8615
  }
8507
8616
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8508
8617
  addonSecretTree() {
8509
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8618
+ if (this._addonSecretTree === null) {
8619
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8620
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8621
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8622
+ }
8510
8623
  return this._addonSecretTree;
8511
8624
  }
8512
8625
  deviceSecretTree() {
@@ -8756,6 +8869,23 @@ function dropPerNodeFields(fields) {
8756
8869
  }
8757
8870
  return kept;
8758
8871
  }
8872
+ /** The keys of the value-holding fields, recursing into layout containers. */
8873
+ function collectValueFieldKeys(fields) {
8874
+ const collected = [];
8875
+ for (const field of fields) {
8876
+ if (field.type === "group") {
8877
+ collected.push(...collectValueFieldKeys(field.fields));
8878
+ continue;
8879
+ }
8880
+ if (field.type === "sub-tabs") {
8881
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8882
+ continue;
8883
+ }
8884
+ if (field.type === "separator" || field.type === "info") continue;
8885
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8886
+ }
8887
+ return collected;
8888
+ }
8759
8889
  function collectFieldKeys(fields) {
8760
8890
  const collected = [];
8761
8891
  for (const field of fields) {
@@ -9282,6 +9412,7 @@ function defineEntryKind(name, spec) {
9282
9412
  class: "config",
9283
9413
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9284
9414
  retention: { none: "an entry goes only when the operator deletes it" },
9415
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9285
9416
  ...spec.database === void 0 ? {} : { database: spec.database },
9286
9417
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9287
9418
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11947,7 +12078,7 @@ object({
11947
12078
  params: record(string(), union([string(), number()])).optional()
11948
12079
  });
11949
12080
  //#endregion
11950
- //#region ../types/dist/composition-CRNwcFYY.mjs
12081
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11951
12082
  /**
11952
12083
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11953
12084
  *
@@ -13428,7 +13559,7 @@ var RecordIntegrationInstances = class {
13428
13559
  return record === null ? null : this.viewOf(record);
13429
13560
  };
13430
13561
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13431
- createInstance = async ({ id, name, kind }) => {
13562
+ createInstance = async ({ id, name, kind, values }) => {
13432
13563
  const minted = id ?? this.hooks.mintId?.({
13433
13564
  name,
13434
13565
  ...kind !== void 0 ? { kind } : {}
@@ -13438,10 +13569,20 @@ var RecordIntegrationInstances = class {
13438
13569
  id: minted,
13439
13570
  data: this.hooks.initialBody(name, kind)
13440
13571
  }, { ifRev: 0 });
13572
+ const patch = this.patchOf(values ?? {});
13573
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13441
13574
  await this.hooks.onChanged?.(minted, "created");
13442
13575
  return { id: minted };
13443
13576
  };
13444
13577
  updateInstance = async ({ id, name, values }) => {
13578
+ const patch = this.patchOf(values);
13579
+ if (name !== void 0) patch[this.nameField] = name;
13580
+ await this.handle.patch(id, patch);
13581
+ await this.hooks.onChanged?.(id, "updated");
13582
+ return { ok: true };
13583
+ };
13584
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13585
+ patchOf(values) {
13445
13586
  const patch = {};
13446
13587
  for (const [key, value] of Object.entries(values)) {
13447
13588
  if (value === void 0) continue;
@@ -13452,11 +13593,8 @@ var RecordIntegrationInstances = class {
13452
13593
  else if (isSecretClear(value)) patch[key] = null;
13453
13594
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13454
13595
  }
13455
- if (name !== void 0) patch[this.nameField] = name;
13456
- await this.handle.patch(id, patch);
13457
- await this.hooks.onChanged?.(id, "updated");
13458
- return { ok: true };
13459
- };
13596
+ return patch;
13597
+ }
13460
13598
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13461
13599
  setInstanceEnabled = async ({ id, enabled }) => {
13462
13600
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -14538,7 +14676,13 @@ var MigrationLedgerEntrySchema = object({
14538
14676
  * joined by the engine from the catalogues it knows this boot. Absent when no
14539
14677
  * catalogue lists the id (a newer database).
14540
14678
  */
14541
- meta: MigrationMetaSchema.optional()
14679
+ meta: MigrationMetaSchema.optional(),
14680
+ /**
14681
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14682
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14683
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14684
+ */
14685
+ ownerNotLoaded: boolean().optional()
14542
14686
  });
14543
14687
  /** A catalogued migration that is not applied, with where it stands. */
14544
14688
  var MigrationPendingEntrySchema = object({
@@ -15659,6 +15803,39 @@ var UnstampedEventMediaCountSchema = object({
15659
15803
  /** Sum across both, or `null` when either lane could not be counted. */
15660
15804
  total: number().int().nonnegative().nullable()
15661
15805
  }).nullable();
15806
+ /**
15807
+ * Where the stored event media sits, per storage location: how many media rows
15808
+ * and how many bytes carry each location id, how many carry none (legacy,
15809
+ * resolved to the default `eventMedia` location), and how many name a location
15810
+ * that was not asked about (a deleted or never-listed one).
15811
+ *
15812
+ * One covering-index aggregate per asked location plus the NULL one, and one
15813
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
15814
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
15815
+ * it is a deleted location only when every location id was asked.
15816
+ * `null` is "could not be measured", never zero.
15817
+ */
15818
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
15819
+ var MediaLocationCensusSchema = object({
15820
+ locations: array(object({
15821
+ locationId: string(),
15822
+ rows: number().int().nonnegative(),
15823
+ bytes: number().nonnegative()
15824
+ })).readonly(),
15825
+ unstamped: object({
15826
+ rows: number().int().nonnegative(),
15827
+ bytes: number().nonnegative()
15828
+ }),
15829
+ /** Rows stamped with a location id that was not in the asked list. */
15830
+ unlisted: object({
15831
+ rows: number().int().nonnegative(),
15832
+ bytes: number().nonnegative()
15833
+ }),
15834
+ total: object({
15835
+ rows: number().int().nonnegative(),
15836
+ bytes: number().nonnegative()
15837
+ })
15838
+ });
15662
15839
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
15663
15840
  /** The independently selectable logical storage classes — every class
15664
15841
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -46179,52 +46356,7 @@ DeviceType.Light, method(object({
46179
46356
  level: number().min(0).max(100),
46180
46357
  lastChangedAt: number()
46181
46358
  });
46182
- /**
46183
- * `broker` — unified pub/sub broker registry, system-scoped collection.
46184
- *
46185
- * The cap models any kind-tagged message broker the user wants to
46186
- * register and that other addons might consume. The first two kinds
46187
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
46188
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
46189
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
46190
- * the same surface.
46191
- *
46192
- * Why one cap, not one cap per kind:
46193
- * - The integrations page wants a single table of "every broker"
46194
- * across kinds; a unified cap drives it without join logic.
46195
- * - Generic admin operations (add / remove / test / status) are
46196
- * identical across kinds — duplicating them per cap was busywork.
46197
- * - The `network-access` cap precedent: same interface, many
46198
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
46199
- *
46200
- * What stays kind-specific:
46201
- * - `getBrokerConfig` payload (kind decides whether it returns
46202
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
46203
- * `Record<string, unknown>` at the wire; consumers narrow.
46204
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
46205
- * key) — also typed as `Record<string, unknown>` so each kind can
46206
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
46207
- * without rebreaking the cap signature.
46208
- * - `add` `settings` payload (different fields per kind, validated by
46209
- * the kind-specific provider).
46210
- *
46211
- * Bidirectionality:
46212
- * - `publish` is RPC — synchronous result (provider-defined).
46213
- * - `subscribe` returns a subscription handle; the actual message
46214
- * stream flows over the typed event-bus as `broker.message` events
46215
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
46216
- * event handler. Subscriptions persist across reconnects — the
46217
- * provider re-subscribes upstream after a transport drop.
46218
- *
46219
- * Why event-bus push (not a subscription RPC stream):
46220
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
46221
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
46222
- * drop is recoverable via `getState({key})`).
46223
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
46224
- * - Cross-process: events already route through `$event-bus`; no
46225
- * per-broker wire-shaping needed.
46226
- */
46227
- var BrokerStatusEnum = _enum([
46359
+ _enum([
46228
46360
  "connected",
46229
46361
  "disconnected",
46230
46362
  "connecting",
@@ -46232,141 +46364,12 @@ var BrokerStatusEnum = _enum([
46232
46364
  "unreachable",
46233
46365
  "error"
46234
46366
  ]);
46235
- var BrokerInfoSchema = object({
46236
- /** Stable broker id. Persisted; survives addon restarts. */
46237
- id: string(),
46238
- /** Addon id of the provider that OWNS this broker.
46239
- *
46240
- * The `broker` cap is a system-scoped collection: several addons
46241
- * register a `broker` provider, each owning a DISJOINT set of
46242
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
46243
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
46244
- * device belongs to one integration. The admin UI threads this id
46245
- * back as the `{ addonId }` system-collection selector on every
46246
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
46247
- * `testConnection`), so the call routes to the OWNING provider
46248
- * instead of defaulting to the first-registered one. */
46249
- addonId: string(),
46250
- /** Human-readable name (operator-chosen at add-time). */
46251
- name: string(),
46252
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
46253
- kind: string(),
46254
- status: BrokerStatusEnum,
46255
- /** Free-form provider-specific info (HA version, MQTT broker
46256
- * flavour, latency, mTLS active, …). */
46257
- info: record(string(), unknown()),
46258
- /** Ms epoch of the last connection probe / status update. */
46259
- lastCheckedAt: number().nullable(),
46260
- /** Last error message, when `status` indicates failure. */
46261
- error: string().nullable()
46262
- });
46263
46367
  var RegistryStatusSchema = object({
46264
46368
  brokerCount: number().int().nonnegative(),
46265
46369
  connectedCount: number().int().nonnegative()
46266
46370
  });
46267
- /**
46268
- * One entry per `broker` provider — which addon provides which broker
46269
- * kind(s). The unified create picker reads this to know, for each
46270
- * (addonId, kind) pair the operator can pick, where to route the
46271
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
46272
- * `{ addonId }` selector). A provider only advertises a kind here when
46273
- * it implements `getSettingsSchema` for that kind (so the picker never
46274
- * offers a kind whose creation form can't be rendered).
46275
- */
46276
- var BrokerProviderInfoSchema = object({
46277
- /** Addon id of the `broker` provider this entry describes. */
46278
- addonId: string(),
46279
- /** Broker kinds this provider can create, with a display label. */
46280
- kinds: array(object({
46281
- kind: string(),
46282
- label: string(),
46283
- /**
46284
- * Relative hub path to start an interactive account link (OAuth popup).
46285
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
46286
- */
46287
- interactiveConnectPath: string().min(1).optional(),
46288
- /** Relative hub path to disconnect the linked account (same popup pattern). */
46289
- interactiveDisconnectPath: string().min(1).optional(),
46290
- /**
46291
- * The kind signs in through a server-held interactive session instead of
46292
- * (or before) typed credentials. `basePath` is a hub-relative route set
46293
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
46294
- * contract: `POST {basePath}/start` (body: the form's settings) answers
46295
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
46296
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
46297
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
46298
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
46299
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
46300
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
46301
- * offers it as the default path; the fields the session fills are
46302
- * marked `filledBySignIn` in the kind's settings schema.
46303
- *
46304
- * `methods` lists the ways the session can sign in, recommended first:
46305
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
46306
- * settings) answering `{ sessionId, userCode, verificationUrl,
46307
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
46308
- * any device, and the same `status` / `commit` / `cancel` routes follow
46309
- * (status then carries `method`, `userCode`, `verificationUrl`,
46310
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
46311
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
46312
- * `['proxy']`.
46313
- */
46314
- signIn: object({
46315
- basePath: string().min(1),
46316
- methods: array(_enum(["device-code", "proxy"])).optional()
46317
- }).optional()
46318
- }))
46319
- });
46320
- var ListInputSchema = object({
46321
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
46322
- * only HA brokers. Omit to list every broker. */
46323
- kind: string().optional() });
46324
46371
  var GetInputSchema = object({ id: string() });
46325
- var AddInputSchema = object({
46326
- kind: string().min(1),
46327
- name: string().min(1),
46328
- /**
46329
- * ADOPT an existing id instead of minting a new one.
46330
- *
46331
- * Written the day this cost an outage. A broker's id is not a detail: HA
46332
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
46333
- * lost from config and re-added as `ha_001` leaves every one of its devices
46334
- * bound to a broker that no longer exists. Re-entering the password under the
46335
- * ORIGINAL id turns a multi-step device migration back into re-entering a
46336
- * password.
46337
- *
46338
- * A provider MUST refuse an id that is already in use — adopting a live
46339
- * broker's id would silently take it over.
46340
- */
46341
- id: string().min(1).optional(),
46342
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
46343
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
46344
- * branch on receipt — invalid shape rejects the add. */
46345
- settings: record(string(), unknown())
46346
- });
46347
- var AddResultSchema = object({ id: string() });
46348
- var RemoveInputSchema = object({ id: string() });
46349
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
46350
- ok: literal(true),
46351
- latencyMs: number().nonnegative()
46352
- }), object({
46353
- ok: literal(false),
46354
- error: string()
46355
- })]);
46356
46372
  var SettingsRecordSchema$1 = record(string(), unknown());
46357
- var SettingsSchemaInputSchema = object({ kind: string() });
46358
- var TestSettingsInputSchema = object({
46359
- kind: string(),
46360
- settings: SettingsRecordSchema$1
46361
- });
46362
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
46363
- ok: literal(true),
46364
- latencyMs: number().nonnegative().optional()
46365
- }).strict(), object({
46366
- ok: literal(false),
46367
- error: string()
46368
- })]);
46369
- var SettingsSchemaResultSchema = unknown().nullable();
46370
46373
  var PublishInputSchema = object({
46371
46374
  brokerId: string(),
46372
46375
  /** Kind-specific routing target.
@@ -46410,51 +46413,12 @@ var brokerCapability = {
46410
46413
  kind: "poll"
46411
46414
  },
46412
46415
  methods: {
46413
- list: method(ListInputSchema, array(BrokerInfoSchema)),
46414
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
46415
- /** Enumerate which addon provides which broker kind(s) for the
46416
- * unified create picker. The auto-mount fans this array across
46417
- * every registered `broker` provider (array-output method), so the
46418
- * picker sees every kind from every provider in one call. */
46419
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
46420
- add: method(AddInputSchema, AddResultSchema, {
46421
- kind: "mutation",
46422
- auth: "admin"
46423
- }),
46424
- remove: method(RemoveInputSchema, _void(), {
46425
- kind: "mutation",
46426
- auth: "admin"
46427
- }),
46428
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
46429
- kind: "mutation",
46430
- auth: "admin"
46431
- }),
46432
- /** Read the persisted settings record for a broker (kind-specific
46433
- * shape). Admin-only — settings may contain secrets. Returns `null`
46434
- * when the broker id is unknown to the provider (the collection
46435
- * fallback may route a foreign id to the first provider). */
46436
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46437
- /** Overwrite the persisted settings record. The kind-specific
46438
- * provider validates the shape and applies the change (reconnects
46439
- * if credentials changed). */
46440
- setSettings: method(object({
46441
- id: string(),
46442
- settings: SettingsRecordSchema$1
46443
- }), _void(), {
46444
- kind: "mutation",
46445
- auth: "admin"
46446
- }),
46447
46416
  /** Returns the kind-specific connection config the consumer needs
46448
46417
  * to open its own client (MQTT pattern: `{url, username, password,
46449
46418
  * clientIdPrefix}`). HA providers MAY return the auth envelope
46450
46419
  * but typical HA consumers use `publish` / `subscribe` instead.
46451
46420
  * Returns `null` when the broker id is unknown to the provider. */
46452
46421
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46453
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
46454
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
46455
- kind: "mutation",
46456
- auth: "admin"
46457
- }),
46458
46422
  publish: method(PublishInputSchema, unknown(), {
46459
46423
  kind: "mutation",
46460
46424
  auth: "admin"
@@ -49078,6 +49042,9 @@ var RunningProposalEvidenceSchema = object({
49078
49042
  runsFound: number().int().nonnegative(),
49079
49043
  pausesFound: number().int().nonnegative(),
49080
49044
  longestPauseMs: number().nonnegative(),
49045
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49046
+ pulsesFound: number().int().nonnegative(),
49047
+ longestPulseMs: number().nonnegative(),
49081
49048
  idleFloorW: number(),
49082
49049
  noiseCeilingW: number(),
49083
49050
  plateauW: number(),
@@ -50524,7 +50491,14 @@ var DiscoveryCandidateSchema = object({
50524
50491
  /** `stableId` of the gateway candidate this device was found BEHIND. */
50525
50492
  parentStableId: string().optional(),
50526
50493
  /** `false` = shown for context but not pickable. Default true. */
50527
- selectable: boolean().optional()
50494
+ selectable: boolean().optional(),
50495
+ /**
50496
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
50497
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
50498
+ * admin asks for exactly these, on the creation form prefilled with
50499
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
50500
+ */
50501
+ needsParams: array(string()).optional()
50528
50502
  });
50529
50503
  /**
50530
50504
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -50583,12 +50557,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
50583
50557
  }), array(DiscoveryCandidateSchema), {
50584
50558
  kind: "mutation",
50585
50559
  auth: "admin"
50586
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
50560
+ }), method(object({}), CreationSchemaOutputSchema), method(object({
50561
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50562
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
50563
+ candidate: DiscoveryCandidateSchema,
50564
+ /** The integration the operator adopts into; a provider with several instances needs it. */
50565
+ integrationId: string().optional()
50566
+ }), DeviceSummarySchema, {
50587
50567
  kind: "mutation",
50588
50568
  auth: "admin"
50589
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
50569
+ }), method(object({}), boolean()), method(object({
50590
50570
  type: _enum(DeviceType),
50591
- config: record(string(), unknown())
50571
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50572
+ integrationId: string().optional()
50573
+ }), CreationSchemaOutputSchema), method(object({
50574
+ type: _enum(DeviceType),
50575
+ config: record(string(), unknown()),
50576
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
50577
+ integrationId: string().optional()
50592
50578
  }), DeviceSummarySchema, {
50593
50579
  kind: "mutation",
50594
50580
  auth: "admin"
@@ -51298,7 +51284,10 @@ method(object({
51298
51284
  auth: "admin"
51299
51285
  }), method(object({
51300
51286
  addonId: string(),
51301
- type: _enum(DeviceType)
51287
+ type: _enum(DeviceType),
51288
+ /** The integration the form is for; a provider whose integrations differ (a hub
51289
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
51290
+ integrationId: string().optional()
51302
51291
  }), unknown().nullable()), method(object({
51303
51292
  addonId: string(),
51304
51293
  type: _enum(DeviceType),
@@ -51382,7 +51371,11 @@ method(object({
51382
51371
  kind: "mutation",
51383
51372
  auth: "admin",
51384
51373
  timeoutMs: 12e4
51385
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51374
+ }), method(object({
51375
+ addonId: string(),
51376
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
51377
+ integrationId: string().optional()
51378
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51386
51379
  deviceId: number(),
51387
51380
  key: string(),
51388
51381
  value: unknown()
@@ -53978,7 +53971,7 @@ var TestConnectionStatusEnum = _enum([
53978
53971
  "inconclusive",
53979
53972
  "unsupported"
53980
53973
  ]);
53981
- var TestConnectionResultSchema$1 = object({
53974
+ var TestConnectionResultSchema = object({
53982
53975
  /** True ONLY for `validated`. Never true for a test that did not run. */
53983
53976
  success: boolean(),
53984
53977
  error: string().optional(),
@@ -53999,11 +53992,7 @@ var CreateIntegrationInputSchema = object({
53999
53992
  info: record(string(), unknown()).optional(),
54000
53993
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54001
53994
  originStableId: string().optional(),
54002
- settings: record(string(), unknown()).optional(),
54003
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
54004
- * the keychain, not the table (D787). */
54005
- secretKeys: array(string()).optional(),
54006
- skipRestart: boolean().optional()
53995
+ settings: record(string(), unknown()).optional()
54007
53996
  });
54008
53997
  var UpdateIntegrationInputSchema = object({
54009
53998
  id: string(),
@@ -54024,20 +54013,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54024
54013
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54025
54014
  kind: "mutation",
54026
54015
  auth: "admin"
54027
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
54028
- id: string(),
54029
- settings: record(string(), unknown()),
54030
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
54031
- secretKeys: array(string()).optional(),
54032
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
54033
- skipRestart: boolean().optional()
54034
- }), object({ success: boolean() }), {
54035
- kind: "mutation",
54036
- auth: "admin"
54037
54016
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54038
54017
  addonId: string(),
54039
54018
  settings: record(string(), unknown())
54040
- }), TestConnectionResultSchema$1, {
54019
+ }), TestConnectionResultSchema, {
54041
54020
  kind: "mutation",
54042
54021
  auth: "admin"
54043
54022
  });
@@ -54064,6 +54043,14 @@ var ConfigSchemaPassthrough$3 = custom();
54064
54043
  var IntegrationInstanceIdSchema = string().min(1);
54065
54044
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54066
54045
  /**
54046
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
54047
+ * instance is an integration that brings devices (the registry owns its on/off switch);
54048
+ * `services`: a connection configured from the addon's own function page, never listed on
54049
+ * Integrations.
54050
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
54051
+ */
54052
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
54053
+ /**
54067
54054
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54068
54055
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54069
54056
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -54074,6 +54061,22 @@ var IntegrationInstanceKindSchema = object({
54074
54061
  /** Stable id of the kind, sent back as `createInstance.kind`. */
54075
54062
  id: string().min(1),
54076
54063
  label: string().min(1),
54064
+ /**
54065
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
54066
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
54067
+ * provider with more than one section must name it on every kind.
54068
+ */
54069
+ functionCap: string().min(1),
54070
+ /**
54071
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
54072
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
54073
+ * finds the instances whose kind names it; absent takes the kind `id`.
54074
+ */
54075
+ brokerKind: string().min(1).optional(),
54076
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
54077
+ role: IntegrationInstanceRoleSchema.optional(),
54078
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
54079
+ canTest: boolean().optional(),
54077
54080
  /** The kind signs in through a server-held interactive session. */
54078
54081
  signIn: object({
54079
54082
  basePath: string().min(1),
@@ -54086,11 +54089,7 @@ var IntegrationInstanceKindSchema = object({
54086
54089
  });
54087
54090
  var IntegrationInstanceDescriptionSchema = object({
54088
54091
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54089
- role: _enum([
54090
- "devices",
54091
- "services",
54092
- "both"
54093
- ]),
54092
+ role: IntegrationInstanceRoleSchema,
54094
54093
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54095
54094
  discovery: object({
54096
54095
  mode: _enum([
@@ -54165,7 +54164,11 @@ var integrationInstancesCapability = {
54165
54164
  methods: {
54166
54165
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54167
54166
  /** The form; `id: null` is the create form. */
54168
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54167
+ instanceFormSchema: method(object({
54168
+ id: IntegrationInstanceIdSchema.nullable(),
54169
+ /** The kind of the create form (`id: null`) of a provider with several. */
54170
+ kind: string().min(1).optional()
54171
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54169
54172
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54170
54173
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54171
54174
  /**
@@ -54175,7 +54178,9 @@ var integrationInstancesCapability = {
54175
54178
  createInstance: method(object({
54176
54179
  id: IntegrationInstanceIdSchema.optional(),
54177
54180
  name: string().min(1),
54178
- kind: string().min(1).optional()
54181
+ kind: string().min(1).optional(),
54182
+ /** Form values written with the record (a link picked in the create dialog). */
54183
+ values: record(string(), unknown()).optional()
54179
54184
  }), object({ id: string() }), {
54180
54185
  kind: "mutation",
54181
54186
  auth: "admin"
@@ -54202,8 +54207,10 @@ var integrationInstancesCapability = {
54202
54207
  }),
54203
54208
  testInstance: optionalMethod(object({
54204
54209
  id: IntegrationInstanceIdSchema.nullable(),
54205
- values: record(string(), unknown())
54206
- }), TestConnectionResultSchema$1, {
54210
+ values: record(string(), unknown()),
54211
+ /** The kind to test when `id` is null and the provider has several. */
54212
+ kind: string().min(1).optional()
54213
+ }), TestConnectionResultSchema, {
54207
54214
  kind: "mutation",
54208
54215
  auth: "admin"
54209
54216
  })
@@ -56708,6 +56715,380 @@ var SystemMetricsSchema = object({
56708
56715
  gpuMemoryPercent: number().optional()
56709
56716
  });
56710
56717
  method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), 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() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
56718
+ /**
56719
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
56720
+ * config documents, mirrors. The third storage API next to `ctx.settings`
56721
+ * (operator intent, D870) and device-history (numbers over time, D850).
56722
+ *
56723
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
56724
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
56725
+ * that with the addon id closed over). It is deliberately NOT in any input
56726
+ * schema, so a session can never supply it, and a call with no stamp is refused
56727
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
56728
+ * addons go through the owner's capability.
56729
+ *
56730
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
56731
+ * never `[]`. Secret fields are never on the wire: a read shows the
56732
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
56733
+ * resolves itself, and a write may carry only a keychain marker bound to the
56734
+ * row's own ref.
56735
+ *
56736
+ * Internal: addons reach it through `ctx.records`, never directly.
56737
+ */
56738
+ var RecordIdSchema = string().min(1).max(256);
56739
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
56740
+ var RecordBodySchema = record(string(), unknown());
56741
+ var RecordInputSchema = object({
56742
+ id: RecordIdSchema,
56743
+ /** The device this row belongs to (D865 removal, per-device reads). */
56744
+ deviceId: number().int().optional(),
56745
+ /** The domain clock, ms: observed / created / opened. */
56746
+ at: number().int().optional(),
56747
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
56748
+ expiresAt: number().int().optional(),
56749
+ data: RecordBodySchema
56750
+ });
56751
+ var RecordViewSchema = object({
56752
+ id: string(),
56753
+ deviceId: number().int().nullable(),
56754
+ at: number().nullable(),
56755
+ expiresAt: number().nullable(),
56756
+ rev: number().int(),
56757
+ updatedAt: number(),
56758
+ data: RecordBodySchema
56759
+ });
56760
+ /** Declared index fields only. Anything else does not compile, by design. */
56761
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
56762
+ string(),
56763
+ number(),
56764
+ boolean(),
56765
+ object({ in: array(union([
56766
+ string(),
56767
+ number(),
56768
+ boolean()
56769
+ ])).min(1).max(RECORD_BATCH_MAX) }),
56770
+ object({
56771
+ gt: union([string(), number()]).optional(),
56772
+ gte: union([string(), number()]).optional(),
56773
+ lt: union([string(), number()]).optional(),
56774
+ lte: union([string(), number()]).optional()
56775
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
56776
+ ]));
56777
+ var KindShape = { kind: RecordKindNameSchema };
56778
+ var TimeWindowShape = {
56779
+ deviceId: number().int().optional(),
56780
+ atFrom: number().optional(),
56781
+ atTo: number().optional()
56782
+ };
56783
+ var RecordQueryInputSchema = object({
56784
+ ...KindShape,
56785
+ ...TimeWindowShape,
56786
+ where: RecordWhereSchema.optional(),
56787
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
56788
+ order: _enum(["at", "id"]).optional(),
56789
+ desc: boolean().optional(),
56790
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
56791
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
56792
+ cursor: string().min(1).optional()
56793
+ });
56794
+ var RecordQueryResultSchema = object({
56795
+ records: array(RecordViewSchema),
56796
+ nextCursor: string().nullable()
56797
+ });
56798
+ var RecordKindStateSchema = _enum([
56799
+ "created",
56800
+ "unchanged",
56801
+ "updated"
56802
+ ]);
56803
+ var DeclareKindResultSchema = object({
56804
+ kindId: number().int(),
56805
+ state: RecordKindStateSchema,
56806
+ createdIndexes: array(string())
56807
+ });
56808
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
56809
+ var SecretRefsList = array(string());
56810
+ /**
56811
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
56812
+ * All ops of one call share ONE database file and ONE `transactFamily`.
56813
+ */
56814
+ var RecordTransactOpSchema = discriminatedUnion("op", [
56815
+ object({
56816
+ op: literal("put"),
56817
+ kind: RecordKindNameSchema,
56818
+ record: RecordInputSchema,
56819
+ ifRev: number().int().min(0).optional()
56820
+ }),
56821
+ object({
56822
+ op: literal("patch"),
56823
+ kind: RecordKindNameSchema,
56824
+ id: RecordIdSchema,
56825
+ data: RecordBodySchema,
56826
+ at: number().int().nullable().optional(),
56827
+ expiresAt: number().int().nullable().optional(),
56828
+ ifRev: number().int().min(1).optional()
56829
+ }),
56830
+ object({
56831
+ op: literal("delete"),
56832
+ kind: RecordKindNameSchema,
56833
+ id: RecordIdSchema
56834
+ })
56835
+ ]);
56836
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
56837
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
56838
+ kind: "mutation",
56839
+ auth: "admin"
56840
+ }), method(object({
56841
+ ...KindShape,
56842
+ id: RecordIdSchema
56843
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
56844
+ ...KindShape,
56845
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56846
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
56847
+ ...KindShape,
56848
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56849
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
56850
+ ...KindShape,
56851
+ record: RecordInputSchema,
56852
+ /** Compare-and-set. `0` means "the row must not exist". */
56853
+ ifRev: number().int().min(0).optional()
56854
+ }), object({
56855
+ rev: number().int(),
56856
+ releasedRefs: SecretRefsList
56857
+ }), {
56858
+ kind: "mutation",
56859
+ auth: "admin"
56860
+ }), method(object({
56861
+ ...KindShape,
56862
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
56863
+ }), object({
56864
+ count: number().int(),
56865
+ releasedRefs: SecretRefsList
56866
+ }), {
56867
+ kind: "mutation",
56868
+ auth: "admin"
56869
+ }), method(object({
56870
+ ...KindShape,
56871
+ id: RecordIdSchema,
56872
+ data: RecordBodySchema,
56873
+ at: number().int().nullable().optional(),
56874
+ expiresAt: number().int().nullable().optional(),
56875
+ ifRev: number().int().min(1).optional()
56876
+ }), object({
56877
+ rev: number().int(),
56878
+ releasedRefs: SecretRefsList
56879
+ }), {
56880
+ kind: "mutation",
56881
+ auth: "admin"
56882
+ }), method(object({
56883
+ ...KindShape,
56884
+ id: RecordIdSchema
56885
+ }), object({
56886
+ deleted: boolean(),
56887
+ secretRefs: SecretRefsList
56888
+ }), {
56889
+ kind: "mutation",
56890
+ auth: "admin"
56891
+ }), method(object({
56892
+ ...KindShape,
56893
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56894
+ }), object({
56895
+ deleted: number().int(),
56896
+ secretRefs: SecretRefsList
56897
+ }), {
56898
+ kind: "mutation",
56899
+ auth: "admin"
56900
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
56901
+ ...KindShape,
56902
+ ...TimeWindowShape,
56903
+ where: RecordWhereSchema.optional()
56904
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
56905
+ ...KindShape,
56906
+ deviceId: number().int().optional(),
56907
+ from: number(),
56908
+ to: number(),
56909
+ bucketMs: number().int().positive()
56910
+ }), object({ buckets: array(object({
56911
+ bucketStart: number(),
56912
+ count: number().int()
56913
+ })) }), { auth: "admin" }), method(object({
56914
+ ...KindShape,
56915
+ op: _enum([
56916
+ "sum",
56917
+ "min",
56918
+ "max",
56919
+ "count"
56920
+ ]),
56921
+ field: RecordFieldPathSchema.optional(),
56922
+ groupBy: RecordFieldPathSchema.optional(),
56923
+ where: RecordWhereSchema.optional()
56924
+ }), object({ rows: array(object({
56925
+ group: union([
56926
+ string(),
56927
+ number(),
56928
+ _null()
56929
+ ]),
56930
+ value: number().nullable()
56931
+ })) }), { auth: "admin" }), method(object({
56932
+ ...KindShape,
56933
+ deviceId: number().int().optional(),
56934
+ atBefore: number().optional(),
56935
+ where: RecordWhereSchema.optional()
56936
+ }), object({
56937
+ deleted: number().int(),
56938
+ secretRefs: SecretRefsList
56939
+ }), {
56940
+ kind: "mutation",
56941
+ auth: "admin"
56942
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
56943
+ results: array(object({
56944
+ rev: number().int().nullable(),
56945
+ deleted: boolean()
56946
+ })),
56947
+ releasedRefs: SecretRefsList,
56948
+ secretRefs: SecretRefsList
56949
+ }), {
56950
+ kind: "mutation",
56951
+ auth: "admin"
56952
+ }), method(object({
56953
+ deviceId: number().int(),
56954
+ kinds: array(RecordKindNameSchema).optional()
56955
+ }), object({
56956
+ deleted: record(string(), number().int()),
56957
+ secretRefs: SecretRefsList
56958
+ }), {
56959
+ kind: "mutation",
56960
+ auth: "admin"
56961
+ }), method(object(KindShape), object({ id: number().int() }), {
56962
+ kind: "mutation",
56963
+ auth: "admin"
56964
+ });
56965
+ /**
56966
+ * The wire shapes of the migration API, version 1 (planning#447).
56967
+ *
56968
+ * A migration is code that outlives the server it was written against: an addon at
56969
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
56970
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
56971
+ * is the contract of every method of it, one Zod pair per method.
56972
+ *
56973
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
56974
+ * field is never removed or made required, an enum never narrows, an output field is
56975
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
56976
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
56977
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
56978
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
56979
+ */
56980
+ var Rows = record(string(), unknown());
56981
+ var IntegrationId = object({ integrationId: string().min(1) });
56982
+ var RecordId = object({ id: string().min(1) });
56983
+ var RecordRowSchema = object({
56984
+ id: string(),
56985
+ deviceId: number().int().nullable(),
56986
+ at: number().nullable(),
56987
+ expiresAt: number().nullable(),
56988
+ rev: number().int(),
56989
+ updatedAt: number(),
56990
+ data: Rows
56991
+ });
56992
+ var RecordPutShape = {
56993
+ id: string().min(1),
56994
+ deviceId: number().int().optional(),
56995
+ at: number().int().optional(),
56996
+ expiresAt: number().int().optional(),
56997
+ data: Rows
56998
+ };
56999
+ var MigrationIntegrationSchema = object({
57000
+ id: string(),
57001
+ addonId: string(),
57002
+ name: string(),
57003
+ info: Rows,
57004
+ originStableId: string().optional()
57005
+ });
57006
+ var MigrationDeviceSchema = object({
57007
+ id: number().int(),
57008
+ integrationId: string().nullable().optional(),
57009
+ /** The owning device; null or absent for a root. */
57010
+ parentDeviceId: number().int().nullable().optional(),
57011
+ type: string().optional(),
57012
+ name: string().optional(),
57013
+ stableId: string().optional()
57014
+ });
57015
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
57016
+ var MigrationApiInfoSchema = object({
57017
+ version: number().int(),
57018
+ methods: array(string())
57019
+ });
57020
+ object({
57021
+ message: string(),
57022
+ meta: Rows.optional(),
57023
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
57024
+ tags: object({ deviceId: number().int().optional() }).optional()
57025
+ });
57026
+ object({
57027
+ owner: string().min(1),
57028
+ metas: array(MigrationMetaSchema)
57029
+ }), object({
57030
+ bootId: string(),
57031
+ snapshot: _enum([
57032
+ "none",
57033
+ "written",
57034
+ "refused"
57035
+ ]),
57036
+ steps: array(MigrationOwnerStepSchema),
57037
+ /** The migration API the engine serves; absent from a server that predates it. */
57038
+ migrationApi: MigrationApiInfoSchema.optional()
57039
+ }), object({ id: string().min(1) }), object({
57040
+ proceed: boolean(),
57041
+ state: union([literal("pending"), MigrationStatusSchema]),
57042
+ reason: string().nullable()
57043
+ }), object({
57044
+ id: string().min(1),
57045
+ result: MigrationOwnerResultSchema
57046
+ }), object({
57047
+ status: MigrationStatusSchema,
57048
+ error: string().nullable(),
57049
+ rowsChanged: number().int().nonnegative()
57050
+ }), object({
57051
+ rows: number().int().nonnegative(),
57052
+ secrets: number().int().nonnegative()
57053
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
57054
+ present: boolean(),
57055
+ removed: Rows
57056
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
57057
+ ...RecordPutShape,
57058
+ ifRev: number().int().optional()
57059
+ }), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
57060
+ partial: Rows,
57061
+ ifRev: number().int().optional()
57062
+ }), object({ rev: number().int() }), object({ deleted: boolean() }), object({
57063
+ deviceId: number().int().optional(),
57064
+ atFrom: number().optional(),
57065
+ atTo: number().optional(),
57066
+ where: RecordWhereSchema.optional(),
57067
+ limit: number().int().min(1).optional(),
57068
+ cursor: string().min(1).optional(),
57069
+ resolveSecrets: boolean().optional()
57070
+ }), object({
57071
+ records: array(RecordRowSchema),
57072
+ nextCursor: string().nullable()
57073
+ }), object({
57074
+ deviceId: number().int().optional(),
57075
+ atBefore: number().optional(),
57076
+ where: RecordWhereSchema.optional()
57077
+ }), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
57078
+ ref: string().min(1),
57079
+ value: string()
57080
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
57081
+ addonId: string().min(1),
57082
+ integrationId: string().min(1).optional()
57083
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
57084
+ id: number().int(),
57085
+ integrationId: string().nullable(),
57086
+ /** Whether the device carries any stored config; its content is not offered. */
57087
+ hasConfig: boolean()
57088
+ }).nullable(), object({
57089
+ deviceId: number().int(),
57090
+ integrationId: string().min(1)
57091
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
56711
57092
  method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
56712
57093
  id: string().min(1),
56713
57094
  error: string().min(1)
@@ -56737,7 +57118,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56737
57118
  "written",
56738
57119
  "refused"
56739
57120
  ]),
56740
- steps: array(MigrationOwnerStepSchema)
57121
+ steps: array(MigrationOwnerStepSchema),
57122
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
57123
+ migrationApi: MigrationApiInfoSchema.optional()
56741
57124
  }), {
56742
57125
  kind: "mutation",
56743
57126
  auth: "admin"
@@ -56758,6 +57141,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56758
57141
  }), {
56759
57142
  kind: "mutation",
56760
57143
  auth: "admin"
57144
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
57145
+ rows: number().int().nonnegative(),
57146
+ secrets: number().int().nonnegative()
57147
+ }), {
57148
+ kind: "mutation",
57149
+ auth: "admin"
57150
+ }), method(object({
57151
+ integrationId: string().min(1),
57152
+ patch: record(string(), unknown())
57153
+ }), object({ written: boolean() }), {
57154
+ kind: "mutation",
57155
+ auth: "admin"
57156
+ }), method(object({
57157
+ integrationId: string().min(1),
57158
+ keys: array(string().min(1))
57159
+ }), object({
57160
+ present: boolean(),
57161
+ removed: record(string(), unknown())
57162
+ }), {
57163
+ kind: "mutation",
57164
+ auth: "admin"
56761
57165
  });
56762
57166
  method(object({
56763
57167
  sourceUrl: string(),
@@ -59383,7 +59787,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
59383
59787
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
59384
59788
  kind: "mutation",
59385
59789
  auth: "admin"
59386
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59790
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
59791
+ kind: "query",
59792
+ auth: "admin"
59793
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59387
59794
  kind: "query",
59388
59795
  auth: "admin"
59389
59796
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -61790,7 +62197,7 @@ var BrokerStatusSchema$1 = _enum([
61790
62197
  "unreachable",
61791
62198
  "tls-error"
61792
62199
  ]);
61793
- var BrokerInfoSchema$1 = object({
62200
+ var BrokerInfoSchema = object({
61794
62201
  id: string(),
61795
62202
  name: string(),
61796
62203
  url: string(),
@@ -61863,7 +62270,7 @@ var mqttBrokerCapability = {
61863
62270
  kind: "poll"
61864
62271
  },
61865
62272
  methods: {
61866
- listBrokers: method(_void(), array(BrokerInfoSchema$1)),
62273
+ listBrokers: method(_void(), array(BrokerInfoSchema)),
61867
62274
  getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
61868
62275
  addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
61869
62276
  kind: "mutation",
@@ -68190,253 +68597,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
68190
68597
  auth: "admin"
68191
68598
  });
68192
68599
  /**
68193
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
68194
- * config documents, mirrors. The third storage API next to `ctx.settings`
68195
- * (operator intent, D870) and device-history (numbers over time, D850).
68196
- *
68197
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
68198
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
68199
- * that with the addon id closed over). It is deliberately NOT in any input
68200
- * schema, so a session can never supply it, and a call with no stamp is refused
68201
- * `records/no-caller`. A kind is readable and writable only by its owner; other
68202
- * addons go through the owner's capability.
68203
- *
68204
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
68205
- * never `[]`. Secret fields are never on the wire: a read shows the
68206
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
68207
- * resolves itself, and a write may carry only a keychain marker bound to the
68208
- * row's own ref.
68209
- *
68210
- * Internal: addons reach it through `ctx.records`, never directly.
68211
- */
68212
- var RecordIdSchema = string().min(1).max(256);
68213
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
68214
- var RecordBodySchema = record(string(), unknown());
68215
- var RecordInputSchema = object({
68216
- id: RecordIdSchema,
68217
- /** The device this row belongs to (D865 removal, per-device reads). */
68218
- deviceId: number().int().optional(),
68219
- /** The domain clock, ms: observed / created / opened. */
68220
- at: number().int().optional(),
68221
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
68222
- expiresAt: number().int().optional(),
68223
- data: RecordBodySchema
68224
- });
68225
- var RecordViewSchema = object({
68226
- id: string(),
68227
- deviceId: number().int().nullable(),
68228
- at: number().nullable(),
68229
- expiresAt: number().nullable(),
68230
- rev: number().int(),
68231
- updatedAt: number(),
68232
- data: RecordBodySchema
68233
- });
68234
- /** Declared index fields only. Anything else does not compile, by design. */
68235
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
68236
- string(),
68237
- number(),
68238
- boolean(),
68239
- object({ in: array(union([
68240
- string(),
68241
- number(),
68242
- boolean()
68243
- ])).min(1).max(RECORD_BATCH_MAX) }),
68244
- object({
68245
- gt: union([string(), number()]).optional(),
68246
- gte: union([string(), number()]).optional(),
68247
- lt: union([string(), number()]).optional(),
68248
- lte: union([string(), number()]).optional()
68249
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
68250
- ]));
68251
- var KindShape = { kind: RecordKindNameSchema };
68252
- var TimeWindowShape = {
68253
- deviceId: number().int().optional(),
68254
- atFrom: number().optional(),
68255
- atTo: number().optional()
68256
- };
68257
- var RecordQueryInputSchema = object({
68258
- ...KindShape,
68259
- ...TimeWindowShape,
68260
- where: RecordWhereSchema.optional(),
68261
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
68262
- order: _enum(["at", "id"]).optional(),
68263
- desc: boolean().optional(),
68264
- limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
68265
- /** Opaque keyset cursor from a previous answer. There is no offset. */
68266
- cursor: string().min(1).optional()
68267
- });
68268
- var RecordQueryResultSchema = object({
68269
- records: array(RecordViewSchema),
68270
- nextCursor: string().nullable()
68271
- });
68272
- var RecordKindStateSchema = _enum([
68273
- "created",
68274
- "unchanged",
68275
- "updated"
68276
- ]);
68277
- var DeclareKindResultSchema = object({
68278
- kindId: number().int(),
68279
- state: RecordKindStateSchema,
68280
- createdIndexes: array(string())
68281
- });
68282
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
68283
- var SecretRefsList = array(string());
68284
- /**
68285
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
68286
- * All ops of one call share ONE database file and ONE `transactFamily`.
68287
- */
68288
- var RecordTransactOpSchema = discriminatedUnion("op", [
68289
- object({
68290
- op: literal("put"),
68291
- kind: RecordKindNameSchema,
68292
- record: RecordInputSchema,
68293
- ifRev: number().int().min(0).optional()
68294
- }),
68295
- object({
68296
- op: literal("patch"),
68297
- kind: RecordKindNameSchema,
68298
- id: RecordIdSchema,
68299
- data: RecordBodySchema,
68300
- at: number().int().nullable().optional(),
68301
- expiresAt: number().int().nullable().optional(),
68302
- ifRev: number().int().min(1).optional()
68303
- }),
68304
- object({
68305
- op: literal("delete"),
68306
- kind: RecordKindNameSchema,
68307
- id: RecordIdSchema
68308
- })
68309
- ]);
68310
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
68311
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
68312
- kind: "mutation",
68313
- auth: "admin"
68314
- }), method(object({
68315
- ...KindShape,
68316
- id: RecordIdSchema
68317
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
68318
- ...KindShape,
68319
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68320
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
68321
- ...KindShape,
68322
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68323
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
68324
- ...KindShape,
68325
- record: RecordInputSchema,
68326
- /** Compare-and-set. `0` means "the row must not exist". */
68327
- ifRev: number().int().min(0).optional()
68328
- }), object({
68329
- rev: number().int(),
68330
- releasedRefs: SecretRefsList
68331
- }), {
68332
- kind: "mutation",
68333
- auth: "admin"
68334
- }), method(object({
68335
- ...KindShape,
68336
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
68337
- }), object({
68338
- count: number().int(),
68339
- releasedRefs: SecretRefsList
68340
- }), {
68341
- kind: "mutation",
68342
- auth: "admin"
68343
- }), method(object({
68344
- ...KindShape,
68345
- id: RecordIdSchema,
68346
- data: RecordBodySchema,
68347
- at: number().int().nullable().optional(),
68348
- expiresAt: number().int().nullable().optional(),
68349
- ifRev: number().int().min(1).optional()
68350
- }), object({
68351
- rev: number().int(),
68352
- releasedRefs: SecretRefsList
68353
- }), {
68354
- kind: "mutation",
68355
- auth: "admin"
68356
- }), method(object({
68357
- ...KindShape,
68358
- id: RecordIdSchema
68359
- }), object({
68360
- deleted: boolean(),
68361
- secretRefs: SecretRefsList
68362
- }), {
68363
- kind: "mutation",
68364
- auth: "admin"
68365
- }), method(object({
68366
- ...KindShape,
68367
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68368
- }), object({
68369
- deleted: number().int(),
68370
- secretRefs: SecretRefsList
68371
- }), {
68372
- kind: "mutation",
68373
- auth: "admin"
68374
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
68375
- ...KindShape,
68376
- ...TimeWindowShape,
68377
- where: RecordWhereSchema.optional()
68378
- }), object({ count: number().int() }), { auth: "admin" }), method(object({
68379
- ...KindShape,
68380
- deviceId: number().int().optional(),
68381
- from: number(),
68382
- to: number(),
68383
- bucketMs: number().int().positive()
68384
- }), object({ buckets: array(object({
68385
- bucketStart: number(),
68386
- count: number().int()
68387
- })) }), { auth: "admin" }), method(object({
68388
- ...KindShape,
68389
- op: _enum([
68390
- "sum",
68391
- "min",
68392
- "max",
68393
- "count"
68394
- ]),
68395
- field: RecordFieldPathSchema.optional(),
68396
- groupBy: RecordFieldPathSchema.optional(),
68397
- where: RecordWhereSchema.optional()
68398
- }), object({ rows: array(object({
68399
- group: union([
68400
- string(),
68401
- number(),
68402
- _null()
68403
- ]),
68404
- value: number().nullable()
68405
- })) }), { auth: "admin" }), method(object({
68406
- ...KindShape,
68407
- deviceId: number().int().optional(),
68408
- atBefore: number().optional(),
68409
- where: RecordWhereSchema.optional()
68410
- }), object({
68411
- deleted: number().int(),
68412
- secretRefs: SecretRefsList
68413
- }), {
68414
- kind: "mutation",
68415
- auth: "admin"
68416
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
68417
- results: array(object({
68418
- rev: number().int().nullable(),
68419
- deleted: boolean()
68420
- })),
68421
- releasedRefs: SecretRefsList,
68422
- secretRefs: SecretRefsList
68423
- }), {
68424
- kind: "mutation",
68425
- auth: "admin"
68426
- }), method(object({
68427
- deviceId: number().int(),
68428
- kinds: array(RecordKindNameSchema).optional()
68429
- }), object({
68430
- deleted: record(string(), number().int()),
68431
- secretRefs: SecretRefsList
68432
- }), {
68433
- kind: "mutation",
68434
- auth: "admin"
68435
- }), method(object(KindShape), object({ id: number().int() }), {
68436
- kind: "mutation",
68437
- auth: "admin"
68438
- });
68439
- /**
68440
68600
  * record-entries - the ONE admin surface for every list of user-created entries
68441
68601
  * (D881): notification targets, brokers, LLM profiles, anything an addon
68442
68602
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -75938,11 +76098,15 @@ discriminatedUnion("kind", [object({
75938
76098
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
75939
76099
  * near F. A device that is not near one level most of the time has no floor.
75940
76100
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
75941
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
75942
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76101
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76102
+ * display waking): it is neither a run nor a pause. Among the remaining
76103
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76104
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76105
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76106
+ * is no run.
75943
76107
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
75944
- * grid a person would type; stop dwell covers the longest pause; start dwell
75945
- * covers the longest standby blip above T.
76108
+ * grid a person would type; stop dwell covers the longest pause and the
76109
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
75946
76110
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
75947
76111
  * run must hold exactly one true interval and nothing may flap outside.
75948
76112
  */
@@ -75953,6 +76117,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
75953
76117
  30 * SECOND_MS;
75954
76118
  60 * SECOND_MS;
75955
76119
  20 * MINUTE_MS$1;
76120
+ 90 * SECOND_MS;
76121
+ 30 * MINUTE_MS$1;
76122
+ 10 * MINUTE_MS$1;
76123
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
75956
76124
  10 * SECOND_MS;
75957
76125
  10 * SECOND_MS;
75958
76126
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -76971,20 +77139,6 @@ Object.freeze({
76971
77139
  access: "create",
76972
77140
  role: "viewer"
76973
77141
  },
76974
- "broker.add": {
76975
- capName: "broker",
76976
- capScope: "system",
76977
- addonId: null,
76978
- access: "create",
76979
- role: "admin"
76980
- },
76981
- "broker.get": {
76982
- capName: "broker",
76983
- capScope: "system",
76984
- addonId: null,
76985
- access: "view",
76986
- role: "admin"
76987
- },
76988
77142
  "broker.getBrokerConfig": {
76989
77143
  capName: "broker",
76990
77144
  capScope: "system",
@@ -76992,20 +77146,6 @@ Object.freeze({
76992
77146
  access: "view",
76993
77147
  role: "admin"
76994
77148
  },
76995
- "broker.getSettings": {
76996
- capName: "broker",
76997
- capScope: "system",
76998
- addonId: null,
76999
- access: "view",
77000
- role: "admin"
77001
- },
77002
- "broker.getSettingsSchema": {
77003
- capName: "broker",
77004
- capScope: "system",
77005
- addonId: null,
77006
- access: "view",
77007
- role: "admin"
77008
- },
77009
77149
  "broker.getState": {
77010
77150
  capName: "broker",
77011
77151
  capScope: "system",
@@ -77020,20 +77160,6 @@ Object.freeze({
77020
77160
  access: "view",
77021
77161
  role: "admin"
77022
77162
  },
77023
- "broker.list": {
77024
- capName: "broker",
77025
- capScope: "system",
77026
- addonId: null,
77027
- access: "view",
77028
- role: "admin"
77029
- },
77030
- "broker.listProviders": {
77031
- capName: "broker",
77032
- capScope: "system",
77033
- addonId: null,
77034
- access: "view",
77035
- role: "admin"
77036
- },
77037
77163
  "broker.publish": {
77038
77164
  capName: "broker",
77039
77165
  capScope: "system",
@@ -77041,20 +77167,6 @@ Object.freeze({
77041
77167
  access: "create",
77042
77168
  role: "admin"
77043
77169
  },
77044
- "broker.remove": {
77045
- capName: "broker",
77046
- capScope: "system",
77047
- addonId: null,
77048
- access: "delete",
77049
- role: "admin"
77050
- },
77051
- "broker.setSettings": {
77052
- capName: "broker",
77053
- capScope: "system",
77054
- addonId: null,
77055
- access: "create",
77056
- role: "admin"
77057
- },
77058
77170
  "broker.subscribe": {
77059
77171
  capName: "broker",
77060
77172
  capScope: "system",
@@ -77062,20 +77174,6 @@ Object.freeze({
77062
77174
  access: "create",
77063
77175
  role: "admin"
77064
77176
  },
77065
- "broker.testConnection": {
77066
- capName: "broker",
77067
- capScope: "system",
77068
- addonId: null,
77069
- access: "create",
77070
- role: "admin"
77071
- },
77072
- "broker.testSettings": {
77073
- capName: "broker",
77074
- capScope: "system",
77075
- addonId: null,
77076
- access: "create",
77077
- role: "admin"
77078
- },
77079
77177
  "broker.unsubscribe": {
77080
77178
  capName: "broker",
77081
77179
  capScope: "system",
@@ -79708,13 +79806,6 @@ Object.freeze({
79708
79806
  access: "view",
79709
79807
  role: "admin"
79710
79808
  },
79711
- "integrations.getSettings": {
79712
- capName: "integrations",
79713
- capScope: "system",
79714
- addonId: null,
79715
- access: "view",
79716
- role: "admin"
79717
- },
79718
79809
  "integrations.list": {
79719
79810
  capName: "integrations",
79720
79811
  capScope: "system",
@@ -79722,13 +79813,6 @@ Object.freeze({
79722
79813
  access: "view",
79723
79814
  role: "admin"
79724
79815
  },
79725
- "integrations.setSettings": {
79726
- capName: "integrations",
79727
- capScope: "system",
79728
- addonId: null,
79729
- access: "create",
79730
- role: "admin"
79731
- },
79732
79816
  "integrations.testConnection": {
79733
79817
  capName: "integrations",
79734
79818
  capScope: "system",
@@ -80723,6 +80807,13 @@ Object.freeze({
80723
80807
  access: "view",
80724
80808
  role: "admin"
80725
80809
  },
80810
+ "migrations.clearLegacyIntegrationRows": {
80811
+ capName: "migrations",
80812
+ capScope: "system",
80813
+ addonId: null,
80814
+ access: "delete",
80815
+ role: "admin"
80816
+ },
80726
80817
  "migrations.declareOwner": {
80727
80818
  capName: "migrations",
80728
80819
  capScope: "system",
@@ -80730,6 +80821,13 @@ Object.freeze({
80730
80821
  access: "create",
80731
80822
  role: "admin"
80732
80823
  },
80824
+ "migrations.deleteLegacyIntegrationRowKeys": {
80825
+ capName: "migrations",
80826
+ capScope: "system",
80827
+ addonId: null,
80828
+ access: "delete",
80829
+ role: "admin"
80830
+ },
80733
80831
  "migrations.dryRun": {
80734
80832
  capName: "migrations",
80735
80833
  capScope: "system",
@@ -80765,6 +80863,20 @@ Object.freeze({
80765
80863
  access: "create",
80766
80864
  role: "admin"
80767
80865
  },
80866
+ "migrations.patchLegacyIntegrationRows": {
80867
+ capName: "migrations",
80868
+ capScope: "system",
80869
+ addonId: null,
80870
+ access: "create",
80871
+ role: "admin"
80872
+ },
80873
+ "migrations.readLegacyIntegrationRows": {
80874
+ capName: "migrations",
80875
+ capScope: "system",
80876
+ addonId: null,
80877
+ access: "view",
80878
+ role: "admin"
80879
+ },
80768
80880
  "migrations.restorePreMigration": {
80769
80881
  capName: "migrations",
80770
80882
  capScope: "system",
@@ -82025,6 +82137,13 @@ Object.freeze({
82025
82137
  access: "view",
82026
82138
  role: "viewer"
82027
82139
  },
82140
+ "pipelineAnalytics.getMediaLocationCensus": {
82141
+ capName: "pipeline-analytics",
82142
+ capScope: "device",
82143
+ addonId: null,
82144
+ access: "view",
82145
+ role: "admin"
82146
+ },
82028
82147
  "pipelineAnalytics.getMediaReclaimStatus": {
82029
82148
  capName: "pipeline-analytics",
82030
82149
  capScope: "device",
@@ -90966,6 +91085,48 @@ object({
90966
91085
  delay: number().nonnegative().optional(),
90967
91086
  commands: record(string(), unknown())
90968
91087
  });
91088
+ var BrokerInUseError = class extends Error {
91089
+ brokerId;
91090
+ users;
91091
+ name = "BrokerInUseError";
91092
+ constructor(brokerId, users) {
91093
+ super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
91094
+ this.brokerId = brokerId;
91095
+ this.users = users;
91096
+ }
91097
+ };
91098
+ /** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
91099
+ async function assertBrokerUnused(brokerId, finders) {
91100
+ const users = [];
91101
+ for (const finder of finders) users.push(...await finder.find(brokerId));
91102
+ if (users.length > 0) throw new BrokerInUseError(brokerId, users);
91103
+ }
91104
+ /**
91105
+ * Users reached through another addon's listed rows (an addon cannot read a record kind it does
91106
+ * not own): every row whose `field` holds the broker id. `list` is a cap call the owner serves.
91107
+ */
91108
+ function fieldUserFinder(spec) {
91109
+ return { find: async (brokerId) => (await spec.list()).filter((row) => row.formValues[spec.field] === brokerId).map((row) => ({
91110
+ kind: spec.kind,
91111
+ id: row.id,
91112
+ ...row.name !== void 0 ? { name: row.name } : {}
91113
+ })) };
91114
+ }
91115
+ /**
91116
+ * An account provider whose `deleteInstance` is refused while anything still uses the account
91117
+ * (D1012). The account is an `integration-instances` kind (D1013), so the refusal lives on the
91118
+ * delete the instances page calls, not on a separate broker cap.
91119
+ */
91120
+ function guardBrokerDelete(base, users) {
91121
+ return {
91122
+ ...base,
91123
+ deleteInstance: async (input) => {
91124
+ await assertBrokerUnused(input.id, users());
91125
+ return base.deleteInstance(input);
91126
+ }
91127
+ };
91128
+ }
91129
+ entryBody({ brokerId: string() });
90969
91130
  //#endregion
90970
91131
  //#region ../../node_modules/xtend/immutable.js
90971
91132
  var require_immutable = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -123010,7 +123171,7 @@ function readOnboardPort(settings) {
123010
123171
  return raw;
123011
123172
  }
123012
123173
  /**
123013
- * Build an onboard {@link BuiltOnboardEntry} from `broker.add` settings.
123174
+ * Build an onboard {@link BuiltOnboardEntry} from the create form's values.
123014
123175
  * Pure — the port is pre-resolved by the caller (auto-allocation is a
123015
123176
  * side-effecting concern kept out of this helper), so the helper performs no
123016
123177
  * I/O of its own.
@@ -123096,7 +123257,7 @@ function resolveStoredPassword(value, legacy) {
123096
123257
  function retiredCiphertextPaths(config) {
123097
123258
  const out = [];
123098
123259
  for (const b of config.brokers) if (b.password?.startsWith("enc:v1:") === true) out.push(`brokers[${b.id}].password`);
123099
- if (config.embeddedPassword.startsWith("enc:v1:")) out.push("embeddedPassword");
123260
+ if (config.embeddedPassword?.startsWith("enc:v1:") === true) out.push("embeddedPassword");
123100
123261
  return out;
123101
123262
  }
123102
123263
  //#endregion
@@ -123130,6 +123291,229 @@ var mqttBrokersEntryKind = defineEntryKind("brokers", {
123130
123291
  writeTrigger: "an operator adds or removes an external MQTT broker entry"
123131
123292
  });
123132
123293
  //#endregion
123294
+ //#region src/mqtt-broker-users.ts
123295
+ /** The addon whose integrations name an MQTT broker, and the field they keep that id in. */
123296
+ var MQTT_HA_ADDON_ID = "provider-mqtt-ha";
123297
+ var MQTT_HA_BROKER_FIELD = "mqttBrokerId";
123298
+ /** The finders for everything that still points at an MQTT broker. */
123299
+ function mqttBrokerUsers(listMqttHaInstances) {
123300
+ return [fieldUserFinder({
123301
+ kind: "integration",
123302
+ field: MQTT_HA_BROKER_FIELD,
123303
+ list: listMqttHaInstances
123304
+ })];
123305
+ }
123306
+ /** MQTT Discovery is installed but has not registered as a provider: not known, NOT absent (D315). */
123307
+ var MqttDiscoveryNotReadyError = class extends Error {
123308
+ constructor() {
123309
+ super("MQTT Discovery is installed but not answering yet (still starting, or switched off), so its integrations cannot be checked: try again once it is running");
123310
+ this.name = "MqttDiscoveryNotReadyError";
123311
+ }
123312
+ };
123313
+ /**
123314
+ * The integrations of the MQTT Discovery addon. Nothing can name a broker only when the registry
123315
+ * ANSWERED that the addon supplies no `integration-instances` AND the addon roster ANSWERED that
123316
+ * it is not installed: then the answer is none. An addon that is installed but has not registered
123317
+ * (booting, switched off) is not-yet-known, and a registry or roster that does not answer, or a
123318
+ * read that fails, is unknown too: all of those throw, so the delete is refused (D49, D315).
123319
+ */
123320
+ async function listMqttHaInstancesOver(api) {
123321
+ const info = await api.capabilities.getCapability.query({ name: "integration-instances" });
123322
+ if (info === null) throw new Error("the capability registry did not answer");
123323
+ if (!info.providers.includes("provider-mqtt-ha")) {
123324
+ if ((await api.addons.list.query()).some((row) => row.manifest.id === "provider-mqtt-ha")) throw new MqttDiscoveryNotReadyError();
123325
+ return [];
123326
+ }
123327
+ return api.integrationInstances.listInstances.query({ addonId: MQTT_HA_ADDON_ID });
123328
+ }
123329
+ //#endregion
123330
+ //#region src/mqtt-broker-instances.ts
123331
+ /**
123332
+ * The MQTT brokers as `integration-instances` (D1013): one instance per `brokers` record. An
123333
+ * external broker is a URL; an ONBOARD broker (`onboard: true` on the create form) is a CamStack
123334
+ * `aedes` server on this hub: the create allocates its port (or honours the operator's) and the
123335
+ * addon spawns it from the record, the delete tears it down. Removing a broker an MQTT Discovery
123336
+ * integration still names is refused (D1012).
123337
+ */
123338
+ var MQTT_INSTANCE_KIND = "mqtt";
123339
+ /** The create-form keys that only mean "run an onboard broker"; the record keeps a descriptor. */
123340
+ var ONBOARD_FORM_KEYS = new Set([
123341
+ "onboard",
123342
+ "port",
123343
+ "allowAnonymous"
123344
+ ]);
123345
+ var PROBE_LABELS = {
123346
+ connected: "Connected",
123347
+ disconnected: "Disconnected",
123348
+ "auth-failed": "Authentication failed",
123349
+ unreachable: "Unreachable",
123350
+ "tls-error": "TLS error"
123351
+ };
123352
+ /** The instance's health word: connected / auth-failed / unreachable / tls-error, latency, clients. */
123353
+ function mqttHealthOf(body, live, now) {
123354
+ if (body.enabled === false) return { status: {
123355
+ state: "off",
123356
+ label: "Switched off"
123357
+ } };
123358
+ if (body.onboard !== void 0) return live.onboard === null ? {
123359
+ status: {
123360
+ state: "error",
123361
+ label: "Not listening"
123362
+ },
123363
+ lastCheckedAt: now
123364
+ } : {
123365
+ status: {
123366
+ state: "ok",
123367
+ label: "Listening",
123368
+ detail: `${live.onboard.connectedClients} connected clients`
123369
+ },
123370
+ lastCheckedAt: now
123371
+ };
123372
+ const probe = live.probe;
123373
+ if (probe === void 0) return { status: {
123374
+ state: "unknown",
123375
+ label: "Not checked yet"
123376
+ } };
123377
+ const detail = probe.status === "connected" ? probe.latencyMs !== null ? `${probe.latencyMs} ms` : void 0 : probe.error;
123378
+ return {
123379
+ status: {
123380
+ state: probe.status === "connected" ? "ok" : "error",
123381
+ label: PROBE_LABELS[probe.status],
123382
+ ...detail !== void 0 ? { detail } : {}
123383
+ },
123384
+ lastCheckedAt: probe.lastCheckedAt
123385
+ };
123386
+ }
123387
+ function withoutOnboardKeys(values) {
123388
+ return Object.fromEntries(Object.entries(values).filter(([key]) => !ONBOARD_FORM_KEYS.has(key)));
123389
+ }
123390
+ function typedText(value) {
123391
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
123392
+ }
123393
+ /** A password the form really typed: not blank, not the redaction token, not the hidden view. */
123394
+ function typedSecret(value) {
123395
+ const text = typedText(value);
123396
+ return text === void 0 || text === "__camstack_redacted__" || isSecretFieldView(text) ? void 0 : text;
123397
+ }
123398
+ function resultOf(probe) {
123399
+ return probe.ok ? {
123400
+ success: true,
123401
+ status: "validated",
123402
+ ...probe.latencyMs !== void 0 ? { latencyMs: probe.latencyMs } : {}
123403
+ } : {
123404
+ success: false,
123405
+ status: "rejected",
123406
+ error: probe.error ?? "The broker did not answer"
123407
+ };
123408
+ }
123409
+ function buildMqttBrokerInstances(records, deps) {
123410
+ const instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
123411
+ description: {
123412
+ role: "services",
123413
+ instanceMode: "multiple",
123414
+ kind: "brokers",
123415
+ canTest: true,
123416
+ kinds: [{
123417
+ id: MQTT_INSTANCE_KIND,
123418
+ label: "MQTT broker",
123419
+ functionCap: "broker",
123420
+ brokerKind: "mqtt",
123421
+ canTest: true
123422
+ }]
123423
+ },
123424
+ formSchema: (id) => deps.formSchema(id, false),
123425
+ kindOf: () => MQTT_INSTANCE_KIND,
123426
+ initialBody: (name) => ({
123427
+ name,
123428
+ url: "mqtt://"
123429
+ }),
123430
+ configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
123431
+ health: (body, id) => mqttHealthOf(body, deps.liveState(id), Date.now()),
123432
+ onChanged: deps.onChanged,
123433
+ test: async ({ id, values }) => {
123434
+ if (id === null && isOnboardRequested(values)) return {
123435
+ success: true,
123436
+ status: "validated",
123437
+ detail: "A free port is picked when the broker is created"
123438
+ };
123439
+ const saved = id === null ? null : await instances.readResolved(id);
123440
+ if (saved?.body.onboard !== void 0) return deps.onboardListening(saved.id) ? {
123441
+ success: true,
123442
+ status: "validated",
123443
+ detail: `Listening on port ${saved.body.onboard.port}`
123444
+ } : {
123445
+ success: false,
123446
+ status: "rejected",
123447
+ error: "The onboard broker is not listening"
123448
+ };
123449
+ const url = typedText(values["url"]) ?? saved?.body.url;
123450
+ if (url === void 0 || url === "mqtt://") return {
123451
+ success: false,
123452
+ status: "rejected",
123453
+ error: "Enter the broker URL first"
123454
+ };
123455
+ const username = typedText(values["username"]) ?? saved?.body.username;
123456
+ const password = typedSecret(values["password"]) ?? saved?.body.password;
123457
+ return resultOf(await deps.probe({
123458
+ url,
123459
+ ...username !== void 0 ? { username } : {},
123460
+ ...password !== void 0 ? { password } : {}
123461
+ }));
123462
+ }
123463
+ });
123464
+ const base = instances.toProvider();
123465
+ /** Port, descriptor and URL of an onboard broker the form asked for. */
123466
+ const onboardValues = async (id, name, values) => {
123467
+ const built = buildOnboardEntry({
123468
+ id,
123469
+ name,
123470
+ port: readOnboardPort(values) ?? await deps.freePort(),
123471
+ settings: values
123472
+ });
123473
+ return {
123474
+ ...withoutOnboardKeys(values),
123475
+ url: built.url,
123476
+ onboard: built.onboard
123477
+ };
123478
+ };
123479
+ return guardBrokerDelete({
123480
+ ...base,
123481
+ instanceFormSchema: async ({ id }) => {
123482
+ const saved = id === null ? null : await instances.getResolved(id);
123483
+ return deps.formSchema(id, saved?.onboard !== void 0);
123484
+ },
123485
+ createInstance: async (input) => {
123486
+ const values = input.values ?? {};
123487
+ if (!isOnboardRequested(values)) return base.createInstance({
123488
+ ...input,
123489
+ values: withoutOnboardKeys(values)
123490
+ });
123491
+ const id = input.id ?? randomUUID();
123492
+ return base.createInstance({
123493
+ ...input,
123494
+ id,
123495
+ values: await onboardValues(id, input.name, values)
123496
+ });
123497
+ },
123498
+ updateInstance: async (input) => {
123499
+ if (!isOnboardRequested(input.values)) return base.updateInstance({
123500
+ ...input,
123501
+ values: withoutOnboardKeys(input.values)
123502
+ });
123503
+ const current = await instances.getResolved(input.id);
123504
+ if (current === null) throw new Error(`mqtt-broker: broker '${input.id}' not found`);
123505
+ if (current.onboard !== void 0) return base.updateInstance({
123506
+ ...input,
123507
+ values: withoutOnboardKeys(input.values)
123508
+ });
123509
+ return base.updateInstance({
123510
+ ...input,
123511
+ values: await onboardValues(input.id, input.name ?? current.name, input.values)
123512
+ });
123513
+ }
123514
+ }, () => mqttBrokerUsers(deps.listMqttHaInstances));
123515
+ }
123516
+ //#endregion
123133
123517
  //#region src/migrations/brokers-to-records.ts
123134
123518
  /**
123135
123519
  * The external MQTT brokers live in the `brokers` records (D985, D996); the old addon-settings
@@ -123219,46 +123603,163 @@ async function planDropBrokersKey(ports) {
123219
123603
  }] };
123220
123604
  }
123221
123605
  //#endregion
123606
+ //#region src/migrations/embedded-to-onboard.ts
123607
+ /**
123608
+ * The legacy single embedded broker lived in FLAT addon-settings keys (`embeddedEnabled`,
123609
+ * `embeddedPort`, ...). It is an ONBOARD broker record now, so the instance list, the pickers,
123610
+ * the test and the delete all see it (D1013). Two migrations of the catalogue, in this order:
123611
+ *
123612
+ * 1. `embeddedToOnboardRecord` (procedure): a legacy broker (a stopped one as a switched-off
123613
+ * record) becomes a `brokers` record
123614
+ * under the SAME id (`embeddedId`, else the `'embedded'` id the old code derived), so a
123615
+ * consumer that stored that id still resolves. It never overwrites a record of that id.
123616
+ * 2. `planDropEmbeddedKeys` (plan): the flat keys are deleted from the canonical
123617
+ * `addon-settings` rows, and only while the record that replaces them exists and carries the
123618
+ * password the legacy key held. `embeddedClients` / `embeddedAcl` are not flat broker keys
123619
+ * and stay.
123620
+ */
123621
+ /** The flat keys that described the legacy embedded broker. */
123622
+ var LEGACY_EMBEDDED_KEYS = [
123623
+ "embeddedEnabled",
123624
+ "embeddedId",
123625
+ "embeddedPort",
123626
+ "embeddedAllowAnonymous",
123627
+ "embeddedUsername",
123628
+ "embeddedPassword"
123629
+ ];
123630
+ var LEGACY_EMBEDDED_NAME = "Embedded broker";
123631
+ function onboardBodyOf(legacy) {
123632
+ return {
123633
+ name: LEGACY_EMBEDDED_NAME,
123634
+ url: `mqtt://127.0.0.1:${legacy.port}`,
123635
+ ...legacy.username !== void 0 ? { username: legacy.username } : {},
123636
+ ...legacy.password !== void 0 ? { password: legacy.password } : {},
123637
+ onboard: {
123638
+ port: legacy.port,
123639
+ allowAnonymous: legacy.allowAnonymous
123640
+ },
123641
+ ...legacy.enabled ? {} : { enabled: false }
123642
+ };
123643
+ }
123644
+ async function embeddedToOnboardRecord(ports) {
123645
+ const legacy = await ports.readLegacyEmbedded();
123646
+ if (legacy === null) return {
123647
+ outcome: "already-done",
123648
+ summary: { migrated: 0 }
123649
+ };
123650
+ if ((await ports.readRecords()).has(legacy.id)) return {
123651
+ outcome: "already-done",
123652
+ summary: { migrated: 0 }
123653
+ };
123654
+ await ports.putOnboardRecord(legacy.id, onboardBodyOf(legacy));
123655
+ return {
123656
+ outcome: "done",
123657
+ summary: { migrated: 1 }
123658
+ };
123659
+ }
123660
+ async function planDropEmbeddedKeys(ports) {
123661
+ const raw = await ports.readRawEmbeddedKeys();
123662
+ const present = LEGACY_EMBEDDED_KEYS.filter((key) => key in raw);
123663
+ if (present.length === 0) return { edits: [] };
123664
+ const refuse = (reason) => {
123665
+ ports.warn(`mqtt-broker migration: the legacy embedded keys are kept: ${reason}`);
123666
+ throw new Error(`the legacy embedded broker keys are not deleted: ${reason}`);
123667
+ };
123668
+ const legacy = await ports.readLegacyEmbedded();
123669
+ if (legacy !== null) {
123670
+ const record = (await ports.readRecords()).get(legacy.id);
123671
+ if (record === void 0) return refuse(`the record '${legacy.id}' does not exist`);
123672
+ if ((legacy.password ?? "").length > 0 && !record.hasPassword) return refuse(`the record '${legacy.id}' holds no password`);
123673
+ }
123674
+ return { edits: [{
123675
+ op: "deleteKeys",
123676
+ scope: ADDON_SETTINGS_TABLE,
123677
+ keys: present.map((key) => `${MQTT_BROKER_ADDON_ID}.${key}`),
123678
+ precondition: preconditionOf(Object.fromEntries(present.map((key) => [`${MQTT_BROKER_ADDON_ID}.${key}`, {
123679
+ addonId: MQTT_BROKER_ADDON_ID,
123680
+ key,
123681
+ value: raw[key]
123682
+ }])))
123683
+ }] };
123684
+ }
123685
+ //#endregion
123222
123686
  //#region src/migrations/index.ts
123223
123687
  var BROKERS_TO_RECORDS_ID = "mqtt-broker/2026-11-03-brokers-settings-to-records";
123224
123688
  var DROP_BROKERS_KEY_ID = "mqtt-broker/2026-11-03-drop-brokers-key";
123689
+ var EMBEDDED_TO_ONBOARD_ID = "mqtt-broker/2026-11-05-embedded-broker-to-onboard-record";
123690
+ var DROP_EMBEDDED_KEYS_ID = "mqtt-broker/2026-11-05-drop-embedded-keys";
123225
123691
  var FIXTURE_MARK = "a setting key the migration does not know";
123226
123692
  /**
123227
123693
  * The `mqtt-broker` catalogue (D987), offered to the engine when the addon initialises. Both
123228
123694
  * entries are `tolerate`: the embedded broker and every record keep working while a legacy key
123229
123695
  * stays where it is.
123230
123696
  */
123231
- var ADDON_MIGRATIONS = [{
123232
- meta: {
123233
- id: BROKERS_TO_RECORDS_ID,
123234
- owner: "mqtt-broker",
123235
- level: "addon-state",
123236
- seq: 1,
123237
- dependsOn: [],
123238
- store: "settings",
123239
- scope: "cluster",
123240
- onFailure: "tolerate",
123241
- bodyKind: "procedure",
123242
- description: "Check that every external broker of the old settings list is a brokers record; never deletes",
123243
- fixtureMark: FIXTURE_MARK
123244
- },
123245
- procedure: verifyBrokersAreRecords
123246
- }, {
123247
- meta: {
123248
- id: DROP_BROKERS_KEY_ID,
123249
- owner: "mqtt-broker",
123250
- level: "addon-state",
123251
- seq: 2,
123252
- dependsOn: [BROKERS_TO_RECORDS_ID],
123253
- store: "settings",
123254
- scope: "cluster",
123255
- onFailure: "tolerate",
123256
- bodyKind: "plan",
123257
- description: "Delete the legacy brokers settings key once every broker is a record",
123258
- fixtureMark: FIXTURE_MARK
123259
- },
123260
- plan: planDropBrokersKey
123261
- }];
123697
+ var ADDON_MIGRATIONS = [
123698
+ {
123699
+ meta: {
123700
+ id: BROKERS_TO_RECORDS_ID,
123701
+ owner: "mqtt-broker",
123702
+ level: "addon-state",
123703
+ seq: 1,
123704
+ dependsOn: [],
123705
+ store: "settings",
123706
+ scope: "cluster",
123707
+ onFailure: "tolerate",
123708
+ bodyKind: "procedure",
123709
+ description: "Check that every external broker of the old settings list is a brokers record; never deletes",
123710
+ fixtureMark: FIXTURE_MARK
123711
+ },
123712
+ procedure: verifyBrokersAreRecords
123713
+ },
123714
+ {
123715
+ meta: {
123716
+ id: DROP_BROKERS_KEY_ID,
123717
+ owner: "mqtt-broker",
123718
+ level: "addon-state",
123719
+ seq: 2,
123720
+ dependsOn: [BROKERS_TO_RECORDS_ID],
123721
+ store: "settings",
123722
+ scope: "cluster",
123723
+ onFailure: "tolerate",
123724
+ bodyKind: "plan",
123725
+ description: "Delete the legacy brokers settings key once every broker is a record",
123726
+ fixtureMark: FIXTURE_MARK
123727
+ },
123728
+ plan: planDropBrokersKey
123729
+ },
123730
+ {
123731
+ meta: {
123732
+ id: EMBEDDED_TO_ONBOARD_ID,
123733
+ owner: "mqtt-broker",
123734
+ level: "addon-state",
123735
+ seq: 3,
123736
+ dependsOn: [DROP_BROKERS_KEY_ID],
123737
+ store: "settings",
123738
+ scope: "cluster",
123739
+ onFailure: "tolerate",
123740
+ bodyKind: "procedure",
123741
+ description: "The legacy embedded broker (flat embedded* keys) becomes an onboard brokers record under the same id; never overwrites a record",
123742
+ fixtureMark: FIXTURE_MARK
123743
+ },
123744
+ procedure: embeddedToOnboardRecord
123745
+ },
123746
+ {
123747
+ meta: {
123748
+ id: DROP_EMBEDDED_KEYS_ID,
123749
+ owner: "mqtt-broker",
123750
+ level: "addon-state",
123751
+ seq: 4,
123752
+ dependsOn: [EMBEDDED_TO_ONBOARD_ID],
123753
+ store: "settings",
123754
+ scope: "cluster",
123755
+ onFailure: "tolerate",
123756
+ bodyKind: "plan",
123757
+ description: "Delete the flat embedded broker keys once the onboard record holds them",
123758
+ fixtureMark: FIXTURE_MARK
123759
+ },
123760
+ plan: planDropEmbeddedKeys
123761
+ }
123762
+ ];
123262
123763
  //#endregion
123263
123764
  //#region src/mqtt-entry-record-kinds.ts
123264
123765
  var aclAccessSchema = _enum([
@@ -123460,129 +123961,31 @@ var MqttAclStore = class MqttAclStore {
123460
123961
  * The addon has NO standalone admin page. All configuration — external
123461
123962
  * broker entries, the embedded `aedes` broker, embedded credentials,
123462
123963
  * and the topic ACL — is exposed through the standard
123463
- * `globalSettingsSchema()` and rendered automatically inside the
123964
+ * `clusterSettingsSchema()` and rendered automatically inside the
123464
123965
  * Integrations → Brokers tab's docked Settings panel, like every other
123465
123966
  * integration-type addon.
123466
123967
  *
123467
123968
  * Lifecycle:
123468
123969
  * - External broker entries are persisted in `this.activeExternalBrokers`.
123469
- * - The embedded broker is persisted as flat `embedded*` config keys
123470
- * (`embeddedEnabled`, `embeddedPort`, …) so the standard settings
123471
- * form can drive it; {@link MqttBrokerAddon.deriveEmbeddedEntry}
123472
- * reassembles them into an `EmbeddedBrokerEntry`.
123473
- * - On boot we start the embedded broker when `embeddedEnabled`.
123474
- * - `addBroker` / `removeBroker` / `startEmbeddedBroker` /
123475
- * `stopEmbeddedBroker` mutate the config and the runtime state.
123970
+ * - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
123971
+ * `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
123972
+ * - `addBroker` / `removeBroker` mutate the records and the runtime state.
123476
123973
  * - `testConnection` does a one-shot `mqtt.connect` race against a 3s
123477
123974
  * timeout AND refreshes the probe cache for the broker.
123478
123975
  */
123479
123976
  /** bcrypt cost factor — exported for tests. */
123480
123977
  var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
123481
123978
  var PROBE_INTERVAL_MS = 3e4;
123482
- var DEFAULT_EMBEDDED_PORT = 1883;
123483
- /**
123484
- * Per-kind broker creation form for kind='mqtt', rendered by the unified
123485
- * Broker manager / create picker (via `broker.getSettingsSchema`). PURE
123486
- * DATA — emits the flat `{ url, username, password, clientIdPrefix }`
123487
- * shape `broker.add`'s `settings` payload consumes. Mirrors the external
123488
- * broker fields `addBroker` accepts.
123489
- */
123490
- var mqttBrokerSettingsSchema = { sections: [{
123491
- id: "mqtt-broker-connection",
123492
- title: "MQTT connection",
123493
- titleKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.title",
123494
- columns: 1,
123495
- fields: [
123496
- {
123497
- type: "boolean",
123498
- key: "onboard",
123499
- label: "Run an onboard broker",
123500
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.label",
123501
- description: "Spin up a CamStack-hosted aedes broker on this hub instead of connecting to an external one. Each onboard broker gets its own dedicated server + port.",
123502
- descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.onboard.description",
123503
- default: false
123504
- },
123505
- {
123506
- type: "number",
123507
- key: "port",
123508
- label: "Port",
123509
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.label",
123510
- placeholder: "(auto)",
123511
- description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
123512
- descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.port.description",
123513
- showWhen: {
123514
- field: "onboard",
123515
- equals: true
123516
- }
123517
- },
123518
- {
123519
- type: "boolean",
123520
- key: "allowAnonymous",
123521
- label: "Allow anonymous",
123522
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.label",
123523
- description: "Permit clients to connect without a username/password.",
123524
- descriptionKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.allowAnonymous.description",
123525
- default: false,
123526
- showWhen: {
123527
- field: "onboard",
123528
- equals: true
123529
- }
123530
- },
123531
- {
123532
- type: "text",
123533
- key: "url",
123534
- label: "Broker URL",
123535
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.url.label",
123536
- placeholder: "mqtt://localhost:1883",
123537
- showWhen: {
123538
- field: "onboard",
123539
- equals: false
123540
- }
123541
- },
123542
- {
123543
- type: "text",
123544
- key: "username",
123545
- label: "Username",
123546
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.username.label",
123547
- placeholder: "(optional)"
123548
- },
123549
- {
123550
- type: "password",
123551
- key: "password",
123552
- label: "Password",
123553
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.password.label",
123554
- showToggle: true
123555
- },
123556
- {
123557
- type: "text",
123558
- key: "clientIdPrefix",
123559
- label: "Client ID prefix",
123560
- labelKey: "mqtt-broker:mqttBrokerSettings.s_mqtt-broker-connection.clientIdPrefix.label",
123561
- placeholder: "(optional)",
123562
- showWhen: {
123563
- field: "onboard",
123564
- equals: false
123565
- }
123566
- }
123567
- ]
123568
- }] };
123569
123979
  var DEFAULT_CONFIG = {
123570
- embeddedEnabled: false,
123571
- embeddedId: "",
123572
- embeddedPort: DEFAULT_EMBEDDED_PORT,
123573
- embeddedAllowAnonymous: false,
123574
- embeddedUsername: "",
123575
- embeddedPassword: "",
123576
123980
  embeddedClients: [],
123577
123981
  embeddedAcl: []
123578
123982
  };
123579
123983
  var MqttBrokerAddon = class extends BaseAddon {
123580
- embeddedRuntime = null;
123581
- /** Per-entry onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
123984
+ /** Per-record onboard `aedes` runtimes, keyed by `ExternalBrokerEntry.id`. */
123582
123985
  onboardRuntimes = /* @__PURE__ */ new Map();
123583
123986
  probeCache = null;
123584
123987
  brokerStore = null;
123585
- instances = null;
123988
+ brokerInstances = null;
123586
123989
  clientStore = null;
123587
123990
  aclStore = null;
123588
123991
  /** The external broker rows: the `brokers` records, secrets resolved. */
@@ -123596,6 +123999,20 @@ var MqttBrokerAddon = class extends BaseAddon {
123596
123999
  if (this.brokerStore === null) throw new Error("mqtt-broker: the broker records are not open");
123597
124000
  return this.brokerStore;
123598
124001
  }
124002
+ /** What the addon knows of a broker right now: its onboard server or its last probe. */
124003
+ liveState(id) {
124004
+ const running = this.onboardRuntimes.get(id);
124005
+ const probe = this.probeCache?.get(id);
124006
+ return {
124007
+ onboard: running === void 0 ? null : { connectedClients: running.aedes.connectedClients },
124008
+ ...probe !== void 0 ? { probe: {
124009
+ status: probe.status,
124010
+ latencyMs: probe.latencyMs,
124011
+ ...probe.error !== void 0 ? { error: probe.error } : {},
124012
+ lastCheckedAt: probe.lastCheckedAt
124013
+ } } : {}
124014
+ };
124015
+ }
123599
124016
  async persistExternalBrokers(entries) {
123600
124017
  this.activeExternalBrokers = [...entries];
123601
124018
  await this.requireBrokerStore().replaceAll(entries);
@@ -123645,19 +124062,13 @@ var MqttBrokerAddon = class extends BaseAddon {
123645
124062
  this.brokerStore = MqttBrokerStore.of(records);
123646
124063
  this.clientStore = MqttClientStore.of(records);
123647
124064
  this.aclStore = MqttAclStore.of(records);
123648
- this.instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
123649
- description: {
123650
- role: "services",
123651
- instanceMode: "multiple",
123652
- kind: "brokers",
123653
- canTest: false
123654
- },
123655
- formSchema: () => this.brokerForm(),
123656
- initialBody: (name) => ({
123657
- name,
123658
- url: "mqtt://"
123659
- }),
123660
- configured: (body) => body.url.trim().length > 0 && body.url.trim() !== "mqtt://",
124065
+ this.brokerInstances = buildMqttBrokerInstances(records, {
124066
+ formSchema: (id, onboard) => this.brokerForm(id, onboard),
124067
+ listMqttHaInstances: () => listMqttHaInstancesOver(this.ctx.api),
124068
+ freePort: () => findFreePort(),
124069
+ probe: (cfg) => this.probeMqtt(cfg),
124070
+ onboardListening: (id) => this.onboardRuntimes.has(id),
124071
+ liveState: (id) => this.liveState(id),
123661
124072
  onChanged: async () => {
123662
124073
  await this.refreshExternalBrokers();
123663
124074
  }
@@ -123686,29 +124097,9 @@ var MqttBrokerAddon = class extends BaseAddon {
123686
124097
  });
123687
124098
  this.probeCache.setBrokersSource(() => this.collectExternalProbeInputs());
123688
124099
  this.probeCache.start();
123689
- const persistedEmbedded = this.deriveEmbeddedEntry();
123690
- if (persistedEmbedded) try {
123691
- await this.spawnEmbedded(persistedEmbedded);
123692
- this.ctx.logger.info("embedded mqtt broker restarted", {
123693
- meta: { port: persistedEmbedded.port },
123694
- tags: {
123695
- topic: "mqtt-broker",
123696
- phase: "embedded-restarted"
123697
- }
123698
- });
123699
- } catch (err) {
123700
- this.ctx.logger.warn("embedded mqtt broker failed to restart", {
123701
- meta: { error: errMsg(err) },
123702
- tags: {
123703
- topic: "mqtt-broker",
123704
- phase: "embedded-restart-failed"
123705
- }
123706
- });
123707
- }
123708
124100
  await this.reconcileOnboardRuntimes();
123709
124101
  this.ctx.addDisposer(async () => {
123710
124102
  await this.probeCache?.stop();
123711
- await this.shutdownEmbedded().catch(() => void 0);
123712
124103
  await this.shutdownAllOnboard();
123713
124104
  });
123714
124105
  const provider = {
@@ -123720,11 +124111,13 @@ var MqttBrokerAddon = class extends BaseAddon {
123720
124111
  startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
123721
124112
  stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
123722
124113
  getStatus: async () => ({
123723
- brokerCount: this.activeExternalBrokers.length + (this.config.embeddedEnabled ? 1 : 0),
123724
- embeddedRunning: this.embeddedRuntime !== null
124114
+ brokerCount: this.activeExternalBrokers.length,
124115
+ embeddedRunning: this.onboardRuntimes.size > 0
123725
124116
  })
123726
124117
  };
123727
124118
  const unifiedProvider = this.buildUnifiedBrokerProvider();
124119
+ const brokerInstances = this.brokerInstances;
124120
+ if (brokerInstances === null) throw new Error("mqtt-broker: the broker instances are not built");
123728
124121
  return [
123729
124122
  {
123730
124123
  capability: mqttBrokerCapability,
@@ -123736,111 +124129,21 @@ var MqttBrokerAddon = class extends BaseAddon {
123736
124129
  },
123737
124130
  {
123738
124131
  capability: integrationInstancesCapability,
123739
- provider: this.instances.toProvider()
124132
+ provider: brokerInstances
123740
124133
  }
123741
124134
  ];
123742
124135
  }
124136
+ /**
124137
+ * The bus-only `broker` cap (D1013). The brokers themselves are the `integration-instances`
124138
+ * provider; this one only answers the config read and the status.
124139
+ */
123743
124140
  buildUnifiedBrokerProvider() {
123744
- const KIND = "mqtt";
123745
- const ownerAddonId = this.ctx.id;
123746
- const adapt = (info) => ({
123747
- id: info.id,
123748
- addonId: ownerAddonId,
123749
- name: info.name,
123750
- kind: KIND,
123751
- status: mapMqttStatus(info.status),
123752
- info: {
123753
- ...info.latencyMs !== null ? { latencyMs: info.latencyMs } : {},
123754
- ...info.kind === "embedded" ? {
123755
- embedded: true,
123756
- connectedClients: info.connectedClients ?? 0
123757
- } : {}
123758
- },
123759
- lastCheckedAt: info.lastCheckedAt ?? null,
123760
- error: info.error ?? null
123761
- });
123762
- const owns = (id) => {
123763
- if (this.activeExternalBrokers.some((b) => b.id === id)) return true;
123764
- const embedded = this.deriveEmbeddedEntry();
123765
- return embedded !== null && embedded.id === id;
123766
- };
124141
+ const owns = (id) => this.activeExternalBrokers.some((b) => b.id === id);
123767
124142
  return {
123768
- list: async ({ kind }) => {
123769
- if (kind && kind !== KIND) return [];
123770
- return (await this.listBrokers()).map(adapt);
123771
- },
123772
- get: async ({ id }) => {
123773
- const match = (await this.listBrokers()).find((b) => b.id === id);
123774
- return match ? adapt(match) : null;
123775
- },
123776
- listProviders: async () => [{
123777
- addonId: ownerAddonId,
123778
- kinds: [{
123779
- kind: KIND,
123780
- label: "MQTT"
123781
- }]
123782
- }],
123783
- add: async ({ kind, name, settings, id }) => {
123784
- if (kind !== KIND) throw new Error(`mqtt-broker: only kind '${KIND}' is handled here (got '${kind}')`);
123785
- if (id !== void 0) throw new Error("mqtt-broker: adopting an existing broker id is not supported by this provider");
123786
- if (isOnboardRequested(settings)) return this.addOnboardBroker(name, settings);
123787
- const url = typeof settings["url"] === "string" ? settings["url"] : "";
123788
- if (!url) throw new Error("mqtt-broker.add: settings.url is required");
123789
- const input = {
123790
- name,
123791
- url,
123792
- ...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
123793
- ...typeof settings["password"] === "string" ? { password: settings["password"] } : {},
123794
- ...typeof settings["clientIdPrefix"] === "string" ? { clientIdPrefix: settings["clientIdPrefix"] } : {}
123795
- };
123796
- return this.addBroker(input);
123797
- },
123798
- remove: async ({ id }) => {
123799
- if (!owns(id)) return;
123800
- await this.removeBroker(id);
123801
- },
123802
- testConnection: async ({ id }) => {
123803
- if (!owns(id)) return {
123804
- ok: false,
123805
- error: "unknown broker"
123806
- };
123807
- return this.testConnection(id);
123808
- },
123809
- getSettings: async ({ id }) => {
123810
- if (!owns(id)) return null;
123811
- return this.getBrokerConfig(id);
123812
- },
123813
- setSettings: async () => {
123814
- throw new Error("mqtt-broker.setSettings: not supported — use remove + add to replace a broker");
123815
- },
123816
124143
  getBrokerConfig: async ({ id }) => {
123817
124144
  if (!owns(id)) return null;
123818
124145
  return this.getBrokerConfig(id);
123819
124146
  },
123820
- getSettingsSchema: async ({ kind }) => {
123821
- if (kind !== KIND) return null;
123822
- return mqttBrokerSettingsSchema;
123823
- },
123824
- testSettings: async ({ kind, settings }) => {
123825
- if (kind !== KIND) return {
123826
- ok: false,
123827
- error: `unsupported kind: ${kind}`
123828
- };
123829
- if (isOnboardRequested(settings)) return {
123830
- ok: true,
123831
- latencyMs: 0
123832
- };
123833
- const url = typeof settings["url"] === "string" ? settings["url"] : "";
123834
- if (!url) return {
123835
- ok: false,
123836
- error: "settings.url is required"
123837
- };
123838
- return this.probeMqtt({
123839
- url,
123840
- ...typeof settings["username"] === "string" ? { username: settings["username"] } : {},
123841
- ...typeof settings["password"] === "string" ? { password: settings["password"] } : {}
123842
- });
123843
- },
123844
124147
  publish: async () => {
123845
124148
  throw new Error("mqtt-broker.publish: not supported — fetch getBrokerConfig + open your own mqtt.js client (see \"NOT a pub/sub proxy\" in mqtt-broker.cap.ts)");
123846
124149
  },
@@ -123854,7 +124157,7 @@ var MqttBrokerAddon = class extends BaseAddon {
123854
124157
  getStatus: async () => {
123855
124158
  const all = await this.listBrokers();
123856
124159
  return {
123857
- brokerCount: this.activeExternalBrokers.length + (this.config.embeddedEnabled ? 1 : 0),
124160
+ brokerCount: this.activeExternalBrokers.length,
123858
124161
  connectedCount: all.filter((b) => b.status === "connected").length
123859
124162
  };
123860
124163
  }
@@ -123862,35 +124165,13 @@ var MqttBrokerAddon = class extends BaseAddon {
123862
124165
  }
123863
124166
  async onShutdown() {
123864
124167
  await this.probeCache?.stop();
123865
- await this.shutdownEmbedded().catch(() => void 0);
123866
124168
  await this.shutdownAllOnboard();
123867
124169
  }
123868
124170
  async onConfigChanged() {
123869
124171
  this.activeExternalBrokers = await this.requireBrokerStore().listResolved();
123870
124172
  await this.syncEmbeddedEntriesFromConfig();
123871
124173
  await this.reconcileOnboardRuntimes();
123872
- const desired = this.deriveEmbeddedEntry();
123873
- const running = this.embeddedRuntime?.entry ?? null;
123874
- if (!desired && running) {
123875
- await this.shutdownEmbedded();
123876
- return;
123877
- }
123878
- if (desired && !running) {
123879
- await this.spawnEmbedded(desired).catch((err) => {
123880
- this.ctx.logger.warn("embedded mqtt broker spawn (on config change) failed", { meta: { error: errMsg(err) } });
123881
- });
123882
- return;
123883
- }
123884
- if (desired && running) {
123885
- if (requiresEmbeddedRespawn(desired, running)) {
123886
- await this.shutdownEmbedded();
123887
- await this.spawnEmbedded(desired).catch((err) => {
123888
- this.ctx.logger.warn("embedded mqtt broker respawn failed", { meta: { error: errMsg(err) } });
123889
- });
123890
- return;
123891
- }
123892
- this.reconcileEmbeddedClients();
123893
- }
124174
+ this.reconcileEmbeddedClients();
123894
124175
  }
123895
124176
  async listBrokers() {
123896
124177
  const out = [];
@@ -123924,22 +124205,6 @@ var MqttBrokerAddon = class extends BaseAddon {
123924
124205
  };
123925
124206
  out.push(info);
123926
124207
  }
123927
- const embedded = this.deriveEmbeddedEntry();
123928
- if (embedded) {
123929
- const running = this.embeddedRuntime;
123930
- const status = running !== null ? "connected" : "disconnected";
123931
- const info = {
123932
- id: embedded.id,
123933
- name: embedded.name,
123934
- url: `mqtt://127.0.0.1:${embedded.port}`,
123935
- kind: "embedded",
123936
- status,
123937
- latencyMs: null,
123938
- ...running ? { connectedClients: running.aedes.connectedClients } : {},
123939
- lastCheckedAt: Date.now()
123940
- };
123941
- out.push(info);
123942
- }
123943
124208
  return out;
123944
124209
  }
123945
124210
  getBrokerConfig(id) {
@@ -123954,15 +124219,6 @@ var MqttBrokerAddon = class extends BaseAddon {
123954
124219
  ...external.clientIdPrefix !== void 0 ? { clientIdPrefix: external.clientIdPrefix } : {}
123955
124220
  };
123956
124221
  }
123957
- const embedded = this.deriveEmbeddedEntry();
123958
- if (embedded && embedded.id === id) {
123959
- const password = this.storedPassword(embedded.password);
123960
- return {
123961
- url: `mqtt://127.0.0.1:${embedded.port}`,
123962
- ...embedded.username !== void 0 ? { username: embedded.username } : {},
123963
- ...password !== void 0 ? { password } : {}
123964
- };
123965
- }
123966
124222
  throw new Error(`mqtt-broker: broker '${id}' not found`);
123967
124223
  }
123968
124224
  async addBroker(input) {
@@ -123983,44 +124239,6 @@ var MqttBrokerAddon = class extends BaseAddon {
123983
124239
  } });
123984
124240
  return { id };
123985
124241
  }
123986
- /**
123987
- * Create a per-entry ONBOARD broker from `broker.add` settings: allocate
123988
- * (or honour) a port, persist an `ExternalBrokerEntry` carrying an
123989
- * `onboard` descriptor, then spawn its dedicated `aedes` server.
123990
- */
123991
- async addOnboardBroker(name, settings) {
123992
- const id = randomUUID();
123993
- const port = readOnboardPort(settings) ?? await findFreePort();
123994
- const built = buildOnboardEntry({
123995
- id,
123996
- name,
123997
- port,
123998
- settings
123999
- });
124000
- const entry = {
124001
- id: built.id,
124002
- name: built.name,
124003
- url: built.url,
124004
- ...built.username !== void 0 ? { username: built.username } : {},
124005
- ...built.password !== void 0 ? { password: built.password } : {},
124006
- onboard: built.onboard
124007
- };
124008
- const next = [...this.activeExternalBrokers, entry];
124009
- await this.persistExternalBrokers(next);
124010
- const spawn = this.onboardSpawnEntry(entry);
124011
- if (spawn) await this.spawnOnboard(spawn);
124012
- this.ctx.logger.info("onboard mqtt broker added", {
124013
- meta: {
124014
- id,
124015
- port
124016
- },
124017
- tags: {
124018
- topic: "mqtt-broker",
124019
- phase: "onboard-started"
124020
- }
124021
- });
124022
- return { id };
124023
- }
124024
124242
  async removeBroker(id) {
124025
124243
  const removed = this.activeExternalBrokers.find((b) => b.id === id);
124026
124244
  if (removed) {
@@ -124031,13 +124249,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124031
124249
  this.ctx.logger.info("mqtt broker removed", { meta: { id } });
124032
124250
  return;
124033
124251
  }
124034
- const embedded = this.deriveEmbeddedEntry();
124035
- if (embedded && embedded.id === id) {
124036
- await this.shutdownEmbedded();
124037
- await this.updateGlobalSettings({ embeddedEnabled: false });
124038
- this.ctx.logger.info("embedded mqtt broker removed", { meta: { id } });
124039
- return;
124040
- }
124041
124252
  }
124042
124253
  async testConnection(id) {
124043
124254
  let cfg;
@@ -124056,7 +124267,7 @@ var MqttBrokerAddon = class extends BaseAddon {
124056
124267
  * One-shot connectivity probe against an MQTT endpoint built from a
124057
124268
  * flat config (`{url, username?, password?}`). Shared by the
124058
124269
  * id-keyed `testConnection` (existing broker) and the pre-creation
124059
- * `testSettings` (throwaway settings the operator hasn't saved yet).
124270
+ * the instance test (`testInstance`) of a broker not saved yet (the typed values).
124060
124271
  * Never throws — every failure resolves to `{ ok: false, error }`.
124061
124272
  */
124062
124273
  async probeMqtt(cfg) {
@@ -124101,30 +124312,46 @@ var MqttBrokerAddon = class extends BaseAddon {
124101
124312
  });
124102
124313
  });
124103
124314
  }
124315
+ /** The `brokers` records, for the two legacy cap methods that create or switch off one. */
124316
+ brokerRecordHandle() {
124317
+ const handle = this.ctx.records?.kind(mqttBrokersEntryKind);
124318
+ if (handle === void 0) throw new Error("mqtt-broker: the broker records are not open");
124319
+ return handle;
124320
+ }
124321
+ /** Create an ONBOARD broker record; the reconcile then spawns its `aedes` server. */
124104
124322
  async startEmbeddedBroker(input) {
124105
- if (this.embeddedRuntime) throw new Error("embedded broker already running — call stopEmbeddedBroker first");
124106
- const id = this.config.embeddedId || randomUUID();
124107
- const entry = {
124323
+ const id = randomUUID();
124324
+ const built = buildOnboardEntry({
124108
124325
  id,
124109
- name: "Embedded broker",
124326
+ name: LEGACY_EMBEDDED_NAME,
124110
124327
  port: input.port,
124111
- allowAnonymous: input.allowAnonymous,
124112
- ...input.username !== void 0 ? { username: input.username } : {},
124113
- ...input.password !== void 0 ? { password: input.password } : {}
124114
- };
124115
- await this.spawnEmbedded(entry);
124116
- await this.updateGlobalSettings({
124117
- embeddedEnabled: true,
124118
- embeddedId: id,
124119
- embeddedPort: input.port,
124120
- embeddedAllowAnonymous: input.allowAnonymous,
124121
- embeddedUsername: input.username ?? "",
124122
- embeddedPassword: input.password ?? ""
124328
+ settings: {
124329
+ allowAnonymous: input.allowAnonymous,
124330
+ ...input.username !== void 0 ? { username: input.username } : {},
124331
+ ...input.password !== void 0 ? { password: input.password } : {}
124332
+ }
124123
124333
  });
124334
+ const handle = this.brokerRecordHandle();
124335
+ await handle.put({
124336
+ id,
124337
+ data: mqttBrokersEntryKind.schema.parse({
124338
+ name: built.name,
124339
+ url: built.url,
124340
+ ...built.username !== void 0 ? { username: built.username } : {},
124341
+ ...built.password !== void 0 ? { password: built.password } : {},
124342
+ onboard: built.onboard
124343
+ })
124344
+ }, { ifRev: 0 });
124345
+ await this.refreshExternalBrokers();
124346
+ if (!this.onboardRuntimes.has(id)) {
124347
+ await handle.delete(id);
124348
+ await this.refreshExternalBrokers();
124349
+ throw new Error(`embedded broker could not listen on port ${input.port}`);
124350
+ }
124124
124351
  this.ctx.logger.info("embedded mqtt broker started", {
124125
124352
  meta: {
124126
124353
  id,
124127
- port: entry.port
124354
+ port: input.port
124128
124355
  },
124129
124356
  tags: {
124130
124357
  topic: "mqtt-broker",
@@ -124146,20 +124373,22 @@ var MqttBrokerAddon = class extends BaseAddon {
124146
124373
  });
124147
124374
  return {
124148
124375
  id,
124149
- url: `mqtt://127.0.0.1:${entry.port}`
124376
+ url: built.url
124150
124377
  };
124151
124378
  }
124379
+ /** Switch an onboard broker off (its record stays, so it can be switched on again). */
124152
124380
  async stopEmbeddedBroker(id) {
124153
- if (!this.embeddedRuntime || this.embeddedRuntime.entry.id !== id) return;
124154
- await this.shutdownEmbedded();
124155
- await this.updateGlobalSettings({ embeddedEnabled: false });
124381
+ const handle = this.brokerRecordHandle();
124382
+ const row = await handle.get(id);
124383
+ if (row === null || row.data.onboard === void 0) return;
124384
+ await handle.patch(id, { enabled: false });
124385
+ await this.refreshExternalBrokers();
124156
124386
  this.ctx.logger.info("embedded mqtt broker stopped", { meta: { id } });
124157
124387
  }
124158
124388
  /**
124159
124389
  * Build + listen an `aedes` server for `entry` and RETURN the runtime
124160
- * without assigning it to any field. Shared by the legacy single
124161
- * embedded broker ({@link spawnEmbedded}) and the per-entry onboard
124162
- * brokers ({@link spawnOnboard}).
124390
+ * without assigning it to any field (the onboard brokers keep theirs in
124391
+ * {@link spawnOnboard}).
124163
124392
  */
124164
124393
  async spawnAedes(entry) {
124165
124394
  const aedes = new import_aedes.default();
@@ -124198,15 +124427,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124198
124427
  aedes.close(() => resolve());
124199
124428
  });
124200
124429
  }
124201
- async spawnEmbedded(entry) {
124202
- this.embeddedRuntime = await this.spawnAedes(entry);
124203
- }
124204
- async shutdownEmbedded() {
124205
- if (!this.embeddedRuntime) return;
124206
- const runtime = this.embeddedRuntime;
124207
- this.embeddedRuntime = null;
124208
- await this.closeRuntime(runtime);
124209
- }
124210
124430
  /** Spawn a dedicated onboard `aedes` server for an onboard entry. */
124211
124431
  async spawnOnboard(entry) {
124212
124432
  const runtime = await this.spawnAedes(entry);
@@ -124343,8 +124563,9 @@ var MqttBrokerAddon = class extends BaseAddon {
124343
124563
  * (or whose username was removed entirely) get force-closed.
124344
124564
  */
124345
124565
  reconcileEmbeddedClients() {
124346
- const runtime = this.embeddedRuntime;
124347
- if (!runtime) return;
124566
+ for (const runtime of this.onboardRuntimes.values()) this.reconcileRuntimeClients(runtime);
124567
+ }
124568
+ reconcileRuntimeClients(runtime) {
124348
124569
  const prevSnapshot = runtime.credentialsSnapshot;
124349
124570
  const nextSnapshot = this.buildCredentialsSnapshot();
124350
124571
  const affectedUsers = [];
@@ -124383,26 +124604,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124383
124604
  for (const c of this.activeEmbeddedClients) m.set(c.username, c.passwordHash);
124384
124605
  return m;
124385
124606
  }
124386
- /**
124387
- * Reassemble the internal {@link EmbeddedBrokerEntry} from the flat
124388
- * `embedded*` config keys the settings form writes. Returns `null`
124389
- * when the embedded broker is disabled — that's the signal
124390
- * `onConfigChanged` / `onInitialize` use to decide whether to spawn
124391
- * or shut down the `aedes` instance.
124392
- */
124393
- deriveEmbeddedEntry() {
124394
- if (!this.config.embeddedEnabled) return null;
124395
- const username = this.config.embeddedUsername;
124396
- const password = this.config.embeddedPassword;
124397
- return {
124398
- id: this.config.embeddedId || "embedded",
124399
- name: "Embedded broker",
124400
- port: this.config.embeddedPort,
124401
- allowAnonymous: this.config.embeddedAllowAnonymous,
124402
- ...username.length > 0 ? { username } : {},
124403
- ...password.length > 0 ? { password } : {}
124404
- };
124405
- }
124406
124607
  collectExternalProbeInputs() {
124407
124608
  const out = [];
124408
124609
  for (const b of this.activeExternalBrokers) {
@@ -124439,19 +124640,13 @@ var MqttBrokerAddon = class extends BaseAddon {
124439
124640
  * here (no migration code ships): the operator's one-off `mqtt` step does.
124440
124641
  */
124441
124642
  reportRetiredCiphertext() {
124442
- const paths = retiredCiphertextPaths({
124443
- brokers: this.activeExternalBrokers,
124444
- embeddedPassword: this.config.embeddedPassword
124445
- });
124643
+ const paths = retiredCiphertextPaths({ brokers: this.activeExternalBrokers });
124446
124644
  if (paths.length === 0) return;
124447
124645
  const unreadable = [];
124448
124646
  const values = [...this.activeExternalBrokers.map((b) => ({
124449
124647
  id: `brokers[${b.id}].password`,
124450
124648
  v: b.password
124451
- })), {
124452
- id: "embeddedPassword",
124453
- v: this.config.embeddedPassword
124454
- }];
124649
+ }))];
124455
124650
  for (const { id, v } of values) try {
124456
124651
  this.storedPassword(v);
124457
124652
  } catch (err) {
@@ -124475,13 +124670,79 @@ var MqttBrokerAddon = class extends BaseAddon {
124475
124670
  }
124476
124671
  });
124477
124672
  }
124478
- brokerForm() {
124673
+ /**
124674
+ * The broker form. The create form (`id` null) offers "run an onboard broker" (CamStack hosts an
124675
+ * `aedes` server on this hub: a port, anonymous access) or an external URL; an existing onboard
124676
+ * broker edits only its credentials, since its address is the one it was given.
124677
+ */
124678
+ brokerForm(id, onboard) {
124679
+ const credentials = [{
124680
+ type: "text",
124681
+ key: "username",
124682
+ label: "Username",
124683
+ labelKey: "mqtt-broker:fullSettings.brokers.username.label"
124684
+ }, {
124685
+ type: "password",
124686
+ key: "password",
124687
+ label: "Password",
124688
+ labelKey: "mqtt-broker:fullSettings.brokers.password.label",
124689
+ showToggle: true
124690
+ }];
124691
+ if (onboard) return this.schema({ sections: [{
124692
+ id: "broker",
124693
+ title: "Onboard broker",
124694
+ description: "A CamStack-hosted broker on this hub. Its port was chosen when it was created.",
124695
+ columns: 1,
124696
+ fields: credentials
124697
+ }] });
124698
+ const creating = id === null;
124699
+ const external = {
124700
+ field: "onboard",
124701
+ equals: false
124702
+ };
124479
124703
  return this.schema({ sections: [{
124480
124704
  id: "broker",
124481
- title: "External broker",
124482
- description: "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
124705
+ title: creating ? "MQTT broker" : "External broker",
124706
+ description: creating ? "Run a broker on this hub, or connect to one running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain." : "A broker running outside CamStack (mosquitto, EMQX, HiveMQ, cloud bridges, …). The password is kept in the keychain.",
124483
124707
  columns: 1,
124484
124708
  fields: [
124709
+ ...creating ? [
124710
+ {
124711
+ type: "boolean",
124712
+ key: "onboard",
124713
+ label: "Run an onboard broker",
124714
+ labelKey: "mqtt-broker:fullSettings.brokers.onboard.label",
124715
+ description: "Spin up a CamStack-hosted aedes broker on this hub instead of connecting to an external one. Each onboard broker gets its own dedicated server + port.",
124716
+ descriptionKey: "mqtt-broker:fullSettings.brokers.onboard.description",
124717
+ default: false
124718
+ },
124719
+ {
124720
+ type: "number",
124721
+ key: "port",
124722
+ label: "Port",
124723
+ labelKey: "mqtt-broker:fullSettings.brokers.port.label",
124724
+ placeholder: "(auto)",
124725
+ description: "TCP port for the onboard broker. Leave blank to auto-allocate a free port.",
124726
+ descriptionKey: "mqtt-broker:fullSettings.brokers.port.description",
124727
+ showWhen: {
124728
+ field: "onboard",
124729
+ equals: true
124730
+ }
124731
+ },
124732
+ {
124733
+ type: "boolean",
124734
+ key: "allowAnonymous",
124735
+ label: "Allow anonymous",
124736
+ labelKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.label",
124737
+ description: "Permit clients to connect without a username/password.",
124738
+ descriptionKey: "mqtt-broker:fullSettings.brokers.allowAnonymous.description",
124739
+ default: false,
124740
+ showWhen: {
124741
+ field: "onboard",
124742
+ equals: true
124743
+ }
124744
+ }
124745
+ ] : [],
124485
124746
  {
124486
124747
  type: "text",
124487
124748
  key: "url",
@@ -124489,26 +124750,15 @@ var MqttBrokerAddon = class extends BaseAddon {
124489
124750
  labelKey: "mqtt-broker:fullSettings.brokers.url.label",
124490
124751
  description: "mqtt://, mqtts://, ws://, wss://",
124491
124752
  descriptionKey: "mqtt-broker:fullSettings.brokers.url.description",
124492
- required: true
124493
- },
124494
- {
124495
- type: "text",
124496
- key: "username",
124497
- label: "Username",
124498
- labelKey: "mqtt-broker:fullSettings.brokers.username.label"
124499
- },
124500
- {
124501
- type: "password",
124502
- key: "password",
124503
- label: "Password",
124504
- labelKey: "mqtt-broker:fullSettings.brokers.password.label",
124505
- showToggle: true
124753
+ ...creating ? { showWhen: external } : { required: true }
124506
124754
  },
124755
+ ...credentials,
124507
124756
  {
124508
124757
  type: "text",
124509
124758
  key: "clientIdPrefix",
124510
124759
  label: "Client id prefix",
124511
- labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label"
124760
+ labelKey: "mqtt-broker:fullSettings.brokers.clientIdPrefix.label",
124761
+ ...creating ? { showWhen: external } : {}
124512
124762
  }
124513
124763
  ]
124514
124764
  }] });
@@ -124519,6 +124769,41 @@ var MqttBrokerAddon = class extends BaseAddon {
124519
124769
  if (handle === void 0) throw new Error("records are not available");
124520
124770
  return {
124521
124771
  readRawBrokers: async () => (await this.ctx.settings?.readAddonStore())?.["brokers"],
124772
+ readLegacyEmbedded: async () => {
124773
+ const raw = await this.ctx.settings?.readAddonStore() ?? {};
124774
+ const secrets = this.ctx.secrets;
124775
+ const { value } = secrets === void 0 ? { value: raw } : await resolveSecretMarkers(raw, secrets, {
124776
+ unresolved: "keep-marker",
124777
+ ownedBy: secrets.owner
124778
+ });
124779
+ const text = (key) => {
124780
+ const v = value[key];
124781
+ return typeof v === "string" && v.length > 0 ? v : void 0;
124782
+ };
124783
+ const port = value["embeddedPort"];
124784
+ const username = text("embeddedUsername");
124785
+ const password = this.storedPassword(text("embeddedPassword"));
124786
+ const enabled = value["embeddedEnabled"] === true;
124787
+ if (!(enabled || text("embeddedId") !== void 0 || username !== void 0 || password !== void 0)) return null;
124788
+ return {
124789
+ enabled,
124790
+ id: text("embeddedId") ?? "embedded",
124791
+ port: typeof port === "number" && Number.isInteger(port) ? port : 1883,
124792
+ allowAnonymous: value["embeddedAllowAnonymous"] === true,
124793
+ ...username !== void 0 ? { username } : {},
124794
+ ...password !== void 0 ? { password } : {}
124795
+ };
124796
+ },
124797
+ readRawEmbeddedKeys: async () => {
124798
+ const raw = await this.ctx.settings?.readAddonStore() ?? {};
124799
+ return Object.fromEntries(LEGACY_EMBEDDED_KEYS.filter((key) => key in raw).map((key) => [key, raw[key]]));
124800
+ },
124801
+ putOnboardRecord: async (id, body) => {
124802
+ await handle.put({
124803
+ id,
124804
+ data: mqttBrokersEntryKind.schema.parse(body)
124805
+ }, { ifRev: 0 });
124806
+ },
124522
124807
  readRecords: async () => {
124523
124808
  const page = await handle.query({ limit: 1e3 });
124524
124809
  return new Map(page.records.map((r) => [r.id, { hasPassword: (r.data.password ?? "").length > 0 }]));
@@ -124531,184 +124816,109 @@ var MqttBrokerAddon = class extends BaseAddon {
124531
124816
  }
124532
124817
  };
124533
124818
  }
124819
+ scopedSettingsCapabilities() {
124820
+ return ["mqtt-broker", "broker"];
124821
+ }
124534
124822
  /**
124535
- * The embedded broker, its credentials and the ACL. The external brokers are records, listed
124823
+ * The credentials and the ACL of the onboard brokers. The brokers are records, listed
124536
124824
  * on the Brokers page and edited as integration instances (`brokerForm`).
124537
124825
  */
124538
- globalSettingsSchema() {
124539
- return this.schema({ sections: [
124540
- {
124541
- id: "mqtt-broker-embedded",
124542
- title: "Embedded broker",
124543
- titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.title",
124544
- description: "Optional CamStack-hosted aedes broker. Enable it to run a broker on this hub without standing up an external one — its connection details are exposed through the `mqtt-broker` cap like any external entry.",
124545
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.description",
124546
- columns: 2,
124547
- fields: [
124548
- this.field({
124549
- type: "boolean",
124550
- key: "embeddedEnabled",
124551
- label: "Run embedded broker",
124552
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedEnabled.label",
124553
- description: "Starts an aedes broker on this hub. Disabling it shuts the broker down and disconnects clients.",
124554
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedEnabled.description",
124555
- style: "switch",
124556
- default: false
124557
- }),
124558
- this.field({
124559
- type: "number",
124560
- key: "embeddedPort",
124561
- label: "Port",
124562
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPort.label",
124563
- description: "TCP port the embedded broker listens on.",
124564
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPort.description",
124565
- min: 1,
124566
- max: 65535,
124567
- default: DEFAULT_EMBEDDED_PORT,
124568
- showWhen: {
124569
- field: "embeddedEnabled",
124570
- equals: true
124571
- }
124572
- }),
124573
- this.field({
124574
- type: "boolean",
124575
- key: "embeddedAllowAnonymous",
124576
- label: "Allow anonymous",
124577
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedAllowAnonymous.label",
124578
- description: "Permit clients to connect without a username/password.",
124579
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedAllowAnonymous.description",
124580
- style: "switch",
124581
- default: false,
124582
- showWhen: {
124583
- field: "embeddedEnabled",
124584
- equals: true
124585
- }
124586
- }),
124587
- this.field({
124588
- type: "text",
124589
- key: "embeddedUsername",
124590
- label: "Shared username",
124591
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedUsername.label",
124592
- description: "Legacy single-user fallback — used only when no per-client credentials are configured below.",
124593
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedUsername.description",
124594
- default: "",
124595
- showWhen: {
124596
- field: "embeddedEnabled",
124597
- equals: true
124598
- }
124599
- }),
124600
- this.field({
124601
- type: "password",
124602
- key: "embeddedPassword",
124603
- label: "Shared password",
124604
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.label",
124605
- description: "Kept in the keychain. Paired with the shared username above.",
124606
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded.embeddedPassword.description",
124607
- showToggle: true,
124608
- default: "",
124609
- showWhen: {
124610
- field: "embeddedEnabled",
124611
- equals: true
124612
- }
124613
- })
124614
- ]
124615
- },
124616
- {
124617
- id: "mqtt-broker-embedded-clients",
124618
- title: "Embedded broker — credentials",
124619
- titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
124620
- description: "Per-client username + bcrypt password hash for the embedded aedes broker. Generate the hash with `npx bcrypt-cli` (or any bcrypt tool, cost factor 10) and paste it here.",
124621
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
124622
- columns: 1,
124623
- fields: [{
124624
- type: "editable-array",
124625
- key: "embeddedClients",
124626
- label: "Clients",
124627
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
124628
- emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
124629
- addLabel: "Add client",
124630
- rowTitleTemplate: "{username}",
124631
- defaultItem: {
124632
- username: "",
124633
- passwordHash: ""
124634
- },
124635
- itemFields: [{
124826
+ async clusterSettingsSchema(cap) {
124827
+ if (cap !== "mqtt-broker" && cap !== "broker") return null;
124828
+ return this.schema({ sections: [{
124829
+ id: "mqtt-broker-embedded-clients",
124830
+ title: "Embedded broker — credentials",
124831
+ titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.title",
124832
+ description: "Per-client username + bcrypt password hash for the embedded aedes broker. Generate the hash with `npx bcrypt-cli` (or any bcrypt tool, cost factor 10) and paste it here.",
124833
+ descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.description",
124834
+ columns: 1,
124835
+ fields: [{
124836
+ type: "editable-array",
124837
+ key: "embeddedClients",
124838
+ label: "Clients",
124839
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.label",
124840
+ emptyMessage: "No clients configured — embedded broker is in shared-creds / anonymous mode.",
124841
+ addLabel: "Add client",
124842
+ rowTitleTemplate: "{username}",
124843
+ defaultItem: {
124844
+ username: "",
124845
+ passwordHash: ""
124846
+ },
124847
+ itemFields: [{
124848
+ type: "text",
124849
+ key: "username",
124850
+ label: "Username",
124851
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.username.label"
124852
+ }, {
124853
+ type: "text",
124854
+ key: "passwordHash",
124855
+ label: "bcrypt hash",
124856
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.label",
124857
+ description: "bcrypt hash of the password (cost factor 10).",
124858
+ descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.description"
124859
+ }]
124860
+ }]
124861
+ }, {
124862
+ id: "mqtt-broker-embedded-acl",
124863
+ title: "Embedded broker — topic ACL",
124864
+ titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
124865
+ description: "Per-user pub/sub allow-list. With zero rules the broker is fully open to authenticated clients. With at least one rule, every user needs at least one rule that matches the topic.",
124866
+ descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
124867
+ columns: 1,
124868
+ fields: [{
124869
+ type: "editable-array",
124870
+ key: "embeddedAcl",
124871
+ label: "Rules",
124872
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
124873
+ emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
124874
+ addLabel: "Add rule",
124875
+ rowTitleTemplate: "{username} → {topicPattern} ({access})",
124876
+ defaultItem: {
124877
+ username: "",
124878
+ topicPattern: "#",
124879
+ access: "both"
124880
+ },
124881
+ itemFields: [
124882
+ {
124636
124883
  type: "text",
124637
124884
  key: "username",
124638
124885
  label: "Username",
124639
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.username.label"
124640
- }, {
124886
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.username.label"
124887
+ },
124888
+ {
124641
124889
  type: "text",
124642
- key: "passwordHash",
124643
- label: "bcrypt hash",
124644
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.label",
124645
- description: "bcrypt hash of the password (cost factor 10).",
124646
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-clients.embeddedClients.passwordHash.description"
124647
- }]
124648
- }]
124649
- },
124650
- {
124651
- id: "mqtt-broker-embedded-acl",
124652
- title: "Embedded broker — topic ACL",
124653
- titleKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.title",
124654
- description: "Per-user pub/sub allow-list. With zero rules the broker is fully open to authenticated clients. With at least one rule, every user needs at least one rule that matches the topic.",
124655
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.description",
124656
- columns: 1,
124657
- fields: [{
124658
- type: "editable-array",
124659
- key: "embeddedAcl",
124660
- label: "Rules",
124661
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.label",
124662
- emptyMessage: "No ACL rules — embedded broker is wide open to authenticated users.",
124663
- addLabel: "Add rule",
124664
- rowTitleTemplate: "{username} → {topicPattern} ({access})",
124665
- defaultItem: {
124666
- username: "",
124667
- topicPattern: "#",
124668
- access: "both"
124890
+ key: "topicPattern",
124891
+ label: "Topic pattern",
124892
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.label",
124893
+ description: "MQTT wildcards: `+` = single level, `#` = multi-level.",
124894
+ descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.description"
124669
124895
  },
124670
- itemFields: [
124671
- {
124672
- type: "text",
124673
- key: "username",
124674
- label: "Username",
124675
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.username.label"
124676
- },
124677
- {
124678
- type: "text",
124679
- key: "topicPattern",
124680
- label: "Topic pattern",
124681
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.label",
124682
- description: "MQTT wildcards: `+` = single level, `#` = multi-level.",
124683
- descriptionKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.topicPattern.description"
124684
- },
124685
- {
124686
- type: "select",
124687
- key: "access",
124688
- label: "Access",
124689
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.label",
124690
- options: [
124691
- {
124692
- value: "sub",
124693
- label: "Subscribe only",
124694
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_sub.label"
124695
- },
124696
- {
124697
- value: "pub",
124698
- label: "Publish only",
124699
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
124700
- },
124701
- {
124702
- value: "both",
124703
- label: "Subscribe + publish",
124704
- labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
124705
- }
124706
- ]
124707
- }
124708
- ]
124709
- }]
124710
- }
124711
- ] });
124896
+ {
124897
+ type: "select",
124898
+ key: "access",
124899
+ label: "Access",
124900
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.label",
124901
+ options: [
124902
+ {
124903
+ value: "sub",
124904
+ label: "Subscribe only",
124905
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_sub.label"
124906
+ },
124907
+ {
124908
+ value: "pub",
124909
+ label: "Publish only",
124910
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_pub.label"
124911
+ },
124912
+ {
124913
+ value: "both",
124914
+ label: "Subscribe + publish",
124915
+ labelKey: "mqtt-broker:fullSettings.s_mqtt-broker-embedded-acl.embeddedAcl.access.o_both.label"
124916
+ }
124917
+ ]
124918
+ }
124919
+ ]
124920
+ }]
124921
+ }] });
124712
124922
  }
124713
124923
  };
124714
124924
  /**
@@ -124738,13 +124948,6 @@ function bcryptCompareSafe(plain, hash) {
124738
124948
  }
124739
124949
  }
124740
124950
  /**
124741
- * Decide whether a config update to the embedded broker requires a full
124742
- * restart (port change) or can be hot-reloaded (credential / ACL change).
124743
- */
124744
- function requiresEmbeddedRespawn(desired, running) {
124745
- return desired.id !== running.id || desired.port !== running.port || desired.allowAnonymous !== running.allowAnonymous || desired.username !== running.username || desired.password !== running.password;
124746
- }
124747
- /**
124748
124951
  * Read the `username` field off an aedes Client without leaking a cast.
124749
124952
  * aedes attaches the username at runtime but its `.d.ts` doesn't carry
124750
124953
  * it — we narrow with a small structural check.
@@ -124797,15 +125000,5 @@ async function findFreePort() {
124797
125000
  });
124798
125001
  });
124799
125002
  }
124800
- function mapMqttStatus(s) {
124801
- switch (s) {
124802
- case "connected": return "connected";
124803
- case "disconnected": return "disconnected";
124804
- case "auth-failed": return "auth-failed";
124805
- case "unreachable": return "unreachable";
124806
- case "tls-error": return "error";
124807
- default: return "error";
124808
- }
124809
- }
124810
125003
  //#endregion
124811
125004
  export { MQTT_EMBEDDED_BCRYPT_ROUNDS, MqttBrokerAddon, MqttBrokerAddon as default };