@camstack/addon-mqtt-broker 1.2.155 → 1.2.156

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.
@@ -46,7 +46,7 @@ let node_fs = require("node:fs");
46
46
  node_fs = __toESM(node_fs);
47
47
  let node_path = require("node:path");
48
48
  node_path = __toESM(node_path);
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
@@ -832,7 +849,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
832
849
  return EventCategory;
833
850
  }({});
834
851
  //#endregion
835
- //#region ../types/dist/device-type-D1fH5kaC.mjs
852
+ //#region ../types/dist/device-type-DxCT0krO.mjs
836
853
  /**
837
854
  import { errMsg } from '@camstack/types'
838
855
  * Extract a human-readable message from an unknown error value.
@@ -994,6 +1011,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
994
1011
  * filters that want "all devices with on-motion behaviour".
995
1012
  */
996
1013
  DeviceFeature["MotionTrigger"] = "motion-trigger";
1014
+ /**
1015
+ * Device can be run for a chosen time and then stopped. Mirrors the
1016
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
1017
+ * a driver, so the flag is derived from the device's bindings
1018
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
1019
+ */
1020
+ DeviceFeature["TimedRun"] = "timed-run";
997
1021
  /** Light supports rgb-triplet color via `color` cap. */
998
1022
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
999
1023
  /** Light supports HSV color via `color` cap. */
@@ -6028,7 +6052,7 @@ var ZodIssueCode = {
6028
6052
  var ZodFirstPartyTypeKind;
6029
6053
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6030
6054
  //#endregion
6031
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6055
+ //#region ../types/dist/sleep-7El4r5Bl.mjs
6032
6056
  /**
6033
6057
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6034
6058
  * window to float samples (D455).
@@ -6392,6 +6416,59 @@ Object.fromEntries([
6392
6416
  function isValuelessField(field) {
6393
6417
  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";
6394
6418
  }
6419
+ /**
6420
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6421
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6422
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6423
+ * pre-resolving) lets callers both read the effective value AND detect
6424
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6425
+ * malformed input yields an empty map, never throws.
6426
+ */
6427
+ function collectHydratedFieldEntries(schema) {
6428
+ const out = /* @__PURE__ */ new Map();
6429
+ if (typeof schema !== "object" || schema === null) return out;
6430
+ const sections = schema.sections;
6431
+ if (!Array.isArray(sections)) return out;
6432
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6433
+ return out;
6434
+ }
6435
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6436
+ function collectFieldEntriesInto(fields, into) {
6437
+ if (!Array.isArray(fields)) return;
6438
+ for (const field of fields) {
6439
+ if (typeof field !== "object" || field === null) continue;
6440
+ const type = field.type;
6441
+ if (type === "group") {
6442
+ collectFieldEntriesInto(field.fields, into);
6443
+ continue;
6444
+ }
6445
+ if (type === "sub-tabs") {
6446
+ const tabs = field.tabs;
6447
+ if (Array.isArray(tabs)) {
6448
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6449
+ }
6450
+ continue;
6451
+ }
6452
+ const key = field.key;
6453
+ if (typeof key !== "string" || key.length === 0) continue;
6454
+ if (!("value" in field)) continue;
6455
+ into.set(key, {
6456
+ value: field.value,
6457
+ default: field.default
6458
+ });
6459
+ }
6460
+ }
6461
+ /**
6462
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6463
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6464
+ * common "just give me the resolved values" case. Built on
6465
+ * {@link collectHydratedFieldEntries}.
6466
+ */
6467
+ function collectHydratedFieldValues(schema) {
6468
+ const out = /* @__PURE__ */ new Map();
6469
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6470
+ return out;
6471
+ }
6395
6472
  function hydrateSchema(schema, values, options = {}) {
6396
6473
  return {
6397
6474
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7268,17 +7345,35 @@ var RecordIntegrationInstances = class {
7268
7345
  ...kind !== void 0 ? { kind } : {}
7269
7346
  });
7270
7347
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7348
+ const initial = this.hooks.initialBody(name, kind);
7349
+ const patch = this.patchOf(values ?? {});
7350
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7351
+ kind,
7352
+ body: {
7353
+ ...initial,
7354
+ ...patch
7355
+ }
7356
+ }));
7271
7357
  await this.handle.put({
7272
7358
  id: minted,
7273
- data: this.hooks.initialBody(name, kind)
7359
+ data: initial
7274
7360
  }, { ifRev: 0 });
7275
- const patch = this.patchOf(values ?? {});
7276
7361
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7277
7362
  await this.hooks.onChanged?.(minted, "created");
7278
7363
  return { id: minted };
7279
7364
  };
7280
7365
  updateInstance = async ({ id, name, values }) => {
7281
7366
  const patch = this.patchOf(values);
7367
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7368
+ const stored = await this.handle.get(id);
7369
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7370
+ kind: void 0,
7371
+ body: {
7372
+ ...stored.data,
7373
+ ...patch
7374
+ }
7375
+ }));
7376
+ }
7282
7377
  if (name !== void 0) patch[this.nameField] = name;
7283
7378
  await this.handle.patch(id, patch);
7284
7379
  await this.hooks.onChanged?.(id, "updated");
@@ -7589,6 +7684,21 @@ var SCOPED_SETTINGS_SCOPES = [
7589
7684
  "device-defaults",
7590
7685
  "node"
7591
7686
  ];
7687
+ /** The one refusal of a key a scope did not declare. */
7688
+ function assertDeclaredKeys(keys, declared) {
7689
+ const undeclared = keys.filter((key) => !declared.has(key));
7690
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7691
+ }
7692
+ /** Build the handle over an addon's scoped read, write and unset. */
7693
+ function createScopedSettings(deps) {
7694
+ return {
7695
+ async get() {
7696
+ return deps.schema.parse(await deps.read());
7697
+ },
7698
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7699
+ unset: (keys) => deps.unset(keys)
7700
+ };
7701
+ }
7592
7702
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7593
7703
  var KeychainWriteRefusedError = class extends Error {
7594
7704
  fieldPath;
@@ -8164,7 +8274,16 @@ function createHandle(raw, opts) {
8164
8274
  return blob;
8165
8275
  }
8166
8276
  },
8167
- writeAddonStore: writeAddon
8277
+ writeAddonStore: writeAddon,
8278
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8279
+ const stored = await readStoredAddonBlob(raw);
8280
+ index.seen("addon", stored);
8281
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8282
+ if (removable.length === 0) return;
8283
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8284
+ await raw.writeAddonStore(patch);
8285
+ index.wrote("addon", patch);
8286
+ })
8168
8287
  };
8169
8288
  }
