@camstack/addon-import-alexa 0.2.194 → 0.2.196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -48,7 +48,7 @@ node_fs = __toESM(node_fs, 1);
48
48
  let node_child_process = require("node:child_process");
49
49
  require("node:zlib");
50
50
  let node_url = require("node:url");
51
- //#region ../types/dist/event-category-BmPm98n0.mjs
51
+ //#region ../types/dist/event-category-78tcBemo.mjs
52
52
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
53
53
  EventCategory["SystemBoot"] = "system.boot";
54
54
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -169,6 +169,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
169
169
  */
170
170
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
171
171
  /**
172
+ * An addon is RUNNING although its installed package is below what the
173
+ * running server requires (`camstack.requires.addons` of the server, or the
174
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
175
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
176
+ * operator alert per node and package. The addon is never held back for it.
177
+ *
178
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
179
+ */
180
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
181
+ /**
182
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
183
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
184
+ *
185
+ * Payload: `{ packageName, addonId, nodeId }`.
186
+ */
187
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
188
+ /**
172
189
  * Monitor scheduled the next retry for a failed addon. Transient —
173
190
  * surfaced to the UI for live-updating the "next retry in Ns"
174
191
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -279,7 +296,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
279
296
  EventCategory["ProcessRestarted"] = "process.restarted";
280
297
  /**
281
298
  * The SET of storage locations changed — one was created, edited, enabled,
282
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
299
+ * disabled or deleted through the location instances or the forced-delete action.
283
300
  *
284
301
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
285
302
  * it must also converge on its own periodic path, because a dropped event
@@ -6044,7 +6061,7 @@ function number(params) {
6044
6061
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6045
6062
  }
6046
6063
  //#endregion
6047
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6064
+ //#region ../types/dist/sleep-CQP-REJp.mjs
6048
6065
  /**
6049
6066
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6050
6067
  * window to float samples (D455).
@@ -6408,6 +6425,59 @@ Object.fromEntries([
6408
6425
  function isValuelessField(field) {
6409
6426
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
6410
6427
  }
6428
+ /**
6429
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6430
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6431
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6432
+ * pre-resolving) lets callers both read the effective value AND detect
6433
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6434
+ * malformed input yields an empty map, never throws.
6435
+ */
6436
+ function collectHydratedFieldEntries(schema) {
6437
+ const out = /* @__PURE__ */ new Map();
6438
+ if (typeof schema !== "object" || schema === null) return out;
6439
+ const sections = schema.sections;
6440
+ if (!Array.isArray(sections)) return out;
6441
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6442
+ return out;
6443
+ }
6444
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6445
+ function collectFieldEntriesInto(fields, into) {
6446
+ if (!Array.isArray(fields)) return;
6447
+ for (const field of fields) {
6448
+ if (typeof field !== "object" || field === null) continue;
6449
+ const type = field.type;
6450
+ if (type === "group") {
6451
+ collectFieldEntriesInto(field.fields, into);
6452
+ continue;
6453
+ }
6454
+ if (type === "sub-tabs") {
6455
+ const tabs = field.tabs;
6456
+ if (Array.isArray(tabs)) {
6457
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6458
+ }
6459
+ continue;
6460
+ }
6461
+ const key = field.key;
6462
+ if (typeof key !== "string" || key.length === 0) continue;
6463
+ if (!("value" in field)) continue;
6464
+ into.set(key, {
6465
+ value: field.value,
6466
+ default: field.default
6467
+ });
6468
+ }
6469
+ }
6470
+ /**
6471
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6472
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6473
+ * common "just give me the resolved values" case. Built on
6474
+ * {@link collectHydratedFieldEntries}.
6475
+ */
6476
+ function collectHydratedFieldValues(schema) {
6477
+ const out = /* @__PURE__ */ new Map();
6478
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6479
+ return out;
6480
+ }
6411
6481
  function hydrateSchema(schema, values, options = {}) {
6412
6482
  return {
6413
6483
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7308,17 +7378,35 @@ var RecordIntegrationInstances = class {
7308
7378
  ...kind !== void 0 ? { kind } : {}
7309
7379
  });
7310
7380
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7381
+ const initial = this.hooks.initialBody(name, kind);
7382
+ const patch = this.patchOf(values ?? {});
7383
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7384
+ kind,
7385
+ body: {
7386
+ ...initial,
7387
+ ...patch
7388
+ }
7389
+ }));
7311
7390
  await this.handle.put({
7312
7391
  id: minted,
7313
- data: this.hooks.initialBody(name, kind)
7392
+ data: initial
7314
7393
  }, { ifRev: 0 });
7315
- const patch = this.patchOf(values ?? {});
7316
7394
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7317
7395
  await this.hooks.onChanged?.(minted, "created");
7318
7396
  return { id: minted };
7319
7397
  };
7320
7398
  updateInstance = async ({ id, name, values }) => {
7321
7399
  const patch = this.patchOf(values);
7400
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7401
+ const stored = await this.handle.get(id);
7402
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7403
+ kind: void 0,
7404
+ body: {
7405
+ ...stored.data,
7406
+ ...patch
7407
+ }
7408
+ }));
7409
+ }
7322
7410
  if (name !== void 0) patch[this.nameField] = name;
7323
7411
  await this.handle.patch(id, patch);
7324
7412
  await this.hooks.onChanged?.(id, "updated");
@@ -7629,6 +7717,21 @@ var SCOPED_SETTINGS_SCOPES = [
7629
7717
  "device-defaults",
7630
7718
  "node"
7631
7719
  ];
7720
+ /** The one refusal of a key a scope did not declare. */
7721
+ function assertDeclaredKeys(keys, declared) {
7722
+ const undeclared = keys.filter((key) => !declared.has(key));
7723
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7724
+ }
7725
+ /** Build the handle over an addon's scoped read, write and unset. */
7726
+ function createScopedSettings(deps) {
7727
+ return {
7728
+ async get() {
7729
+ return deps.schema.parse(await deps.read());
7730
+ },
7731
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7732
+ unset: (keys) => deps.unset(keys)
7733
+ };
7734
+ }
7632
7735
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7633
7736
  var KeychainWriteRefusedError = class extends Error {
7634
7737
  fieldPath;
@@ -8204,7 +8307,16 @@ function createHandle(raw, opts) {
8204
8307
  return blob;
8205
8308
  }
8206
8309
  },
8207
- writeAddonStore: writeAddon
8310
+ writeAddonStore: writeAddon,
8311
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8312
+ const stored = await readStoredAddonBlob(raw);
8313
+ index.seen("addon", stored);
8314
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8315
+ if (removable.length === 0) return;
8316
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8317
+ await raw.writeAddonStore(patch);
8318
+ index.wrote("addon", patch);
8319
+ })
8208
8320
  };
8209
8321
  }
8210
8322
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8749,6 +8861,151 @@ var BaseAddon = class {
8749
8861
  }
8750
8862
  }
