@camstack/addon-smtp-nodemailer 1.2.147 → 1.2.149

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.
@@ -6018,7 +6018,7 @@ var ZodIssueCode = {
6018
6018
  var ZodFirstPartyTypeKind;
6019
6019
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6020
6020
  //#endregion
6021
- //#region ../types/dist/sleep-BaWhXZFm.mjs
6021
+ //#region ../types/dist/sleep-BX_MqzUe.mjs
6022
6022
  /**
6023
6023
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6024
6024
  * window to float samples (D455).
@@ -6618,7 +6618,7 @@ var NOT_RUN = {
6618
6618
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6619
6619
  * because the hub (and this addon's boot) must not hang on a migration.
6620
6620
  */
6621
- async function runAddonMigrations(host, migrations, logger) {
6621
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6622
6622
  const first = migrations[0];
6623
6623
  if (first === void 0) return NOT_RUN;
6624
6624
  const owner = first.meta.owner;
@@ -6647,7 +6647,9 @@ async function runAddonMigrations(host, migrations, logger) {
6647
6647
  else if (status === "blocked") blocked.push(id);
6648
6648
  else skipped.push(id);
6649
6649
  };
6650
+ const selected = options.only === void 0 ? null : new Set(options.only);
6650
6651
  for (const step of declaration.steps) {
6652
+ if (selected !== null && !selected.has(step.id)) continue;
6651
6653
  const bound = byId.get(step.id);
6652
6654
  if (bound === void 0 || step.action === "skip") {
6653
6655
  skipped.push(step.id);
@@ -6825,6 +6827,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6825
6827
  }
6826
6828
  return out;
6827
6829
  }
6830
+ /** Every scope, in the order the guard and the docs list them. */
6831
+ var SCOPED_SETTINGS_SCOPES = [
6832
+ "cluster",
6833
+ "device-defaults",
6834
+ "node"
6835
+ ];
6828
6836
  /**
6829
6837
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6830
6838
  *
@@ -7639,6 +7647,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
7639
7647
  /** Coalesces a burst of `keychain.changed` events into one re-resolve. */
7640
7648
  var SECRET_CHANGE_DEBOUNCE_MS = 250;
7641
7649
  /**
7650
+ * Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
7651
+ * place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
7652
+ * through `api` (guard `check-migration-ports-surface`).
7653
+ */
7654
+ function migrationSet(catalogue, bindPorts) {
7655
+ return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
7656
+ }
7657
+ /** A mixed array is a mistake: the other kind would be dropped without a word. */
7658
+ function refuseMixed(form, kept, dropped) {
7659
+ 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`);
7660
+ }
7661
+ function catalogueOf(list) {
7662
+ const kept = list.filter((entry) => !("execute" in entry));
7663
+ refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
7664
+ return kept;
7665
+ }
7666
+ function boundOf(list) {
7667
+ const kept = list.filter((entry) => "execute" in entry);
7668
+ refuseMixed("bound migrations", kept.length, list.length - kept.length);
7669
+ return kept;
7670
+ }
7671
+ /**
7642
7672
  * Base class for CamStack addons. Eliminates settings boilerplate:
7643
7673
  *
7644
7674
  * - Typed `config` property with automatic resolution from store + defaults
@@ -7716,6 +7746,13 @@ var BaseAddon = class {
7716
7746
  */
7717
7747
  secretStatePaths = [];
7718
7748
  _addonSecretTree = null;
7749
+ /**
7750
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7751
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7752
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7753
+ */
7754
+ _scopedByCap = /* @__PURE__ */ new Map();
7755
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7719
7756
  _deviceSecretTree = null;
7720
7757
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7721
7758
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7785,6 +7822,7 @@ var BaseAddon = class {
7785
7822
  this._ctx = context;
7786
7823
  this._secretSettings = null;
7787
7824
  }
7825
+ await this.loadScopedSchemas();
7788
7826
  await this.resolveConfig();
7789
7827
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7790
7828
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7794,27 +7832,30 @@ var BaseAddon = class {
7794
7832
  const normalized = normalizeAddonInitResult(result);
7795
7833
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7796
7834
  this._registeredCapNames = providers.map((p) => p.capability.name);
7835
+ await this.loadScopedSchemas();
7797
7836
  return normalized;
7798
7837
  }
7838
+ async runMigrations(first, second, third) {
7839
+ if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
7840
+ return this.runBound(boundOf(first), second);
7841
+ }
7799
7842
  /**
7800
- * Run this addon's migration catalogue (D987 M3) through the hub's `migrations`
7801
- * engine. Call it at the point of the boot where the data must already have the new
7802
- * shape — `onInitialize`, or `onRestoreDevices` for a migration that needs the
7803
- * restored devices — with the catalogue bound to the addon's ports
7804
- * (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
7805
- *
7806
- * An engine that cannot be reached or refuses the catalogue is logged and runs
7807
- * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7808
- * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7809
- * serve data whose shape it cannot trust; `tolerate` migrations never do.
7843
+ * Several catalogues of ONE owner in one declaration (the engine replaces an owner's
7844
+ * catalogue on every declare, so two calls would each judge the other's ids "newer").
7845
+ * Build each entry with `migrationSet(catalogue, (api) => ports)`.
7810
7846
  */
7811
- async runMigrations(migrations) {
7847
+ async runMigrationSets(sets, options) {
7848
+ const api = this.ctx.migrationApi;
7849
+ if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
7850
+ return this.runBound(sets.flatMap((set) => set.bind(api)), options);
7851
+ }
7852
+ async runBound(migrations, options) {
7812
7853
  const api = this.ctx.api.migrations;
7813
7854
  const report = await runAddonMigrations({
7814
7855
  declareOwner: (input) => api.declareOwner.mutate(input),
7815
7856
  startMigration: (input) => api.startMigration.mutate(input),
7816
7857
  finishMigration: (input) => api.finishMigration.mutate(input)
7817
- }, migrations, this.ctx.logger);
7858
+ }, migrations, this.ctx.logger, options);
7818
7859
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7819
7860
  return report;
7820
7861
  }
@@ -7878,6 +7919,16 @@ var BaseAddon = class {
7878
7919
  schema(schema) {
7879
7920
  return schema;
7880
7921
  }
7922
+ /**
7923
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7924
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7925
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7926
+ * must already know which keys are per node. Names the providers register at init are added
7927
+ * once known.
7928
+ */
7929
+ scopedSettingsCapabilities() {
7930
+ return [];
7931
+ }
7881
7932
  /** Override to provide global-level settings UI schema. */
7882
7933
  globalSettingsSchema(_cap) {
7883
7934
  return null;
@@ -7921,7 +7972,8 @@ var BaseAddon = class {
7921
7972
  return [];
7922
7973
  }
7923
7974
  async getGlobalSettings(overlay, cap, nodeId) {
7924
- const schema = this.globalSettingsSchema(cap);
7975
+ await this.loadScopedSchemas(cap);
7976
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7925
7977
  if (!schema) return { sections: [] };
7926
7978
  const projected = await this.readRawProjectedStore(nodeId, cap);
7927
7979
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7956,10 +8008,9 @@ var BaseAddon = class {
7956
8008
  *
7957
8009
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7958
8010
  * needs its connection fields, the default integration the addon's own sections. It is the
7959
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7960
- * unchanged (`integrations.setSettings` for an account integration).
8011
+ * SHAPE of the form only — the values live on the instance record.
7961
8012
  */
7962
- async integrationSettingsSchema(_instance) {
8013
+ async integrationFormSchema(_instance) {
7963
8014
  return null;
7964
8015
  }
7965
8016
  /**
@@ -8005,7 +8056,7 @@ var BaseAddon = class {
8005
8056
  * per-instance schema when it declares one, else its global schema (what they always showed).
8006
8057
  */
8007
8058
  async getIntegrationFormSchema(instance, nodeId) {
8008
- const scoped = await this.integrationSettingsSchema(instance);
8059
+ const scoped = await this.integrationFormSchema(instance);
8009
8060
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8010
8061
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8011
8062
  }
@@ -8053,6 +8104,7 @@ var BaseAddon = class {
8053
8104
  * resolved blob back is a no-op for every unchanged secret.
8054
8105
  */
8055
8106
  async resolveGlobalStore(nodeId, cap) {
8107
+ await this.loadScopedSchemas(cap);
8056
8108
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8057
8109
  return this.projectForNode(raw, nodeId, cap);
8058
8110
  }
@@ -8063,6 +8115,7 @@ var BaseAddon = class {
8063
8115
  }
8064
8116
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8065
8117
  async readRawProjectedStore(nodeId, cap) {
8118
+ await this.loadScopedSchemas(cap);
8066
8119
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8067
8120
  return this.projectForNode(raw, nodeId, cap);
8068
8121
  }
@@ -8165,6 +8218,7 @@ var BaseAddon = class {
8165
8218
  await this.applyGlobalPatch(patch, nodeId);
8166
8219
  }
8167
8220
  async applyGlobalPatch(barePatch, nodeId) {
8221
+ await this.loadScopedSchemas();
8168
8222
  const keys = this.perNodeKeys();
8169
8223
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8170
8224
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8178,7 +8232,8 @@ var BaseAddon = class {
8178
8232
  await this.resolveConfig();
8179
8233
  await this.onConfigChanged();
8180
8234
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8181
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8235
+ const legacy = this.globalSettingsSchema();
8236
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8182
8237
  }
8183
8238
  /**
8184
8239
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8186,14 +8241,67 @@ var BaseAddon = class {
8186
8241
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8187
8242
  * settings API behaves exactly like the legacy node-agnostic one.
8188
8243
  */
8189
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8244
+ /**
8245
+ * Load the scoped schemas of every capability this addon declares (the override and the
8246
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8247
+ * derived from the previous set. Idempotent.
8248
+ */
8249
+ async loadScopedSchemas(extraCap) {
8250
+ const caps = new Set([
8251
+ ...this.scopedSettingsCapabilities(),
8252
+ ...this._registeredCapNames,
8253
+ ...extraCap === void 0 ? [] : [extraCap]
8254
+ ]);
8255
+ let added = false;
8256
+ for (const cap of caps) {
8257
+ if (this._scopedByCap.has(cap)) continue;
8258
+ const loaded = [];
8259
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8260
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8261
+ if (schema !== null) loaded.push({
8262
+ scope,
8263
+ schema
8264
+ });
8265
+ }
8266
+ this._scopedByCap.set(cap, loaded);
8267
+ added = true;
8268
+ }
8269
+ if (!added) return;
8270
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8271
+ this._addonSecretTree = null;
8272
+ }
8273
+ /**
8274
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8275
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8276
+ */
8277
+ scopedFormSchema(cap) {
8278
+ const byId = /* @__PURE__ */ new Map();
8279
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8280
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8281
+ }
8282
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8283
+ loadedScopedSchemas(cap) {
8284
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8285
+ return [...this._scopedByCap.values()].flat();
8286
+ }
8287
+ /**
8288
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8289
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8290
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8291
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8292
+ * Call {@link loadScopedSchemas} first (the async seams do).
8293
+ */
8190
8294
  perNodeKeys(cap) {
8191
8295
  const cacheKey = cap ?? "";
8192
- const cached = this._perNodeKeysCache.get(cacheKey);
8296
+ const cached = this._nodeKeysByCap.get(cacheKey);
8193
8297
  if (cached) return cached;
8194
- const schema = this.globalSettingsSchema(cap);
8195
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8196
- this._perNodeKeysCache.set(cacheKey, keys);
8298
+ const legacy = this.globalSettingsSchema(cap);
8299
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8300
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8301
+ if (scope !== "node") continue;
8302
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8303
+ }
8304
+ this._nodeKeysByCap.set(cacheKey, keys);
8197
8305
  return keys;
8198
8306
  }
8199
8307
  /**
@@ -8203,10 +8311,9 @@ var BaseAddon = class {
8203
8311
  * time to return its response before the addon is torn down and
8204
8312
  * re-initialised by `AddonRegistryService.restartAddon`.
8205
8313
  */
8206
- maybeAutoRestart(patchKeys, schema) {
8207
- if (!schema) return;
8314
+ maybeAutoRestart(patchKeys, schemas) {
8208
8315
  const restartKeys = /* @__PURE__ */ new Set();
8209
- for (const section of schema.sections) for (const field of section.fields) {
8316
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8210
8317
  if (field.type === "separator" || field.type === "info") continue;
8211
8318
  if (field.requiresRestart) restartKeys.add(field.key);
8212
8319
  }
@@ -8402,12 +8509,14 @@ var BaseAddon = class {
8402
8509
  * here, in the addon's own process, so the hub keeps no copy of it.
8403
8510
  */
8404
8511
  async getDeclaredSettingsKeys() {
8512
+ await this.loadScopedSchemas();
8405
8513
  return { keys: [...this.declaredKeySet()].toSorted() };
8406
8514
  }
8407
8515
  declaredKeySet() {
8408
8516
  const keys = new Set(Object.keys(this.defaults));
8409
8517
  const schema = this.globalSettingsSchema();
8410
8518
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8519
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8411
8520
  for (const key of this._declaredStateKeys) keys.add(key);
8412
8521
  for (const path of this.secretStatePaths) {
8413
8522
  const root = path.split(/[.[]/, 1)[0];
@@ -8503,7 +8612,11 @@ var BaseAddon = class {
8503
8612
  }
8504
8613
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8505
8614
  addonSecretTree() {
8506
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8615
+ if (this._addonSecretTree === null) {
8616
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8617
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8618
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8619
+ }
8507
8620
  return this._addonSecretTree;
8508
8621
  }
8509
8622
  deviceSecretTree() {
@@ -8753,6 +8866,23 @@ function dropPerNodeFields(fields) {
8753
8866
  }
8754
8867
  return kept;
8755
8868
  }
8869
+ /** The keys of the value-holding fields, recursing into layout containers. */
8870
+ function collectValueFieldKeys(fields) {
8871
+ const collected = [];
8872
+ for (const field of fields) {
8873
+ if (field.type === "group") {
8874
+ collected.push(...collectValueFieldKeys(field.fields));
8875
+ continue;
8876
+ }
8877
+ if (field.type === "sub-tabs") {
8878
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8879
+ continue;
8880
+ }
8881
+ if (field.type === "separator" || field.type === "info") continue;
8882
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8883
+ }
8884
+ return collected;
8885
+ }
8756
8886
  function collectFieldKeys(fields) {
8757
8887
  const collected = [];
8758
8888
  for (const field of fields) {
@@ -9286,6 +9416,7 @@ function defineEntryKind(name, spec) {
9286
9416
  class: "config",
9287
9417
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9288
9418
  retention: { none: "an entry goes only when the operator deletes it" },
9419
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9289
9420
  ...spec.database === void 0 ? {} : { database: spec.database },
9290
9421
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9291
9422
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11965,7 +12096,7 @@ function userError(code, params, englishFallback) {
11965
12096
  return new CamstackError(code, params, englishFallback);
11966
12097
  }
11967
12098
  //#endregion
11968
- //#region ../types/dist/composition-CRNwcFYY.mjs
12099
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11969
12100
  /**
11970
12101
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11971
12102
  *
@@ -13463,7 +13594,7 @@ var RecordIntegrationInstances = class {
13463
13594
  return record === null ? null : this.viewOf(record);
13464
13595
  };
13465
13596
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13466
- createInstance = async ({ id, name, kind }) => {
13597
+ createInstance = async ({ id, name, kind, values }) => {
13467
13598
  const minted = id ?? this.hooks.mintId?.({
13468
13599
  name,
13469
13600
  ...kind !== void 0 ? { kind } : {}
@@ -13473,10 +13604,20 @@ var RecordIntegrationInstances = class {
13473
13604
  id: minted,
13474
13605
  data: this.hooks.initialBody(name, kind)
13475
13606
  }, { ifRev: 0 });
13607
+ const patch = this.patchOf(values ?? {});
13608
+ if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
13476
13609
  await this.hooks.onChanged?.(minted, "created");
13477
13610
  return { id: minted };
13478
13611
  };
13479
13612
  updateInstance = async ({ id, name, values }) => {
13613
+ const patch = this.patchOf(values);
13614
+ if (name !== void 0) patch[this.nameField] = name;
13615
+ await this.handle.patch(id, patch);
13616
+ await this.hooks.onChanged?.(id, "updated");
13617
+ return { ok: true };
13618
+ };
13619
+ /** The record patch a form `values` object means: secrets kept, cleared or replaced. */
13620
+ patchOf(values) {
13480
13621
  const patch = {};
13481
13622
  for (const [key, value] of Object.entries(values)) {
13482
13623
  if (value === void 0) continue;
@@ -13487,11 +13628,8 @@ var RecordIntegrationInstances = class {
13487
13628
  else if (isSecretClear(value)) patch[key] = null;
13488
13629
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13489
13630
  }
13490
- if (name !== void 0) patch[this.nameField] = name;
13491
- await this.handle.patch(id, patch);
13492
- await this.hooks.onChanged?.(id, "updated");
13493
- return { ok: true };
13494
- };
13631
+ return patch;
13632
+ }
13495
13633
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13496
13634
  setInstanceEnabled = async ({ id, enabled }) => {
13497
13635
  if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
@@ -13528,6 +13666,76 @@ var RecordIntegrationInstances = class {
13528
13666
  };
13529
13667
  }
13530
13668
  };
13669
+ /** No part holds the id a call named. */
13670
+ var UnknownInstanceError = class extends Error {
13671
+ instanceId;
13672
+ constructor(instanceId) {
13673
+ super(`Unknown instance '${instanceId}'`);
13674
+ this.instanceId = instanceId;
13675
+ this.name = "UnknownInstanceError";
13676
+ }
13677
+ };
13678
+ /** A create or a form named no kind, or a kind no part serves. */
13679
+ var UnknownInstanceKindError = class extends Error {
13680
+ kindId;
13681
+ constructor(kindId) {
13682
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13683
+ this.kindId = kindId;
13684
+ this.name = "UnknownInstanceKindError";
13685
+ }
13686
+ };
13687
+ function composeInstanceKinds(parts, base) {
13688
+ const byKind = (kindId) => {
13689
+ if (kindId === void 0) {
13690
+ const only = parts.length === 1 ? parts[0] : void 0;
13691
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13692
+ return only;
13693
+ }
13694
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13695
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13696
+ return part;
13697
+ };
13698
+ const holderOf = async (id) => {
13699
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13700
+ throw new UnknownInstanceError(id);
13701
+ };
13702
+ const tagged = (part, instance) => ({
13703
+ ...instance,
13704
+ kind: part.kind.id
13705
+ });
13706
+ return {
13707
+ describe: async () => ({
13708
+ ...base,
13709
+ kinds: parts.map((part) => part.kind)
13710
+ }),
13711
+ instanceFormSchema: async ({ id, kind }) => {
13712
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13713
+ },
13714
+ listInstances: async () => {
13715
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13716
+ },
13717
+ getInstance: async ({ id }) => {
13718
+ for (const part of parts) {
13719
+ const found = await part.provider.getInstance({ id });
13720
+ if (found !== null) return tagged(part, found);
13721
+ }
13722
+ return null;
13723
+ },
13724
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13725
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13726
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13727
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13728
+ testInstance: async (input) => {
13729
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13730
+ if (test === void 0) return {
13731
+ success: false,
13732
+ status: "inconclusive",
13733
+ error: "This kind cannot be tested"
13734
+ };
13735
+ return test(input);
13736
+ }
13737
+ };
13738
+ }
13531
13739
  var EncodeProfileSchema = object({
13532
13740
  video: object({
13533
13741
  codec: _enum([
@@ -14573,7 +14781,13 @@ var MigrationLedgerEntrySchema = object({
14573
14781
  * joined by the engine from the catalogues it knows this boot. Absent when no
14574
14782
  * catalogue lists the id (a newer database).
14575
14783
  */
14576
- meta: MigrationMetaSchema.optional()
14784
+ meta: MigrationMetaSchema.optional(),
14785
+ /**
14786
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14787
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14788
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14789
+ */
14790
+ ownerNotLoaded: boolean().optional()
14577
14791
  });
14578
14792
  /** A catalogued migration that is not applied, with where it stands. */
14579
14793
  var MigrationPendingEntrySchema = object({
@@ -15694,6 +15908,39 @@ var UnstampedEventMediaCountSchema = object({
15694
15908
  /** Sum across both, or `null` when either lane could not be counted. */
15695
15909
  total: number().int().nonnegative().nullable()
15696
15910
  }).nullable();
15911
+ /**
15912
+ * Where the stored event media sits, per storage location: how many media rows
15913
+ * and how many bytes carry each location id, how many carry none (legacy,
15914
+ * resolved to the default `eventMedia` location), and how many name a location
15915
+ * that was not asked about (a deleted or never-listed one).
15916
+ *
15917
+ * One covering-index aggregate per asked location plus the NULL one, and one
15918
+ * unfiltered total that scans the covering index: O(rows), not a walk of the
15919
+ * rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
15920
+ * it is a deleted location only when every location id was asked.
15921
+ * `null` is "could not be measured", never zero.
15922
+ */
15923
+ var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
15924
+ var MediaLocationCensusSchema = object({
15925
+ locations: array(object({
15926
+ locationId: string(),
15927
+ rows: number().int().nonnegative(),
15928
+ bytes: number().nonnegative()
15929
+ })).readonly(),
15930
+ unstamped: object({
15931
+ rows: number().int().nonnegative(),
15932
+ bytes: number().nonnegative()
15933
+ }),
15934
+ /** Rows stamped with a location id that was not in the asked list. */
15935
+ unlisted: object({
15936
+ rows: number().int().nonnegative(),
15937
+ bytes: number().nonnegative()
15938
+ }),
15939
+ total: object({
15940
+ rows: number().int().nonnegative(),
15941
+ bytes: number().nonnegative()
15942
+ })
15943
+ });
15697
15944
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
15698
15945
  /** The independently selectable logical storage classes — every class
15699
15946
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -46214,52 +46461,7 @@ DeviceType.Light, method(object({
46214
46461
  level: number().min(0).max(100),
46215
46462
  lastChangedAt: number()
46216
46463
  });
46217
- /**
46218
- * `broker` — unified pub/sub broker registry, system-scoped collection.
46219
- *
46220
- * The cap models any kind-tagged message broker the user wants to
46221
- * register and that other addons might consume. The first two kinds
46222
- * are MQTT (mosquitto / aedes embedded / cloud bridge) and
46223
- * Home Assistant (HA WebSocket — subscribe_entities + call_service).
46224
- * Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
46225
- * the same surface.
46226
- *
46227
- * Why one cap, not one cap per kind:
46228
- * - The integrations page wants a single table of "every broker"
46229
- * across kinds; a unified cap drives it without join logic.
46230
- * - Generic admin operations (add / remove / test / status) are
46231
- * identical across kinds — duplicating them per cap was busywork.
46232
- * - The `network-access` cap precedent: same interface, many
46233
- * `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
46234
- *
46235
- * What stays kind-specific:
46236
- * - `getBrokerConfig` payload (kind decides whether it returns
46237
- * mqtt URL+creds, HA baseUrl+token, …) — typed as
46238
- * `Record<string, unknown>` at the wire; consumers narrow.
46239
- * - `publish` / `subscribe` / `getState` arguments (target / filter /
46240
- * key) — also typed as `Record<string, unknown>` so each kind can
46241
- * evolve its surface (MQTT topic+qos vs HA entity_id+domain)
46242
- * without rebreaking the cap signature.
46243
- * - `add` `settings` payload (different fields per kind, validated by
46244
- * the kind-specific provider).
46245
- *
46246
- * Bidirectionality:
46247
- * - `publish` is RPC — synchronous result (provider-defined).
46248
- * - `subscribe` returns a subscription handle; the actual message
46249
- * stream flows over the typed event-bus as `broker.message` events
46250
- * keyed by `(brokerId, subscriptionId)`. Consumers filter in their
46251
- * event handler. Subscriptions persist across reconnects — the
46252
- * provider re-subscribes upstream after a transport drop.
46253
- *
46254
- * Why event-bus push (not a subscription RPC stream):
46255
- * - Matches the rest of CamStack's D8 contract — events for telemetry,
46256
- * RPC for loss-is-a-bug. Broker messages are telemetry (a single
46257
- * drop is recoverable via `getState({key})`).
46258
- * - Frees `broker.subscribe` from holding a long-lived RPC channel.
46259
- * - Cross-process: events already route through `$event-bus`; no
46260
- * per-broker wire-shaping needed.
46261
- */
46262
- var BrokerStatusEnum = _enum([
46464
+ _enum([
46263
46465
  "connected",
46264
46466
  "disconnected",
46265
46467
  "connecting",
@@ -46267,141 +46469,12 @@ var BrokerStatusEnum = _enum([
46267
46469
  "unreachable",
46268
46470
  "error"
46269
46471
  ]);
46270
- var BrokerInfoSchema = object({
46271
- /** Stable broker id. Persisted; survives addon restarts. */
46272
- id: string(),
46273
- /** Addon id of the provider that OWNS this broker.
46274
- *
46275
- * The `broker` cap is a system-scoped collection: several addons
46276
- * register a `broker` provider, each owning a DISJOINT set of
46277
- * brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
46278
- * `ha_*`). A broker therefore belongs to exactly one addon — like a
46279
- * device belongs to one integration. The admin UI threads this id
46280
- * back as the `{ addonId }` system-collection selector on every
46281
- * id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
46282
- * `testConnection`), so the call routes to the OWNING provider
46283
- * instead of defaulting to the first-registered one. */
46284
- addonId: string(),
46285
- /** Human-readable name (operator-chosen at add-time). */
46286
- name: string(),
46287
- /** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
46288
- kind: string(),
46289
- status: BrokerStatusEnum,
46290
- /** Free-form provider-specific info (HA version, MQTT broker
46291
- * flavour, latency, mTLS active, …). */
46292
- info: record(string(), unknown()),
46293
- /** Ms epoch of the last connection probe / status update. */
46294
- lastCheckedAt: number().nullable(),
46295
- /** Last error message, when `status` indicates failure. */
46296
- error: string().nullable()
46297
- });
46298
46472
  var RegistryStatusSchema = object({
46299
46473
  brokerCount: number().int().nonnegative(),
46300
46474
  connectedCount: number().int().nonnegative()
46301
46475
  });
46302
- /**
46303
- * One entry per `broker` provider — which addon provides which broker
46304
- * kind(s). The unified create picker reads this to know, for each
46305
- * (addonId, kind) pair the operator can pick, where to route the
46306
- * follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
46307
- * `{ addonId }` selector). A provider only advertises a kind here when
46308
- * it implements `getSettingsSchema` for that kind (so the picker never
46309
- * offers a kind whose creation form can't be rendered).
46310
- */
46311
- var BrokerProviderInfoSchema = object({
46312
- /** Addon id of the `broker` provider this entry describes. */
46313
- addonId: string(),
46314
- /** Broker kinds this provider can create, with a display label. */
46315
- kinds: array(object({
46316
- kind: string(),
46317
- label: string(),
46318
- /**
46319
- * Relative hub path to start an interactive account link (OAuth popup).
46320
- * May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
46321
- */
46322
- interactiveConnectPath: string().min(1).optional(),
46323
- /** Relative hub path to disconnect the linked account (same popup pattern). */
46324
- interactiveDisconnectPath: string().min(1).optional(),
46325
- /**
46326
- * The kind signs in through a server-held interactive session instead of
46327
- * (or before) typed credentials. `basePath` is a hub-relative route set
46328
- * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
46329
- * contract: `POST {basePath}/start` (body: the form's settings) answers
46330
- * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
46331
- * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
46332
- * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
46333
- * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
46334
- * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
46335
- * (`{ sessionId }`) drops the session. The dialog that edits a broker
46336
- * offers it as the default path; the fields the session fills are
46337
- * marked `filledBySignIn` in the kind's settings schema.
46338
- *
46339
- * `methods` lists the ways the session can sign in, recommended first:
46340
- * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
46341
- * settings) answering `{ sessionId, userCode, verificationUrl,
46342
- * expiresAt }` — the operator types `userCode` at `verificationUrl` on
46343
- * any device, and the same `status` / `commit` / `cancel` routes follow
46344
- * (status then carries `method`, `userCode`, `verificationUrl`,
46345
- * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
46346
- * `'proxy'` is the `{basePath}/start` flow above. Absent means
46347
- * `['proxy']`.
46348
- */
46349
- signIn: object({
46350
- basePath: string().min(1),
46351
- methods: array(_enum(["device-code", "proxy"])).optional()
46352
- }).optional()
46353
- }))
46354
- });
46355
- var ListInputSchema = object({
46356
- /** Optional kind filter — `list({kind:'home-assistant'})` returns
46357
- * only HA brokers. Omit to list every broker. */
46358
- kind: string().optional() });
46359
46476
  var GetInputSchema = object({ id: string() });
46360
- var AddInputSchema = object({
46361
- kind: string().min(1),
46362
- name: string().min(1),
46363
- /**
46364
- * ADOPT an existing id instead of minting a new one.
46365
- *
46366
- * Written the day this cost an outage. A broker's id is not a detail: HA
46367
- * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
46368
- * lost from config and re-added as `ha_001` leaves every one of its devices
46369
- * bound to a broker that no longer exists. Re-entering the password under the
46370
- * ORIGINAL id turns a multi-step device migration back into re-entering a
46371
- * password.
46372
- *
46373
- * A provider MUST refuse an id that is already in use — adopting a live
46374
- * broker's id would silently take it over.
46375
- */
46376
- id: string().min(1).optional(),
46377
- /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
46378
- * `{baseUrl,accessToken}`). Validated by the kind-specific provider
46379
- * branch on receipt — invalid shape rejects the add. */
46380
- settings: record(string(), unknown())
46381
- });
46382
- var AddResultSchema = object({ id: string() });
46383
- var RemoveInputSchema = object({ id: string() });
46384
- var TestConnectionResultSchema = discriminatedUnion("ok", [object({
46385
- ok: literal(true),
46386
- latencyMs: number().nonnegative()
46387
- }), object({
46388
- ok: literal(false),
46389
- error: string()
46390
- })]);
46391
46477
  var SettingsRecordSchema$1 = record(string(), unknown());
46392
- var SettingsSchemaInputSchema = object({ kind: string() });
46393
- var TestSettingsInputSchema = object({
46394
- kind: string(),
46395
- settings: SettingsRecordSchema$1
46396
- });
46397
- var TestSettingsResultSchema = discriminatedUnion("ok", [object({
46398
- ok: literal(true),
46399
- latencyMs: number().nonnegative().optional()
46400
- }).strict(), object({
46401
- ok: literal(false),
46402
- error: string()
46403
- })]);
46404
- var SettingsSchemaResultSchema = unknown().nullable();
46405
46478
  var PublishInputSchema = object({
46406
46479
  brokerId: string(),
46407
46480
  /** Kind-specific routing target.
@@ -46445,51 +46518,12 @@ var brokerCapability = {
46445
46518
  kind: "poll"
46446
46519
  },
46447
46520
  methods: {
46448
- list: method(ListInputSchema, array(BrokerInfoSchema)),
46449
- get: method(GetInputSchema, BrokerInfoSchema.nullable()),
46450
- /** Enumerate which addon provides which broker kind(s) for the
46451
- * unified create picker. The auto-mount fans this array across
46452
- * every registered `broker` provider (array-output method), so the
46453
- * picker sees every kind from every provider in one call. */
46454
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
46455
- add: method(AddInputSchema, AddResultSchema, {
46456
- kind: "mutation",
46457
- auth: "admin"
46458
- }),
46459
- remove: method(RemoveInputSchema, _void(), {
46460
- kind: "mutation",
46461
- auth: "admin"
46462
- }),
46463
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
46464
- kind: "mutation",
46465
- auth: "admin"
46466
- }),
46467
- /** Read the persisted settings record for a broker (kind-specific
46468
- * shape). Admin-only — settings may contain secrets. Returns `null`
46469
- * when the broker id is unknown to the provider (the collection
46470
- * fallback may route a foreign id to the first provider). */
46471
- getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46472
- /** Overwrite the persisted settings record. The kind-specific
46473
- * provider validates the shape and applies the change (reconnects
46474
- * if credentials changed). */
46475
- setSettings: method(object({
46476
- id: string(),
46477
- settings: SettingsRecordSchema$1
46478
- }), _void(), {
46479
- kind: "mutation",
46480
- auth: "admin"
46481
- }),
46482
46521
  /** Returns the kind-specific connection config the consumer needs
46483
46522
  * to open its own client (MQTT pattern: `{url, username, password,
46484
46523
  * clientIdPrefix}`). HA providers MAY return the auth envelope
46485
46524
  * but typical HA consumers use `publish` / `subscribe` instead.
46486
46525
  * Returns `null` when the broker id is unknown to the provider. */
46487
46526
  getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
46488
- getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
46489
- testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
46490
- kind: "mutation",
46491
- auth: "admin"
46492
- }),
46493
46527
  publish: method(PublishInputSchema, unknown(), {
46494
46528
  kind: "mutation",
46495
46529
  auth: "admin"
@@ -49113,6 +49147,9 @@ var RunningProposalEvidenceSchema = object({
49113
49147
  runsFound: number().int().nonnegative(),
49114
49148
  pausesFound: number().int().nonnegative(),
49115
49149
  longestPauseMs: number().nonnegative(),
49150
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49151
+ pulsesFound: number().int().nonnegative(),
49152
+ longestPulseMs: number().nonnegative(),
49116
49153
  idleFloorW: number(),
49117
49154
  noiseCeilingW: number(),
49118
49155
  plateauW: number(),
@@ -50559,7 +50596,14 @@ var DiscoveryCandidateSchema = object({
50559
50596
  /** `stableId` of the gateway candidate this device was found BEHIND. */
50560
50597
  parentStableId: string().optional(),
50561
50598
  /** `false` = shown for context but not pickable. Default true. */
50562
- selectable: boolean().optional()
50599
+ selectable: boolean().optional(),
50600
+ /**
50601
+ * Keys of the provider's creation form (`getChildCreationSchema`) this candidate
50602
+ * cannot be adopted without — a key for an encrypted board, a pairing code. The
50603
+ * admin asks for exactly these, on the creation form prefilled with
50604
+ * `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
50605
+ */
50606
+ needsParams: array(string()).optional()
50563
50607
  });
50564
50608
  /**
50565
50609
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -50618,12 +50662,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
50618
50662
  }), array(DiscoveryCandidateSchema), {
50619
50663
  kind: "mutation",
50620
50664
  auth: "admin"
50621
- }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
50665
+ }), method(object({}), CreationSchemaOutputSchema), method(object({
50666
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50667
+ integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
50668
+ candidate: DiscoveryCandidateSchema,
50669
+ /** The integration the operator adopts into; a provider with several instances needs it. */
50670
+ integrationId: string().optional()
50671
+ }), DeviceSummarySchema, {
50622
50672
  kind: "mutation",
50623
50673
  auth: "admin"
50624
- }), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
50674
+ }), method(object({}), boolean()), method(object({
50625
50675
  type: _enum(DeviceType),
50626
- config: record(string(), unknown())
50676
+ /** The integration asked about; a provider whose integrations differ answers per one. */
50677
+ integrationId: string().optional()
50678
+ }), CreationSchemaOutputSchema), method(object({
50679
+ type: _enum(DeviceType),
50680
+ config: record(string(), unknown()),
50681
+ /** The integration the operator adds the device to; a provider with several instances needs it. */
50682
+ integrationId: string().optional()
50627
50683
  }), DeviceSummarySchema, {
50628
50684
  kind: "mutation",
50629
50685
  auth: "admin"
@@ -51333,7 +51389,10 @@ method(object({
51333
51389
  auth: "admin"
51334
51390
  }), method(object({
51335
51391
  addonId: string(),
51336
- type: _enum(DeviceType)
51392
+ type: _enum(DeviceType),
51393
+ /** The integration the form is for; a provider whose integrations differ (a hub
51394
+ * integration takes no manual add) answers per integration. Absent = the addon's own. */
51395
+ integrationId: string().optional()
51337
51396
  }), unknown().nullable()), method(object({
51338
51397
  addonId: string(),
51339
51398
  type: _enum(DeviceType),
@@ -51417,7 +51476,11 @@ method(object({
51417
51476
  kind: "mutation",
51418
51477
  auth: "admin",
51419
51478
  timeoutMs: 12e4
51420
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51479
+ }), method(object({
51480
+ addonId: string(),
51481
+ /** The integration the Add-Device dialog is open on; absent = the addon's own. */
51482
+ integrationId: string().optional()
51483
+ }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51421
51484
  deviceId: number(),
51422
51485
  key: string(),
51423
51486
  value: unknown()
@@ -54013,7 +54076,7 @@ var TestConnectionStatusEnum = _enum([
54013
54076
  "inconclusive",
54014
54077
  "unsupported"
54015
54078
  ]);
54016
- var TestConnectionResultSchema$1 = object({
54079
+ var TestConnectionResultSchema = object({
54017
54080
  /** True ONLY for `validated`. Never true for a test that did not run. */
54018
54081
  success: boolean(),
54019
54082
  error: string().optional(),
@@ -54034,11 +54097,7 @@ var CreateIntegrationInputSchema = object({
54034
54097
  info: record(string(), unknown()).optional(),
54035
54098
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54036
54099
  originStableId: string().optional(),
54037
- settings: record(string(), unknown()).optional(),
54038
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
54039
- * the keychain, not the table (D787). */
54040
- secretKeys: array(string()).optional(),
54041
- skipRestart: boolean().optional()
54100
+ settings: record(string(), unknown()).optional()
54042
54101
  });
54043
54102
  var UpdateIntegrationInputSchema = object({
54044
54103
  id: string(),
@@ -54059,20 +54118,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54059
54118
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54060
54119
  kind: "mutation",
54061
54120
  auth: "admin"
54062
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
54063
- id: string(),
54064
- settings: record(string(), unknown()),
54065
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
54066
- secretKeys: array(string()).optional(),
54067
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
54068
- skipRestart: boolean().optional()
54069
- }), object({ success: boolean() }), {
54070
- kind: "mutation",
54071
- auth: "admin"
54072
54121
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54073
54122
  addonId: string(),
54074
54123
  settings: record(string(), unknown())
54075
- }), TestConnectionResultSchema$1, {
54124
+ }), TestConnectionResultSchema, {
54076
54125
  kind: "mutation",
54077
54126
  auth: "admin"
54078
54127
  });
@@ -54099,6 +54148,14 @@ var ConfigSchemaPassthrough$3 = custom();
54099
54148
  var IntegrationInstanceIdSchema = string().min(1);
54100
54149
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54101
54150
  /**
54151
+ * What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
54152
+ * instance is an integration that brings devices (the registry owns its on/off switch);
54153
+ * `services`: a connection configured from the addon's own function page, never listed on
54154
+ * Integrations.
54155
+ * It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
54156
+ */
54157
+ var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
54158
+ /**
54102
54159
  * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54103
54160
  * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54104
54161
  * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
@@ -54109,6 +54166,22 @@ var IntegrationInstanceKindSchema = object({
54109
54166
  /** Stable id of the kind, sent back as `createInstance.kind`. */
54110
54167
  id: string().min(1),
54111
54168
  label: string().min(1),
54169
+ /**
54170
+ * The function capability this kind is configured for (`broker`, `smtp-provider`,
54171
+ * `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
54172
+ * provider with more than one section must name it on every kind.
54173
+ */
54174
+ functionCap: string().min(1),
54175
+ /**
54176
+ * For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
54177
+ * (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
54178
+ * finds the instances whose kind names it; absent takes the kind `id`.
54179
+ */
54180
+ brokerKind: string().min(1).optional(),
54181
+ /** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
54182
+ role: IntegrationInstanceRoleSchema.optional(),
54183
+ /** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
54184
+ canTest: boolean().optional(),
54112
54185
  /** The kind signs in through a server-held interactive session. */
54113
54186
  signIn: object({
54114
54187
  basePath: string().min(1),
@@ -54121,11 +54194,7 @@ var IntegrationInstanceKindSchema = object({
54121
54194
  });
54122
54195
  var IntegrationInstanceDescriptionSchema = object({
54123
54196
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54124
- role: _enum([
54125
- "devices",
54126
- "services",
54127
- "both"
54128
- ]),
54197
+ role: IntegrationInstanceRoleSchema,
54129
54198
  /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54130
54199
  discovery: object({
54131
54200
  mode: _enum([
@@ -54200,7 +54269,11 @@ var integrationInstancesCapability = {
54200
54269
  methods: {
54201
54270
  describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
54202
54271
  /** The form; `id: null` is the create form. */
54203
- instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54272
+ instanceFormSchema: method(object({
54273
+ id: IntegrationInstanceIdSchema.nullable(),
54274
+ /** The kind of the create form (`id: null`) of a provider with several. */
54275
+ kind: string().min(1).optional()
54276
+ }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54204
54277
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54205
54278
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54206
54279
  /**
@@ -54210,7 +54283,9 @@ var integrationInstancesCapability = {
54210
54283
  createInstance: method(object({
54211
54284
  id: IntegrationInstanceIdSchema.optional(),
54212
54285
  name: string().min(1),
54213
- kind: string().min(1).optional()
54286
+ kind: string().min(1).optional(),
54287
+ /** Form values written with the record (a link picked in the create dialog). */
54288
+ values: record(string(), unknown()).optional()
54214
54289
  }), object({ id: string() }), {
54215
54290
  kind: "mutation",
54216
54291
  auth: "admin"
@@ -54237,8 +54312,10 @@ var integrationInstancesCapability = {
54237
54312
  }),
54238
54313
  testInstance: optionalMethod(object({
54239
54314
  id: IntegrationInstanceIdSchema.nullable(),
54240
- values: record(string(), unknown())
54241
- }), TestConnectionResultSchema$1, {
54315
+ values: record(string(), unknown()),
54316
+ /** The kind to test when `id` is null and the provider has several. */
54317
+ kind: string().min(1).optional()
54318
+ }), TestConnectionResultSchema, {
54242
54319
  kind: "mutation",
54243
54320
  auth: "admin"
54244
54321
  })
@@ -56743,6 +56820,380 @@ var SystemMetricsSchema = object({
56743
56820
  gpuMemoryPercent: number().optional()
56744
56821
  });
56745
56822
  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);
56823
+ /**
56824
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
56825
+ * config documents, mirrors. The third storage API next to `ctx.settings`
56826
+ * (operator intent, D870) and device-history (numbers over time, D850).
56827
+ *
56828
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
56829
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
56830
+ * that with the addon id closed over). It is deliberately NOT in any input
56831
+ * schema, so a session can never supply it, and a call with no stamp is refused
56832
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
56833
+ * addons go through the owner's capability.
56834
+ *
56835
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
56836
+ * never `[]`. Secret fields are never on the wire: a read shows the
56837
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
56838
+ * resolves itself, and a write may carry only a keychain marker bound to the
56839
+ * row's own ref.
56840
+ *
56841
+ * Internal: addons reach it through `ctx.records`, never directly.
56842
+ */
56843
+ var RecordIdSchema = string().min(1).max(256);
56844
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
56845
+ var RecordBodySchema = record(string(), unknown());
56846
+ var RecordInputSchema = object({
56847
+ id: RecordIdSchema,
56848
+ /** The device this row belongs to (D865 removal, per-device reads). */
56849
+ deviceId: number().int().optional(),
56850
+ /** The domain clock, ms: observed / created / opened. */
56851
+ at: number().int().optional(),
56852
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
56853
+ expiresAt: number().int().optional(),
56854
+ data: RecordBodySchema
56855
+ });
56856
+ var RecordViewSchema = object({
56857
+ id: string(),
56858
+ deviceId: number().int().nullable(),
56859
+ at: number().nullable(),
56860
+ expiresAt: number().nullable(),
56861
+ rev: number().int(),
56862
+ updatedAt: number(),
56863
+ data: RecordBodySchema
56864
+ });
56865
+ /** Declared index fields only. Anything else does not compile, by design. */
56866
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
56867
+ string(),
56868
+ number(),
56869
+ boolean(),
56870
+ object({ in: array(union([
56871
+ string(),
56872
+ number(),
56873
+ boolean()
56874
+ ])).min(1).max(RECORD_BATCH_MAX) }),
56875
+ object({
56876
+ gt: union([string(), number()]).optional(),
56877
+ gte: union([string(), number()]).optional(),
56878
+ lt: union([string(), number()]).optional(),
56879
+ lte: union([string(), number()]).optional()
56880
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
56881
+ ]));
56882
+ var KindShape = { kind: RecordKindNameSchema };
56883
+ var TimeWindowShape = {
56884
+ deviceId: number().int().optional(),
56885
+ atFrom: number().optional(),
56886
+ atTo: number().optional()
56887
+ };
56888
+ var RecordQueryInputSchema = object({
56889
+ ...KindShape,
56890
+ ...TimeWindowShape,
56891
+ where: RecordWhereSchema.optional(),
56892
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
56893
+ order: _enum(["at", "id"]).optional(),
56894
+ desc: boolean().optional(),
56895
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
56896
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
56897
+ cursor: string().min(1).optional()
56898
+ });
56899
+ var RecordQueryResultSchema = object({
56900
+ records: array(RecordViewSchema),
56901
+ nextCursor: string().nullable()
56902
+ });
56903
+ var RecordKindStateSchema = _enum([
56904
+ "created",
56905
+ "unchanged",
56906
+ "updated"
56907
+ ]);
56908
+ var DeclareKindResultSchema = object({
56909
+ kindId: number().int(),
56910
+ state: RecordKindStateSchema,
56911
+ createdIndexes: array(string())
56912
+ });
56913
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
56914
+ var SecretRefsList = array(string());
56915
+ /**
56916
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
56917
+ * All ops of one call share ONE database file and ONE `transactFamily`.
56918
+ */
56919
+ var RecordTransactOpSchema = discriminatedUnion("op", [
56920
+ object({
56921
+ op: literal("put"),
56922
+ kind: RecordKindNameSchema,
56923
+ record: RecordInputSchema,
56924
+ ifRev: number().int().min(0).optional()
56925
+ }),
56926
+ object({
56927
+ op: literal("patch"),
56928
+ kind: RecordKindNameSchema,
56929
+ id: RecordIdSchema,
56930
+ data: RecordBodySchema,
56931
+ at: number().int().nullable().optional(),
56932
+ expiresAt: number().int().nullable().optional(),
56933
+ ifRev: number().int().min(1).optional()
56934
+ }),
56935
+ object({
56936
+ op: literal("delete"),
56937
+ kind: RecordKindNameSchema,
56938
+ id: RecordIdSchema
56939
+ })
56940
+ ]);
56941
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
56942
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
56943
+ kind: "mutation",
56944
+ auth: "admin"
56945
+ }), method(object({
56946
+ ...KindShape,
56947
+ id: RecordIdSchema
56948
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
56949
+ ...KindShape,
56950
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56951
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
56952
+ ...KindShape,
56953
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56954
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
56955
+ ...KindShape,
56956
+ record: RecordInputSchema,
56957
+ /** Compare-and-set. `0` means "the row must not exist". */
56958
+ ifRev: number().int().min(0).optional()
56959
+ }), object({
56960
+ rev: number().int(),
56961
+ releasedRefs: SecretRefsList
56962
+ }), {
56963
+ kind: "mutation",
56964
+ auth: "admin"
56965
+ }), method(object({
56966
+ ...KindShape,
56967
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
56968
+ }), object({
56969
+ count: number().int(),
56970
+ releasedRefs: SecretRefsList
56971
+ }), {
56972
+ kind: "mutation",
56973
+ auth: "admin"
56974
+ }), method(object({
56975
+ ...KindShape,
56976
+ id: RecordIdSchema,
56977
+ data: RecordBodySchema,
56978
+ at: number().int().nullable().optional(),
56979
+ expiresAt: number().int().nullable().optional(),
56980
+ ifRev: number().int().min(1).optional()
56981
+ }), object({
56982
+ rev: number().int(),
56983
+ releasedRefs: SecretRefsList
56984
+ }), {
56985
+ kind: "mutation",
56986
+ auth: "admin"
56987
+ }), method(object({
56988
+ ...KindShape,
56989
+ id: RecordIdSchema
56990
+ }), object({
56991
+ deleted: boolean(),
56992
+ secretRefs: SecretRefsList
56993
+ }), {
56994
+ kind: "mutation",
56995
+ auth: "admin"
56996
+ }), method(object({
56997
+ ...KindShape,
56998
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
56999
+ }), object({
57000
+ deleted: number().int(),
57001
+ secretRefs: SecretRefsList
57002
+ }), {
57003
+ kind: "mutation",
57004
+ auth: "admin"
57005
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
57006
+ ...KindShape,
57007
+ ...TimeWindowShape,
57008
+ where: RecordWhereSchema.optional()
57009
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
57010
+ ...KindShape,
57011
+ deviceId: number().int().optional(),
57012
+ from: number(),
57013
+ to: number(),
57014
+ bucketMs: number().int().positive()
57015
+ }), object({ buckets: array(object({
57016
+ bucketStart: number(),
57017
+ count: number().int()
57018
+ })) }), { auth: "admin" }), method(object({
57019
+ ...KindShape,
57020
+ op: _enum([
57021
+ "sum",
57022
+ "min",
57023
+ "max",
57024
+ "count"
57025
+ ]),
57026
+ field: RecordFieldPathSchema.optional(),
57027
+ groupBy: RecordFieldPathSchema.optional(),
57028
+ where: RecordWhereSchema.optional()
57029
+ }), object({ rows: array(object({
57030
+ group: union([
57031
+ string(),
57032
+ number(),
57033
+ _null()
57034
+ ]),
57035
+ value: number().nullable()
57036
+ })) }), { auth: "admin" }), method(object({
57037
+ ...KindShape,
57038
+ deviceId: number().int().optional(),
57039
+ atBefore: number().optional(),
57040
+ where: RecordWhereSchema.optional()
57041
+ }), object({
57042
+ deleted: number().int(),
57043
+ secretRefs: SecretRefsList
57044
+ }), {
57045
+ kind: "mutation",
57046
+ auth: "admin"
57047
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
57048
+ results: array(object({
57049
+ rev: number().int().nullable(),
57050
+ deleted: boolean()
57051
+ })),
57052
+ releasedRefs: SecretRefsList,
57053
+ secretRefs: SecretRefsList
57054
+ }), {
57055
+ kind: "mutation",
57056
+ auth: "admin"
57057
+ }), method(object({
57058
+ deviceId: number().int(),
57059
+ kinds: array(RecordKindNameSchema).optional()
57060
+ }), object({
57061
+ deleted: record(string(), number().int()),
57062
+ secretRefs: SecretRefsList
57063
+ }), {
57064
+ kind: "mutation",
57065
+ auth: "admin"
57066
+ }), method(object(KindShape), object({ id: number().int() }), {
57067
+ kind: "mutation",
57068
+ auth: "admin"
57069
+ });
57070
+ /**
57071
+ * The wire shapes of the migration API, version 1 (planning#447).
57072
+ *
57073
+ * A migration is code that outlives the server it was written against: an addon at
57074
+ * version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
57075
+ * the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
57076
+ * is the contract of every method of it, one Zod pair per method.
57077
+ *
57078
+ * APPEND-ONLY. A released method is never removed and never changes meaning: an input
57079
+ * field is never removed or made required, an enum never narrows, an output field is
57080
+ * never removed. A change that cannot be expressed that way is `migration-api.v2`, next
57081
+ * to this file. `scripts/check-migration-api-append-only.ts` holds the line against
57082
+ * `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
57083
+ * `it('<id>'` in `migration-api.contract.spec.ts`.
57084
+ */
57085
+ var Rows = record(string(), unknown());
57086
+ var IntegrationId = object({ integrationId: string().min(1) });
57087
+ var RecordId = object({ id: string().min(1) });
57088
+ var RecordRowSchema = object({
57089
+ id: string(),
57090
+ deviceId: number().int().nullable(),
57091
+ at: number().nullable(),
57092
+ expiresAt: number().nullable(),
57093
+ rev: number().int(),
57094
+ updatedAt: number(),
57095
+ data: Rows
57096
+ });
57097
+ var RecordPutShape = {
57098
+ id: string().min(1),
57099
+ deviceId: number().int().optional(),
57100
+ at: number().int().optional(),
57101
+ expiresAt: number().int().optional(),
57102
+ data: Rows
57103
+ };
57104
+ var MigrationIntegrationSchema = object({
57105
+ id: string(),
57106
+ addonId: string(),
57107
+ name: string(),
57108
+ info: Rows,
57109
+ originStableId: string().optional()
57110
+ });
57111
+ var MigrationDeviceSchema = object({
57112
+ id: number().int(),
57113
+ integrationId: string().nullable().optional(),
57114
+ /** The owning device; null or absent for a root. */
57115
+ parentDeviceId: number().int().nullable().optional(),
57116
+ type: string().optional(),
57117
+ name: string().optional(),
57118
+ stableId: string().optional()
57119
+ });
57120
+ /** What the engine says about the migration API it serves: lets an addon notice an older server. */
57121
+ var MigrationApiInfoSchema = object({
57122
+ version: number().int(),
57123
+ methods: array(string())
57124
+ });
57125
+ object({
57126
+ message: string(),
57127
+ meta: Rows.optional(),
57128
+ /** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
57129
+ tags: object({ deviceId: number().int().optional() }).optional()
57130
+ });
57131
+ object({
57132
+ owner: string().min(1),
57133
+ metas: array(MigrationMetaSchema)
57134
+ }), object({
57135
+ bootId: string(),
57136
+ snapshot: _enum([
57137
+ "none",
57138
+ "written",
57139
+ "refused"
57140
+ ]),
57141
+ steps: array(MigrationOwnerStepSchema),
57142
+ /** The migration API the engine serves; absent from a server that predates it. */
57143
+ migrationApi: MigrationApiInfoSchema.optional()
57144
+ }), object({ id: string().min(1) }), object({
57145
+ proceed: boolean(),
57146
+ state: union([literal("pending"), MigrationStatusSchema]),
57147
+ reason: string().nullable()
57148
+ }), object({
57149
+ id: string().min(1),
57150
+ result: MigrationOwnerResultSchema
57151
+ }), object({
57152
+ status: MigrationStatusSchema,
57153
+ error: string().nullable(),
57154
+ rowsChanged: number().int().nonnegative()
57155
+ }), object({
57156
+ rows: number().int().nonnegative(),
57157
+ secrets: number().int().nonnegative()
57158
+ }), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
57159
+ present: boolean(),
57160
+ removed: Rows
57161
+ }), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
57162
+ ...RecordPutShape,
57163
+ ifRev: number().int().optional()
57164
+ }), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
57165
+ partial: Rows,
57166
+ ifRev: number().int().optional()
57167
+ }), object({ rev: number().int() }), object({ deleted: boolean() }), object({
57168
+ deviceId: number().int().optional(),
57169
+ atFrom: number().optional(),
57170
+ atTo: number().optional(),
57171
+ where: RecordWhereSchema.optional(),
57172
+ limit: number().int().min(1).optional(),
57173
+ cursor: string().min(1).optional(),
57174
+ resolveSecrets: boolean().optional()
57175
+ }), object({
57176
+ records: array(RecordRowSchema),
57177
+ nextCursor: string().nullable()
57178
+ }), object({
57179
+ deviceId: number().int().optional(),
57180
+ atBefore: number().optional(),
57181
+ where: RecordWhereSchema.optional()
57182
+ }), 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({
57183
+ ref: string().min(1),
57184
+ value: string()
57185
+ }), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
57186
+ addonId: string().min(1),
57187
+ integrationId: string().min(1).optional()
57188
+ }), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
57189
+ id: number().int(),
57190
+ integrationId: string().nullable(),
57191
+ /** Whether the device carries any stored config; its content is not offered. */
57192
+ hasConfig: boolean()
57193
+ }).nullable(), object({
57194
+ deviceId: number().int(),
57195
+ integrationId: string().min(1)
57196
+ }), object({}), object({}), object({ nodeId: string() }), object({}), object({});
56746
57197
  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({
56747
57198
  id: string().min(1),
56748
57199
  error: string().min(1)
@@ -56772,7 +57223,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56772
57223
  "written",
56773
57224
  "refused"
56774
57225
  ]),
56775
- steps: array(MigrationOwnerStepSchema)
57226
+ steps: array(MigrationOwnerStepSchema),
57227
+ /** The migration API this engine serves (additive: a server that predates it omits it). */
57228
+ migrationApi: MigrationApiInfoSchema.optional()
56776
57229
  }), {
56777
57230
  kind: "mutation",
56778
57231
  auth: "admin"
@@ -56793,6 +57246,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56793
57246
  }), {
56794
57247
  kind: "mutation",
56795
57248
  auth: "admin"
57249
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
57250
+ rows: number().int().nonnegative(),
57251
+ secrets: number().int().nonnegative()
57252
+ }), {
57253
+ kind: "mutation",
57254
+ auth: "admin"
57255
+ }), method(object({
57256
+ integrationId: string().min(1),
57257
+ patch: record(string(), unknown())
57258
+ }), object({ written: boolean() }), {
57259
+ kind: "mutation",
57260
+ auth: "admin"
57261
+ }), method(object({
57262
+ integrationId: string().min(1),
57263
+ keys: array(string().min(1))
57264
+ }), object({
57265
+ present: boolean(),
57266
+ removed: record(string(), unknown())
57267
+ }), {
57268
+ kind: "mutation",
57269
+ auth: "admin"
56796
57270
  });
56797
57271
  method(object({
56798
57272
  sourceUrl: string(),
@@ -59418,7 +59892,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
59418
59892
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
59419
59893
  kind: "mutation",
59420
59894
  auth: "admin"
59421
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59895
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
59896
+ kind: "query",
59897
+ auth: "admin"
59898
+ }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
59422
59899
  kind: "query",
59423
59900
  auth: "admin"
59424
59901
  }), method(object({}), array(RelocateJobSchema).readonly(), {
@@ -61825,7 +62302,7 @@ var BrokerStatusSchema$1 = _enum([
61825
62302
  "unreachable",
61826
62303
  "tls-error"
61827
62304
  ]);
61828
- var BrokerInfoSchema$1 = object({
62305
+ var BrokerInfoSchema = object({
61829
62306
  id: string(),
61830
62307
  name: string(),
61831
62308
  url: string(),
@@ -61888,7 +62365,7 @@ var StatusSchema = object({
61888
62365
  brokerCount: number(),
61889
62366
  embeddedRunning: boolean()
61890
62367
  });
61891
- method(_void(), array(BrokerInfoSchema$1)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
62368
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
61892
62369
  kind: "mutation",
61893
62370
  auth: "admin"
61894
62371
  }), method(IdInputSchema, _void(), {
@@ -68206,253 +68683,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
68206
68683
  auth: "admin"
68207
68684
  });
68208
68685
  /**
68209
- * records - an addon's own facts and documents (D881): ledgers, audit rows,
68210
- * config documents, mirrors. The third storage API next to `ctx.settings`
68211
- * (operator intent, D870) and device-history (numbers over time, D850).
68212
- *
68213
- * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
68214
- * crossed a process and stated by a hub in-process caller (`ctx.records` does
68215
- * that with the addon id closed over). It is deliberately NOT in any input
68216
- * schema, so a session can never supply it, and a call with no stamp is refused
68217
- * `records/no-caller`. A kind is readable and writable only by its owner; other
68218
- * addons go through the owner's capability.
68219
- *
68220
- * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
68221
- * never `[]`. Secret fields are never on the wire: a read shows the
68222
- * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
68223
- * resolves itself, and a write may carry only a keychain marker bound to the
68224
- * row's own ref.
68225
- *
68226
- * Internal: addons reach it through `ctx.records`, never directly.
68227
- */
68228
- var RecordIdSchema = string().min(1).max(256);
68229
- /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
68230
- var RecordBodySchema = record(string(), unknown());
68231
- var RecordInputSchema = object({
68232
- id: RecordIdSchema,
68233
- /** The device this row belongs to (D865 removal, per-device reads). */
68234
- deviceId: number().int().optional(),
68235
- /** The domain clock, ms: observed / created / opened. */
68236
- at: number().int().optional(),
68237
- /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
68238
- expiresAt: number().int().optional(),
68239
- data: RecordBodySchema
68240
- });
68241
- var RecordViewSchema = object({
68242
- id: string(),
68243
- deviceId: number().int().nullable(),
68244
- at: number().nullable(),
68245
- expiresAt: number().nullable(),
68246
- rev: number().int(),
68247
- updatedAt: number(),
68248
- data: RecordBodySchema
68249
- });
68250
- /** Declared index fields only. Anything else does not compile, by design. */
68251
- var RecordWhereSchema = record(RecordFieldPathSchema, union([
68252
- string(),
68253
- number(),
68254
- boolean(),
68255
- object({ in: array(union([
68256
- string(),
68257
- number(),
68258
- boolean()
68259
- ])).min(1).max(RECORD_BATCH_MAX) }),
68260
- object({
68261
- gt: union([string(), number()]).optional(),
68262
- gte: union([string(), number()]).optional(),
68263
- lt: union([string(), number()]).optional(),
68264
- lte: union([string(), number()]).optional()
68265
- }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
68266
- ]));
68267
- var KindShape = { kind: RecordKindNameSchema };
68268
- var TimeWindowShape = {
68269
- deviceId: number().int().optional(),
68270
- atFrom: number().optional(),
68271
- atTo: number().optional()
68272
- };
68273
- var RecordQueryInputSchema = object({
68274
- ...KindShape,
68275
- ...TimeWindowShape,
68276
- where: RecordWhereSchema.optional(),
68277
- /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
68278
- order: _enum(["at", "id"]).optional(),
68279
- desc: boolean().optional(),
68280
- limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
68281
- /** Opaque keyset cursor from a previous answer. There is no offset. */
68282
- cursor: string().min(1).optional()
68283
- });
68284
- var RecordQueryResultSchema = object({
68285
- records: array(RecordViewSchema),
68286
- nextCursor: string().nullable()
68287
- });
68288
- var RecordKindStateSchema = _enum([
68289
- "created",
68290
- "unchanged",
68291
- "updated"
68292
- ]);
68293
- var DeclareKindResultSchema = object({
68294
- kindId: number().int(),
68295
- state: RecordKindStateSchema,
68296
- createdIndexes: array(string())
68297
- });
68298
- /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
68299
- var SecretRefsList = array(string());
68300
- /**
68301
- * One op of a `transact`: the single-row writes, each on a kind of the caller.
68302
- * All ops of one call share ONE database file and ONE `transactFamily`.
68303
- */
68304
- var RecordTransactOpSchema = discriminatedUnion("op", [
68305
- object({
68306
- op: literal("put"),
68307
- kind: RecordKindNameSchema,
68308
- record: RecordInputSchema,
68309
- ifRev: number().int().min(0).optional()
68310
- }),
68311
- object({
68312
- op: literal("patch"),
68313
- kind: RecordKindNameSchema,
68314
- id: RecordIdSchema,
68315
- data: RecordBodySchema,
68316
- at: number().int().nullable().optional(),
68317
- expiresAt: number().int().nullable().optional(),
68318
- ifRev: number().int().min(1).optional()
68319
- }),
68320
- object({
68321
- op: literal("delete"),
68322
- kind: RecordKindNameSchema,
68323
- id: RecordIdSchema
68324
- })
68325
- ]);
68326
- var RecordSecretRefsSchema = record(string(), record(string(), string()));
68327
- method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
68328
- kind: "mutation",
68329
- auth: "admin"
68330
- }), method(object({
68331
- ...KindShape,
68332
- id: RecordIdSchema
68333
- }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
68334
- ...KindShape,
68335
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68336
- }), array(RecordViewSchema), { auth: "admin" }), method(object({
68337
- ...KindShape,
68338
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68339
- }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
68340
- ...KindShape,
68341
- record: RecordInputSchema,
68342
- /** Compare-and-set. `0` means "the row must not exist". */
68343
- ifRev: number().int().min(0).optional()
68344
- }), object({
68345
- rev: number().int(),
68346
- releasedRefs: SecretRefsList
68347
- }), {
68348
- kind: "mutation",
68349
- auth: "admin"
68350
- }), method(object({
68351
- ...KindShape,
68352
- records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
68353
- }), object({
68354
- count: number().int(),
68355
- releasedRefs: SecretRefsList
68356
- }), {
68357
- kind: "mutation",
68358
- auth: "admin"
68359
- }), method(object({
68360
- ...KindShape,
68361
- id: RecordIdSchema,
68362
- data: RecordBodySchema,
68363
- at: number().int().nullable().optional(),
68364
- expiresAt: number().int().nullable().optional(),
68365
- ifRev: number().int().min(1).optional()
68366
- }), object({
68367
- rev: number().int(),
68368
- releasedRefs: SecretRefsList
68369
- }), {
68370
- kind: "mutation",
68371
- auth: "admin"
68372
- }), method(object({
68373
- ...KindShape,
68374
- id: RecordIdSchema
68375
- }), object({
68376
- deleted: boolean(),
68377
- secretRefs: SecretRefsList
68378
- }), {
68379
- kind: "mutation",
68380
- auth: "admin"
68381
- }), method(object({
68382
- ...KindShape,
68383
- ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
68384
- }), object({
68385
- deleted: number().int(),
68386
- secretRefs: SecretRefsList
68387
- }), {
68388
- kind: "mutation",
68389
- auth: "admin"
68390
- }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
68391
- ...KindShape,
68392
- ...TimeWindowShape,
68393
- where: RecordWhereSchema.optional()
68394
- }), object({ count: number().int() }), { auth: "admin" }), method(object({
68395
- ...KindShape,
68396
- deviceId: number().int().optional(),
68397
- from: number(),
68398
- to: number(),
68399
- bucketMs: number().int().positive()
68400
- }), object({ buckets: array(object({
68401
- bucketStart: number(),
68402
- count: number().int()
68403
- })) }), { auth: "admin" }), method(object({
68404
- ...KindShape,
68405
- op: _enum([
68406
- "sum",
68407
- "min",
68408
- "max",
68409
- "count"
68410
- ]),
68411
- field: RecordFieldPathSchema.optional(),
68412
- groupBy: RecordFieldPathSchema.optional(),
68413
- where: RecordWhereSchema.optional()
68414
- }), object({ rows: array(object({
68415
- group: union([
68416
- string(),
68417
- number(),
68418
- _null()
68419
- ]),
68420
- value: number().nullable()
68421
- })) }), { auth: "admin" }), method(object({
68422
- ...KindShape,
68423
- deviceId: number().int().optional(),
68424
- atBefore: number().optional(),
68425
- where: RecordWhereSchema.optional()
68426
- }), object({
68427
- deleted: number().int(),
68428
- secretRefs: SecretRefsList
68429
- }), {
68430
- kind: "mutation",
68431
- auth: "admin"
68432
- }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
68433
- results: array(object({
68434
- rev: number().int().nullable(),
68435
- deleted: boolean()
68436
- })),
68437
- releasedRefs: SecretRefsList,
68438
- secretRefs: SecretRefsList
68439
- }), {
68440
- kind: "mutation",
68441
- auth: "admin"
68442
- }), method(object({
68443
- deviceId: number().int(),
68444
- kinds: array(RecordKindNameSchema).optional()
68445
- }), object({
68446
- deleted: record(string(), number().int()),
68447
- secretRefs: SecretRefsList
68448
- }), {
68449
- kind: "mutation",
68450
- auth: "admin"
68451
- }), method(object(KindShape), object({ id: number().int() }), {
68452
- kind: "mutation",
68453
- auth: "admin"
68454
- });
68455
- /**
68456
68686
  * record-entries - the ONE admin surface for every list of user-created entries
68457
68687
  * (D881): notification targets, brokers, LLM profiles, anything an addon
68458
68688
  * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
@@ -75967,11 +76197,15 @@ discriminatedUnion("kind", [object({
75967
76197
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
75968
76198
  * near F. A device that is not near one level most of the time has no floor.
75969
76199
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
75970
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
75971
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
76200
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76201
+ * display waking): it is neither a run nor a pause. Among the remaining
76202
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76203
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76204
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76205
+ * is no run.
75972
76206
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
75973
- * grid a person would type; stop dwell covers the longest pause; start dwell
75974
- * covers the longest standby blip above T.
76207
+ * grid a person would type; stop dwell covers the longest pause and the
76208
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
75975
76209
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
75976
76210
  * run must hold exactly one true interval and nothing may flap outside.
75977
76211
  */