8170
8289
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8294,7 +8413,7 @@ function boundOf(list) {
8294
8413
  * Base class for CamStack addons. Eliminates settings boilerplate:
8295
8414
  *
8296
8415
  * - Typed `config` property with automatic resolution from store + defaults
8297
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8416
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8298
8417
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8299
8418
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8300
8419
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8304,7 +8423,7 @@ function boundOf(list) {
8304
8423
  * Subclasses override:
8305
8424
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8306
8425
  * - `onShutdown()` — cleanup
8307
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8426
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8308
8427
  *
8309
8428
  * @example
8310
8429
  * ```ts
@@ -8318,7 +8437,7 @@ function boundOf(list) {
8318
8437
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8319
8438
  * }
8320
8439
  *
8321
- * protected globalSettingsSchema() {
8440
+ * protected clusterSettingsSchema() {
8322
8441
  * return this.schema({
8323
8442
  * sections: [{
8324
8443
  * id: 'main', title: 'Settings',
@@ -8361,7 +8480,7 @@ var BaseAddon = class {
8361
8480
  defaults;
8362
8481
  /**
8363
8482
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8364
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8483
+ * itself (`state()`, the scoped writers) that no form field declares.
8365
8484
  * Stored in the keychain exactly like a `password` field. Syntax:
8366
8485
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8367
8486
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8371,7 +8490,7 @@ var BaseAddon = class {
8371
8490
  /**
8372
8491
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8373
8492
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8374
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8493
+ * see them. Empty until loaded.
8375
8494
  */
8376
8495
  _scopedByCap = /* @__PURE__ */ new Map();
8377
8496
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8557,7 +8676,7 @@ var BaseAddon = class {
8557
8676
  /** Addon-specific cleanup. Override if needed. */
8558
8677
  async onShutdown() {}
8559
8678
  /**
8560
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8679
+ * Called after config is resolved during a settings write.
8561
8680
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8562
8681
  * Not called during initialize() — use onInitialize() for initial setup.
8563
8682
  */
@@ -8586,82 +8705,12 @@ var BaseAddon = class {
8586
8705
  scopedSettingsCapabilities() {
8587
8706
  return [];
8588
8707
  }
8589
- /** Override to provide global-level settings UI schema. */
8590
- globalSettingsSchema(_cap) {
8591
- return null;
8592
- }
8593
8708
  /** Override to provide device-level settings UI schema. */
8594
8709
  deviceSettingsSchema() {
8595
8710
  return null;
8596
8711
  }
8597
8712
  /**
8598
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8599
- * ARE the configuration of its integration.
8600
- *
8601
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8602
- * operator should find on the addon's integration page (System →
8603
- * Integrations → <name>) rather than only in the cluster-wide list of every
8604
- * addon. Empty (the default) means the addon has no integration-level
8605
- * settings and no such surface is offered — this is opt-in, because whether
8606
- * an addon's configuration IS its integration's configuration depends on the
8607
- * nature of the integration.
8608
- *
8609
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8610
- * the ONE global schema, in the ONE addon store, written by the ONE
8611
- * `updateGlobalSettings` path. There is deliberately no
8612
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8613
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8614
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8615
- *
8616
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8617
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8618
- * removed with the reason recorded at
8619
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8620
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8621
- * marker sprinkled across sections also has to borrow a field that already
8622
- * means something else; borrowing `section.tab` put the literal word
8623
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8624
- * GROUP this visually" and cannot also mean "where this lives" (D269
8625
- * supersedes D268). One declaration, in one place, next to the schema whose
8626
- * ids it names.
8627
- */
8628
- integrationSettingSections() {
8629
- return [];
8630
- }
8631
- async getGlobalSettings(overlay, cap, nodeId) {
8632
- await this.loadScopedSchemas(cap);
8633
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8634
- if (!schema) return { sections: [] };
8635
- const projected = await this.readRawProjectedStore(nodeId, cap);
8636
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8637
- }
8638
- /**
8639
- * The integration-level view of this addon's settings: exactly the sections
8640
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8641
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8642
- *
8643
- * Returns `null` when the addon declared nothing — an addon that opts out has
8644
- * no integration settings surface at all, rather than an empty one that reads
8645
- * as a failed load.
8646
- *
8647
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8648
- * and not in whichever UI happens to render this:
8649
- *
8650
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8651
- * shown here is the same field, with the same bare key, that the addon's
8652
- * own page shows. There is no integration-specific writer — callers save
8653
- * through `updateGlobalSettings` — so a second store key is unreachable,
8654
- * not merely discouraged.
8655
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8656
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8657
- * such a field silently picked would be a wrong answer for the operator
8658
- * who opened the page (D266).
8659
- * 3. **No silent typo.** A declared id that names no section throws. The
8660
- * alternative — skip it — turns a rename into a surface that quietly
8661
- * empties, which looks exactly like an addon with nothing to configure.
8662
- */
8663
- /**
8664
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8713
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8665
8714
  *
8666
8715
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8667
8716
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8709,37 +8758,178 @@ var BaseAddon = class {
8709
8758
  }
8710
8759
  }
8711
8760
  /**
8761
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8762
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8763
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8764
+ */
8765
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8766
+ return null;
8767
+ }
8768
+ /**
8769
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8770
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8771
+ */
8772
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8773
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8774
+ clusterSettings(cap, schema) {
8775
+ return this.scopedHandle("cluster", cap, schema);
8776
+ }
8777
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8778
+ deviceDefaults(cap, schema) {
8779
+ return this.scopedHandle("device-defaults", cap, schema);
8780
+ }
8781
+ /**
8782
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8783
+ * with no row yields the schema defaults — never a bare-key fallback.
8784
+ */
8785
+ nodeSettings(cap, schema, nodeId) {
8786
+ return this.scopedHandle("node", cap, schema, nodeId);
8787
+ }
8788
+ /**
8789
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8790
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8791
+ * central delete. `nodeId` omitted ⇒ the local node.
8792
+ */
8793
+ nodeState(key, schema, fallback, nodeId) {
8794
+ this._declaredStateKeys.add(key);
8795
+ return createDurableState({
8796
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8797
+ schema,
8798
+ fallback,
8799
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8800
+ write: async (patch) => {
8801
+ await this._ctx?.settings?.writeAddonStore(patch);
8802
+ },
8803
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8804
+ addonId: this._ctx?.id,
8805
+ key: k,
8806
+ error: String(e)
8807
+ } })
8808
+ });
8809
+ }
8810
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8811
+ async declaredScopedSchema(scope, cap) {
8812
+ await this.loadScopedSchemas(cap);
8813
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8814
+ }
8815
+ async requireScopedSchema(scope, cap) {
8816
+ const schema = await this.declaredScopedSchema(scope, cap);
8817
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8818
+ return schema;
8819
+ }
8820
+ scopedKeys(schema) {
8821
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8822
+ }
8823
+ /** `store` narrowed to the keys `schema` declares. */
8824
+ narrowToScope(store, schema) {
8825
+ const keys = this.scopedKeys(schema);
8826
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8827
+ }
8828
+ scopedHandle(scope, cap, schema, nodeId) {
8829
+ return createScopedSettings({
8830
+ schema,
8831
+ read: async () => {
8832
+ const declared = await this.requireScopedSchema(scope, cap);
8833
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8834
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8835
+ return {
8836
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8837
+ ...stored
8838
+ };
8839
+ },
8840
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8841
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8842
+ });
8843
+ }
8844
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8845
+ async writeScoped(scope, cap, patch, nodeId) {
8846
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8847
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8848
+ }
8849
+ /**
8850
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8851
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8852
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8853
+ */
8854
+ async unsetScoped(scope, cap, keys, nodeId) {
8855
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8856
+ const target = scope === "node" ? nodeId : void 0;
8857
+ const tree = this.addonSecretTree();
8858
+ const secretKeys = keys.filter((key) => tree.has(key));
8859
+ const plainKeys = keys.filter((key) => !tree.has(key));
8860
+ let changed = false;
8861
+ if (plainKeys.length > 0) {
8862
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8863
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8864
+ }
8865
+ if (secretKeys.length > 0) {
8866
+ await this.clearSecretKeys(secretKeys, target, cap);
8867
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8868
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8869
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8870
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8871
+ changed = true;
8872
+ }
8873
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8874
+ }
8875
+ /**
8876
+ * The hydrated form of one scope of one capability's settings (any
8877
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8878
+ */
8879
+ async getScopedSettings(scope, cap, nodeId) {
8880
+ const schema = await this.declaredScopedSchema(scope, cap);
8881
+ if (schema === null) return null;
8882
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8883
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8884
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8885
+ }
8886
+ /**
8887
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8888
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8889
+ * nothing.
8890
+ */
8891
+ async listScopedSettings() {
8892
+ await this.loadScopedSchemas();
8893
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8894
+ cap,
8895
+ scope: entry.scope,
8896
+ sectionIds: entry.schema.sections.map((section) => section.id)
8897
+ })));
8898
+ }
8899
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8900
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8901
+ this._operatorWriteDepth++;
8902
+ try {
8903
+ await this.writeScoped(scope, cap, patch, nodeId);
8904
+ } finally {
8905
+ this._operatorWriteDepth--;
8906
+ }
8907
+ }
8908
+ /**
8909
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8910
+ * its value. `null` when the capability declares nothing for the scope.
8911
+ */
8912
+ async readScopedValues(scope, cap, nodeId) {
8913
+ const schema = await this.declaredScopedSchema(scope, cap);
8914
+ if (schema === null) return null;
8915
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8916
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8917
+ }
8918
+ /**
8712
8919
  * What the create wizard and the integration edit page render for one instance: the addon's
8713
8920
  * per-instance schema when it declares one, else its global schema (what they always showed).
8714
8921
  */
8715
8922
  async getIntegrationFormSchema(instance, nodeId) {
8716
8923
  const scoped = await this.integrationFormSchema(instance);
8717
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8924
+ if (scoped === null) return this.scopedForm(nodeId);
8718
8925
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8719
8926
  }
8720
- async getIntegrationSettings(nodeId) {
8721
- const declared = this.integrationSettingSections();
8722
- if (declared.length === 0) return null;
8723
- const schema = this.globalSettingsSchema();
8724
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8725
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8726
- const sections = [];
8727
- for (const id of declared) {
8728
- const section = byId.get(id);
8729
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8730
- const fields = dropPerNodeFields(section.fields);
8731
- if (fields.length === 0) continue;
8732
- sections.push({
8733
- ...section,
8734
- fields
8735
- });
8736
- }
8737
- if (sections.length === 0) return null;
8738
- const projected = await this.readRawProjectedStore(nodeId);
8739
- return hydrateSchema({
8740
- ...schema,
8741
- sections
8742
- }, projected, { inlineSecretsAsViews: true });
8927
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8928
+ async scopedForm(nodeId) {
8929
+ await this.loadScopedSchemas();
8930
+ const schema = this.scopedFormSchema();
8931
+ if (!schema) return { sections: [] };
8932
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8743
8933
  }
8744
8934
  /**
8745
8935
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8748,8 +8938,8 @@ var BaseAddon = class {
8748
8938
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8749
8939
  * A no-op passthrough when the schema declares no `perNode` field.
8750
8940
  *
8751
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8752
- * the store for custom option logic (option narrowing, value snapping) to
8941
+ * This is the sanctioned way for an addon that needs
8942
+ * the store for custom logic (option narrowing, value snapping) to
8753
8943
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8754
8944
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8755
8945
  *
@@ -8801,7 +8991,7 @@ var BaseAddon = class {
8801
8991
  }
8802
8992
  /**
8803
8993
  * Whether ONE global key of this addon's store was written, read three-way
8804
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8994
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8805
8995
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8806
8996
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8807
8997
  * not supported here: its stored name is scoped.
@@ -8835,7 +9025,7 @@ var BaseAddon = class {
8835
9025
  }
8836
9026
  /**
8837
9027
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8838
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9028
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8839
9029
  * the operator sends is stored as sent even when it equals a pre-keychain
8840
9030
  * inline value: the operator re-entering a password is a change, an addon
8841
9031
  * writing back what it read is not (D787).
@@ -8848,9 +9038,6 @@ var BaseAddon = class {
8848
9038
  this._operatorWriteDepth--;
8849
9039
  }
8850
9040
  }
8851
- async updateGlobalSettings(patch, nodeId) {
8852
- await this.applyGlobalPatch(patch, nodeId);
8853
- }
8854
9041
  /**
8855
9042
  * REMOVE stored secrets — their keychain values and their markers (D787).
8856
9043
  *
@@ -8860,6 +9047,9 @@ var BaseAddon = class {
8860
9047
  * a keychain (an older kernel) it writes `''`, as such code always did.
8861
9048
  */
8862
9049
  async clearSecrets(keys, nodeId) {
9050
+ await this.clearSecretKeys(keys, nodeId);
9051
+ }
9052
+ async clearSecretKeys(keys, nodeId, cap) {
8863
9053
  const tree = this.addonSecretTree();
8864
9054
  const keychain = this._ctx?.secrets !== void 0;
8865
9055
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8872,25 +9062,26 @@ var BaseAddon = class {
8872
9062
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8873
9063
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8874
9064
  }
8875
- await this.applyGlobalPatch(patch, nodeId);
9065
+ await this.applyGlobalPatch(patch, nodeId, cap);
8876
9066
  }
8877
- async applyGlobalPatch(barePatch, nodeId) {
8878
- await this.loadScopedSchemas();
8879
- const keys = this.perNodeKeys();
9067
+ /** Resolves `true` when the write changed stored values. */
9068
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9069
+ await this.loadScopedSchemas(cap);
9070
+ const keys = this.perNodeKeys(cap);
8880
9071
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8881
9072
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8882
9073
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8883
9074
  const handle = this._secretSettings;
8884
9075
  if (handle !== null) {
8885
9076
  const { changed } = await handle.writeAddonStore(scoped);
8886
- if (!changed) return;
9077
+ if (!changed) return false;
8887
9078
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8888
- if (target !== localNode) return;
9079
+ if (target !== localNode) return true;
8889
9080
  await this.resolveConfig();
8890
9081
  await this.onConfigChanged();
8891
9082
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8892
- const legacy = this.globalSettingsSchema();
8893
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9083
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9084
+ return true;
8894
9085
  }
8895
9086
  /**
8896
9087
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8927,10 +9118,7 @@ var BaseAddon = class {
8927
9118
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8928
9119
  this._addonSecretTree = null;
8929
9120
  }
8930
- /**
8931
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8932
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8933
- */
9121
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8934
9122
  scopedFormSchema(cap) {
8935
9123
  const byId = /* @__PURE__ */ new Map();
8936
9124
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8942,8 +9130,7 @@ var BaseAddon = class {
8942
9130
  return [...this._scopedByCap.values()].flat();
8943
9131
  }
8944
9132
  /**
8945
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8946
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9133
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8947
9134
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8948
9135
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8949
9136
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8952,8 +9139,7 @@ var BaseAddon = class {
8952
9139
  const cacheKey = cap ?? "";
8953
9140
  const cached = this._nodeKeysByCap.get(cacheKey);
8954
9141
  if (cached) return cached;
8955
- const legacy = this.globalSettingsSchema(cap);
8956
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9142
+ const keys = /* @__PURE__ */ new Set();
8957
9143
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8958
9144
  if (scope !== "node") continue;
8959
9145
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9171,8 +9357,6 @@ var BaseAddon = class {
9171
9357
  }
9172
9358
  declaredKeySet() {
9173
9359
  const keys = new Set(Object.keys(this.defaults));
9174
- const schema = this.globalSettingsSchema();
9175
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9176
9360
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9177
9361
  for (const key of this._declaredStateKeys) keys.add(key);
9178
9362
  for (const path of this.secretStatePaths) {
@@ -9270,7 +9454,7 @@ var BaseAddon = class {
9270
9454
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9271
9455
  addonSecretTree() {
9272
9456
  if (this._addonSecretTree === null) {
9273
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9457
+ const tree = /* @__PURE__ */ new Map();
9274
9458
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9275
9459
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9276
9460
  }
@@ -9482,47 +9666,6 @@ function classifyStoreError(err) {
9482
9666
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9483
9667
  return "permanent";
9484
9668
  }
9485
- /**
9486
- * Collect the keys of every field marked `perNode: true`, recursing into
9487
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9488
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9489
- * don't declare `perNode` and are excluded by the `in` narrowing.
9490
- */
9491
- /**
9492
- * The same fields with every `perNode: true` one removed, recursing into layout
9493
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9494
- * with no child is dropped rather than rendered empty.
9495
- *
9496
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9497
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9498
- */
9499
- function dropPerNodeFields(fields) {
9500
- const kept = [];
9501
- for (const field of fields) {
9502
- if (field.type === "group") {
9503
- const inner = dropPerNodeFields(field.fields);
9504
- if (inner.length > 0) kept.push({
9505
- ...field,
9506
- fields: inner
9507
- });
9508
- continue;
9509
- }
9510
- if (field.type === "sub-tabs") {
9511
- const tabs = field.tabs.map((tab) => ({
9512
- ...tab,
9513
- fields: dropPerNodeFields(tab.fields)
9514
- })).filter((tab) => tab.fields.length > 0);
9515
- if (tabs.length > 0) kept.push({
9516
- ...field,
9517
- tabs
9518
- });
9519
- continue;
9520
- }
9521
- if ("perNode" in field && field.perNode === true) continue;
9522
- kept.push(field);
9523
- }
9524
- return kept;
9525
- }
9526
9669
  /** The keys of the value-holding fields, recursing into layout containers. */
9527
9670
  function collectValueFieldKeys(fields) {
9528
9671
  const collected = [];
@@ -9555,21 +9698,6 @@ function collectFieldKeys(fields) {
9555
9698
  }
9556
9699
  return collected;
9557
9700
  }
9558
- function collectPerNodeFieldKeys(fields) {
9559
- const collected = [];
9560
- for (const field of fields) {
9561
- if (field.type === "group") {
9562
- collected.push(...collectPerNodeFieldKeys(field.fields));
9563
- continue;
9564
- }
9565
- if (field.type === "sub-tabs") {
9566
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9567
- continue;
9568
- }
9569
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9570
- }
9571
- return collected;
9572
- }
9573
9701
  /**
9574
9702
  * Normalize an `ICamstackAddon.initialize()` return value into the
9575
9703
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10620,7 +10748,6 @@ var CAP_NAMES = Object.freeze([
10620
10748
  "notifier",
10621
10749
  "numeric-sensor",
10622
10750
  "oauth-integration",
10623
- "oidc-connections",
10624
10751
  "osd",
10625
10752
  "osd-manager",
10626
10753
  "pet-feeder",
@@ -12327,13 +12454,25 @@ Object.freeze({
12327
12454
  "path": "temperature",
12328
12455
  "valueType": "number"
12329
12456
  }],
12330
- "timed-run": [{
12331
- "path": "activeUntil",
12332
- "valueType": "number"
12333
- }, {
12334
- "path": "requestedBy",
12335
- "valueType": "string"
12336
- }],
12457
+ "timed-run": [
12458
+ {
12459
+ "path": "activeUntil",
12460
+ "valueType": "number"
12461
+ },
12462
+ {
12463
+ "path": "requestedBy",
12464
+ "valueType": "string"
12465
+ },
12466
+ {
12467
+ "path": "quickRun.duration",
12468
+ "valueType": "number"
12469
+ },
12470
+ {
12471
+ "path": "quickRun.unit",
12472
+ "valueType": "enum",
12473
+ "enumValues": ["s", "min"]
12474
+ }
12475
+ ],
12337
12476
  "update": [
12338
12477
  {
12339
12478
  "path": "currentVersion",
@@ -12512,8 +12651,7 @@ Object.freeze({
12512
12651
  "device-provider": { "reloadDevice": 18e4 },
12513
12652
  "llm": {
12514
12653
  "generate": 6e5,
12515
- "generateVision": 6e5,
12516
- "testProfile": 6e5
12654
+ "generateVision": 6e5
12517
12655
  },
12518
12656
  "llm-runtime": {
12519
12657
  "complete": 6e5,
@@ -12558,7 +12696,7 @@ object({
12558
12696
  params: record(string(), union([string(), number()])).optional()
12559
12697
  });
12560
12698
  //#endregion
12561
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12699
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12562
12700
  /**
12563
12701
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12564
12702
  *
@@ -13720,6 +13858,387 @@ var NodeColorIdSchema = _enum([
13720
13858
  "node-h"
13721
13859
  ]);
13722
13860
  //#endregion
13861
+ //#region ../types/dist/convert-BAzH15HU.mjs
13862
+ /**
13863
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13864
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13865
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13866
+ * decimals). K derives from `°C = K − 273.15`.
13867
+ */
13868
+ var UNIT_TABLE = {
13869
+ "°C": {
13870
+ dimension: "temperature",
13871
+ factor: 1,
13872
+ offset: 0
13873
+ },
13874
+ "°F": {
13875
+ dimension: "temperature",
13876
+ factor: 5 / 9,
13877
+ offset: -160 / 9
13878
+ },
13879
+ K: {
13880
+ dimension: "temperature",
13881
+ factor: 1,
13882
+ offset: -273.15
13883
+ },
13884
+ hPa: {
13885
+ dimension: "pressure",
13886
+ factor: 1,
13887
+ offset: 0
13888
+ },
13889
+ kPa: {
13890
+ dimension: "pressure",
13891
+ factor: 10,
13892
+ offset: 0
13893
+ },
13894
+ Pa: {
13895
+ dimension: "pressure",
13896
+ factor: .01,
13897
+ offset: 0
13898
+ },
13899
+ mbar: {
13900
+ dimension: "pressure",
13901
+ factor: 1,
13902
+ offset: 0
13903
+ },
13904
+ bar: {
13905
+ dimension: "pressure",
13906
+ factor: 1e3,
13907
+ offset: 0
13908
+ },
13909
+ inHg: {
13910
+ dimension: "pressure",
13911
+ factor: 33.8639,
13912
+ offset: 0
13913
+ },
13914
+ mmHg: {
13915
+ dimension: "pressure",
13916
+ factor: 1.33322,
13917
+ offset: 0
13918
+ },
13919
+ psi: {
13920
+ dimension: "pressure",
13921
+ factor: 68.9476,
13922
+ offset: 0
13923
+ },
13924
+ "m/s": {
13925
+ dimension: "speed",
13926
+ factor: 1,
13927
+ offset: 0
13928
+ },
13929
+ "km/h": {
13930
+ dimension: "speed",
13931
+ factor: 1 / 3.6,
13932
+ offset: 0
13933
+ },
13934
+ mph: {
13935
+ dimension: "speed",
13936
+ factor: .44704,
13937
+ offset: 0
13938
+ },
13939
+ kn: {
13940
+ dimension: "speed",
13941
+ factor: .514444,
13942
+ offset: 0
13943
+ },
13944
+ "mm/h": {
13945
+ dimension: "precipitation-rate",
13946
+ factor: 1,
13947
+ offset: 0
13948
+ },
13949
+ "in/h": {
13950
+ dimension: "precipitation-rate",
13951
+ factor: 25.4,
13952
+ offset: 0
13953
+ },
13954
+ m: {
13955
+ dimension: "length",
13956
+ factor: 1,
13957
+ offset: 0
13958
+ },
13959
+ mm: {
13960
+ dimension: "length",
13961
+ factor: .001,
13962
+ offset: 0
13963
+ },
13964
+ cm: {
13965
+ dimension: "length",
13966
+ factor: .01,
13967
+ offset: 0
13968
+ },
13969
+ km: {
13970
+ dimension: "length",
13971
+ factor: 1e3,
13972
+ offset: 0
13973
+ },
13974
+ in: {
13975
+ dimension: "length",
13976
+ factor: .0254,
13977
+ offset: 0
13978
+ },
13979
+ ft: {
13980
+ dimension: "length",
13981
+ factor: .3048,
13982
+ offset: 0
13983
+ },
13984
+ mi: {
13985
+ dimension: "length",
13986
+ factor: 1609.344,
13987
+ offset: 0
13988
+ },
13989
+ "m²": {
13990
+ dimension: "area",
13991
+ factor: 1,
13992
+ offset: 0
13993
+ },
13994
+ lx: {
13995
+ dimension: "illuminance",
13996
+ factor: 1,
13997
+ offset: 0
13998
+ },
13999
+ "W/m²": {
14000
+ dimension: "irradiance",
14001
+ factor: 1,
14002
+ offset: 0
14003
+ },
14004
+ W: {
14005
+ dimension: "power",
14006
+ factor: 1,
14007
+ offset: 0
14008
+ },
14009
+ kW: {
14010
+ dimension: "power",
14011
+ factor: 1e3,
14012
+ offset: 0
14013
+ },
14014
+ VA: {
14015
+ dimension: "apparent-power",
14016
+ factor: 1,
14017
+ offset: 0
14018
+ },
14019
+ Wh: {
14020
+ dimension: "energy",
14021
+ factor: 1,
14022
+ offset: 0
14023
+ },
14024
+ kWh: {
14025
+ dimension: "energy",
14026
+ factor: 1e3,
14027
+ offset: 0
14028
+ },
14029
+ MWh: {
14030
+ dimension: "energy",
14031
+ factor: 1e6,
14032
+ offset: 0
14033
+ },
14034
+ V: {
14035
+ dimension: "voltage",
14036
+ factor: 1,
14037
+ offset: 0
14038
+ },
14039
+ mV: {
14040
+ dimension: "voltage",
14041
+ factor: .001,
14042
+ offset: 0
14043
+ },
14044
+ A: {
14045
+ dimension: "current",
14046
+ factor: 1,
14047
+ offset: 0
14048
+ },
14049
+ mA: {
14050
+ dimension: "current",
14051
+ factor: .001,
14052
+ offset: 0
14053
+ },
14054
+ ppm: {
14055
+ dimension: "concentration-volume",
14056
+ factor: 1,
14057
+ offset: 0
14058
+ },
14059
+ ppb: {
14060
+ dimension: "concentration-volume",
14061
+ factor: .001,
14062
+ offset: 0
14063
+ },
14064
+ "µg/m³": {
14065
+ dimension: "concentration-mass",
14066
+ factor: 1,
14067
+ offset: 0
14068
+ },
14069
+ "mg/m³": {
14070
+ dimension: "concentration-mass",
14071
+ factor: 1e3,
14072
+ offset: 0
14073
+ },
14074
+ kg: {
14075
+ dimension: "mass",
14076
+ factor: 1,
14077
+ offset: 0
14078
+ },
14079
+ g: {
14080
+ dimension: "mass",
14081
+ factor: .001,
14082
+ offset: 0
14083
+ },
14084
+ lb: {
14085
+ dimension: "mass",
14086
+ factor: .453592,
14087
+ offset: 0
14088
+ },
14089
+ oz: {
14090
+ dimension: "mass",
14091
+ factor: .0283495,
14092
+ offset: 0
14093
+ },
14094
+ "%": {
14095
+ dimension: "percentage",
14096
+ factor: 1,
14097
+ offset: 0
14098
+ },
14099
+ L: {
14100
+ dimension: "volume",
14101
+ factor: 1,
14102
+ offset: 0
14103
+ },
14104
+ mL: {
14105
+ dimension: "volume",
14106
+ factor: .001,
14107
+ offset: 0
14108
+ },
14109
+ gal: {
14110
+ dimension: "volume",
14111
+ factor: 3.785411784,
14112
+ offset: 0
14113
+ },
14114
+ s: {
14115
+ dimension: "duration",
14116
+ factor: 1,
14117
+ offset: 0
14118
+ },
14119
+ ms: {
14120
+ dimension: "duration",
14121
+ factor: .001,
14122
+ offset: 0
14123
+ },
14124
+ min: {
14125
+ dimension: "duration",
14126
+ factor: 60,
14127
+ offset: 0
14128
+ },
14129
+ h: {
14130
+ dimension: "duration",
14131
+ factor: 3600,
14132
+ offset: 0
14133
+ },
14134
+ d: {
14135
+ dimension: "duration",
14136
+ factor: 86400,
14137
+ offset: 0
14138
+ },
14139
+ B: {
14140
+ dimension: "information",
14141
+ factor: 1,
14142
+ offset: 0
14143
+ },
14144
+ KiB: {
14145
+ dimension: "information",
14146
+ factor: 1024,
14147
+ offset: 0
14148
+ },
14149
+ MiB: {
14150
+ dimension: "information",
14151
+ factor: 1024 ** 2,
14152
+ offset: 0
14153
+ },
14154
+ GiB: {
14155
+ dimension: "information",
14156
+ factor: 1024 ** 3,
14157
+ offset: 0
14158
+ },
14159
+ TiB: {
14160
+ dimension: "information",
14161
+ factor: 1024 ** 4,
14162
+ offset: 0
14163
+ },
14164
+ kB: {
14165
+ dimension: "information",
14166
+ factor: 1e3,
14167
+ offset: 0
14168
+ },
14169
+ MB: {
14170
+ dimension: "information",
14171
+ factor: 1e6,
14172
+ offset: 0
14173
+ },
14174
+ GB: {
14175
+ dimension: "information",
14176
+ factor: 1e9,
14177
+ offset: 0
14178
+ },
14179
+ "°": {
14180
+ dimension: "angle",
14181
+ factor: 1,
14182
+ offset: 0
14183
+ },
14184
+ dBFS: {
14185
+ dimension: "sound-level",
14186
+ factor: 1,
14187
+ offset: 0
14188
+ },
14189
+ dBm: {
14190
+ dimension: "signal-level",
14191
+ factor: 1,
14192
+ offset: 0
14193
+ },
14194
+ mired: {
14195
+ dimension: "colour-temperature",
14196
+ factor: 1,
14197
+ offset: 0
14198
+ },
14199
+ kbps: {
14200
+ dimension: "bitrate",
14201
+ factor: 1,
14202
+ offset: 0
14203
+ },
14204
+ Mbps: {
14205
+ dimension: "bitrate",
14206
+ factor: 1e3,
14207
+ offset: 0
14208
+ },
14209
+ fps: {
14210
+ dimension: "rate",
14211
+ factor: 1,
14212
+ offset: 0
14213
+ }
14214
+ };
14215
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14216
+ function specFor(unit) {
14217
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14218
+ }
14219
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14220
+ function toCanonical(value, spec) {
14221
+ return value * spec.factor + spec.offset;
14222
+ }
14223
+ /** Canonical-unit value → the unit described by `spec`. */
14224
+ function fromCanonical(canonical, spec) {
14225
+ return (canonical - spec.offset) / spec.factor;
14226
+ }
14227
+ /**
14228
+ * Graceful variant for the render path: returns the converted value, or
14229
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14230
+ * Never throws — a render caller falls back to the raw value + source unit.
14231
+ */
14232
+ function tryConvertUnit(value, from, to) {
14233
+ if (from === to) return value;
14234
+ const fromSpec = specFor(from);
14235
+ if (!fromSpec) return void 0;
14236
+ const toSpec = specFor(to);
14237
+ if (!toSpec) return void 0;
14238
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14239
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14240
+ }
14241
+ //#endregion
13723
14242
  //#region ../types/dist/index.mjs
13724
14243
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13725
14244
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -13916,17 +14435,18 @@ var IntegrationInstanceRefSchema = object({
13916
14435
  * message to what the kind supports — callers never special-case a service.
13917
14436
  *
13918
14437
  * DESIGN DECISIONS (locked):
13919
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13920
- * `setTargetEnabled`), each provider persisting via the `settings-store`
13921
- * cap. Rationale: the admin UI needs one uniform surface across the
13922
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13923
- * alternative would fork the UI per addon and cannot host the
13924
- * discovery→adopt flow.
13925
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13926
- * the generated cap-mount auto-`concatCollection`-fans them across every
13927
- * registered provider (notifiers addon + HA addon) so one catalog is
13928
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13929
- * `addonId` the generated collection router extracts from the call input.
14438
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14439
+ * providers serve their targets as `integration-instances` records, one
14440
+ * record kind per target kind, and the admin lists, adds, edits, switches
14441
+ * and deletes them through that cap. This cap keeps what an instance cannot
14442
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14443
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14444
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14445
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14446
+ * cap-mount auto-`concatCollection`-fans them across every registered
14447
+ * provider so one catalog is routable. `send` / `testTarget` /
14448
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14449
+ * collection router extracts from the call input.
13930
14450
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13931
14451
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13932
14452
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14171,7 +14691,7 @@ var TargetKindCapsSchema = object({
14171
14691
  * the union is large and not meant for runtime validation here; the exported
14172
14692
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14173
14693
  */
14174
- var ConfigSchemaPassthrough$4 = unknown();
14694
+ var ConfigSchemaPassthrough$3 = unknown();
14175
14695
  var TargetKindSchema = object({
14176
14696
  kind: string(),
14177
14697
  label: string(),
@@ -14208,7 +14728,7 @@ var TargetKindSchema = object({
14208
14728
  * its raster path, which is the safe default for an unknown image.
14209
14729
  */
14210
14730
  iconMediaType: string().optional(),
14211
- configSchema: ConfigSchemaPassthrough$4,
14731
+ configSchema: ConfigSchemaPassthrough$3,
14212
14732
  /**
14213
14733
  * The config keys that decide WHERE this kind's secrets are sent (server,
14214
14734
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14230,9 +14750,11 @@ var TargetKindSchema = object({
14230
14750
  caps: TargetKindCapsSchema
14231
14751
  });
14232
14752
  /**
14233
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14234
- * (return a presence marker only) when serving `listTargets` — never
14235
- * round-trip a stored secret to the UI.
14753
+ * A target as a provider's own code sees it (the notification-center bridge, the
14754
+ * send path): the flat record body split into the name, the switch and the
14755
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14756
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14757
+ * secret to the UI.
14236
14758
  */
14237
14759
  var TargetSchema = object({
14238
14760
  id: string(),
@@ -14275,13 +14797,13 @@ var ProbeTargetConfigResultSchema = object({
14275
14797
  stateInstance: string().optional(),
14276
14798
  error: string().optional()
14277
14799
  });
14278
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14800
+ method(object({}), array(TargetKindSchema)), method(object({
14279
14801
  kind: string(),
14280
14802
  config: record(string(), unknown()).optional(),
14281
14803
  /**
14282
14804
  * When editing / duplicating, the blank password field never round-trips
14283
14805
  * the stored token. Pass the source target id so the provider can restore
14284
- * secrets server-side before dialling (same merge as upsert).
14806
+ * secrets server-side before dialling.
14285
14807
  */
14286
14808
  targetId: string().optional()
14287
14809
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14304,27 +14826,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14304
14826
  }), TestResultSchema, {
14305
14827
  kind: "mutation",
14306
14828
  auth: "admin"
14307
- }), method(object({
14308
- target: TargetSchema,
14309
- /**
14310
- * Duplicate flow: create a NEW target id while copying secrets from an
14311
- * existing row. The UI never holds the stored token; blank / redacted
14312
- * password fields are restored from this source (D634 still applies if
14313
- * destinations diverge without a re-typed secret).
14314
- */
14315
- cloneSecretsFrom: string().optional()
14316
- }), TargetSchema, {
14317
- kind: "mutation",
14318
- auth: "admin"
14319
- }), method(object({ targetId: string() }), _void(), {
14320
- kind: "mutation",
14321
- auth: "admin"
14322
- }), method(object({
14323
- targetId: string(),
14324
- enabled: boolean()
14325
- }), _void(), {
14326
- kind: "mutation",
14327
- auth: "admin"
14328
14829
  });
14329
14830
  looseObject({
14330
14831
  name: string().min(1),
@@ -22592,7 +23093,6 @@ var SYSTEM_CAP_VOCABULARY = {
22592
23093
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22593
23094
  "notification-rules": system("notification rules — authored on their own page"),
22594
23095
  "oauth-integration": system(IDENTITY),
22595
- "oidc-connections": system(IDENTITY),
22596
23096
  "osd-manager": system(PIPELINE),
22597
23097
  "pipeline-executor": system(PIPELINE),
22598
23098
  "pipeline-orchestrator": system(PIPELINE),
@@ -42613,6 +43113,7 @@ var ExpressionBindingSourceSchema = union([
42613
43113
  object({
42614
43114
  kind: literal("global"),
42615
43115
  sourceStableId: string(),
43116
+ addonId: string().optional(),
42616
43117
  cap: string(),
42617
43118
  fieldPath: string()
42618
43119
  })
@@ -42679,385 +43180,6 @@ var SourceInfoSchema = object({
42679
43180
  raw: record(string(), unknown()).optional()
42680
43181
  });
42681
43182
  /**
42682
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42683
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42684
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42685
- * decimals). K derives from `°C = K − 273.15`.
42686
- */
42687
- var UNIT_TABLE = {
42688
- "°C": {
42689
- dimension: "temperature",
42690
- factor: 1,
42691
- offset: 0
42692
- },
42693
- "°F": {
42694
- dimension: "temperature",
42695
- factor: 5 / 9,
42696
- offset: -160 / 9
42697
- },
42698
- K: {
42699
- dimension: "temperature",
42700
- factor: 1,
42701
- offset: -273.15
42702
- },
42703
- hPa: {
42704
- dimension: "pressure",
42705
- factor: 1,
42706
- offset: 0
42707
- },
42708
- kPa: {
42709
- dimension: "pressure",
42710
- factor: 10,
42711
- offset: 0
42712
- },
42713
- Pa: {
42714
- dimension: "pressure",
42715
- factor: .01,
42716
- offset: 0
42717
- },
42718
- mbar: {
42719
- dimension: "pressure",
42720
- factor: 1,
42721
- offset: 0
42722
- },
42723
- bar: {
42724
- dimension: "pressure",
42725
- factor: 1e3,
42726
- offset: 0
42727
- },
42728
- inHg: {
42729
- dimension: "pressure",
42730
- factor: 33.8639,
42731
- offset: 0
42732
- },
42733
- mmHg: {
42734
- dimension: "pressure",
42735
- factor: 1.33322,
42736
- offset: 0
42737
- },
42738
- psi: {
42739
- dimension: "pressure",
42740
- factor: 68.9476,
42741
- offset: 0
42742
- },
42743
- "m/s": {
42744
- dimension: "speed",
42745
- factor: 1,
42746
- offset: 0
42747
- },
42748
- "km/h": {
42749
- dimension: "speed",
42750
- factor: 1 / 3.6,
42751
- offset: 0
42752
- },
42753
- mph: {
42754
- dimension: "speed",
42755
- factor: .44704,
42756
- offset: 0
42757
- },
42758
- kn: {
42759
- dimension: "speed",
42760
- factor: .514444,
42761
- offset: 0
42762
- },
42763
- "mm/h": {
42764
- dimension: "precipitation-rate",
42765
- factor: 1,
42766
- offset: 0
42767
- },
42768
- "in/h": {
42769
- dimension: "precipitation-rate",
42770
- factor: 25.4,
42771
- offset: 0
42772
- },
42773
- m: {
42774
- dimension: "length",
42775
- factor: 1,
42776
- offset: 0
42777
- },
42778
- mm: {
42779
- dimension: "length",
42780
- factor: .001,
42781
- offset: 0
42782
- },
42783
- cm: {
42784
- dimension: "length",
42785
- factor: .01,
42786
- offset: 0
42787
- },
42788
- km: {
42789
- dimension: "length",
42790
- factor: 1e3,
42791
- offset: 0
42792
- },
42793
- in: {
42794
- dimension: "length",
42795
- factor: .0254,
42796
- offset: 0
42797
- },
42798
- ft: {
42799
- dimension: "length",
42800
- factor: .3048,
42801
- offset: 0
42802
- },
42803
- mi: {
42804
- dimension: "length",
42805
- factor: 1609.344,
42806
- offset: 0
42807
- },
42808
- "m²": {
42809
- dimension: "area",
42810
- factor: 1,
42811
- offset: 0
42812
- },
42813
- lx: {
42814
- dimension: "illuminance",
42815
- factor: 1,
42816
- offset: 0
42817
- },
42818
- "W/m²": {
42819
- dimension: "irradiance",
42820
- factor: 1,
42821
- offset: 0
42822
- },
42823
- W: {
42824
- dimension: "power",
42825
- factor: 1,
42826
- offset: 0
42827
- },
42828
- kW: {
42829
- dimension: "power",
42830
- factor: 1e3,
42831
- offset: 0
42832
- },
42833
- VA: {
42834
- dimension: "apparent-power",
42835
- factor: 1,
42836
- offset: 0
42837
- },
42838
- Wh: {
42839
- dimension: "energy",
42840
- factor: 1,
42841
- offset: 0
42842
- },
42843
- kWh: {
42844
- dimension: "energy",
42845
- factor: 1e3,
42846
- offset: 0
42847
- },
42848
- MWh: {
42849
- dimension: "energy",
42850
- factor: 1e6,
42851
- offset: 0
42852
- },
42853
- V: {
42854
- dimension: "voltage",
42855
- factor: 1,
42856
- offset: 0
42857
- },
42858
- mV: {
42859
- dimension: "voltage",
42860
- factor: .001,
42861
- offset: 0
42862
- },
42863
- A: {
42864
- dimension: "current",
42865
- factor: 1,
42866
- offset: 0
42867
- },
42868
- mA: {
42869
- dimension: "current",
42870
- factor: .001,
42871
- offset: 0
42872
- },
42873
- ppm: {
42874
- dimension: "concentration-volume",
42875
- factor: 1,
42876
- offset: 0
42877
- },
42878
- ppb: {
42879
- dimension: "concentration-volume",
42880
- factor: .001,
42881
- offset: 0
42882
- },
42883
- "µg/m³": {
42884
- dimension: "concentration-mass",
42885
- factor: 1,
42886
- offset: 0
42887
- },
42888
- "mg/m³": {
42889
- dimension: "concentration-mass",
42890
- factor: 1e3,
42891
- offset: 0
42892
- },
42893
- kg: {
42894
- dimension: "mass",
42895
- factor: 1,
42896
- offset: 0
42897
- },
42898
- g: {
42899
- dimension: "mass",
42900
- factor: .001,
42901
- offset: 0
42902
- },
42903
- lb: {
42904
- dimension: "mass",
42905
- factor: .453592,
42906
- offset: 0
42907
- },
42908
- oz: {
42909
- dimension: "mass",
42910
- factor: .0283495,
42911
- offset: 0
42912
- },
42913
- "%": {
42914
- dimension: "percentage",
42915
- factor: 1,
42916
- offset: 0
42917
- },
42918
- L: {
42919
- dimension: "volume",
42920
- factor: 1,
42921
- offset: 0
42922
- },
42923
- mL: {
42924
- dimension: "volume",
42925
- factor: .001,
42926
- offset: 0
42927
- },
42928
- gal: {
42929
- dimension: "volume",
42930
- factor: 3.785411784,
42931
- offset: 0
42932
- },
42933
- s: {
42934
- dimension: "duration",
42935
- factor: 1,
42936
- offset: 0
42937
- },
42938
- ms: {
42939
- dimension: "duration",
42940
- factor: .001,
42941
- offset: 0
42942
- },
42943
- min: {
42944
- dimension: "duration",
42945
- factor: 60,
42946
- offset: 0
42947
- },
42948
- h: {
42949
- dimension: "duration",
42950
- factor: 3600,
42951
- offset: 0
42952
- },
42953
- d: {
42954
- dimension: "duration",
42955
- factor: 86400,
42956
- offset: 0
42957
- },
42958
- B: {
42959
- dimension: "information",
42960
- factor: 1,
42961
- offset: 0
42962
- },
42963
- KiB: {
42964
- dimension: "information",
42965
- factor: 1024,
42966
- offset: 0
42967
- },
42968
- MiB: {
42969
- dimension: "information",
42970
- factor: 1024 ** 2,
42971
- offset: 0
42972
- },
42973
- GiB: {
42974
- dimension: "information",
42975
- factor: 1024 ** 3,
42976
- offset: 0
42977
- },
42978
- TiB: {
42979
- dimension: "information",
42980
- factor: 1024 ** 4,
42981
- offset: 0
42982
- },
42983
- kB: {
42984
- dimension: "information",
42985
- factor: 1e3,
42986
- offset: 0
42987
- },
42988
- MB: {
42989
- dimension: "information",
42990
- factor: 1e6,
42991
- offset: 0
42992
- },
42993
- GB: {
42994
- dimension: "information",
42995
- factor: 1e9,
42996
- offset: 0
42997
- },
42998
- "°": {
42999
- dimension: "angle",
43000
- factor: 1,
43001
- offset: 0
43002
- },
43003
- dBFS: {
43004
- dimension: "sound-level",
43005
- factor: 1,
43006
- offset: 0
43007
- },
43008
- dBm: {
43009
- dimension: "signal-level",
43010
- factor: 1,
43011
- offset: 0
43012
- },
43013
- mired: {
43014
- dimension: "colour-temperature",
43015
- factor: 1,
43016
- offset: 0
43017
- },
43018
- kbps: {
43019
- dimension: "bitrate",
43020
- factor: 1,
43021
- offset: 0
43022
- },
43023
- Mbps: {
43024
- dimension: "bitrate",
43025
- factor: 1e3,
43026
- offset: 0
43027
- },
43028
- fps: {
43029
- dimension: "rate",
43030
- factor: 1,
43031
- offset: 0
43032
- }
43033
- };
43034
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43035
- function specFor(unit) {
43036
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43037
- }
43038
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43039
- function toCanonical(value, spec) {
43040
- return value * spec.factor + spec.offset;
43041
- }
43042
- /** Canonical-unit value → the unit described by `spec`. */
43043
- function fromCanonical(canonical, spec) {
43044
- return (canonical - spec.offset) / spec.factor;
43045
- }
43046
- /**
43047
- * Graceful variant for the render path: returns the converted value, or
43048
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43049
- * Never throws — a render caller falls back to the raw value + source unit.
43050
- */
43051
- function tryConvertUnit(value, from, to) {
43052
- if (from === to) return value;
43053
- const fromSpec = specFor(from);
43054
- if (!fromSpec) return void 0;
43055
- const toSpec = specFor(to);
43056
- if (!toSpec) return void 0;
43057
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43058
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43059
- }
43060
- /**
43061
43183
  * A number a person types for a physical quantity — the input side of the
43062
43184
  * value + unit contract (D972).
43063
43185
  *
@@ -44734,15 +44856,28 @@ var SettingsSchemaWithValuesSchema = object({
44734
44856
  });
44735
44857
  /** Patch object — keys are field names, values are the new field values. */
44736
44858
  var SettingsPatchSchema = record(string(), unknown());
44859
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
44860
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
44861
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
44862
+ var ScopedSettingsDeclarationSchema = object({
44863
+ cap: string(),
44864
+ scope: ScopedSettingsScopeSchema,
44865
+ sectionIds: array(string())
44866
+ });
44737
44867
  /** Standard success response for update operations. */
44738
44868
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44739
44869
  method(object({
44740
44870
  addonId: string(),
44741
- nodeId: string().optional(),
44742
- overlay: record(string(), unknown()).optional(),
44743
- cap: string().optional()
44871
+ cap: string(),
44872
+ scope: ScopedSettingsScopeSchema,
44873
+ nodeId: string().optional()
44744
44874
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44745
44875
  addonId: string(),
44876
+ nodeId: string().optional()
44877
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
44878
+ addonId: string(),
44879
+ cap: string(),
44880
+ scope: ScopedSettingsScopeSchema,
44746
44881
  nodeId: string().optional(),
44747
44882
  patch: SettingsPatchSchema
44748
44883
  }), SettingsUpdateResultSchema, {
@@ -44750,8 +44885,10 @@ method(object({
44750
44885
  auth: "admin"
44751
44886
  }), method(object({
44752
44887
  addonId: string(),
44888
+ cap: string(),
44889
+ scope: ScopedSettingsScopeSchema,
44753
44890
  nodeId: string().optional()
44754
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44891
+ }), record(string(), unknown()).nullable()), method(object({
44755
44892
  addonId: string(),
44756
44893
  nodeId: string().optional(),
44757
44894
  instance: IntegrationInstanceRefSchema
@@ -46091,7 +46228,7 @@ var BackupDestinationInfoSchema = object({
46091
46228
  id: string(),
46092
46229
  /**
46093
46230
  * Manifest id of the owning addon. The Settings button on the
46094
- * destination card opens that addon's `globalSettingsSchema` panel.
46231
+ * destination card opens that addon's scoped settings panel.
46095
46232
  */
46096
46233
  addonId: string(),
46097
46234
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50561,6 +50698,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50561
50698
  kind: "mutation",
50562
50699
  auth: "admin"
50563
50700
  });
50701
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50564
50702
  /**
50565
50703
  * A SITE ZONE — a named part of the property that several locations belong to.
50566
50704
  *
@@ -50954,6 +51092,12 @@ var DeviceInfoSchema = object({
50954
51092
  addonId: string(),
50955
51093
  type: _enum(DeviceType),
50956
51094
  name: string(),
51095
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51096
+ * entity_id style. Generated once from the name at first registration, unique
51097
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51098
+ * Optional: a record produced by an older build omits it until the boot
51099
+ * backfill has run. */
51100
+ alias: DeviceAliasSchema.optional(),
50957
51101
  /** Operator-organisational location label. `null` when unset. */
50958
51102
  location: string().nullable(),
50959
51103
  /** Soft-disabled flag. */
@@ -51273,6 +51417,25 @@ method(object({
51273
51417
  }), _void(), {
51274
51418
  kind: "mutation",
51275
51419
  auth: "admin"
51420
+ }), method(object({
51421
+ deviceId: number(),
51422
+ alias: string()
51423
+ }), discriminatedUnion("ok", [object({
51424
+ ok: literal(true),
51425
+ alias: DeviceAliasSchema
51426
+ }), object({
51427
+ ok: literal(false),
51428
+ reason: _enum([
51429
+ "empty",
51430
+ "too-long",
51431
+ "grammar",
51432
+ "reserved",
51433
+ "taken"
51434
+ ]),
51435
+ takenBy: number().optional()
51436
+ })]), {
51437
+ kind: "mutation",
51438
+ auth: "admin"
51276
51439
  }), method(object({
51277
51440
  deviceId: number(),
51278
51441
  location: string().nullable()
@@ -54298,7 +54461,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54298
54461
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54299
54462
  * clears it.
54300
54463
  */
54301
- var ConfigSchemaPassthrough$3 = custom();
54464
+ var ConfigSchemaPassthrough$2 = custom();
54302
54465
  var IntegrationInstanceIdSchema = string().min(1);
54303
54466
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54304
54467
  /**
@@ -54344,7 +54507,16 @@ var IntegrationInstanceKindSchema = object({
54344
54507
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54345
54508
  connectPath: string().min(1).optional(),
54346
54509
  /** Relative hub path to disconnect that account. */
54347
- disconnectPath: string().min(1).optional()
54510
+ disconnectPath: string().min(1).optional(),
54511
+ /**
54512
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54513
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54514
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54515
+ */
54516
+ widgets: array(object({
54517
+ id: string().min(1),
54518
+ placement: _enum(["connection", "model"])
54519
+ })).optional()
54348
54520
  });
54349
54521
  /**
54350
54522
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54449,7 +54621,7 @@ var integrationInstancesCapability = {
54449
54621
  id: IntegrationInstanceIdSchema.nullable(),
54450
54622
  /** The kind of the create form (`id: null`) of a provider with several. */
54451
54623
  kind: string().min(1).optional()
54452
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54624
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54453
54625
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54454
54626
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54455
54627
  /**
@@ -55400,12 +55572,12 @@ var LlmModelInfoSchema = object({
55400
55572
  });
55401
55573
  /**
55402
55574
  * The provider-specific part of a profile, validated by the provider that owns
55403
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55575
+ * the kind (saving an instance refuses what it does not accept). A field its
55404
55576
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55405
55577
  * keychain, never in the row, and reads back as the redaction marker.
55406
55578
  */
55407
55579
  var LlmProviderConfigSchema = record(string(), unknown());
55408
- var LlmProfileSchema = object({
55580
+ object({
55409
55581
  id: string(),
55410
55582
  name: string(),
55411
55583
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55499,8 +55671,8 @@ var LlmProviderWidgetSchema = object({
55499
55671
  /** ConfigUISchema tree passed through untyped on the wire (the
55500
55672
  * notification-output `ConfigSchemaPassthrough` precedent at
55501
55673
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55502
- var ConfigSchemaPassthrough$2 = unknown();
55503
- var LlmProfileKindDescriptorSchema = object({
55674
+ var ConfigSchemaPassthrough$1 = unknown();
55675
+ object({
55504
55676
  kind: LlmProfileKindSchema,
55505
55677
  label: string(),
55506
55678
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55508,9 +55680,9 @@ var LlmProfileKindDescriptorSchema = object({
55508
55680
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55509
55681
  addonId: string(),
55510
55682
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55511
- connectionSchema: ConfigSchemaPassthrough$2,
55683
+ connectionSchema: ConfigSchemaPassthrough$1,
55512
55684
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55513
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55685
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55514
55686
  /** The kind's defaults; a model from `listModels` may override any of them. */
55515
55687
  features: LlmModelFeaturesSchema,
55516
55688
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55639,20 +55811,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55639
55811
  }), method(object({
55640
55812
  addonId: string().optional(),
55641
55813
  requestId: string()
55642
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55643
- kind: "mutation",
55644
- auth: "admin"
55645
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55646
- kind: "mutation",
55647
- auth: "admin"
55648
- }), method(ProfileRefInputSchema, _void(), {
55649
- kind: "mutation",
55650
- auth: "admin"
55651
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55652
- kind: "mutation",
55653
- auth: "admin",
55654
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55655
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55814
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55656
55815
  kind: "mutation",
55657
55816
  auth: "admin"
55658
55817
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56647,20 +56806,7 @@ method(_void(), MeshStatusSchema), method(object({
56647
56806
  /** Optional hostname override the host should advertise once joined. */
56648
56807
  hostname: string().optional() }), object({
56649
56808
  /** Authentication URL the operator should open in a browser. */
56650
- 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({
56651
- /** Optional auth key — when provided, probes the key validity
56652
- * against the provider's API. Omit when already joined to
56653
- * just ping the daemon. */
56654
- authKey: string().optional() }), object({
56655
- ok: boolean(),
56656
- /** Provider-side identifier resolved by the probe (tailnet
56657
- * name for Tailscale, network id for ZeroTier, etc.). */
56658
- tenant: string().optional(),
56659
- /** Daemon binary version, when reachable. */
56660
- daemonVersion: string().optional(),
56661
- /** Human-readable error when `ok: false`. */
56662
- error: string().optional()
56663
- }), { kind: "mutation" });
56809
+ 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() }));
56664
56810
  object({
56665
56811
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56666
56812
  lastFetchedAt: number()
@@ -64039,6 +64185,11 @@ var NcScheduleSchema = object({
64039
64185
  * as a STATE SUMMARY at the end of each window of `schedule`.
64040
64186
  */
64041
64187
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64188
+ _enum([
64189
+ "activation",
64190
+ "deactivation",
64191
+ "schedule"
64192
+ ]);
64042
64193
  /**
64043
64194
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64044
64195
  * every section; `section` = one notification per section with something to
@@ -64055,11 +64206,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64055
64206
  * midnight crossing); the send time is each window's END. Refused on the
64056
64207
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64057
64208
  */
64058
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64059
- mode: literal("summary"),
64060
- schedule: NcScheduleSchema,
64061
- split: NcStateSummarySplitSchema.optional()
64062
- })]);
64209
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64210
+ object({ mode: literal("instant") }),
64211
+ object({
64212
+ mode: literal("summary"),
64213
+ schedule: NcScheduleSchema,
64214
+ split: NcStateSummarySplitSchema.optional()
64215
+ }),
64216
+ object({
64217
+ mode: literal("modes"),
64218
+ /** The bad edge, and every kind that has a single edge. */
64219
+ onActivation: boolean(),
64220
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64221
+ onDeactivation: boolean(),
64222
+ /** A state summary at the end of each window; absent = no summary. */
64223
+ onSchedule: object({
64224
+ schedule: NcScheduleSchema,
64225
+ grouping: NcStateSummarySplitSchema
64226
+ }).optional()
64227
+ })
64228
+ ]);
64063
64229
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64064
64230
  var NcPlateMatcherSchema = object({
64065
64231
  /**
@@ -66336,61 +66502,6 @@ var OauthIntegrationDescriptorSchema = object({
66336
66502
  });
66337
66503
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66338
66504
  /**
66339
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66340
- *
66341
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66342
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66343
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66344
- * immutable after create.
66345
- */
66346
- var ConfigSchemaPassthrough$1 = unknown();
66347
- var OidcConnectionSchema = object({
66348
- id: string().min(1),
66349
- displayName: string(),
66350
- icon: string(),
66351
- issuerUrl: string(),
66352
- clientId: string(),
66353
- /** Client id + secret present — never the secret value. */
66354
- configured: boolean(),
66355
- /**
66356
- * Values for the connection form schema. `clientSecret` is a keychain
66357
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66358
- */
66359
- formValues: record(string(), unknown()).optional()
66360
- });
66361
- /**
66362
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66363
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66364
- */
66365
- var OidcConnectionUpdateInputSchema = object({
66366
- id: string().min(1),
66367
- displayName: string().min(1),
66368
- icon: string().optional(),
66369
- issuerUrl: string().optional(),
66370
- clientId: string().optional(),
66371
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66372
- redirectUri: string().optional(),
66373
- scopes: string().optional(),
66374
- usernameClaim: _enum([
66375
- "preferred_username",
66376
- "email",
66377
- "sub"
66378
- ]).optional()
66379
- });
66380
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66381
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66382
- displayName: string().min(1)
66383
- }), object({ id: string() }), {
66384
- kind: "mutation",
66385
- auth: "admin"
66386
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66387
- kind: "mutation",
66388
- auth: "admin"
66389
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66390
- kind: "mutation",
66391
- auth: "admin"
66392
- });
66393
- /**
66394
66505
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66395
66506
  * a firmware-defined list of overlays (channel name, timestamp,
66396
66507
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -72563,21 +72674,6 @@ method(object({
72563
72674
  */
72564
72675
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72565
72676
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72566
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72567
- providerId: string().min(1),
72568
- displayName: string().min(1),
72569
- configSchema: unknown(),
72570
- nodeLocal: boolean(),
72571
- shouldSaveDiskSpace: literal(true),
72572
- minFreePercent: number().min(0).max(100)
72573
- }), object({
72574
- providerId: string().min(1),
72575
- displayName: string().min(1),
72576
- configSchema: unknown(),
72577
- nodeLocal: boolean(),
72578
- shouldSaveDiskSpace: literal(false),
72579
- minFreePercent: literal(null)
72580
- })]);
72581
72677
  method(object({
72582
72678
  location: StorageLocationRefSchema,
72583
72679
  relativePath: string()
@@ -72639,42 +72735,7 @@ method(object({
72639
72735
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72640
72736
  kind: "mutation",
72641
72737
  auth: "admin"
72642
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72643
- createdAt: true,
72644
- updatedAt: true
72645
- }), StorageLocationSchema, {
72646
- kind: "mutation",
72647
- auth: "admin"
72648
- }), method(object({
72649
- id: string(),
72650
- force: boolean().optional()
72651
- }), _void(), {
72652
- kind: "mutation",
72653
- auth: "admin"
72654
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72655
- ok: boolean(),
72656
- error: string().optional()
72657
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72658
- providerId: string(),
72659
- /**
72660
- * The location this config is an UNSAVED edit of, when there is one.
72661
- *
72662
- * `listLocations` replaces every declared secret with the redaction
72663
- * sentinel, so the edit modal's form state holds the sentinel for any
72664
- * credential the operator did not retype — and posting that here
72665
- * without a way to resolve it makes the provider try to authenticate
72666
- * as `__camstack_redacted__` and report the operator's own working
72667
- * password as wrong. Given this id, the orchestrator restores each
72668
- * sentinel from the stored config (same rule as `upsertLocation`)
72669
- * before dispatching. Omitted by the "Add location" wizard, where
72670
- * every value was typed just now and nothing is stored yet.
72671
- */
72672
- locationId: string().optional(),
72673
- config: record(string(), unknown())
72674
- }), object({
72675
- ok: boolean(),
72676
- error: string().optional()
72677
- }), { auth: "admin" });
72738
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72678
72739
  /** One table of the settings database. */
72679
72740
  var StorageTableSchema = object({
72680
72741
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -73905,7 +73966,7 @@ var TerminalInstanceInfoSchema = object({
73905
73966
  /**
73906
73967
  * False while the node the instance is bound to is not in the cluster
73907
73968
  * topology (forgotten, renamed, re-paired). The instance is kept; the
73908
- * operator re-points it with `updateInstance({ targetNodeId })`.
73969
+ * operator re-points it on the Terminal page.
73909
73970
  */
73910
73971
  nodeInCluster: boolean(),
73911
73972
  profileId: string(),
@@ -73943,41 +74004,7 @@ var TerminalOutputBatchSchema = object({
73943
74004
  snapshot: string().optional(),
73944
74005
  events: array(TerminalOutputEventSchema).readonly()
73945
74006
  });
73946
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
73947
- targetNodeId: string().min(1),
73948
- profileId: string().min(1),
73949
- name: string().trim().min(1).max(160).optional(),
73950
- executable: string().max(1024).optional(),
73951
- args: array(string().max(2048)).max(64).optional(),
73952
- cwd: string().max(1024).optional(),
73953
- environment: array(string().max(4096)).max(64).optional(),
73954
- profileSettings: ProfileSettingsBagSchema.optional()
73955
- }), TerminalInstanceInfoSchema, {
73956
- kind: "mutation",
73957
- auth: "admin"
73958
- }), method(object({
73959
- instanceId: string().min(1),
73960
- /** Re-point the instance to another node; the profile must exist there. */
73961
- targetNodeId: string().min(1).max(256).optional(),
73962
- name: string().trim().min(1).max(160).optional(),
73963
- executable: string().max(1024).optional(),
73964
- args: array(string().max(2048)).max(64).optional(),
73965
- cwd: string().max(1024).optional(),
73966
- environment: array(string().max(4096)).max(64).optional(),
73967
- profileSettings: ProfileSettingsBagSchema.optional()
73968
- }), TerminalInstanceInfoSchema, {
73969
- kind: "mutation",
73970
- auth: "admin"
73971
- }), method(object({ instanceId: string().min(1) }), _void(), {
73972
- kind: "mutation",
73973
- auth: "admin"
73974
- }), method(object({
73975
- instanceId: string().min(1),
73976
- enabled: boolean()
73977
- }), TerminalInstanceInfoSchema, {
73978
- kind: "mutation",
73979
- auth: "admin"
73980
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74007
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
73981
74008
  stableId: string().min(1),
73982
74009
  name: string().trim().min(1).max(160).optional()
73983
74010
  }), TerminalInstanceInfoSchema, {
@@ -74046,6 +74073,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74046
74073
  _enum(["switch", "pet-fountain"]);
74047
74074
  DeviceType.Heater, DeviceType.PetFountain;
74048
74075
  DeviceType.Heater, DeviceType.PetFountain;
74076
+ /** One duration, as the one-tap run asks for it. */
74077
+ var TimedRunQuickRunSchema = object({
74078
+ duration: number().positive().finite(),
74079
+ unit: TimedRunUnitSchema
74080
+ });
74049
74081
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74050
74082
  var TimedRunRefusalSchema = _enum([
74051
74083
  "unsupported-device",
@@ -74066,9 +74098,30 @@ object({
74066
74098
  activeUntil: number().nullable(),
74067
74099
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74068
74100
  requestedBy: string().nullable(),
74101
+ /**
74102
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74103
+ * the device is gone or its type has no timed run; a status from before this field
74104
+ * existed has none, and a surface falls back to the type's default.
74105
+ */
74106
+ quickRun: TimedRunQuickRunSchema.nullable(),
74069
74107
  lastFetchedAt: number()
74070
74108
  });
74071
- method(object({
74109
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74110
+ var TimedRunOptionsSchema = object({
74111
+ unit: TimedRunUnitSchema,
74112
+ /** The longest run, in `unit`. */
74113
+ maxDuration: number(),
74114
+ defaultDuration: number()
74115
+ });
74116
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74117
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74118
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74119
+ deviceId: number().int(),
74120
+ settings: TimedRunSettingsPatchSchema
74121
+ }), _void(), {
74122
+ kind: "mutation",
74123
+ auth: "admin"
74124
+ }), method(object({
74072
74125
  deviceId: number().int(),
74073
74126
  duration: number().positive().finite(),
74074
74127
  unit: TimedRunUnitSchema
@@ -76774,21 +76827,28 @@ Object.freeze({
76774
76827
  access: "view",
76775
76828
  role: "admin"
76776
76829
  },
76777
- "addonSettings.getGlobalSettings": {
76830
+ "addonSettings.getIntegrationFormSchema": {
76831
+ capName: "addon-settings",
76832
+ capScope: "system",
76833
+ addonId: null,
76834
+ access: "view",
76835
+ role: "admin"
76836
+ },
76837
+ "addonSettings.getScopedSettings": {
76778
76838
  capName: "addon-settings",
76779
76839
  capScope: "system",
76780
76840
  addonId: null,
76781
76841
  access: "view",
76782
76842
  role: "admin"
76783
76843
  },
76784
- "addonSettings.getIntegrationFormSchema": {
76844
+ "addonSettings.listScopedSettings": {
76785
76845
  capName: "addon-settings",
76786
76846
  capScope: "system",
76787
76847
  addonId: null,
76788
76848
  access: "view",
76789
76849
  role: "admin"
76790
76850
  },
76791
- "addonSettings.getIntegrationSettings": {
76851
+ "addonSettings.readScopedValues": {
76792
76852
  capName: "addon-settings",
76793
76853
  capScope: "system",
76794
76854
  addonId: null,
@@ -76809,7 +76869,7 @@ Object.freeze({
76809
76869
  access: "create",
76810
76870
  role: "admin"
76811
76871
  },
76812
- "addonSettings.updateGlobalSettings": {
76872
+ "addonSettings.updateScopedSettings": {
76813
76873
  capName: "addon-settings",
76814
76874
  capScope: "system",
76815
76875
  addonId: null,
@@ -78979,6 +79039,13 @@ Object.freeze({
78979
79039
  access: "create",
78980
79040
  role: "admin"
78981
79041
  },
79042
+ "deviceManager.setAlias": {
79043
+ capName: "device-manager",
79044
+ capScope: "system",
79045
+ addonId: null,
79046
+ access: "create",
79047
+ role: "admin"
79048
+ },
78982
79049
  "deviceManager.setChildLayout": {
78983
79050
  capName: "device-manager",
78984
79051
  capScope: "system",
@@ -80267,13 +80334,6 @@ Object.freeze({
80267
80334
  access: "delete",
80268
80335
  role: "admin"
80269
80336
  },
80270
- "llm.deleteProfile": {
80271
- capName: "llm",
80272
- capScope: "system",
80273
- addonId: null,
80274
- access: "delete",
80275
- role: "admin"
80276
- },
80277
80337
  "llm.generate": {
80278
80338
  capName: "llm",
80279
80339
  capScope: "system",
@@ -80337,20 +80397,6 @@ Object.freeze({
80337
80397
  access: "view",
80338
80398
  role: "admin"
80339
80399
  },
80340
- "llm.listProfileKinds": {
80341
- capName: "llm",
80342
- capScope: "system",
80343
- addonId: null,
80344
- access: "view",
80345
- role: "admin"
80346
- },
80347
- "llm.listProfiles": {
80348
- capName: "llm",
80349
- capScope: "system",
80350
- addonId: null,
80351
- access: "view",
80352
- role: "admin"
80353
- },
80354
80400
  "llm.listRuntimeNodes": {
80355
80401
  capName: "llm",
80356
80402
  capScope: "system",
@@ -80358,13 +80404,6 @@ Object.freeze({
80358
80404
  access: "view",
80359
80405
  role: "admin"
80360
80406
  },
80361
- "llm.migrateRawSecrets": {
80362
- capName: "llm",
80363
- capScope: "system",
80364
- addonId: null,
80365
- access: "create",
80366
- role: "admin"
80367
- },
80368
80407
  "llm.resolveModelRef": {
80369
80408
  capName: "llm",
80370
80409
  capScope: "system",
@@ -80393,20 +80432,6 @@ Object.freeze({
80393
80432
  access: "create",
80394
80433
  role: "admin"
80395
80434
  },
80396
- "llm.testProfile": {
80397
- capName: "llm",
80398
- capScope: "system",
80399
- addonId: null,
80400
- access: "create",
80401
- role: "admin"
80402
- },
80403
- "llm.upsertProfile": {
80404
- capName: "llm",
80405
- capScope: "system",
80406
- addonId: null,
80407
- access: "create",
80408
- role: "admin"
80409
- },
80410
80435
  "llmRuntime.appleAvailability": {
80411
80436
  capName: "llm-runtime",
80412
80437
  capScope: "system",
@@ -80911,13 +80936,6 @@ Object.freeze({
80911
80936
  access: "create",
80912
80937
  role: "admin"
80913
80938
  },
80914
- "meshNetwork.testConnection": {
80915
- capName: "mesh-network",
80916
- capScope: "system",
80917
- addonId: null,
80918
- access: "create",
80919
- role: "admin"
80920
- },
80921
80939
  "meter.getStatus": {
80922
80940
  capName: "meter",
80923
80941
  capScope: "device",
@@ -81576,13 +81594,6 @@ Object.freeze({
81576
81594
  access: "create",
81577
81595
  role: "admin"
81578
81596
  },
81579
- "notificationOutput.deleteTarget": {
81580
- capName: "notification-output",
81581
- capScope: "system",
81582
- addonId: null,
81583
- access: "delete",
81584
- role: "admin"
81585
- },
81586
81597
  "notificationOutput.discoverTargets": {
81587
81598
  capName: "notification-output",
81588
81599
  capScope: "system",
@@ -81590,13 +81601,6 @@ Object.freeze({
81590
81601
  access: "view",
81591
81602
  role: "admin"
81592
81603
  },
81593
- "notificationOutput.getTarget": {
81594
- capName: "notification-output",
81595
- capScope: "system",
81596
- addonId: null,
81597
- access: "view",
81598
- role: "admin"
81599
- },
81600
81604
  "notificationOutput.listTargetKinds": {
81601
81605
  capName: "notification-output",
81602
81606
  capScope: "system",
@@ -81604,13 +81608,6 @@ Object.freeze({
81604
81608
  access: "view",
81605
81609
  role: "admin"
81606
81610
  },
81607
- "notificationOutput.listTargets": {
81608
- capName: "notification-output",
81609
- capScope: "system",
81610
- addonId: null,
81611
- access: "view",
81612
- role: "admin"
81613
- },
81614
81611
  "notificationOutput.probeTargetConfig": {
81615
81612
  capName: "notification-output",
81616
81613
  capScope: "system",
@@ -81625,13 +81622,6 @@ Object.freeze({
81625
81622
  access: "create",
81626
81623
  role: "admin"
81627
81624
  },
81628
- "notificationOutput.setTargetEnabled": {
81629
- capName: "notification-output",
81630
- capScope: "system",
81631
- addonId: null,
81632
- access: "create",
81633
- role: "admin"
81634
- },
81635
81625
  "notificationOutput.testTarget": {
81636
81626
  capName: "notification-output",
81637
81627
  capScope: "system",
@@ -81639,13 +81629,6 @@ Object.freeze({
81639
81629
  access: "create",
81640
81630
  role: "admin"
81641
81631
  },
81642
- "notificationOutput.upsertTarget": {
81643
- capName: "notification-output",
81644
- capScope: "system",
81645
- addonId: null,
81646
- access: "create",
81647
- role: "admin"
81648
- },
81649
81632
  "notificationRules.activateAlarmProfile": {
81650
81633
  capName: "notification-rules",
81651
81634
  capScope: "system",
@@ -81905,41 +81888,6 @@ Object.freeze({
81905
81888
  access: "view",
81906
81889
  role: "admin"
81907
81890
  },
81908
- "oidcConnections.connectionFormSchema": {
81909
- capName: "oidc-connections",
81910
- capScope: "system",
81911
- addonId: null,
81912
- access: "view",
81913
- role: "admin"
81914
- },
81915
- "oidcConnections.createConnection": {
81916
- capName: "oidc-connections",
81917
- capScope: "system",
81918
- addonId: null,
81919
- access: "create",
81920
- role: "admin"
81921
- },
81922
- "oidcConnections.deleteConnection": {
81923
- capName: "oidc-connections",
81924
- capScope: "system",
81925
- addonId: null,
81926
- access: "delete",
81927
- role: "admin"
81928
- },
81929
- "oidcConnections.listConnections": {
81930
- capName: "oidc-connections",
81931
- capScope: "system",
81932
- addonId: null,
81933
- access: "view",
81934
- role: "admin"
81935
- },
81936
- "oidcConnections.updateConnection": {
81937
- capName: "oidc-connections",
81938
- capScope: "system",
81939
- addonId: null,
81940
- access: "create",
81941
- role: "admin"
81942
- },
81943
81891
  "osd.getStatus": {
81944
81892
  capName: "osd",
81945
81893
  capScope: "device",
@@ -84971,13 +84919,6 @@ Object.freeze({
84971
84919
  access: "delete",
84972
84920
  role: "admin"
84973
84921
  },
84974
- "storage.deleteLocation": {
84975
- capName: "storage",
84976
- capScope: "system",
84977
- addonId: null,
84978
- access: "delete",
84979
- role: "admin"
84980
- },
84981
84922
  "storage.endDownload": {
84982
84923
  capName: "storage",
84983
84924
  capScope: "system",
@@ -85034,13 +84975,6 @@ Object.freeze({
85034
84975
  access: "view",
85035
84976
  role: "admin"
85036
84977
  },
85037
- "storage.listProviders": {
85038
- capName: "storage",
85039
- capScope: "system",
85040
- addonId: null,
85041
- access: "view",
85042
- role: "admin"
85043
- },
85044
84978
  "storage.read": {
85045
84979
  capName: "storage",
85046
84980
  capScope: "system",
@@ -85062,27 +84996,6 @@ Object.freeze({
85062
84996
  access: "view",
85063
84997
  role: "admin"
85064
84998
  },
85065
- "storage.testConfig": {
85066
- capName: "storage",
85067
- capScope: "system",
85068
- addonId: null,
85069
- access: "view",
85070
- role: "admin"
85071
- },
85072
- "storage.testLocation": {
85073
- capName: "storage",
85074
- capScope: "system",
85075
- addonId: null,
85076
- access: "view",
85077
- role: "admin"
85078
- },
85079
- "storage.upsertLocation": {
85080
- capName: "storage",
85081
- capScope: "system",
85082
- addonId: null,
85083
- access: "create",
85084
- role: "admin"
85085
- },
85086
84999
  "storage.write": {
85087
85000
  capName: "storage",
85088
85001
  capScope: "system",
@@ -85860,27 +85773,6 @@ Object.freeze({
85860
85773
  access: "create",
85861
85774
  role: "admin"
85862
85775
  },
85863
- "terminalSession.createInstance": {
85864
- capName: "terminal-session",
85865
- capScope: "system",
85866
- addonId: null,
85867
- access: "create",
85868
- role: "admin"
85869
- },
85870
- "terminalSession.deleteInstance": {
85871
- capName: "terminal-session",
85872
- capScope: "system",
85873
- addonId: null,
85874
- access: "delete",
85875
- role: "admin"
85876
- },
85877
- "terminalSession.listInstances": {
85878
- capName: "terminal-session",
85879
- capScope: "system",
85880
- addonId: null,
85881
- access: "view",
85882
- role: "admin"
85883
- },
85884
85776
  "terminalSession.listLegacyCameras": {
85885
85777
  capName: "terminal-session",
85886
85778
  capScope: "system",
@@ -85923,20 +85815,6 @@ Object.freeze({
85923
85815
  access: "create",
85924
85816
  role: "admin"
85925
85817
  },
85926
- "terminalSession.setInstanceEnabled": {
85927
- capName: "terminal-session",
85928
- capScope: "system",
85929
- addonId: null,
85930
- access: "create",
85931
- role: "admin"
85932
- },
85933
- "terminalSession.updateInstance": {
85934
- capName: "terminal-session",
85935
- capScope: "system",
85936
- addonId: null,
85937
- access: "create",
85938
- role: "admin"
85939
- },
85940
85818
  "terminalSession.writeInput": {
85941
85819
  capName: "terminal-session",
85942
85820
  capScope: "system",
@@ -85951,6 +85829,13 @@ Object.freeze({
85951
85829
  access: "create",
85952
85830
  role: "viewer"
85953
85831
  },
85832
+ "timedRun.getOptions": {
85833
+ capName: "timed-run",
85834
+ capScope: "device",
85835
+ addonId: null,
85836
+ access: "view",
85837
+ role: "viewer"
85838
+ },
85954
85839
  "timedRun.getStatus": {
85955
85840
  capName: "timed-run",
85956
85841
  capScope: "device",
@@ -85965,6 +85850,13 @@ Object.freeze({
85965
85850
  access: "create",
85966
85851
  role: "viewer"
85967
85852
  },
85853
+ "timedRun.setSettings": {
85854
+ capName: "timed-run",
85855
+ capScope: "device",
85856
+ addonId: null,
85857
+ access: "create",
85858
+ role: "admin"
85859
+ },
85968
85860
  "toast.onToast": {
85969
85861
  capName: "toast",
85970
85862
  capScope: "system",
@@ -86895,6 +86787,7 @@ function offered(received) {
86895
86787
  };
86896
86788
  }
86897
86789
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86790
+ string().min(1).max(4096);
86898
86791
  Object.freeze({
86899
86792
  "accessories.getStatus": [{
86900
86793
  name: "deviceId",
@@ -87578,6 +87471,11 @@ Object.freeze({
87578
87471
  form: "single",
87579
87472
  optional: false
87580
87473
  }],
87474
+ "deviceManager.setAlias": [{
87475
+ name: "deviceId",
87476
+ form: "single",
87477
+ optional: false
87478
+ }],
87581
87479
  "deviceManager.setChildLayout": [{
87582
87480
  name: "deviceId",
87583
87481
  form: "single",
@@ -89642,6 +89540,11 @@ Object.freeze({
89642
89540
  form: "single",
89643
89541
  optional: false
89644
89542
  }],
89543
+ "timedRun.getOptions": [{
89544
+ name: "deviceId",
89545
+ form: "single",
89546
+ optional: false
89547
+ }],
89645
89548
  "timedRun.getStatus": [{
89646
89549
  name: "deviceId",
89647
89550
  form: "single",
@@ -89652,6 +89555,11 @@ Object.freeze({
89652
89555
  form: "single",
89653
89556
  optional: false
89654
89557
  }],
89558
+ "timedRun.setSettings": [{
89559
+ name: "deviceId",
89560
+ form: "single",
89561
+ optional: false
89562
+ }],
89655
89563
  "update.getStatus": [{
89656
89564
  name: "deviceId",
89657
89565
  form: "single",
@@ -90514,6 +90422,7 @@ var STATE_SUMMARY_VARS = [
90514
90422
  [...RULE_TEXT],
90515
90423
  [...RULE_TEXT],
90516
90424
  [...RULE_TEXT],
90425
+ [...RULE_TEXT],
90517
90426
  [...RULE_BODY_ONLY],
90518
90427
  [...RULE_TEXT],
90519
90428
  ([...RULE_TEXT], [...DEVICE_EVENTS]),