8751
8863
  /**
8864
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8865
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8866
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8867
+ */
8868
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8869
+ return null;
8870
+ }
8871
+ /**
8872
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8873
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8874
+ */
8875
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8876
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8877
+ clusterSettings(cap, schema) {
8878
+ return this.scopedHandle("cluster", cap, schema);
8879
+ }
8880
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8881
+ deviceDefaults(cap, schema) {
8882
+ return this.scopedHandle("device-defaults", cap, schema);
8883
+ }
8884
+ /**
8885
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8886
+ * with no row yields the schema defaults — never a bare-key fallback.
8887
+ */
8888
+ nodeSettings(cap, schema, nodeId) {
8889
+ return this.scopedHandle("node", cap, schema, nodeId);
8890
+ }
8891
+ /**
8892
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8893
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8894
+ * central delete. `nodeId` omitted ⇒ the local node.
8895
+ */
8896
+ nodeState(key, schema, fallback, nodeId) {
8897
+ this._declaredStateKeys.add(key);
8898
+ return createDurableState({
8899
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8900
+ schema,
8901
+ fallback,
8902
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8903
+ write: async (patch) => {
8904
+ await this._ctx?.settings?.writeAddonStore(patch);
8905
+ },
8906
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8907
+ addonId: this._ctx?.id,
8908
+ key: k,
8909
+ error: String(e)
8910
+ } })
8911
+ });
8912
+ }
8913
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8914
+ async declaredScopedSchema(scope, cap) {
8915
+ await this.loadScopedSchemas(cap);
8916
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8917
+ }
8918
+ async requireScopedSchema(scope, cap) {
8919
+ const schema = await this.declaredScopedSchema(scope, cap);
8920
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8921
+ return schema;
8922
+ }
8923
+ scopedKeys(schema) {
8924
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8925
+ }
8926
+ /** `store` narrowed to the keys `schema` declares. */
8927
+ narrowToScope(store, schema) {
8928
+ const keys = this.scopedKeys(schema);
8929
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8930
+ }
8931
+ scopedHandle(scope, cap, schema, nodeId) {
8932
+ return createScopedSettings({
8933
+ schema,
8934
+ read: async () => {
8935
+ const declared = await this.requireScopedSchema(scope, cap);
8936
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8937
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8938
+ return {
8939
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8940
+ ...stored
8941
+ };
8942
+ },
8943
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8944
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8945
+ });
8946
+ }
8947
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8948
+ async writeScoped(scope, cap, patch, nodeId) {
8949
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8950
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8951
+ }
8952
+ /**
8953
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8954
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8955
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8956
+ */
8957
+ async unsetScoped(scope, cap, keys, nodeId) {
8958
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8959
+ const target = scope === "node" ? nodeId : void 0;
8960
+ const tree = this.addonSecretTree();
8961
+ const secretKeys = keys.filter((key) => tree.has(key));
8962
+ const plainKeys = keys.filter((key) => !tree.has(key));
8963
+ let changed = false;
8964
+ if (plainKeys.length > 0) {
8965
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8966
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8967
+ }
8968
+ if (secretKeys.length > 0) {
8969
+ await this.clearSecretKeys(secretKeys, target, cap);
8970
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8971
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8972
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8973
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8974
+ changed = true;
8975
+ }
8976
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8977
+ }
8978
+ /**
8979
+ * The hydrated form of one scope of one capability's settings (any
8980
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8981
+ */
8982
+ async getScopedSettings(scope, cap, nodeId) {
8983
+ const schema = await this.declaredScopedSchema(scope, cap);
8984
+ if (schema === null) return null;
8985
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8986
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8987
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8988
+ }
8989
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8990
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8991
+ this._operatorWriteDepth++;
8992
+ try {
8993
+ await this.writeScoped(scope, cap, patch, nodeId);
8994
+ } finally {
8995
+ this._operatorWriteDepth--;
8996
+ }
8997
+ }
8998
+ /**
8999
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
9000
+ * its value. `null` when the capability declares nothing for the scope.
9001
+ */
9002
+ async readScopedValues(scope, cap, nodeId) {
9003
+ const schema = await this.declaredScopedSchema(scope, cap);
9004
+ if (schema === null) return null;
9005
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
9006
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
9007
+ }
9008
+ /**
8752
9009
  * What the create wizard and the integration edit page render for one instance: the addon's
8753
9010
  * per-instance schema when it declares one, else its global schema (what they always showed).
8754
9011
  */
