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