@@ -75982,6 +76216,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
75982
76216
  30 * SECOND_MS;
75983
76217
  60 * SECOND_MS;
75984
76218
  20 * MINUTE_MS$1;
76219
+ 90 * SECOND_MS;
76220
+ 30 * MINUTE_MS$1;
76221
+ 10 * MINUTE_MS$1;
76222
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
75985
76223
  10 * SECOND_MS;
75986
76224
  10 * SECOND_MS;
75987
76225
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -77000,20 +77238,6 @@ Object.freeze({
77000
77238
  access: "create",
77001
77239
  role: "viewer"
77002
77240
  },
77003
- "broker.add": {
77004
- capName: "broker",
77005
- capScope: "system",
77006
- addonId: null,
77007
- access: "create",
77008
- role: "admin"
77009
- },
77010
- "broker.get": {
77011
- capName: "broker",
77012
- capScope: "system",
77013
- addonId: null,
77014
- access: "view",
77015
- role: "admin"
77016
- },
77017
77241
  "broker.getBrokerConfig": {
77018
77242
  capName: "broker",
77019
77243
  capScope: "system",
@@ -77021,20 +77245,6 @@ Object.freeze({
77021
77245
  access: "view",
77022
77246
  role: "admin"
77023
77247
  },
77024
- "broker.getSettings": {
77025
- capName: "broker",
77026
- capScope: "system",
77027
- addonId: null,
77028
- access: "view",
77029
- role: "admin"
77030
- },
77031
- "broker.getSettingsSchema": {
77032
- capName: "broker",
77033
- capScope: "system",
77034
- addonId: null,
77035
- access: "view",
77036
- role: "admin"
77037
- },
77038
77248
  "broker.getState": {
77039
77249
  capName: "broker",
77040
77250
  capScope: "system",
@@ -77049,20 +77259,6 @@ Object.freeze({
77049
77259
  access: "view",
77050
77260
  role: "admin"
77051
77261
  },
77052
- "broker.list": {
77053
- capName: "broker",
77054
- capScope: "system",
77055
- addonId: null,
77056
- access: "view",
77057
- role: "admin"
77058
- },
77059
- "broker.listProviders": {
77060
- capName: "broker",
77061
- capScope: "system",
77062
- addonId: null,
77063
- access: "view",
77064
- role: "admin"
77065
- },
77066
77262
  "broker.publish": {
77067
77263
  capName: "broker",
77068
77264
  capScope: "system",
@@ -77070,20 +77266,6 @@ Object.freeze({
77070
77266
  access: "create",
77071
77267
  role: "admin"
77072
77268
  },
77073
- "broker.remove": {
77074
- capName: "broker",
77075
- capScope: "system",
77076
- addonId: null,
77077
- access: "delete",
77078
- role: "admin"
77079
- },
77080
- "broker.setSettings": {
77081
- capName: "broker",
77082
- capScope: "system",
77083
- addonId: null,
77084
- access: "create",
77085
- role: "admin"
77086
- },
77087
77269
  "broker.subscribe": {
77088
77270
  capName: "broker",
77089
77271
  capScope: "system",
@@ -77091,20 +77273,6 @@ Object.freeze({
77091
77273
  access: "create",
77092
77274
  role: "admin"
77093
77275
  },
77094
- "broker.testConnection": {
77095
- capName: "broker",
77096
- capScope: "system",
77097
- addonId: null,
77098
- access: "create",
77099
- role: "admin"
77100
- },
77101
- "broker.testSettings": {
77102
- capName: "broker",
77103
- capScope: "system",
77104
- addonId: null,
77105
- access: "create",
77106
- role: "admin"
77107
- },
77108
77276
  "broker.unsubscribe": {
77109
77277
  capName: "broker",
77110
77278
  capScope: "system",
@@ -79737,13 +79905,6 @@ Object.freeze({
79737
79905
  access: "view",
79738
79906
  role: "admin"
79739
79907
  },
79740
- "integrations.getSettings": {
79741
- capName: "integrations",
79742
- capScope: "system",
79743
- addonId: null,
79744
- access: "view",
79745
- role: "admin"
79746
- },
79747
79908
  "integrations.list": {
79748
79909
  capName: "integrations",
79749
79910
  capScope: "system",
@@ -79751,13 +79912,6 @@ Object.freeze({
79751
79912
  access: "view",
79752
79913
  role: "admin"
79753
79914
  },
79754
- "integrations.setSettings": {
79755
- capName: "integrations",
79756
- capScope: "system",
79757
- addonId: null,
79758
- access: "create",
79759
- role: "admin"
79760
- },
79761
79915
  "integrations.testConnection": {
79762
79916
  capName: "integrations",
79763
79917
  capScope: "system",
@@ -80752,6 +80906,13 @@ Object.freeze({
80752
80906
  access: "view",
80753
80907
  role: "admin"
80754
80908
  },
80909
+ "migrations.clearLegacyIntegrationRows": {
80910
+ capName: "migrations",
80911
+ capScope: "system",
80912
+ addonId: null,
80913
+ access: "delete",
80914
+ role: "admin"
80915
+ },
80755
80916
  "migrations.declareOwner": {
80756
80917
  capName: "migrations",
80757
80918
  capScope: "system",
@@ -80759,6 +80920,13 @@ Object.freeze({
80759
80920
  access: "create",
80760
80921
  role: "admin"
80761
80922
  },
80923
+ "migrations.deleteLegacyIntegrationRowKeys": {
80924
+ capName: "migrations",
80925
+ capScope: "system",
80926
+ addonId: null,
80927
+ access: "delete",
80928
+ role: "admin"
80929
+ },
80762
80930
  "migrations.dryRun": {
80763
80931
  capName: "migrations",
80764
80932
  capScope: "system",
@@ -80794,6 +80962,20 @@ Object.freeze({
80794
80962
  access: "create",
80795
80963
  role: "admin"
80796
80964
  },
80965
+ "migrations.patchLegacyIntegrationRows": {
80966
+ capName: "migrations",
80967
+ capScope: "system",
80968
+ addonId: null,
80969
+ access: "create",
80970
+ role: "admin"
80971
+ },
80972
+ "migrations.readLegacyIntegrationRows": {
80973
+ capName: "migrations",
80974
+ capScope: "system",
80975
+ addonId: null,
80976
+ access: "view",
80977
+ role: "admin"
80978
+ },
80797
80979
  "migrations.restorePreMigration": {
80798
80980
  capName: "migrations",
80799
80981
  capScope: "system",
@@ -82054,6 +82236,13 @@ Object.freeze({
82054
82236
  access: "view",
82055
82237
  role: "viewer"
82056
82238
  },
82239
+ "pipelineAnalytics.getMediaLocationCensus": {
82240
+ capName: "pipeline-analytics",
82241
+ capScope: "device",
82242
+ addonId: null,
82243
+ access: "view",
82244
+ role: "admin"
82245
+ },
82057
82246
  "pipelineAnalytics.getMediaReclaimStatus": {
82058
82247
  capName: "pipeline-analytics",
82059
82248
  capScope: "device",
@@ -90995,6 +91184,23 @@ object({
90995
91184
  delay: number().nonnegative().optional(),
90996
91185
  commands: record(string(), unknown())
90997
91186
  });
91187
+ var BrokerInUseError = class extends Error {
91188
+ brokerId;
91189
+ users;
91190
+ name = "BrokerInUseError";
91191
+ constructor(brokerId, users) {
91192
+ super(`broker '${brokerId}' is still in use by ${users.map((u) => `${u.kind} '${u.name ?? u.id}' (${u.id})`).join(", ")}; unlink them first`);
91193
+ this.brokerId = brokerId;
91194
+ this.users = users;
91195
+ }
91196
+ };
91197
+ /** Throw {@link BrokerInUseError} when any finder names a user of `brokerId`. */
91198
+ async function assertBrokerUnused(brokerId, finders) {
91199
+ const users = [];
91200
+ for (const finder of finders) users.push(...await finder.find(brokerId));
91201
+ if (users.length > 0) throw new BrokerInUseError(brokerId, users);
91202
+ }
91203
+ entryBody({ brokerId: string() });
90998
91204
  //#endregion
90999
91205
  //#region src/relay-record-kinds.ts
91000
91206
  /**
@@ -91244,29 +91450,6 @@ var MessageRing = class {
91244
91450
  }
91245
91451
  };
91246
91452
  //#endregion
91247
- //#region src/mailbox-record-kinds.ts
91248
- /**
91249
- * The `smtp-inbound` mailbox as a record (D999): one `mailboxes` row per mailbox, owned by
91250
- * `smtp-nodemailer`. The row id is the broker id. The SMTP AUTH password is a keychain-bound secret
91251
- * field; nothing else holds a copy.
91252
- */
91253
- var MailboxBodySchema = entryBody({
91254
- port: number().int().min(1).max(65535),
91255
- bindHost: string().min(1),
91256
- domain: string().min(1),
91257
- requireAuth: boolean(),
91258
- username: string(),
91259
- password: string().optional().meta({ secret: true }),
91260
- starttls: boolean(),
91261
- maxMessageBytes: number().int().positive()
91262
- });
91263
- var mailboxesKind = defineEntryKind("mailboxes", {
91264
- title: "Inbound mailboxes",
91265
- nameField: "name",
91266
- schema: MailboxBodySchema,
91267
- writeTrigger: "an operator (or a consumer addon, once) adds or edits an inbound mailbox"
91268
- });
91269
- //#endregion
91270
91453
  //#region src/inbound-broker.ts
91271
91454
  /**
91272
91455
  * The `smtp-inbound` broker kind (D999): a mailbox a device can send alarm mail to.
@@ -91280,7 +91463,6 @@ var mailboxesKind = defineEntryKind("mailboxes", {
91280
91463
  var REBIND_DELAY_MS = 5e3;
91281
91464
  var MAX_REBIND_ATTEMPTS = 6;
91282
91465
  var NO_INTERNAL_HOST_REASON = "The hub has no internal address. Flag one as \"Internal\" under System → Network; cameras need it to reach this mailbox.";
91283
- var KIND_LABEL = "Inbound mailbox (SMTP)";
91284
91466
  var PAGE = 500;
91285
91467
  /** The hostname of a role's resolved base URL (`http://192.0.2.10:4480` → `192.0.2.10`). */
91286
91468
  function hostFromRole(resolved) {
@@ -91353,6 +91535,20 @@ var SmtpInboundBroker = class {
91353
91535
  this.rebindTimers.clear();
91354
91536
  await Promise.all([...this.runtimes.keys()].map((id) => this.unbind(id)));
91355
91537
  }
91538
+ /** Bring the listener in line with the record after the instances page changed it. */
91539
+ reconcileMailbox(id) {
91540
+ return this.reconcile(id);
91541
+ }
91542
+ /** The runtime's view of one mailbox; null while it has no listener. */
91543
+ runtimeOf(id) {
91544
+ const runtime = this.runtimes.get(id);
91545
+ if (runtime === void 0) return null;
91546
+ return {
91547
+ status: runtime.status,
91548
+ error: runtime.error,
91549
+ checkedAt: runtime.checkedAt
91550
+ };
91551
+ }
91356
91552
  reconcile(id) {
91357
91553
  const next = (this.chains.get(id) ?? Promise.resolve()).then(() => this.applyRecord(id));
91358
91554
  this.chains.set(id, next.catch(() => void 0));
@@ -91470,28 +91666,6 @@ var SmtpInboundBroker = class {
91470
91666
  payload: message
91471
91667
  });
91472
91668
  }
91473
- async exists(id) {
91474
- return await this.deps.handle.get(id) !== null;
91475
- }
91476
- async info(id) {
91477
- const row = await this.deps.handle.get(id);
91478
- if (row === null) return null;
91479
- const runtime = this.runtimes.get(id);
91480
- return {
91481
- id,
91482
- addonId: this.deps.addonId,
91483
- name: row.data.name,
91484
- kind: SMTP_INBOUND_KIND,
91485
- status: runtime?.status ?? "disconnected",
91486
- info: {
91487
- port: row.data.port,
91488
- domain: row.data.domain,
91489
- recipients: runtime === void 0 ? 0 : [...runtime.subscriptions.values()].reduce((n, s) => n + s.recipients.size, 0)
91490
- },
91491
- lastCheckedAt: runtime?.checkedAt ?? null,
91492
- error: runtime?.error ?? null
91493
- };
91494
- }
91495
91669
  async subscribe(brokerId, filter) {
91496
91670
  const parsed = SmtpInboundFilterSchema.parse(filter);
91497
91671
  const runtime = this.runtimes.get(brokerId);
@@ -91511,85 +91685,19 @@ var SmtpInboundBroker = class {
91511
91685
  runtime.subscriptions = next;
91512
91686
  return { subscriptionId: id };
91513
91687
  }
91688
+ /** The subscribers (camera email push) that still read one mailbox. */
91689
+ subscriberUsers() {
91690
+ return { find: async (brokerId) => [...this.runtimes.get(brokerId)?.subscriptions.values() ?? []].map((sub) => ({
91691
+ kind: "subscriber",
91692
+ id: sub.subscriberId
91693
+ })) };
91694
+ }
91695
+ /**
91696
+ * The `broker` cap is the bus only (D1013): the mailboxes themselves are the `mailboxes` kind of
91697
+ * the addon's `integration-instances` provider (`mailbox-instances.ts`).
91698
+ */
91514
91699
  provider() {
91515
- const owns = (id) => this.exists(id);
91516
91700
  return {
91517
- list: async ({ kind }) => {
91518
- if (kind !== void 0 && kind !== "smtp-inbound") return [];
91519
- const page = await this.deps.handle.query({ limit: PAGE });
91520
- return (await Promise.all(page.records.map((r) => this.info(r.id)))).flatMap((i) => i === null ? [] : [i]);
91521
- },
91522
- get: async ({ id }) => this.info(id),
91523
- listProviders: async () => [{
91524
- addonId: this.deps.addonId,
91525
- kinds: [{
91526
- kind: SMTP_INBOUND_KIND,
91527
- label: KIND_LABEL
91528
- }]
91529
- }],
91530
- add: async ({ kind, name, id, settings }) => {
91531
- if (kind !== "smtp-inbound") throw new Error(`smtp-nodemailer: only kind '${SMTP_INBOUND_KIND}' is handled here (got '${kind}')`);
91532
- const parsed = SmtpInboundSettingsSchema.parse(settings);
91533
- const brokerId = id ?? `smtp-inbound-${randomUUID().slice(0, 8)}`;
91534
- if (await this.exists(brokerId)) throw new Error(`broker id '${brokerId}' is already in use`);
91535
- const body = MailboxBodySchema.parse({
91536
- name,
91537
- port: parsed.port,
91538
- bindHost: parsed.bindHost,
91539
- domain: parsed.domain,
91540
- requireAuth: parsed.requireAuth,
91541
- username: parsed.username.length > 0 ? parsed.username : `camstack-${randomBytes(4).toString("hex")}`,
91542
- password: parsed.password.length > 0 ? parsed.password : randomBytes(18).toString("base64url"),
91543
- starttls: parsed.starttls,
91544
- maxMessageBytes: parsed.maxMessageBytes
91545
- });
91546
- await this.deps.handle.put({
91547
- id: brokerId,
91548
- data: body
91549
- }, { ifRev: 0 });
91550
- await this.reconcile(brokerId);
91551
- return { id: brokerId };
91552
- },
91553
- remove: async ({ id }) => {
91554
- if (!await owns(id)) return;
91555
- await this.deps.handle.delete(id);
91556
- await this.reconcile(id);
91557
- },
91558
- testConnection: async ({ id }) => {
91559
- if (!await owns(id)) return {
91560
- ok: false,
91561
- error: "unknown broker"
91562
- };
91563
- const runtime = this.runtimes.get(id);
91564
- if (runtime?.status === "connected") return {
91565
- ok: true,
91566
- latencyMs: 0
91567
- };
91568
- return {
91569
- ok: false,
91570
- error: runtime?.error ?? "The mailbox is not listening"
91571
- };
91572
- },
91573
- getSettings: async ({ id }) => {
91574
- const row = await this.deps.handle.get(id);
91575
- if (row === null) return null;
91576
- const { name: _name, password: _password, ...settings } = row.data;
91577
- return settings;
91578
- },
91579
- setSettings: async ({ id, settings }) => {
91580
- const row = await this.deps.handle.getResolved(id);
91581
- if (row === null) return;
91582
- const parsed = SmtpInboundSettingsSchema.partial().parse(settings);
91583
- const next = MailboxBodySchema.parse({
91584
- ...row.data,
91585
- ...Object.fromEntries(Object.entries(parsed).filter(([, v]) => v !== void 0 && v !== ""))
91586
- });
91587
- await this.deps.handle.put({
91588
- id,
91589
- data: next
91590
- });
91591
- await this.reconcile(id);
91592
- },
91593
91701
  getBrokerConfig: async ({ id }) => {
91594
91702
  let row;
91595
91703
  try {
@@ -91611,19 +91719,6 @@ var SmtpInboundBroker = class {
91611
91719
  starttls: row.data.starttls
91612
91720
  };
91613
91721
  },
91614
- getSettingsSchema: async ({ kind }) => kind === "smtp-inbound" ? mailboxSettingsSchema() : null,
91615
- testSettings: async ({ kind, settings }) => {
91616
- if (kind !== "smtp-inbound") return {
91617
- ok: false,
91618
- error: `unsupported kind: ${kind}`
91619
- };
91620
- const parsed = SmtpInboundSettingsSchema.safeParse(settings);
91621
- if (!parsed.success) return {
91622
- ok: false,
91623
- error: parsed.error.message
91624
- };
91625
- return probeBind(parsed.data.port, parsed.data.bindHost);
91626
- },
91627
91722
  publish: async () => {
91628
91723
  throw new Error("smtp-inbound: a mailbox only receives — publish is not supported");
91629
91724
  },
@@ -91651,23 +91746,6 @@ var SmtpInboundBroker = class {
91651
91746
  };
91652
91747
  }
91653
91748
  };
91654
- /** A bind probe: can this port be taken on this host right now? */
91655
- function probeBind(port, host) {
91656
- const started = Date.now();
91657
- return new Promise((resolve) => {
91658
- const probe = createServer();
91659
- probe.once("error", (err) => resolve({
91660
- ok: false,
91661
- error: err.message
91662
- }));
91663
- probe.listen(port, host, () => {
91664
- probe.close(() => resolve({
91665
- ok: true,
91666
- latencyMs: Date.now() - started
91667
- }));
91668
- });
91669
- });
91670
- }
91671
91749
  function mailboxSettingsSchema() {
91672
91750
  return { sections: [{
91673
91751
  id: "mailbox",
@@ -135957,6 +136035,145 @@ var SmtpServerListener = class {
135957
136035
  }
135958
136036
  };
135959
136037
  //#endregion
136038
+ //#region src/bind-probe.ts
136039
+ /** A bind probe: can this port be taken on this host right now? */
136040
+ function probeBind(port, host) {
136041
+ const started = Date.now();
136042
+ return new Promise((resolve) => {
136043
+ const probe = createServer();
136044
+ probe.once("error", (err) => resolve({
136045
+ ok: false,
136046
+ error: err.message
136047
+ }));
136048
+ probe.listen(port, host, () => {
136049
+ probe.close(() => resolve({
136050
+ ok: true,
136051
+ latencyMs: Date.now() - started
136052
+ }));
136053
+ });
136054
+ });
136055
+ }
136056
+ //#endregion
136057
+ //#region src/mailbox-record-kinds.ts
136058
+ /**
136059
+ * The `smtp-inbound` mailbox as a record (D999): one `mailboxes` row per mailbox, owned by
136060
+ * `smtp-nodemailer`. The row id is the broker id. The SMTP AUTH password is a keychain-bound secret
136061
+ * field; nothing else holds a copy.
136062
+ */
136063
+ var MailboxBodySchema = entryBody({
136064
+ port: number().int().min(1).max(65535),
136065
+ bindHost: string().min(1),
136066
+ domain: string().min(1),
136067
+ requireAuth: boolean(),
136068
+ username: string(),
136069
+ password: string().optional().meta({ secret: true }),
136070
+ starttls: boolean(),
136071
+ maxMessageBytes: number().int().positive()
136072
+ });
136073
+ var mailboxesKind = defineEntryKind("mailboxes", {
136074
+ title: "Inbound mailboxes",
136075
+ nameField: "name",
136076
+ schema: MailboxBodySchema,
136077
+ writeTrigger: "an operator (or a consumer addon, once) adds or edits an inbound mailbox"
136078
+ });
136079
+ //#endregion
136080
+ //#region src/mailbox-instances.ts
136081
+ /**
136082
+ * The inbound mailboxes as `integration-instances` (D1013): one instance per `mailboxes` record,
136083
+ * over the SAME records and ids the `smtp-inbound` broker serves; the `mailboxes` kind of the
136084
+ * composed provider (the relays are the `relays` kind). A change re-applies the listener; removing
136085
+ * a mailbox a subscriber (camera email push) still reads is refused.
136086
+ */
136087
+ var MAILBOX_INSTANCE_KIND = "mailboxes";
136088
+ function statusOf(runtime) {
136089
+ if (runtime === null) return {
136090
+ state: "unknown",
136091
+ label: "Not listening"
136092
+ };
136093
+ if (runtime.status === "connected") return {
136094
+ state: "ok",
136095
+ label: "Listening"
136096
+ };
136097
+ return {
136098
+ state: "error",
136099
+ label: runtime.status,
136100
+ ...runtime.error !== null ? { detail: runtime.error } : {}
136101
+ };
136102
+ }
136103
+ function newMailboxBody(name) {
136104
+ const defaults = SmtpInboundSettingsSchema.parse({});
136105
+ return MailboxBodySchema.parse({
136106
+ name,
136107
+ port: defaults.port,
136108
+ bindHost: defaults.bindHost,
136109
+ domain: defaults.domain,
136110
+ requireAuth: defaults.requireAuth,
136111
+ username: `camstack-${randomBytes(4).toString("hex")}`,
136112
+ password: randomBytes(18).toString("base64url"),
136113
+ starttls: defaults.starttls,
136114
+ maxMessageBytes: defaults.maxMessageBytes
136115
+ });
136116
+ }
136117
+ function buildMailboxInstances(records, deps) {
136118
+ const users = deps.inbound.subscriberUsers();
136119
+ const base = new RecordIntegrationInstances(records, mailboxesKind, {
136120
+ description: {
136121
+ role: "services",
136122
+ instanceMode: "multiple",
136123
+ kind: "mailbox",
136124
+ canTest: true
136125
+ },
136126
+ formSchema: () => mailboxSettingsSchema(),
136127
+ initialBody: (name) => newMailboxBody(name),
136128
+ mintId: () => `smtp-inbound-${randomUUID().slice(0, 8)}`,
136129
+ configured: () => true,
136130
+ health: (_body, id) => {
136131
+ const runtime = deps.inbound.runtimeOf(id);
136132
+ return {
136133
+ status: statusOf(runtime),
136134
+ ...runtime !== null ? { lastCheckedAt: runtime.checkedAt } : {}
136135
+ };
136136
+ },
136137
+ onChanged: (id) => deps.inbound.reconcileMailbox(id),
136138
+ test: async ({ id, values }) => {
136139
+ if (id === null) {
136140
+ const parsed = SmtpInboundSettingsSchema.safeParse(values);
136141
+ if (!parsed.success) return {
136142
+ success: false,
136143
+ status: "rejected",
136144
+ error: parsed.error.message
136145
+ };
136146
+ const bound = await probeBind(parsed.data.port, parsed.data.bindHost);
136147
+ return bound.ok ? {
136148
+ success: true,
136149
+ status: "validated",
136150
+ latencyMs: bound.latencyMs
136151
+ } : {
136152
+ success: false,
136153
+ status: "rejected",
136154
+ error: bound.error
136155
+ };
136156
+ }
136157
+ const runtime = deps.inbound.runtimeOf(id);
136158
+ return runtime?.status === "connected" ? {
136159
+ success: true,
136160
+ status: "validated"
136161
+ } : {
136162
+ success: false,
136163
+ status: "rejected",
136164
+ error: runtime?.error ?? "The mailbox is not listening"
136165
+ };
136166
+ }
136167
+ }).toProvider();
136168
+ return {
136169
+ ...base,
136170
+ deleteInstance: async (input) => {
136171
+ await assertBrokerUnused(input.id, [users]);
136172
+ return base.deleteInstance(input);
136173
+ }
136174
+ };
136175
+ }
136176
+ //#endregion
135960
136177
  //#region ../../node_modules/nodemailer/lib/mime-funcs/mime-types.js
135961
136178
  var require_mime_types = /* @__PURE__ */ __commonJSMin(((exports, module) => {
135962
136179
  var path$1 = __require("path");
@@ -145690,7 +145907,29 @@ var SmtpNodemailerAddon = class extends BaseAddon {
145690
145907
  },
145691
145908
  {
145692
145909
  capability: integrationInstancesCapability,
145693
- provider: this.instances.toProvider()
145910
+ provider: composeInstanceKinds([{
145911
+ kind: {
145912
+ id: "relays",
145913
+ label: "SMTP relay",
145914
+ functionCap: "smtp-provider",
145915
+ role: "services"
145916
+ },
145917
+ provider: this.instances.toProvider()
145918
+ }, {
145919
+ kind: {
145920
+ id: MAILBOX_INSTANCE_KIND,
145921
+ label: "Inbound mailbox (SMTP)",
145922
+ functionCap: "broker",
145923
+ brokerKind: SMTP_INBOUND_KIND,
145924
+ role: "services"
145925
+ },
145926
+ provider: buildMailboxInstances(records, { inbound })
145927
+ }], {
145928
+ role: "services",
145929
+ instanceMode: "multiple",
145930
+ kind: "relays",
145931
+ canTest: true
145932
+ })
145694
145933
  },
145695
145934
  {
145696
145935
  capability: brokerCapability,
@@ -145844,7 +146083,8 @@ var SmtpNodemailerAddon = class extends BaseAddon {
145844
146083
  const rows = (await records.kind(smtpRelaysKind).query({ limit: RELAY_LIST_MAX })).records;
145845
146084
  return (rows.find((row) => row.data.host.length > 0) ?? rows[0])?.id;
145846
146085
  }
145847
- globalSettingsSchema() {
146086
+ /** The relay form. Its values live on the relay record, never in `addon-settings`. */
146087
+ async integrationFormSchema(_instance) {
145848
146088
  return this.relayForm();
145849
146089
  }
145850
146090
  async getGlobalSettings() {