@@ -8900,6 +9157,9 @@ var BaseAddon = class {
8900
9157
  * a keychain (an older kernel) it writes `''`, as such code always did.
8901
9158
  */
8902
9159
  async clearSecrets(keys, nodeId) {
9160
+ await this.clearSecretKeys(keys, nodeId);
9161
+ }
9162
+ async clearSecretKeys(keys, nodeId, cap) {
8903
9163
  const tree = this.addonSecretTree();
8904
9164
  const keychain = this._ctx?.secrets !== void 0;
8905
9165
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8912,25 +9172,27 @@ var BaseAddon = class {
8912
9172
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8913
9173
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8914
9174
  }
8915
- await this.applyGlobalPatch(patch, nodeId);
9175
+ await this.applyGlobalPatch(patch, nodeId, cap);
8916
9176
  }
8917
- async applyGlobalPatch(barePatch, nodeId) {
8918
- await this.loadScopedSchemas();
8919
- const keys = this.perNodeKeys();
9177
+ /** Resolves `true` when the write changed stored values. */
9178
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9179
+ await this.loadScopedSchemas(cap);
9180
+ const keys = this.perNodeKeys(cap);
8920
9181
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8921
9182
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8922
9183
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8923
9184
  const handle = this._secretSettings;
8924
9185
  if (handle !== null) {
8925
9186
  const { changed } = await handle.writeAddonStore(scoped);
8926
- if (!changed) return;
9187
+ if (!changed) return false;
8927
9188
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8928
- if (target !== localNode) return;
9189
+ if (target !== localNode) return true;
8929
9190
  await this.resolveConfig();
8930
9191
  await this.onConfigChanged();
8931
9192
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8932
9193
  const legacy = this.globalSettingsSchema();
8933
9194
  this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9195
+ return true;
8934
9196
  }
8935
9197
  /**
8936
9198
  * The set of field keys the global settings schema declares `perNode: true`
@@ -10664,7 +10926,6 @@ var CAP_NAMES = Object.freeze([
10664
10926
  "notifier",
10665
10927
  "numeric-sensor",
10666
10928
  "oauth-integration",
10667
- "oidc-connections",
10668
10929
  "osd",
10669
10930
  "osd-manager",
10670
10931
  "pet-feeder",
@@ -12556,8 +12817,7 @@ Object.freeze({
12556
12817
  "device-provider": { "reloadDevice": 18e4 },
12557
12818
  "llm": {
12558
12819
  "generate": 6e5,
12559
- "generateVision": 6e5,
12560
- "testProfile": 6e5
12820
+ "generateVision": 6e5
12561
12821
  },
12562
12822
  "llm-runtime": {
12563
12823
  "complete": 6e5,
@@ -14168,17 +14428,18 @@ function composeInstanceKinds(parts, base) {
14168
14428
  * message to what the kind supports — callers never special-case a service.
14169
14429
  *
14170
14430
  * DESIGN DECISIONS (locked):
14171
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
14172
- * `setTargetEnabled`), each provider persisting via the `settings-store`
14173
- * cap. Rationale: the admin UI needs one uniform surface across the
14174
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
14175
- * alternative would fork the UI per addon and cannot host the
14176
- * discovery→adopt flow.
14177
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
14178
- * the generated cap-mount auto-`concatCollection`-fans them across every
14179
- * registered provider (notifiers addon + HA addon) so one catalog is
14180
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
14181
- * `addonId` the generated collection router extracts from the call input.
14431
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14432
+ * providers serve their targets as `integration-instances` records, one
14433
+ * record kind per target kind, and the admin lists, adds, edits, switches
14434
+ * and deletes them through that cap. This cap keeps what an instance cannot
14435
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14436
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14437
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14438
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14439
+ * cap-mount auto-`concatCollection`-fans them across every registered
14440
+ * provider so one catalog is routable. `send` / `testTarget` /
14441
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14442
+ * collection router extracts from the call input.
14182
14443
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14183
14444
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14184
14445
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14423,7 +14684,7 @@ var TargetKindCapsSchema = object({
14423
14684
  * the union is large and not meant for runtime validation here; the exported
14424
14685
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14425
14686
  */
14426
- var ConfigSchemaPassthrough$4 = unknown();
14687
+ var ConfigSchemaPassthrough$3 = unknown();
14427
14688
  var TargetKindSchema = object({
14428
14689
  kind: string(),
14429
14690
  label: string(),
@@ -14460,7 +14721,7 @@ var TargetKindSchema = object({
14460
14721
  * its raster path, which is the safe default for an unknown image.
14461
14722
  */
14462
14723
  iconMediaType: string().optional(),
14463
- configSchema: ConfigSchemaPassthrough$4,
14724
+ configSchema: ConfigSchemaPassthrough$3,
14464
14725
  /**
14465
14726
  * The config keys that decide WHERE this kind's secrets are sent (server,
14466
14727
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14482,9 +14743,11 @@ var TargetKindSchema = object({
14482
14743
  caps: TargetKindCapsSchema
14483
14744
  });
14484
14745
  /**
14485
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14486
- * (return a presence marker only) when serving `listTargets` — never
14487
- * round-trip a stored secret to the UI.
14746
+ * A target as a provider's own code sees it (the notification-center bridge, the
14747
+ * send path): the flat record body split into the name, the switch and the
14748
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14749
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14750
+ * secret to the UI.
14488
14751
  */
14489
14752
  var TargetSchema = object({
14490
14753
  id: string(),
@@ -14533,21 +14796,13 @@ var notificationOutputCapability = {
14533
14796
  mode: "collection",
14534
14797
  methods: {
14535
14798
  listTargetKinds: method(object({}), array(TargetKindSchema)),
14536
- listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
14537
- /**
14538
- * One target WITH secrets — admin edit / duplicate hydration only.
14539
- * `listTargets` stays redacted; the editor loads this so password fields
14540
- * are filled (masked in the input) and a save can round-trip the token
14541
- * when a destination changes (Duplicate with a new chatId, etc.).
14542
- */
14543
- getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
14544
14799
  discoverTargets: method(object({
14545
14800
  kind: string(),
14546
14801
  config: record(string(), unknown()).optional(),
14547
14802
  /**
14548
14803
  * When editing / duplicating, the blank password field never round-trips
14549
14804
  * the stored token. Pass the source target id so the provider can restore
14550
- * secrets server-side before dialling (same merge as upsert).
14805
+ * secrets server-side before dialling.
14551
14806
  */
14552
14807
  targetId: string().optional()
14553
14808
  }), array(DiscoveredTargetSchema), { auth: "admin" }),
@@ -14573,30 +14828,6 @@ var notificationOutputCapability = {
14573
14828
  }), TestResultSchema, {
14574
14829
  kind: "mutation",
14575
14830
  auth: "admin"
14576
- }),
14577
- upsertTarget: method(object({
14578
- target: TargetSchema,
14579
- /**
14580
- * Duplicate flow: create a NEW target id while copying secrets from an
14581
- * existing row. The UI never holds the stored token; blank / redacted
14582
- * password fields are restored from this source (D634 still applies if
14583
- * destinations diverge without a re-typed secret).
14584
- */
14585
- cloneSecretsFrom: string().optional()
14586
- }), TargetSchema, {
14587
- kind: "mutation",
14588
- auth: "admin"
14589
- }),
14590
- deleteTarget: method(object({ targetId: string() }), _void(), {
14591
- kind: "mutation",
14592
- auth: "admin"
14593
- }),
14594
- setTargetEnabled: method(object({
14595
- targetId: string(),
14596
- enabled: boolean()
14597
- }), _void(), {
14598
- kind: "mutation",
14599
- auth: "admin"
14600
14831
  })
14601
14832
  }
14602
14833
  };
@@ -14612,8 +14843,9 @@ var notificationOutputCapability = {
14612
14843
  *
14613
14844
  * Three providers (notifiers, Home Assistant, Alexa) serve the same shape, so the pieces are shared:
14614
14845
  * - {@link defineNotificationTargetKind}: the record kind of one `TargetKind` descriptor;
14615
- * - {@link NotificationTargetStore}: the `Target`-shaped facade the existing provider code keeps
14616
- * using, over one handle per kind (the old `notification-output` CRUD keeps working on it);
14846
+ * - {@link NotificationTargetStore}: the `Target`-shaped facade the provider's own code keeps
14847
+ * using (send, the notification-center bridge), over one handle per kind;
14848
+ * - {@link listNotificationTargets}: the targets as an addon on `ctx.api` reads them;
14617
14849
  * - {@link buildNotificationTargetInstances}: the `integration-instances` provider, one kind each;
14618
14850
  * - {@link moveTargetRows}: the D987 procedure that flattens the old `config` blobs into the bodies.
14619
14851
  */
@@ -14870,6 +15102,10 @@ function notificationTargetParts(records, spec) {
14870
15102
  initialBody: (name) => ({ name }),
14871
15103
  mintId: () => spec.mintId(),
14872
15104
  configured: () => true,
15105
+ validateWrite: ({ body }) => spec.validate?.({
15106
+ kind: descriptor.kind,
15107
+ config: body
15108
+ }),
14873
15109
  test: async ({ id, values }) => {
14874
15110
  if (spec.probe === void 0 || !canTest) return {
14875
15111
  success: false,
@@ -23265,7 +23501,6 @@ var SYSTEM_CAP_VOCABULARY = {
23265
23501
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
23266
23502
  "notification-rules": system("notification rules — authored on their own page"),
23267
23503
  "oauth-integration": system(IDENTITY),
23268
- "oidc-connections": system(IDENTITY),
23269
23504
  "osd-manager": system(PIPELINE),
23270
23505
  "pipeline-executor": system(PIPELINE),
23271
23506
  "pipeline-orchestrator": system(PIPELINE),
@@ -45486,6 +45721,8 @@ var SettingsSchemaWithValuesSchema = object({
45486
45721
  });
45487
45722
  /** Patch object — keys are field names, values are the new field values. */
45488
45723
  var SettingsPatchSchema = record(string(), unknown());
45724
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45725
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45489
45726
  /** Standard success response for update operations. */
45490
45727
  var SettingsUpdateResultSchema = object({ success: literal(true) });
45491
45728
  method(object({
@@ -45501,6 +45738,25 @@ method(object({
45501
45738
  kind: "mutation",
45502
45739
  auth: "admin"
45503
45740
  }), method(object({
45741
+ addonId: string(),
45742
+ cap: string(),
45743
+ scope: ScopedSettingsScopeSchema,
45744
+ nodeId: string().optional()
45745
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45746
+ addonId: string(),
45747
+ cap: string(),
45748
+ scope: ScopedSettingsScopeSchema,
45749
+ nodeId: string().optional(),
45750
+ patch: SettingsPatchSchema
45751
+ }), SettingsUpdateResultSchema, {
45752
+ kind: "mutation",
45753
+ auth: "admin"
45754
+ }), method(object({
45755
+ addonId: string(),
45756
+ cap: string(),
45757
+ scope: ScopedSettingsScopeSchema,
45758
+ nodeId: string().optional()
45759
+ }), record(string(), unknown()).nullable()), method(object({
45504
45760
  addonId: string(),
45505
45761
  nodeId: string().optional()
45506
45762
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
@@ -57057,7 +57313,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
57057
57313
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
57058
57314
  * clears it.
57059
57315
  */
57060
- var ConfigSchemaPassthrough$3 = custom();
57316
+ var ConfigSchemaPassthrough$2 = custom();
57061
57317
  var IntegrationInstanceIdSchema = string().min(1);
57062
57318
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
57063
57319
  /**
@@ -57103,7 +57359,16 @@ var IntegrationInstanceKindSchema = object({
57103
57359
  /** Relative hub path to start an interactive account link of an instance of this kind. */
57104
57360
  connectPath: string().min(1).optional(),
57105
57361
  /** Relative hub path to disconnect that account. */
57106
- disconnectPath: string().min(1).optional()
57362
+ disconnectPath: string().min(1).optional(),
57363
+ /**
57364
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
57365
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
57366
+ * `connection` widgets write connection values, `model` widgets own the model choice.
57367
+ */
57368
+ widgets: array(object({
57369
+ id: string().min(1),
57370
+ placement: _enum(["connection", "model"])
57371
+ })).optional()
57107
57372
  });
57108
57373
  /**
57109
57374
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -57208,7 +57473,7 @@ var integrationInstancesCapability = {
57208
57473
  id: IntegrationInstanceIdSchema.nullable(),
57209
57474
  /** The kind of the create form (`id: null`) of a provider with several. */
57210
57475
  kind: string().min(1).optional()
57211
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
57476
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
57212
57477
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
57213
57478
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
57214
57479
  /**
@@ -58310,12 +58575,12 @@ var LlmModelInfoSchema = object({
58310
58575
  });
58311
58576
  /**
58312
58577
  * The provider-specific part of a profile, validated by the provider that owns
58313
- * the kind (`upsertProfile` refuses what it does not accept). A field its
58578
+ * the kind (saving an instance refuses what it does not accept). A field its
58314
58579
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
58315
58580
  * keychain, never in the row, and reads back as the redaction marker.
58316
58581
  */
58317
58582
  var LlmProviderConfigSchema = record(string(), unknown());
58318
- var LlmProfileSchema = object({
58583
+ object({
58319
58584
  id: string(),
58320
58585
  name: string(),
58321
58586
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -58409,8 +58674,8 @@ var LlmProviderWidgetSchema = object({
58409
58674
  /** ConfigUISchema tree passed through untyped on the wire (the
58410
58675
  * notification-output `ConfigSchemaPassthrough` precedent at
58411
58676
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
58412
- var ConfigSchemaPassthrough$2 = unknown();
58413
- var LlmProfileKindDescriptorSchema = object({
58677
+ var ConfigSchemaPassthrough$1 = unknown();
58678
+ object({
58414
58679
  kind: LlmProfileKindSchema,
58415
58680
  label: string(),
58416
58681
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -58418,9 +58683,9 @@ var LlmProfileKindDescriptorSchema = object({
58418
58683
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
58419
58684
  addonId: string(),
58420
58685
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
58421
- connectionSchema: ConfigSchemaPassthrough$2,
58686
+ connectionSchema: ConfigSchemaPassthrough$1,
58422
58687
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
58423
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
58688
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
58424
58689
  /** The kind's defaults; a model from `listModels` may override any of them. */
58425
58690
  features: LlmModelFeaturesSchema,
58426
58691
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -58549,20 +58814,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
58549
58814
  }), method(object({
58550
58815
  addonId: string().optional(),
58551
58816
  requestId: string()
58552
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
58553
- kind: "mutation",
58554
- auth: "admin"
58555
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
58556
- kind: "mutation",
58557
- auth: "admin"
58558
- }), method(ProfileRefInputSchema, _void(), {
58559
- kind: "mutation",
58560
- auth: "admin"
58561
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
58562
- kind: "mutation",
58563
- auth: "admin",
58564
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
58565
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58817
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58566
58818
  kind: "mutation",
58567
58819
  auth: "admin"
58568
58820
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -59625,20 +59877,7 @@ method(_void(), MeshStatusSchema), method(object({
59625
59877
  /** Optional hostname override the host should advertise once joined. */
59626
59878
  hostname: string().optional() }), object({
59627
59879
  /** Authentication URL the operator should open in a browser. */
59628
- loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() })), method(object({
59629
- /** Optional auth key — when provided, probes the key validity
59630
- * against the provider's API. Omit when already joined to
59631
- * just ping the daemon. */
59632
- authKey: string().optional() }), object({
59633
- ok: boolean(),
59634
- /** Provider-side identifier resolved by the probe (tailnet
59635
- * name for Tailscale, network id for ZeroTier, etc.). */
59636
- tenant: string().optional(),
59637
- /** Daemon binary version, when reachable. */
59638
- daemonVersion: string().optional(),
59639
- /** Human-readable error when `ok: false`. */
59640
- error: string().optional()
59641
- }), { kind: "mutation" });
59880
+ loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }));
59642
59881
  /**
59643
59882
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
59644
59883
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -69845,61 +70084,6 @@ var OauthIntegrationDescriptorSchema = object({
69845
70084
  });
69846
70085
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
69847
70086
  /**
69848
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
69849
- *
69850
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
69851
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
69852
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
69853
- * immutable after create.
69854
- */
69855
- var ConfigSchemaPassthrough$1 = unknown();
69856
- var OidcConnectionSchema = object({
69857
- id: string().min(1),
69858
- displayName: string(),
69859
- icon: string(),
69860
- issuerUrl: string(),
69861
- clientId: string(),
69862
- /** Client id + secret present — never the secret value. */
69863
- configured: boolean(),
69864
- /**
69865
- * Values for the connection form schema. `clientSecret` is a keychain
69866
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
69867
- */
69868
- formValues: record(string(), unknown()).optional()
69869
- });
69870
- /**
69871
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
69872
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
69873
- */
69874
- var OidcConnectionUpdateInputSchema = object({
69875
- id: string().min(1),
69876
- displayName: string().min(1),
69877
- icon: string().optional(),
69878
- issuerUrl: string().optional(),
69879
- clientId: string().optional(),
69880
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
69881
- redirectUri: string().optional(),
69882
- scopes: string().optional(),
69883
- usernameClaim: _enum([
69884
- "preferred_username",
69885
- "email",
69886
- "sub"
69887
- ]).optional()
69888
- });
69889
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
69890
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
69891
- displayName: string().min(1)
69892
- }), object({ id: string() }), {
69893
- kind: "mutation",
69894
- auth: "admin"
69895
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
69896
- kind: "mutation",
69897
- auth: "admin"
69898
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
69899
- kind: "mutation",
69900
- auth: "admin"
69901
- });
69902
- /**
69903
70087
  * Generic on-screen-display (video overlay) cap. Each camera exposes
69904
70088
  * a firmware-defined list of overlays (channel name, timestamp,
69905
70089
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -76981,21 +77165,6 @@ method(object({
76981
77165
  */
76982
77166
  ttlSec: union([number$1().int().positive(), literal("never")]).optional()
76983
77167
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
76984
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
76985
- providerId: string().min(1),
76986
- displayName: string().min(1),
76987
- configSchema: unknown(),
76988
- nodeLocal: boolean(),
76989
- shouldSaveDiskSpace: literal(true),
76990
- minFreePercent: number$1().min(0).max(100)
76991
- }), object({
76992
- providerId: string().min(1),
76993
- displayName: string().min(1),
76994
- configSchema: unknown(),
76995
- nodeLocal: boolean(),
76996
- shouldSaveDiskSpace: literal(false),
76997
- minFreePercent: literal(null)
76998
- })]);
76999
77168
  method(object({
77000
77169
  location: StorageLocationRefSchema,
77001
77170
  relativePath: string()
@@ -77057,42 +77226,7 @@ method(object({
77057
77226
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
77058
77227
  kind: "mutation",
77059
77228
  auth: "admin"
77060
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
77061
- createdAt: true,
77062
- updatedAt: true
77063
- }), StorageLocationSchema, {
77064
- kind: "mutation",
77065
- auth: "admin"
77066
- }), method(object({
77067
- id: string(),
77068
- force: boolean().optional()
77069
- }), _void(), {
77070
- kind: "mutation",
77071
- auth: "admin"
77072
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
77073
- ok: boolean(),
77074
- error: string().optional()
77075
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
77076
- providerId: string(),
77077
- /**
77078
- * The location this config is an UNSAVED edit of, when there is one.
77079
- *
77080
- * `listLocations` replaces every declared secret with the redaction
77081
- * sentinel, so the edit modal's form state holds the sentinel for any
77082
- * credential the operator did not retype — and posting that here
77083
- * without a way to resolve it makes the provider try to authenticate
77084
- * as `__camstack_redacted__` and report the operator's own working
77085
- * password as wrong. Given this id, the orchestrator restores each
77086
- * sentinel from the stored config (same rule as `upsertLocation`)
77087
- * before dispatching. Omitted by the "Add location" wizard, where
77088
- * every value was typed just now and nothing is stored yet.
77089
- */
77090
- locationId: string().optional(),
77091
- config: record(string(), unknown())
77092
- }), object({
77093
- ok: boolean(),
77094
- error: string().optional()
77095
- }), { auth: "admin" });
77229
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
77096
77230
  /** One table of the settings database. */
77097
77231
  var StorageTableSchema = object({
77098
77232
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -78614,7 +78748,7 @@ var TerminalInstanceInfoSchema = object({
78614
78748
  /**
78615
78749
  * False while the node the instance is bound to is not in the cluster
78616
78750
  * topology (forgotten, renamed, re-paired). The instance is kept; the
78617
- * operator re-points it with `updateInstance({ targetNodeId })`.
78751
+ * operator re-points it on the Terminal page.
78618
78752
  */
78619
78753
  nodeInCluster: boolean(),
78620
78754
  profileId: string(),
@@ -78652,41 +78786,7 @@ var TerminalOutputBatchSchema = object({
78652
78786
  snapshot: string().optional(),
78653
78787
  events: array(TerminalOutputEventSchema).readonly()
78654
78788
  });
78655
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
78656
- targetNodeId: string().min(1),
78657
- profileId: string().min(1),
78658
- name: string().trim().min(1).max(160).optional(),
78659
- executable: string().max(1024).optional(),
78660
- args: array(string().max(2048)).max(64).optional(),
78661
- cwd: string().max(1024).optional(),
78662
- environment: array(string().max(4096)).max(64).optional(),
78663
- profileSettings: ProfileSettingsBagSchema.optional()
78664
- }), TerminalInstanceInfoSchema, {
78665
- kind: "mutation",
78666
- auth: "admin"
78667
- }), method(object({
78668
- instanceId: string().min(1),
78669
- /** Re-point the instance to another node; the profile must exist there. */
78670
- targetNodeId: string().min(1).max(256).optional(),
78671
- name: string().trim().min(1).max(160).optional(),
78672
- executable: string().max(1024).optional(),
78673
- args: array(string().max(2048)).max(64).optional(),
78674
- cwd: string().max(1024).optional(),
78675
- environment: array(string().max(4096)).max(64).optional(),
78676
- profileSettings: ProfileSettingsBagSchema.optional()
78677
- }), TerminalInstanceInfoSchema, {
78678
- kind: "mutation",
78679
- auth: "admin"
78680
- }), method(object({ instanceId: string().min(1) }), _void(), {
78681
- kind: "mutation",
78682
- auth: "admin"
78683
- }), method(object({
78684
- instanceId: string().min(1),
78685
- enabled: boolean()
78686
- }), TerminalInstanceInfoSchema, {
78687
- kind: "mutation",
78688
- auth: "admin"
78689
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78789
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78690
78790
  stableId: string().min(1),
78691
78791
  name: string().trim().min(1).max(160).optional()
78692
78792
  }), TerminalInstanceInfoSchema, {
@@ -83506,6 +83606,20 @@ Object.freeze({
83506
83606
  access: "view",
83507
83607
  role: "admin"
83508
83608
  },
83609
+ "addonSettings.getScopedSettings": {
83610
+ capName: "addon-settings",
83611
+ capScope: "system",
83612
+ addonId: null,
83613
+ access: "view",
83614
+ role: "admin"
83615
+ },
83616
+ "addonSettings.readScopedValues": {
83617
+ capName: "addon-settings",
83618
+ capScope: "system",
83619
+ addonId: null,
83620
+ access: "view",
83621
+ role: "admin"
83622
+ },
83509
83623
  "addonSettings.unsetStoredSettingsKeys": {
83510
83624
  capName: "addon-settings",
83511
83625
  capScope: "system",
@@ -83527,6 +83641,13 @@ Object.freeze({
83527
83641
  access: "create",
83528
83642
  role: "admin"
83529
83643
  },
83644
+ "addonSettings.updateScopedSettings": {
83645
+ capName: "addon-settings",
83646
+ capScope: "system",
83647
+ addonId: null,
83648
+ access: "create",
83649
+ role: "admin"
83650
+ },
83530
83651
  "addonWidgets.listWidgets": {
83531
83652
  capName: "addon-widgets",
83532
83653
  capScope: "system",
@@ -86978,13 +87099,6 @@ Object.freeze({
86978
87099
  access: "delete",
86979
87100
  role: "admin"
86980
87101
  },
86981
- "llm.deleteProfile": {
86982
- capName: "llm",
86983
- capScope: "system",
86984
- addonId: null,
86985
- access: "delete",
86986
- role: "admin"
86987
- },
86988
87102
  "llm.generate": {
86989
87103
  capName: "llm",
86990
87104
  capScope: "system",
@@ -87048,20 +87162,6 @@ Object.freeze({
87048
87162
  access: "view",
87049
87163
  role: "admin"
87050
87164
  },
87051
- "llm.listProfileKinds": {
87052
- capName: "llm",
87053
- capScope: "system",
87054
- addonId: null,
87055
- access: "view",
87056
- role: "admin"
87057
- },
87058
- "llm.listProfiles": {
87059
- capName: "llm",
87060
- capScope: "system",
87061
- addonId: null,
87062
- access: "view",
87063
- role: "admin"
87064
- },
87065
87165
  "llm.listRuntimeNodes": {
87066
87166
  capName: "llm",
87067
87167
  capScope: "system",
@@ -87069,13 +87169,6 @@ Object.freeze({
87069
87169
  access: "view",
87070
87170
  role: "admin"
87071
87171
  },
87072
- "llm.migrateRawSecrets": {
87073
- capName: "llm",
87074
- capScope: "system",
87075
- addonId: null,
87076
- access: "create",
87077
- role: "admin"
87078
- },
87079
87172
  "llm.resolveModelRef": {
87080
87173
  capName: "llm",
87081
87174
  capScope: "system",
@@ -87104,20 +87197,6 @@ Object.freeze({
87104
87197
  access: "create",
87105
87198
  role: "admin"
87106
87199
  },
87107
- "llm.testProfile": {
87108
- capName: "llm",
87109
- capScope: "system",
87110
- addonId: null,
87111
- access: "create",
87112
- role: "admin"
87113
- },
87114
- "llm.upsertProfile": {
87115
- capName: "llm",
87116
- capScope: "system",
87117
- addonId: null,
87118
- access: "create",
87119
- role: "admin"
87120
- },
87121
87200
  "llmRuntime.appleAvailability": {
87122
87201
  capName: "llm-runtime",
87123
87202
  capScope: "system",
@@ -87622,13 +87701,6 @@ Object.freeze({
87622
87701
  access: "create",
87623
87702
  role: "admin"
87624
87703
  },
87625
- "meshNetwork.testConnection": {
87626
- capName: "mesh-network",
87627
- capScope: "system",
87628
- addonId: null,
87629
- access: "create",
87630
- role: "admin"
87631
- },
87632
87704
  "meter.getStatus": {
87633
87705
  capName: "meter",
87634
87706
  capScope: "device",
@@ -88287,13 +88359,6 @@ Object.freeze({
88287
88359
  access: "create",
88288
88360
  role: "admin"
88289
88361
  },
88290
- "notificationOutput.deleteTarget": {
88291
- capName: "notification-output",
88292
- capScope: "system",
88293
- addonId: null,
88294
- access: "delete",
88295
- role: "admin"
88296
- },
88297
88362
  "notificationOutput.discoverTargets": {
88298
88363
  capName: "notification-output",
88299
88364
  capScope: "system",
@@ -88301,13 +88366,6 @@ Object.freeze({
88301
88366
  access: "view",
88302
88367
  role: "admin"
88303
88368
  },
88304
- "notificationOutput.getTarget": {
88305
- capName: "notification-output",
88306
- capScope: "system",
88307
- addonId: null,
88308
- access: "view",
88309
- role: "admin"
88310
- },
88311
88369
  "notificationOutput.listTargetKinds": {
88312
88370
  capName: "notification-output",
88313
88371
  capScope: "system",
@@ -88315,13 +88373,6 @@ Object.freeze({
88315
88373
  access: "view",
88316
88374
  role: "admin"
88317
88375
  },
88318
- "notificationOutput.listTargets": {
88319
- capName: "notification-output",
88320
- capScope: "system",
88321
- addonId: null,
88322
- access: "view",
88323
- role: "admin"
88324
- },
88325
88376
  "notificationOutput.probeTargetConfig": {
88326
88377
  capName: "notification-output",
88327
88378
  capScope: "system",
@@ -88336,13 +88387,6 @@ Object.freeze({
88336
88387
  access: "create",
88337
88388
  role: "admin"
88338
88389
  },
88339
- "notificationOutput.setTargetEnabled": {
88340
- capName: "notification-output",
88341
- capScope: "system",
88342
- addonId: null,
88343
- access: "create",
88344
- role: "admin"
88345
- },
88346
88390
  "notificationOutput.testTarget": {
88347
88391
  capName: "notification-output",
88348
88392
  capScope: "system",
@@ -88350,13 +88394,6 @@ Object.freeze({
88350
88394
  access: "create",
88351
88395
  role: "admin"
88352
88396
  },
88353
- "notificationOutput.upsertTarget": {
88354
- capName: "notification-output",
88355
- capScope: "system",
88356
- addonId: null,
88357
- access: "create",
88358
- role: "admin"
88359
- },
88360
88397
  "notificationRules.activateAlarmProfile": {
88361
88398
  capName: "notification-rules",
88362
88399
  capScope: "system",
@@ -88616,41 +88653,6 @@ Object.freeze({
88616
88653
  access: "view",
88617
88654
  role: "admin"
88618
88655
  },
88619
- "oidcConnections.connectionFormSchema": {
88620
- capName: "oidc-connections",
88621
- capScope: "system",
88622
- addonId: null,
88623
- access: "view",
88624
- role: "admin"
88625
- },
88626
- "oidcConnections.createConnection": {
88627
- capName: "oidc-connections",
88628
- capScope: "system",
88629
- addonId: null,
88630
- access: "create",
88631
- role: "admin"
88632
- },
88633
- "oidcConnections.deleteConnection": {
88634
- capName: "oidc-connections",
88635
- capScope: "system",
88636
- addonId: null,
88637
- access: "delete",
88638
- role: "admin"
88639
- },
88640
- "oidcConnections.listConnections": {
88641
- capName: "oidc-connections",
88642
- capScope: "system",
88643
- addonId: null,
88644
- access: "view",
88645
- role: "admin"
88646
- },
88647
- "oidcConnections.updateConnection": {
88648
- capName: "oidc-connections",
88649
- capScope: "system",
88650
- addonId: null,
88651
- access: "create",
88652
- role: "admin"
88653
- },
88654
88656
  "osd.getStatus": {
88655
88657
  capName: "osd",
88656
88658
  capScope: "device",
@@ -91682,13 +91684,6 @@ Object.freeze({
91682
91684
  access: "delete",
91683
91685
  role: "admin"
91684
91686
  },
91685
- "storage.deleteLocation": {
91686
- capName: "storage",
91687
- capScope: "system",
91688
- addonId: null,
91689
- access: "delete",
91690
- role: "admin"
91691
- },
91692
91687
  "storage.endDownload": {
91693
91688
  capName: "storage",
91694
91689
  capScope: "system",
@@ -91745,13 +91740,6 @@ Object.freeze({
91745
91740
  access: "view",
91746
91741
  role: "admin"
91747
91742
  },
91748
- "storage.listProviders": {
91749
- capName: "storage",
91750
- capScope: "system",
91751
- addonId: null,
91752
- access: "view",
91753
- role: "admin"
91754
- },
91755
91743
  "storage.read": {
91756
91744
  capName: "storage",
91757
91745
  capScope: "system",
@@ -91773,27 +91761,6 @@ Object.freeze({
91773
91761
  access: "view",
91774
91762
  role: "admin"
91775
91763
  },
91776
- "storage.testConfig": {
91777
- capName: "storage",
91778
- capScope: "system",
91779
- addonId: null,
91780
- access: "view",
91781
- role: "admin"
91782
- },
91783
- "storage.testLocation": {
91784
- capName: "storage",
91785
- capScope: "system",
91786
- addonId: null,
91787
- access: "view",
91788
- role: "admin"
91789
- },
91790
- "storage.upsertLocation": {
91791
- capName: "storage",
91792
- capScope: "system",
91793
- addonId: null,
91794
- access: "create",
91795
- role: "admin"
91796
- },
91797
91764
  "storage.write": {
91798
91765
  capName: "storage",
91799
91766
  capScope: "system",
@@ -92571,27 +92538,6 @@ Object.freeze({
92571
92538
  access: "create",
92572
92539
  role: "admin"
92573
92540
  },
92574
- "terminalSession.createInstance": {
92575
- capName: "terminal-session",
92576
- capScope: "system",
92577
- addonId: null,
92578
- access: "create",
92579
- role: "admin"
92580
- },
92581
- "terminalSession.deleteInstance": {
92582
- capName: "terminal-session",
92583
- capScope: "system",
92584
- addonId: null,
92585
- access: "delete",
92586
- role: "admin"
92587
- },
92588
- "terminalSession.listInstances": {
92589
- capName: "terminal-session",
92590
- capScope: "system",
92591
- addonId: null,
92592
- access: "view",
92593
- role: "admin"
92594
- },
92595
92541
  "terminalSession.listLegacyCameras": {
92596
92542
  capName: "terminal-session",
92597
92543
  capScope: "system",
@@ -92634,20 +92580,6 @@ Object.freeze({
92634
92580
  access: "create",
92635
92581
  role: "admin"
92636
92582
  },
92637
- "terminalSession.setInstanceEnabled": {
92638
- capName: "terminal-session",
92639
- capScope: "system",
92640
- addonId: null,
92641
- access: "create",
92642
- role: "admin"
92643
- },
92644
- "terminalSession.updateInstance": {
92645
- capName: "terminal-session",
92646
- capScope: "system",
92647
- addonId: null,
92648
- access: "create",
92649
- role: "admin"
92650
- },
92651
92583
  "terminalSession.writeInput": {
92652
92584
  capName: "terminal-session",
92653
92585
  capScope: "system",
@@ -144400,6 +144332,7 @@ var EchoSessionRegistry = class {
144400
144332
  #resolve = null;
144401
144333
  #dnd = /* @__PURE__ */ new Map();
144402
144334
  #volumes = /* @__PURE__ */ new Map();
144335
+ #groupPlayers = /* @__PURE__ */ new Map();
144403
144336
  setResolver(fn) {
144404
144337
  this.#resolve = fn;
144405
144338
  }
@@ -144438,10 +144371,35 @@ var EchoSessionRegistry = class {
144438
144371
  });
144439
144372
  return pending;
144440
144373
  }
144374
+ /**
144375
+ * The player state of a speaker group, shared by its members within one poll cycle.
144376
+ * A failed read is not kept for the next member.
144377
+ */
144378
+ groupPlayer(brokerId, group, now, deviceId) {
144379
+ const key = `${brokerId}\u0000${group.id}`;
144380
+ const shared = this.#groupPlayers.get(key);
144381
+ if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
144382
+ const session = this.get(brokerId);
144383
+ if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
144384
+ const pending = session.playerState(group.id, group.deviceType, deviceId);
144385
+ this.#groupPlayers.set(key, {
144386
+ at: now,
144387
+ pending
144388
+ });
144389
+ pending.catch(() => {
144390
+ if (this.#groupPlayers.get(key)?.pending === pending) this.#groupPlayers.delete(key);
144391
+ });
144392
+ return pending;
144393
+ }
144441
144394
  /** Forget the shared volume list: a list fetched before a set must not answer a poll after it. */
144442
144395
  invalidateVolumes(brokerId) {
144443
144396
  this.#volumes.delete(brokerId);
144444
144397
  }
144398
+ /** Forget the shared group players: a transport command or a push makes a cached group answer stale. */
144399
+ invalidateGroupPlayers(brokerId) {
144400
+ const prefix = `${brokerId}\u0000`;
144401
+ for (const key of [...this.#groupPlayers.keys()]) if (key.startsWith(prefix)) this.#groupPlayers.delete(key);
144402
+ }
144445
144403
  /** Forget the shared Do Not Disturb list, for the same reason. */
144446
144404
  invalidateDnd(brokerId) {
144447
144405
  this.#dnd.delete(brokerId);
@@ -144457,6 +144415,7 @@ var EchoSessionRegistry = class {
144457
144415
  }
144458
144416
  event(brokerId, serial) {
144459
144417
  this.invalidateVolumes(brokerId);
144418
+ this.invalidateGroupPlayers(brokerId);
144460
144419
  this.invalidateDnd(brokerId);
144461
144420
  for (const listener of [...this.#pushListeners.get(`${brokerId}\u0000${serial}`) ?? []]) listener();
144462
144421
  }
@@ -144475,6 +144434,7 @@ var EchoSessionRegistry = class {
144475
144434
  this.#resolve = null;
144476
144435
  this.#dnd.clear();
144477
144436
  this.#volumes.clear();
144437
+ this.#groupPlayers.clear();
144478
144438
  }
144479
144439
  };
144480
144440
  function subscribe(map, key, listener) {
@@ -144517,6 +144477,11 @@ function mapEchoPlaybackState(value) {
144517
144477
  default: return "idle";
144518
144478
  }
144519
144479
  }
144480
+ /** Whether a player answer carries any playback: an active state, a text or artwork. */
144481
+ function hasPlayback(info) {
144482
+ if (mapEchoPlaybackState(info.state) !== "idle") return true;
144483
+ return nonEmpty(info.infoText?.title) !== null || nonEmpty(info.mainArt?.url) !== null;
144484
+ }
144520
144485
  function percent(value) {
144521
144486
  if (typeof value !== "number" || !Number.isFinite(value)) return null;
144522
144487
  return Math.round(Math.max(0, Math.min(100, value)));
@@ -144606,7 +144571,9 @@ var EchoDeviceSchema = object({
144606
144571
  dnd: boolean(),
144607
144572
  deviceType: string(),
144608
144573
  /** The room the account files the device under, when Amazon names one. */
144609
- room: string().optional()
144574
+ room: string().optional(),
144575
+ /** Serials of the speaker groups this device belongs to (Amazon's `parentClusters`); absent when none. */
144576
+ parentClusters: array(string()).optional()
144610
144577
  });
144611
144578
  var LoginStatusSchema = _enum(["ok", "failed"]);
144612
144579
  var SessionStatusSchema = object({
@@ -144835,6 +144802,8 @@ var AlexaEchoDevice = class extends BaseDevice {
144835
144802
  unsubscribers = [];
144836
144803
  polling = false;
144837
144804
  failureLogged = false;
144805
+ /** The name of the speaker group whose playback this Echo shows; null when it plays on its own. */
144806
+ viaGroup = null;
144838
144807
  volumeFailureLogged = false;
144839
144808
  health = "ok";
144840
144809
  /** When this device last wrote its volume / Do Not Disturb optimistically; a read that began earlier must not overwrite it. */
@@ -144962,9 +144931,11 @@ var AlexaEchoDevice = class extends BaseDevice {
144962
144931
  }
144963
144932
  const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
144964
144933
  try {
144965
- const fromPlayer = mapPlayerInfoToStatus(await session.playerState(this.serial, deviceType, this.id), {
144934
+ const own = await session.playerState(this.serial, deviceType, this.id);
144935
+ const now = Date.now();
144936
+ const fromPlayer = mapPlayerInfoToStatus(await this.withGroupPlayback(own, listed, session), {
144966
144937
  online: true,
144967
- now: Date.now()
144938
+ now
144968
144939
  });
144969
144940
  const read = await this.completeVolume(fromPlayer);
144970
144941
  const kept = this.keepNewerVolume(read, readStartedAt);
@@ -144979,6 +144950,49 @@ var AlexaEchoDevice = class extends BaseDevice {
144979
144950
  await this.readDoNotDisturb(readStartedAt);
144980
144951
  }
144981
144952
  /**
144953
+ * A member of a playing speaker group is answered empty by its own player read while
144954
+ * the group plays. When the member's answer carries no playback, serve the first
144955
+ * parent group that is playing or paused (its track and artwork; the volume stays the
144956
+ * member's own). A failed group read leaves the member's own answer.
144957
+ */
144958
+ async withGroupPlayback(own, listed, session) {
144959
+ const parents = listed?.parentClusters ?? [];
144960
+ if (hasPlayback(own) || parents.length === 0) {
144961
+ this.noteViaGroup(null);
144962
+ return own;
144963
+ }
144964
+ for (const parentId of parents) {
144965
+ const group = session.devices.find((device) => device.id === parentId);
144966
+ if (group === void 0 || !group.online) continue;
144967
+ try {
144968
+ const groupInfo = await alexaEchoSessions.groupPlayer(this.brokerId, group, Date.now(), this.id);
144969
+ if (groupInfo.state === "PLAYING" || groupInfo.state === "PAUSED" || groupInfo.state === "BUFFERING") {
144970
+ this.noteViaGroup(group.name);
144971
+ return {
144972
+ ...groupInfo,
144973
+ volume: own.volume
144974
+ };
144975
+ }
144976
+ } catch (err) {
144977
+ this.ctx.logger.debug("alexa echo: the group player could not be read", {
144978
+ tags: { deviceId: this.id },
144979
+ meta: {
144980
+ group: group.name,
144981
+ error: err instanceof Error ? err.message : String(err)
144982
+ }
144983
+ });
144984
+ }
144985
+ }
144986
+ this.noteViaGroup(null);
144987
+ return own;
144988
+ }
144989
+ /** Log each transition into and out of "playing via a group" once. */
144990
+ noteViaGroup(name) {
144991
+ if (name === this.viaGroup) return;
144992
+ this.viaGroup = name;
144993
+ this.ctx.logger.info(name === null ? "alexa echo: no longer playing via a group" : `alexa echo: playing via group ${name}`, { tags: { deviceId: this.id } });
144994
+ }
144995
+ /**
144982
144996
  * An idle Echo's player read carries no volume. Ask the account's volume list
144983
144997
  * (shared by every Echo of the account in this cycle); a failed read leaves null.
144984
144998
  */
@@ -145050,6 +145064,7 @@ var AlexaEchoDevice = class extends BaseDevice {
145050
145064
  }
145051
145065
  async command(command) {
145052
145066
  await this.session().npCommand(this.serial, command, this.id);
145067
+ alexaEchoSessions.invalidateGroupPlayers(this.brokerId);
145053
145068
  this.rereadSoon();
145054
145069
  }
145055
145070
  /**
@@ -145521,6 +145536,33 @@ function quietLogger$1() {
145521
145536
  };
145522
145537
  return logger;
145523
145538
  }
145539
+ /**
145540
+ * The account a target speaks through: the one it names; else the only one there is; else the one
145541
+ * whose Echoes include the target's Echo.
145542
+ */
145543
+ function pickBroker(brokers, config) {
145544
+ const named = readString(config, "brokerId");
145545
+ if (named.length > 0) return brokers.find((broker) => broker.id === named);
145546
+ if (brokers.length === 1) return brokers[0];
145547
+ const echoId = readString(config, "echoId");
145548
+ const owners = brokers.filter((broker) => broker.devices.some((device) => device.id === echoId));
145549
+ return owners.length === 1 ? owners[0] : void 0;
145550
+ }
145551
+ /**
145552
+ * The save-time rule of an Echo target: an Echo and an account must be picked, and the account is
145553
+ * pinned the moment it is known, so a second account added later cannot silently change where an
145554
+ * existing target speaks. Throws the reason; returns the keys to keep with the write.
145555
+ */
145556
+ function validateEchoTarget(brokers, config) {
145557
+ if (readString(config, "echoId").length === 0) throw new Error("Pick the Echo that should speak");
145558
+ const owner = pickBroker(brokers, config);
145559
+ if (owner === void 0) {
145560
+ const named = readString(config, "brokerId");
145561
+ if (named.length > 0) throw new Error(`The Amazon account ${named} is not available`);
145562
+ throw new Error(brokers.length === 0 ? "Add an Amazon account first: an Echo speaks through one" : "Pick the Amazon account this Echo belongs to");
145563
+ }
145564
+ return readString(config, "brokerId").length === 0 ? { brokerId: owner.id } : {};
145565
+ }
145524
145566
  function readString(config, key) {
145525
145567
  const value = config[key];
145526
145568
  return typeof value === "string" ? value : "";
@@ -145550,7 +145592,7 @@ function echoOptionLabel(device) {
145550
145592
  /**
145551
145593
  * The two pickers are not `required`: a required select shows its first option
145552
145594
  * while the form still holds no value, so a save would drop the choice. Left
145553
- * optional they open on "Select", and `upsertTarget` refuses an empty pick.
145595
+ * optional they open on "Select", and `validateEchoTarget` refuses an empty pick on save.
145554
145596
  *
145555
145597
  * The account is a broker pick (existing, or created in place); the Echo list
145556
145598
  * is fetched from that account, so a new Echo shows up without a deploy.
@@ -145642,18 +145684,8 @@ function createAlexaNotificationProvider(ports) {
145642
145684
  function record(row) {
145643
145685
  deliveries = [...deliveries, row];
145644
145686
  }
145645
- /**
145646
- * The account a target speaks through: the one it names; else the only one
145647
- * there is; else the one whose Echoes include the target's Echo.
145648
- */
145649
145687
  function brokerFor(config) {
145650
- const all = ports.brokers();
145651
- const named = readString(config, "brokerId");
145652
- if (named.length > 0) return all.find((broker) => broker.id === named);
145653
- if (all.length === 1) return all[0];
145654
- const echoId = readString(config, "echoId");
145655
- const owners = all.filter((broker) => broker.devices.some((device) => device.id === echoId));
145656
- return owners.length === 1 ? owners[0] : void 0;
145688
+ return pickBroker(ports.brokers(), config);
145657
145689
  }
145658
145690
  async function deliver(target, notification) {
145659
145691
  const speech = speechFromNotification(notification);
@@ -145855,14 +145887,6 @@ function createAlexaNotificationProvider(ports) {
145855
145887
  async listTargetKinds() {
145856
145888
  return [alexaTargetKind()];
145857
145889
  },
145858
- async listTargets() {
145859
- return [...await ports.loadTargets()];
145860
- },
145861
- async getTarget(input) {
145862
- const found = (await ports.loadTargets()).find((target) => target.id === input.targetId);
145863
- if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
145864
- return found;
145865
- },
145866
145890
  async discoverTargets(input) {
145867
145891
  for (const broker of ports.brokers()) if (!broker.ready) ports.logger.warn(`alexa echo discovery: no Echo listed for '${broker.name}', the Amazon account is not signed in for this broker`, { tags: { brokerId: broker.id } });
145868
145892
  const named = readString(input.config ?? {}, "brokerId");
@@ -145923,33 +145947,6 @@ function createAlexaNotificationProvider(ports) {
145923
145947
  format: "text",
145924
145948
  priority: 3
145925
145949
  });
145926
- },
145927
- async upsertTarget(input) {
145928
- const owner = brokerFor(input.target.config);
145929
- if (readString(input.target.config, "echoId").length === 0) throw new Error("Pick the Echo that should speak");
145930
- if (owner === void 0) throw new Error("Pick the Amazon account this Echo belongs to");
145931
- const pinned = readString(input.target.config, "brokerId").length === 0 ? {
145932
- ...input.target.config,
145933
- brokerId: owner.id
145934
- } : input.target.config;
145935
- const target = {
145936
- ...input.target,
145937
- config: pinned,
145938
- addonId: ALEXA_SPEAK_ADDON_ID,
145939
- kind: ALEXA_SPEAK_KIND
145940
- };
145941
- await ports.putTarget(target);
145942
- return target;
145943
- },
145944
- async deleteTarget(input) {
145945
- await ports.removeTarget(input.targetId);
145946
- },
145947
- async setTargetEnabled(input) {
145948
- const found = (await ports.loadTargets()).find((row) => row.id === input.targetId);
145949
- if (found !== void 0) await ports.putTarget({
145950
- ...found,
145951
- enabled: input.enabled
145952
- });
145953
145950
  }
145954
145951
  };
145955
145952
  }
@@ -146040,6 +146037,14 @@ function echoTargetProbe(speak) {
146040
146037
  });
146041
146038
  };
146042
146039
  }
146040
+ /**
146041
+ * The Echo instance's save-time rule over the accounts now (D1013): the check the removed
146042
+ * `upsertTarget` made, on the instance door. Speech not started yet has no account to name, so the
146043
+ * write is refused with the reason, never accepted blind.
146044
+ */
146045
+ function echoTargetValidator(brokers) {
146046
+ return ({ config }) => validateEchoTarget(brokers() ?? [], config);
146047
+ }
146043
146048
  process.platform, process.env["SystemRoot"];
146044
146049
  /**
146045
146050
  * Files only their owner may read: the one place a secret-bearing file is
@@ -146880,8 +146885,6 @@ var AlexaSpeakController = class {
146880
146885
  now: this.now,
146881
146886
  logger: host.logger,
146882
146887
  loadTargets: () => host.readTargets(),
146883
- putTarget: (target) => host.putTarget(target),
146884
- removeTarget: (targetId) => host.removeTarget(targetId),
146885
146888
  speak: (command) => this.utter(command.brokerId, command.mode, command.targets, command.text)
146886
146889
  });
146887
146890
  }
@@ -146911,6 +146914,7 @@ var AlexaSpeakController = class {
146911
146914
  echoFor(brokerId) {
146912
146915
  return this.echoes.get(brokerId) ?? null;
146913
146916
  }
146917
+ /** The accounts right now, as the provider and the save-time rule see them. */
146914
146918
  brokerViews() {
146915
146919
  return this.host.listBrokers().map((ref) => {
146916
146920
  const echo = this.echoes.get(ref.id);
@@ -147458,8 +147462,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
147458
147462
  const speak = new AlexaSpeakController({
147459
147463
  logger: this.ctx.logger.child("speak"),
147460
147464
  readTargets: () => echoStore.list(),
147461
- putTarget: (target) => echoStore.upsert(target),
147462
- removeTarget: (targetId) => echoStore.delete(targetId),
147463
147465
  listBrokers: () => this.activeBrokers.map((entry) => ({
147464
147466
  id: entry.id,
147465
147467
  name: entry.name,
@@ -147561,6 +147563,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
147561
147563
  kinds: alexaNotifyRecordKinds,
147562
147564
  mintId: () => (0, node_crypto.randomUUID)(),
147563
147565
  probe: echoTargetProbe(() => this.speak),
147566
+ validate: echoTargetValidator(() => this.speak?.brokerViews() ?? null),
147564
147567
  canProbe: () => true
147565
147568
  })
147566
147569
  ], {