@camstack/addon-smtp-nodemailer 1.2.151 → 1.2.153

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.
@@ -36,7 +36,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
38
38
  //#endregion
39
- //#region ../types/dist/event-category-BmPm98n0.mjs
39
+ //#region ../types/dist/event-category-78tcBemo.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -157,6 +157,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
157
157
  */
158
158
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
159
159
  /**
160
+ * An addon is RUNNING although its installed package is below what the
161
+ * running server requires (`camstack.requires.addons` of the server, or the
162
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
163
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
164
+ * operator alert per node and package. The addon is never held back for it.
165
+ *
166
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
167
+ */
168
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
169
+ /**
170
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
171
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
172
+ *
173
+ * Payload: `{ packageName, addonId, nodeId }`.
174
+ */
175
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
176
+ /**
160
177
  * Monitor scheduled the next retry for a failed addon. Transient —
161
178
  * surfaced to the UI for live-updating the "next retry in Ns"
162
179
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -267,7 +284,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
267
284
  EventCategory["ProcessRestarted"] = "process.restarted";
268
285
  /**
269
286
  * The SET of storage locations changed — one was created, edited, enabled,
270
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
287
+ * disabled or deleted through the location instances or the forced-delete action.
271
288
  *
272
289
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
273
290
  * it must also converge on its own periodic path, because a dropped event
@@ -822,7 +839,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
822
839
  return EventCategory;
823
840
  }({});
824
841
  //#endregion
825
- //#region ../types/dist/device-type-D1fH5kaC.mjs
842
+ //#region ../types/dist/device-type-DxCT0krO.mjs
826
843
  /**
827
844
  import { errMsg } from '@camstack/types'
828
845
  * Extract a human-readable message from an unknown error value.
@@ -984,6 +1001,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
984
1001
  * filters that want "all devices with on-motion behaviour".
985
1002
  */
986
1003
  DeviceFeature["MotionTrigger"] = "motion-trigger";
1004
+ /**
1005
+ * Device can be run for a chosen time and then stopped. Mirrors the
1006
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
1007
+ * a driver, so the flag is derived from the device's bindings
1008
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
1009
+ */
1010
+ DeviceFeature["TimedRun"] = "timed-run";
987
1011
  /** Light supports rgb-triplet color via `color` cap. */
988
1012
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
989
1013
  /** Light supports HSV color via `color` cap. */
@@ -6018,7 +6042,7 @@ var ZodIssueCode = {
6018
6042
  var ZodFirstPartyTypeKind;
6019
6043
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6020
6044
  //#endregion
6021
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6045
+ //#region ../types/dist/sleep-7El4r5Bl.mjs
6022
6046
  /**
6023
6047
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6024
6048
  * window to float samples (D455).
@@ -6382,6 +6406,59 @@ Object.fromEntries([
6382
6406
  function isValuelessField(field) {
6383
6407
  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";
6384
6408
  }
6409
+ /**
6410
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6411
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6412
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6413
+ * pre-resolving) lets callers both read the effective value AND detect
6414
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6415
+ * malformed input yields an empty map, never throws.
6416
+ */
6417
+ function collectHydratedFieldEntries(schema) {
6418
+ const out = /* @__PURE__ */ new Map();
6419
+ if (typeof schema !== "object" || schema === null) return out;
6420
+ const sections = schema.sections;
6421
+ if (!Array.isArray(sections)) return out;
6422
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6423
+ return out;
6424
+ }
6425
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6426
+ function collectFieldEntriesInto(fields, into) {
6427
+ if (!Array.isArray(fields)) return;
6428
+ for (const field of fields) {
6429
+ if (typeof field !== "object" || field === null) continue;
6430
+ const type = field.type;
6431
+ if (type === "group") {
6432
+ collectFieldEntriesInto(field.fields, into);
6433
+ continue;
6434
+ }
6435
+ if (type === "sub-tabs") {
6436
+ const tabs = field.tabs;
6437
+ if (Array.isArray(tabs)) {
6438
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6439
+ }
6440
+ continue;
6441
+ }
6442
+ const key = field.key;
6443
+ if (typeof key !== "string" || key.length === 0) continue;
6444
+ if (!("value" in field)) continue;
6445
+ into.set(key, {
6446
+ value: field.value,
6447
+ default: field.default
6448
+ });
6449
+ }
6450
+ }
6451
+ /**
6452
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6453
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6454
+ * common "just give me the resolved values" case. Built on
6455
+ * {@link collectHydratedFieldEntries}.
6456
+ */
6457
+ function collectHydratedFieldValues(schema) {
6458
+ const out = /* @__PURE__ */ new Map();
6459
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6460
+ return out;
6461
+ }
6385
6462
  function hydrateSchema(schema, values, options = {}) {
6386
6463
  return {
6387
6464
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7266,17 +7343,35 @@ var RecordIntegrationInstances = class {
7266
7343
  ...kind !== void 0 ? { kind } : {}
7267
7344
  });
7268
7345
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7346
+ const initial = this.hooks.initialBody(name, kind);
7347
+ const patch = this.patchOf(values ?? {});
7348
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7349
+ kind,
7350
+ body: {
7351
+ ...initial,
7352
+ ...patch
7353
+ }
7354
+ }));
7269
7355
  await this.handle.put({
7270
7356
  id: minted,
7271
- data: this.hooks.initialBody(name, kind)
7357
+ data: initial
7272
7358
  }, { ifRev: 0 });
7273
- const patch = this.patchOf(values ?? {});
7274
7359
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7275
7360
  await this.hooks.onChanged?.(minted, "created");
7276
7361
  return { id: minted };
7277
7362
  };
7278
7363
  updateInstance = async ({ id, name, values }) => {
7279
7364
  const patch = this.patchOf(values);
7365
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7366
+ const stored = await this.handle.get(id);
7367
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7368
+ kind: void 0,
7369
+ body: {
7370
+ ...stored.data,
7371
+ ...patch
7372
+ }
7373
+ }));
7374
+ }
7280
7375
  if (name !== void 0) patch[this.nameField] = name;
7281
7376
  await this.handle.patch(id, patch);
7282
7377
  await this.hooks.onChanged?.(id, "updated");
@@ -7587,6 +7682,21 @@ var SCOPED_SETTINGS_SCOPES = [
7587
7682
  "device-defaults",
7588
7683
  "node"
7589
7684
  ];
7685
+ /** The one refusal of a key a scope did not declare. */
7686
+ function assertDeclaredKeys(keys, declared) {
7687
+ const undeclared = keys.filter((key) => !declared.has(key));
7688
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7689
+ }
7690
+ /** Build the handle over an addon's scoped read, write and unset. */
7691
+ function createScopedSettings(deps) {
7692
+ return {
7693
+ async get() {
7694
+ return deps.schema.parse(await deps.read());
7695
+ },
7696
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7697
+ unset: (keys) => deps.unset(keys)
7698
+ };
7699
+ }
7590
7700
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7591
7701
  var KeychainWriteRefusedError = class extends Error {
7592
7702
  fieldPath;
@@ -8162,7 +8272,16 @@ function createHandle(raw, opts) {
8162
8272
  return blob;
8163
8273
  }
8164
8274
  },
8165
- writeAddonStore: writeAddon
8275
+ writeAddonStore: writeAddon,
8276
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8277
+ const stored = await readStoredAddonBlob(raw);
8278
+ index.seen("addon", stored);
8279
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8280
+ if (removable.length === 0) return;
8281
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8282
+ await raw.writeAddonStore(patch);
8283
+ index.wrote("addon", patch);
8284
+ })
8166
8285
  };
8167
8286
  }
8168
8287
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8292,7 +8411,7 @@ function boundOf(list) {
8292
8411
  * Base class for CamStack addons. Eliminates settings boilerplate:
8293
8412
  *
8294
8413
  * - Typed `config` property with automatic resolution from store + defaults
8295
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8414
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8296
8415
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8297
8416
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8298
8417
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8302,7 +8421,7 @@ function boundOf(list) {
8302
8421
  * Subclasses override:
8303
8422
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8304
8423
  * - `onShutdown()` — cleanup
8305
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8424
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8306
8425
  *
8307
8426
  * @example
8308
8427
  * ```ts
@@ -8316,7 +8435,7 @@ function boundOf(list) {
8316
8435
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8317
8436
  * }
8318
8437
  *
8319
- * protected globalSettingsSchema() {
8438
+ * protected clusterSettingsSchema() {
8320
8439
  * return this.schema({
8321
8440
  * sections: [{
8322
8441
  * id: 'main', title: 'Settings',
@@ -8359,7 +8478,7 @@ var BaseAddon = class {
8359
8478
  defaults;
8360
8479
  /**
8361
8480
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8362
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8481
+ * itself (`state()`, the scoped writers) that no form field declares.
8363
8482
  * Stored in the keychain exactly like a `password` field. Syntax:
8364
8483
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8365
8484
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8369,7 +8488,7 @@ var BaseAddon = class {
8369
8488
  /**
8370
8489
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8371
8490
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8372
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8491
+ * see them. Empty until loaded.
8373
8492
  */
8374
8493
  _scopedByCap = /* @__PURE__ */ new Map();
8375
8494
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8555,7 +8674,7 @@ var BaseAddon = class {
8555
8674
  /** Addon-specific cleanup. Override if needed. */
8556
8675
  async onShutdown() {}
8557
8676
  /**
8558
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8677
+ * Called after config is resolved during a settings write.
8559
8678
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8560
8679
  * Not called during initialize() — use onInitialize() for initial setup.
8561
8680
  */
@@ -8584,82 +8703,12 @@ var BaseAddon = class {
8584
8703
  scopedSettingsCapabilities() {
8585
8704
  return [];
8586
8705
  }
8587
- /** Override to provide global-level settings UI schema. */
8588
- globalSettingsSchema(_cap) {
8589
- return null;
8590
- }
8591
8706
  /** Override to provide device-level settings UI schema. */
8592
8707
  deviceSettingsSchema() {
8593
8708
  return null;
8594
8709
  }
8595
8710
  /**
8596
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8597
- * ARE the configuration of its integration.
8598
- *
8599
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8600
- * operator should find on the addon's integration page (System →
8601
- * Integrations → <name>) rather than only in the cluster-wide list of every
8602
- * addon. Empty (the default) means the addon has no integration-level
8603
- * settings and no such surface is offered — this is opt-in, because whether
8604
- * an addon's configuration IS its integration's configuration depends on the
8605
- * nature of the integration.
8606
- *
8607
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8608
- * the ONE global schema, in the ONE addon store, written by the ONE
8609
- * `updateGlobalSettings` path. There is deliberately no
8610
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8611
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8612
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8613
- *
8614
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8615
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8616
- * removed with the reason recorded at
8617
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8618
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8619
- * marker sprinkled across sections also has to borrow a field that already
8620
- * means something else; borrowing `section.tab` put the literal word
8621
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8622
- * GROUP this visually" and cannot also mean "where this lives" (D269
8623
- * supersedes D268). One declaration, in one place, next to the schema whose
8624
- * ids it names.
8625
- */
8626
- integrationSettingSections() {
8627
- return [];
8628
- }
8629
- async getGlobalSettings(overlay, cap, nodeId) {
8630
- await this.loadScopedSchemas(cap);
8631
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8632
- if (!schema) return { sections: [] };
8633
- const projected = await this.readRawProjectedStore(nodeId, cap);
8634
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8635
- }
8636
- /**
8637
- * The integration-level view of this addon's settings: exactly the sections
8638
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8639
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8640
- *
8641
- * Returns `null` when the addon declared nothing — an addon that opts out has
8642
- * no integration settings surface at all, rather than an empty one that reads
8643
- * as a failed load.
8644
- *
8645
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8646
- * and not in whichever UI happens to render this:
8647
- *
8648
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8649
- * shown here is the same field, with the same bare key, that the addon's
8650
- * own page shows. There is no integration-specific writer — callers save
8651
- * through `updateGlobalSettings` — so a second store key is unreachable,
8652
- * not merely discouraged.
8653
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8654
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8655
- * such a field silently picked would be a wrong answer for the operator
8656
- * who opened the page (D266).
8657
- * 3. **No silent typo.** A declared id that names no section throws. The
8658
- * alternative — skip it — turns a rename into a surface that quietly
8659
- * empties, which looks exactly like an addon with nothing to configure.
8660
- */
8661
- /**
8662
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8711
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8663
8712
  *
8664
8713
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8665
8714
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8707,37 +8756,178 @@ var BaseAddon = class {
8707
8756
  }
8708
8757
  }
8709
8758
  /**
8759
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8760
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8761
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8762
+ */
8763
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8764
+ return null;
8765
+ }
8766
+ /**
8767
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8768
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8769
+ */
8770
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8771
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8772
+ clusterSettings(cap, schema) {
8773
+ return this.scopedHandle("cluster", cap, schema);
8774
+ }
8775
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8776
+ deviceDefaults(cap, schema) {
8777
+ return this.scopedHandle("device-defaults", cap, schema);
8778
+ }
8779
+ /**
8780
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8781
+ * with no row yields the schema defaults — never a bare-key fallback.
8782
+ */
8783
+ nodeSettings(cap, schema, nodeId) {
8784
+ return this.scopedHandle("node", cap, schema, nodeId);
8785
+ }
8786
+ /**
8787
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8788
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8789
+ * central delete. `nodeId` omitted ⇒ the local node.
8790
+ */
8791
+ nodeState(key, schema, fallback, nodeId) {
8792
+ this._declaredStateKeys.add(key);
8793
+ return createDurableState({
8794
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8795
+ schema,
8796
+ fallback,
8797
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8798
+ write: async (patch) => {
8799
+ await this._ctx?.settings?.writeAddonStore(patch);
8800
+ },
8801
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8802
+ addonId: this._ctx?.id,
8803
+ key: k,
8804
+ error: String(e)
8805
+ } })
8806
+ });
8807
+ }
8808
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8809
+ async declaredScopedSchema(scope, cap) {
8810
+ await this.loadScopedSchemas(cap);
8811
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8812
+ }
8813
+ async requireScopedSchema(scope, cap) {
8814
+ const schema = await this.declaredScopedSchema(scope, cap);
8815
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8816
+ return schema;
8817
+ }
8818
+ scopedKeys(schema) {
8819
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8820
+ }
8821
+ /** `store` narrowed to the keys `schema` declares. */
8822
+ narrowToScope(store, schema) {
8823
+ const keys = this.scopedKeys(schema);
8824
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8825
+ }
8826
+ scopedHandle(scope, cap, schema, nodeId) {
8827
+ return createScopedSettings({
8828
+ schema,
8829
+ read: async () => {
8830
+ const declared = await this.requireScopedSchema(scope, cap);
8831
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8832
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8833
+ return {
8834
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8835
+ ...stored
8836
+ };
8837
+ },
8838
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8839
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8840
+ });
8841
+ }
8842
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8843
+ async writeScoped(scope, cap, patch, nodeId) {
8844
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8845
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8846
+ }
8847
+ /**
8848
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8849
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8850
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8851
+ */
8852
+ async unsetScoped(scope, cap, keys, nodeId) {
8853
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8854
+ const target = scope === "node" ? nodeId : void 0;
8855
+ const tree = this.addonSecretTree();
8856
+ const secretKeys = keys.filter((key) => tree.has(key));
8857
+ const plainKeys = keys.filter((key) => !tree.has(key));
8858
+ let changed = false;
8859
+ if (plainKeys.length > 0) {
8860
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8861
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8862
+ }
8863
+ if (secretKeys.length > 0) {
8864
+ await this.clearSecretKeys(secretKeys, target, cap);
8865
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8866
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8867
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8868
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8869
+ changed = true;
8870
+ }
8871
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8872
+ }
8873
+ /**
8874
+ * The hydrated form of one scope of one capability's settings (any
8875
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8876
+ */
8877
+ async getScopedSettings(scope, cap, nodeId) {
8878
+ const schema = await this.declaredScopedSchema(scope, cap);
8879
+ if (schema === null) return null;
8880
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8881
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8882
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8883
+ }
8884
+ /**
8885
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8886
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8887
+ * nothing.
8888
+ */
8889
+ async listScopedSettings() {
8890
+ await this.loadScopedSchemas();
8891
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8892
+ cap,
8893
+ scope: entry.scope,
8894
+ sectionIds: entry.schema.sections.map((section) => section.id)
8895
+ })));
8896
+ }
8897
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8898
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8899
+ this._operatorWriteDepth++;
8900
+ try {
8901
+ await this.writeScoped(scope, cap, patch, nodeId);
8902
+ } finally {
8903
+ this._operatorWriteDepth--;
8904
+ }
8905
+ }
8906
+ /**
8907
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8908
+ * its value. `null` when the capability declares nothing for the scope.
8909
+ */
8910
+ async readScopedValues(scope, cap, nodeId) {
8911
+ const schema = await this.declaredScopedSchema(scope, cap);
8912
+ if (schema === null) return null;
8913
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8914
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8915
+ }
8916
+ /**
8710
8917
  * What the create wizard and the integration edit page render for one instance: the addon's
8711
8918
  * per-instance schema when it declares one, else its global schema (what they always showed).
8712
8919
  */
8713
8920
  async getIntegrationFormSchema(instance, nodeId) {
8714
8921
  const scoped = await this.integrationFormSchema(instance);
8715
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8922
+ if (scoped === null) return this.scopedForm(nodeId);
8716
8923
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8717
8924
  }
8718
- async getIntegrationSettings(nodeId) {
8719
- const declared = this.integrationSettingSections();
8720
- if (declared.length === 0) return null;
8721
- const schema = this.globalSettingsSchema();
8722
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8723
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8724
- const sections = [];
8725
- for (const id of declared) {
8726
- const section = byId.get(id);
8727
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8728
- const fields = dropPerNodeFields(section.fields);
8729
- if (fields.length === 0) continue;
8730
- sections.push({
8731
- ...section,
8732
- fields
8733
- });
8734
- }
8735
- if (sections.length === 0) return null;
8736
- const projected = await this.readRawProjectedStore(nodeId);
8737
- return hydrateSchema({
8738
- ...schema,
8739
- sections
8740
- }, projected, { inlineSecretsAsViews: true });
8925
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8926
+ async scopedForm(nodeId) {
8927
+ await this.loadScopedSchemas();
8928
+ const schema = this.scopedFormSchema();
8929
+ if (!schema) return { sections: [] };
8930
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8741
8931
  }
8742
8932
  /**
8743
8933
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8746,8 +8936,8 @@ var BaseAddon = class {
8746
8936
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8747
8937
  * A no-op passthrough when the schema declares no `perNode` field.
8748
8938
  *
8749
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8750
- * the store for custom option logic (option narrowing, value snapping) to
8939
+ * This is the sanctioned way for an addon that needs
8940
+ * the store for custom logic (option narrowing, value snapping) to
8751
8941
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8752
8942
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8753
8943
  *
@@ -8799,7 +8989,7 @@ var BaseAddon = class {
8799
8989
  }
8800
8990
  /**
8801
8991
  * Whether ONE global key of this addon's store was written, read three-way
8802
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8992
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8803
8993
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8804
8994
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8805
8995
  * not supported here: its stored name is scoped.
@@ -8833,7 +9023,7 @@ var BaseAddon = class {
8833
9023
  }
8834
9024
  /**
8835
9025
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8836
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9026
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8837
9027
  * the operator sends is stored as sent even when it equals a pre-keychain
8838
9028
  * inline value: the operator re-entering a password is a change, an addon
8839
9029
  * writing back what it read is not (D787).
@@ -8846,9 +9036,6 @@ var BaseAddon = class {
8846
9036
  this._operatorWriteDepth--;
8847
9037
  }
8848
9038
  }
8849
- async updateGlobalSettings(patch, nodeId) {
8850
- await this.applyGlobalPatch(patch, nodeId);
8851
- }
8852
9039
  /**
8853
9040
  * REMOVE stored secrets — their keychain values and their markers (D787).
8854
9041
  *
@@ -8858,6 +9045,9 @@ var BaseAddon = class {
8858
9045
  * a keychain (an older kernel) it writes `''`, as such code always did.
8859
9046
  */
8860
9047
  async clearSecrets(keys, nodeId) {
9048
+ await this.clearSecretKeys(keys, nodeId);
9049
+ }
9050
+ async clearSecretKeys(keys, nodeId, cap) {
8861
9051
  const tree = this.addonSecretTree();
8862
9052
  const keychain = this._ctx?.secrets !== void 0;
8863
9053
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8870,25 +9060,26 @@ var BaseAddon = class {
8870
9060
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8871
9061
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8872
9062
  }
8873
- await this.applyGlobalPatch(patch, nodeId);
9063
+ await this.applyGlobalPatch(patch, nodeId, cap);
8874
9064
  }
8875
- async applyGlobalPatch(barePatch, nodeId) {
8876
- await this.loadScopedSchemas();
8877
- const keys = this.perNodeKeys();
9065
+ /** Resolves `true` when the write changed stored values. */
9066
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9067
+ await this.loadScopedSchemas(cap);
9068
+ const keys = this.perNodeKeys(cap);
8878
9069
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8879
9070
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8880
9071
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8881
9072
  const handle = this._secretSettings;
8882
9073
  if (handle !== null) {
8883
9074
  const { changed } = await handle.writeAddonStore(scoped);
8884
- if (!changed) return;
9075
+ if (!changed) return false;
8885
9076
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8886
- if (target !== localNode) return;
9077
+ if (target !== localNode) return true;
8887
9078
  await this.resolveConfig();
8888
9079
  await this.onConfigChanged();
8889
9080
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8890
- const legacy = this.globalSettingsSchema();
8891
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9081
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9082
+ return true;
8892
9083
  }
8893
9084
  /**
8894
9085
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8925,10 +9116,7 @@ var BaseAddon = class {
8925
9116
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8926
9117
  this._addonSecretTree = null;
8927
9118
  }
8928
- /**
8929
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8930
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8931
- */
9119
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8932
9120
  scopedFormSchema(cap) {
8933
9121
  const byId = /* @__PURE__ */ new Map();
8934
9122
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8940,8 +9128,7 @@ var BaseAddon = class {
8940
9128
  return [...this._scopedByCap.values()].flat();
8941
9129
  }
8942
9130
  /**
8943
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8944
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9131
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8945
9132
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8946
9133
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8947
9134
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8950,8 +9137,7 @@ var BaseAddon = class {
8950
9137
  const cacheKey = cap ?? "";
8951
9138
  const cached = this._nodeKeysByCap.get(cacheKey);
8952
9139
  if (cached) return cached;
8953
- const legacy = this.globalSettingsSchema(cap);
8954
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9140
+ const keys = /* @__PURE__ */ new Set();
8955
9141
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8956
9142
  if (scope !== "node") continue;
8957
9143
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9169,8 +9355,6 @@ var BaseAddon = class {
9169
9355
  }
9170
9356
  declaredKeySet() {
9171
9357
  const keys = new Set(Object.keys(this.defaults));
9172
- const schema = this.globalSettingsSchema();
9173
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9174
9358
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9175
9359
  for (const key of this._declaredStateKeys) keys.add(key);
9176
9360
  for (const path of this.secretStatePaths) {
@@ -9268,7 +9452,7 @@ var BaseAddon = class {
9268
9452
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9269
9453
  addonSecretTree() {
9270
9454
  if (this._addonSecretTree === null) {
9271
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9455
+ const tree = /* @__PURE__ */ new Map();
9272
9456
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9273
9457
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9274
9458
  }
@@ -9480,47 +9664,6 @@ function classifyStoreError(err) {
9480
9664
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9481
9665
  return "permanent";
9482
9666
  }
9483
- /**
9484
- * Collect the keys of every field marked `perNode: true`, recursing into
9485
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9486
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9487
- * don't declare `perNode` and are excluded by the `in` narrowing.
9488
- */
9489
- /**
9490
- * The same fields with every `perNode: true` one removed, recursing into layout
9491
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9492
- * with no child is dropped rather than rendered empty.
9493
- *
9494
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9495
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9496
- */
9497
- function dropPerNodeFields(fields) {
9498
- const kept = [];
9499
- for (const field of fields) {
9500
- if (field.type === "group") {
9501
- const inner = dropPerNodeFields(field.fields);
9502
- if (inner.length > 0) kept.push({
9503
- ...field,
9504
- fields: inner
9505
- });
9506
- continue;
9507
- }
9508
- if (field.type === "sub-tabs") {
9509
- const tabs = field.tabs.map((tab) => ({
9510
- ...tab,
9511
- fields: dropPerNodeFields(tab.fields)
9512
- })).filter((tab) => tab.fields.length > 0);
9513
- if (tabs.length > 0) kept.push({
9514
- ...field,
9515
- tabs
9516
- });
9517
- continue;
9518
- }
9519
- if ("perNode" in field && field.perNode === true) continue;
9520
- kept.push(field);
9521
- }
9522
- return kept;
9523
- }
9524
9667
  /** The keys of the value-holding fields, recursing into layout containers. */
9525
9668
  function collectValueFieldKeys(fields) {
9526
9669
  const collected = [];
@@ -9553,21 +9696,6 @@ function collectFieldKeys(fields) {
9553
9696
  }
9554
9697
  return collected;
9555
9698
  }
9556
- function collectPerNodeFieldKeys(fields) {
9557
- const collected = [];
9558
- for (const field of fields) {
9559
- if (field.type === "group") {
9560
- collected.push(...collectPerNodeFieldKeys(field.fields));
9561
- continue;
9562
- }
9563
- if (field.type === "sub-tabs") {
9564
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9565
- continue;
9566
- }
9567
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9568
- }
9569
- return collected;
9570
- }
9571
9699
  /**
9572
9700
  * Normalize an `ICamstackAddon.initialize()` return value into the
9573
9701
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10618,7 +10746,6 @@ var CAP_NAMES = Object.freeze([
10618
10746
  "notifier",
10619
10747
  "numeric-sensor",
10620
10748
  "oauth-integration",
10621
- "oidc-connections",
10622
10749
  "osd",
10623
10750
  "osd-manager",
10624
10751
  "pet-feeder",
@@ -12325,13 +12452,25 @@ Object.freeze({
12325
12452
  "path": "temperature",
12326
12453
  "valueType": "number"
12327
12454
  }],
12328
- "timed-run": [{
12329
- "path": "activeUntil",
12330
- "valueType": "number"
12331
- }, {
12332
- "path": "requestedBy",
12333
- "valueType": "string"
12334
- }],
12455
+ "timed-run": [
12456
+ {
12457
+ "path": "activeUntil",
12458
+ "valueType": "number"
12459
+ },
12460
+ {
12461
+ "path": "requestedBy",
12462
+ "valueType": "string"
12463
+ },
12464
+ {
12465
+ "path": "quickRun.duration",
12466
+ "valueType": "number"
12467
+ },
12468
+ {
12469
+ "path": "quickRun.unit",
12470
+ "valueType": "enum",
12471
+ "enumValues": ["s", "min"]
12472
+ }
12473
+ ],
12335
12474
  "update": [
12336
12475
  {
12337
12476
  "path": "currentVersion",
@@ -12510,8 +12649,7 @@ Object.freeze({
12510
12649
  "device-provider": { "reloadDevice": 18e4 },
12511
12650
  "llm": {
12512
12651
  "generate": 6e5,
12513
- "generateVision": 6e5,
12514
- "testProfile": 6e5
12652
+ "generateVision": 6e5
12515
12653
  },
12516
12654
  "llm-runtime": {
12517
12655
  "complete": 6e5,
@@ -12570,7 +12708,7 @@ function userError(code, params, englishFallback) {
12570
12708
  return new CamstackError(code, params, englishFallback);
12571
12709
  }
12572
12710
  //#endregion
12573
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12711
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12574
12712
  /**
12575
12713
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12576
12714
  *
@@ -13732,6 +13870,387 @@ var NodeColorIdSchema = _enum([
13732
13870
  "node-h"
13733
13871
  ]);
13734
13872
  //#endregion
13873
+ //#region ../types/dist/convert-BAzH15HU.mjs
13874
+ /**
13875
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13876
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13877
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13878
+ * decimals). K derives from `°C = K − 273.15`.
13879
+ */
13880
+ var UNIT_TABLE = {
13881
+ "°C": {
13882
+ dimension: "temperature",
13883
+ factor: 1,
13884
+ offset: 0
13885
+ },
13886
+ "°F": {
13887
+ dimension: "temperature",
13888
+ factor: 5 / 9,
13889
+ offset: -160 / 9
13890
+ },
13891
+ K: {
13892
+ dimension: "temperature",
13893
+ factor: 1,
13894
+ offset: -273.15
13895
+ },
13896
+ hPa: {
13897
+ dimension: "pressure",
13898
+ factor: 1,
13899
+ offset: 0
13900
+ },
13901
+ kPa: {
13902
+ dimension: "pressure",
13903
+ factor: 10,
13904
+ offset: 0
13905
+ },
13906
+ Pa: {
13907
+ dimension: "pressure",
13908
+ factor: .01,
13909
+ offset: 0
13910
+ },
13911
+ mbar: {
13912
+ dimension: "pressure",
13913
+ factor: 1,
13914
+ offset: 0
13915
+ },
13916
+ bar: {
13917
+ dimension: "pressure",
13918
+ factor: 1e3,
13919
+ offset: 0
13920
+ },
13921
+ inHg: {
13922
+ dimension: "pressure",
13923
+ factor: 33.8639,
13924
+ offset: 0
13925
+ },
13926
+ mmHg: {
13927
+ dimension: "pressure",
13928
+ factor: 1.33322,
13929
+ offset: 0
13930
+ },
13931
+ psi: {
13932
+ dimension: "pressure",
13933
+ factor: 68.9476,
13934
+ offset: 0
13935
+ },
13936
+ "m/s": {
13937
+ dimension: "speed",
13938
+ factor: 1,
13939
+ offset: 0
13940
+ },
13941
+ "km/h": {
13942
+ dimension: "speed",
13943
+ factor: 1 / 3.6,
13944
+ offset: 0
13945
+ },
13946
+ mph: {
13947
+ dimension: "speed",
13948
+ factor: .44704,
13949
+ offset: 0
13950
+ },
13951
+ kn: {
13952
+ dimension: "speed",
13953
+ factor: .514444,
13954
+ offset: 0
13955
+ },
13956
+ "mm/h": {
13957
+ dimension: "precipitation-rate",
13958
+ factor: 1,
13959
+ offset: 0
13960
+ },
13961
+ "in/h": {
13962
+ dimension: "precipitation-rate",
13963
+ factor: 25.4,
13964
+ offset: 0
13965
+ },
13966
+ m: {
13967
+ dimension: "length",
13968
+ factor: 1,
13969
+ offset: 0
13970
+ },
13971
+ mm: {
13972
+ dimension: "length",
13973
+ factor: .001,
13974
+ offset: 0
13975
+ },
13976
+ cm: {
13977
+ dimension: "length",
13978
+ factor: .01,
13979
+ offset: 0
13980
+ },
13981
+ km: {
13982
+ dimension: "length",
13983
+ factor: 1e3,
13984
+ offset: 0
13985
+ },
13986
+ in: {
13987
+ dimension: "length",
13988
+ factor: .0254,
13989
+ offset: 0
13990
+ },
13991
+ ft: {
13992
+ dimension: "length",
13993
+ factor: .3048,
13994
+ offset: 0
13995
+ },
13996
+ mi: {
13997
+ dimension: "length",
13998
+ factor: 1609.344,
13999
+ offset: 0
14000
+ },
14001
+ "m²": {
14002
+ dimension: "area",
14003
+ factor: 1,
14004
+ offset: 0
14005
+ },
14006
+ lx: {
14007
+ dimension: "illuminance",
14008
+ factor: 1,
14009
+ offset: 0
14010
+ },
14011
+ "W/m²": {
14012
+ dimension: "irradiance",
14013
+ factor: 1,
14014
+ offset: 0
14015
+ },
14016
+ W: {
14017
+ dimension: "power",
14018
+ factor: 1,
14019
+ offset: 0
14020
+ },
14021
+ kW: {
14022
+ dimension: "power",
14023
+ factor: 1e3,
14024
+ offset: 0
14025
+ },
14026
+ VA: {
14027
+ dimension: "apparent-power",
14028
+ factor: 1,
14029
+ offset: 0
14030
+ },
14031
+ Wh: {
14032
+ dimension: "energy",
14033
+ factor: 1,
14034
+ offset: 0
14035
+ },
14036
+ kWh: {
14037
+ dimension: "energy",
14038
+ factor: 1e3,
14039
+ offset: 0
14040
+ },
14041
+ MWh: {
14042
+ dimension: "energy",
14043
+ factor: 1e6,
14044
+ offset: 0
14045
+ },
14046
+ V: {
14047
+ dimension: "voltage",
14048
+ factor: 1,
14049
+ offset: 0
14050
+ },
14051
+ mV: {
14052
+ dimension: "voltage",
14053
+ factor: .001,
14054
+ offset: 0
14055
+ },
14056
+ A: {
14057
+ dimension: "current",
14058
+ factor: 1,
14059
+ offset: 0
14060
+ },
14061
+ mA: {
14062
+ dimension: "current",
14063
+ factor: .001,
14064
+ offset: 0
14065
+ },
14066
+ ppm: {
14067
+ dimension: "concentration-volume",
14068
+ factor: 1,
14069
+ offset: 0
14070
+ },
14071
+ ppb: {
14072
+ dimension: "concentration-volume",
14073
+ factor: .001,
14074
+ offset: 0
14075
+ },
14076
+ "µg/m³": {
14077
+ dimension: "concentration-mass",
14078
+ factor: 1,
14079
+ offset: 0
14080
+ },
14081
+ "mg/m³": {
14082
+ dimension: "concentration-mass",
14083
+ factor: 1e3,
14084
+ offset: 0
14085
+ },
14086
+ kg: {
14087
+ dimension: "mass",
14088
+ factor: 1,
14089
+ offset: 0
14090
+ },
14091
+ g: {
14092
+ dimension: "mass",
14093
+ factor: .001,
14094
+ offset: 0
14095
+ },
14096
+ lb: {
14097
+ dimension: "mass",
14098
+ factor: .453592,
14099
+ offset: 0
14100
+ },
14101
+ oz: {
14102
+ dimension: "mass",
14103
+ factor: .0283495,
14104
+ offset: 0
14105
+ },
14106
+ "%": {
14107
+ dimension: "percentage",
14108
+ factor: 1,
14109
+ offset: 0
14110
+ },
14111
+ L: {
14112
+ dimension: "volume",
14113
+ factor: 1,
14114
+ offset: 0
14115
+ },
14116
+ mL: {
14117
+ dimension: "volume",
14118
+ factor: .001,
14119
+ offset: 0
14120
+ },
14121
+ gal: {
14122
+ dimension: "volume",
14123
+ factor: 3.785411784,
14124
+ offset: 0
14125
+ },
14126
+ s: {
14127
+ dimension: "duration",
14128
+ factor: 1,
14129
+ offset: 0
14130
+ },
14131
+ ms: {
14132
+ dimension: "duration",
14133
+ factor: .001,
14134
+ offset: 0
14135
+ },
14136
+ min: {
14137
+ dimension: "duration",
14138
+ factor: 60,
14139
+ offset: 0
14140
+ },
14141
+ h: {
14142
+ dimension: "duration",
14143
+ factor: 3600,
14144
+ offset: 0
14145
+ },
14146
+ d: {
14147
+ dimension: "duration",
14148
+ factor: 86400,
14149
+ offset: 0
14150
+ },
14151
+ B: {
14152
+ dimension: "information",
14153
+ factor: 1,
14154
+ offset: 0
14155
+ },
14156
+ KiB: {
14157
+ dimension: "information",
14158
+ factor: 1024,
14159
+ offset: 0
14160
+ },
14161
+ MiB: {
14162
+ dimension: "information",
14163
+ factor: 1024 ** 2,
14164
+ offset: 0
14165
+ },
14166
+ GiB: {
14167
+ dimension: "information",
14168
+ factor: 1024 ** 3,
14169
+ offset: 0
14170
+ },
14171
+ TiB: {
14172
+ dimension: "information",
14173
+ factor: 1024 ** 4,
14174
+ offset: 0
14175
+ },
14176
+ kB: {
14177
+ dimension: "information",
14178
+ factor: 1e3,
14179
+ offset: 0
14180
+ },
14181
+ MB: {
14182
+ dimension: "information",
14183
+ factor: 1e6,
14184
+ offset: 0
14185
+ },
14186
+ GB: {
14187
+ dimension: "information",
14188
+ factor: 1e9,
14189
+ offset: 0
14190
+ },
14191
+ "°": {
14192
+ dimension: "angle",
14193
+ factor: 1,
14194
+ offset: 0
14195
+ },
14196
+ dBFS: {
14197
+ dimension: "sound-level",
14198
+ factor: 1,
14199
+ offset: 0
14200
+ },
14201
+ dBm: {
14202
+ dimension: "signal-level",
14203
+ factor: 1,
14204
+ offset: 0
14205
+ },
14206
+ mired: {
14207
+ dimension: "colour-temperature",
14208
+ factor: 1,
14209
+ offset: 0
14210
+ },
14211
+ kbps: {
14212
+ dimension: "bitrate",
14213
+ factor: 1,
14214
+ offset: 0
14215
+ },
14216
+ Mbps: {
14217
+ dimension: "bitrate",
14218
+ factor: 1e3,
14219
+ offset: 0
14220
+ },
14221
+ fps: {
14222
+ dimension: "rate",
14223
+ factor: 1,
14224
+ offset: 0
14225
+ }
14226
+ };
14227
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14228
+ function specFor(unit) {
14229
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14230
+ }
14231
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14232
+ function toCanonical(value, spec) {
14233
+ return value * spec.factor + spec.offset;
14234
+ }
14235
+ /** Canonical-unit value → the unit described by `spec`. */
14236
+ function fromCanonical(canonical, spec) {
14237
+ return (canonical - spec.offset) / spec.factor;
14238
+ }
14239
+ /**
14240
+ * Graceful variant for the render path: returns the converted value, or
14241
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14242
+ * Never throws — a render caller falls back to the raw value + source unit.
14243
+ */
14244
+ function tryConvertUnit(value, from, to) {
14245
+ if (from === to) return value;
14246
+ const fromSpec = specFor(from);
14247
+ if (!fromSpec) return void 0;
14248
+ const toSpec = specFor(to);
14249
+ if (!toSpec) return void 0;
14250
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14251
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14252
+ }
14253
+ //#endregion
13735
14254
  //#region ../types/dist/index.mjs
13736
14255
  /**
13737
14256
  * The `smtp-inbound` broker kind (D999): a mailbox a device can send mail to.
@@ -14036,17 +14555,18 @@ function composeInstanceKinds(parts, base) {
14036
14555
  * message to what the kind supports — callers never special-case a service.
14037
14556
  *
14038
14557
  * DESIGN DECISIONS (locked):
14039
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
14040
- * `setTargetEnabled`), each provider persisting via the `settings-store`
14041
- * cap. Rationale: the admin UI needs one uniform surface across the
14042
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
14043
- * alternative would fork the UI per addon and cannot host the
14044
- * discovery→adopt flow.
14045
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
14046
- * the generated cap-mount auto-`concatCollection`-fans them across every
14047
- * registered provider (notifiers addon + HA addon) so one catalog is
14048
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
14049
- * `addonId` the generated collection router extracts from the call input.
14558
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14559
+ * providers serve their targets as `integration-instances` records, one
14560
+ * record kind per target kind, and the admin lists, adds, edits, switches
14561
+ * and deletes them through that cap. This cap keeps what an instance cannot
14562
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14563
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14564
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14565
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14566
+ * cap-mount auto-`concatCollection`-fans them across every registered
14567
+ * provider so one catalog is routable. `send` / `testTarget` /
14568
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14569
+ * collection router extracts from the call input.
14050
14570
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14051
14571
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14052
14572
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14291,7 +14811,7 @@ var TargetKindCapsSchema = object({
14291
14811
  * the union is large and not meant for runtime validation here; the exported
14292
14812
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14293
14813
  */
14294
- var ConfigSchemaPassthrough$4 = unknown();
14814
+ var ConfigSchemaPassthrough$3 = unknown();
14295
14815
  var TargetKindSchema = object({
14296
14816
  kind: string(),
14297
14817
  label: string(),
@@ -14328,7 +14848,7 @@ var TargetKindSchema = object({
14328
14848
  * its raster path, which is the safe default for an unknown image.
14329
14849
  */
14330
14850
  iconMediaType: string().optional(),
14331
- configSchema: ConfigSchemaPassthrough$4,
14851
+ configSchema: ConfigSchemaPassthrough$3,
14332
14852
  /**
14333
14853
  * The config keys that decide WHERE this kind's secrets are sent (server,
14334
14854
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14350,9 +14870,11 @@ var TargetKindSchema = object({
14350
14870
  caps: TargetKindCapsSchema
14351
14871
  });
14352
14872
  /**
14353
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14354
- * (return a presence marker only) when serving `listTargets` — never
14355
- * round-trip a stored secret to the UI.
14873
+ * A target as a provider's own code sees it (the notification-center bridge, the
14874
+ * send path): the flat record body split into the name, the switch and the
14875
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14876
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14877
+ * secret to the UI.
14356
14878
  */
14357
14879
  var TargetSchema = object({
14358
14880
  id: string(),
@@ -14395,13 +14917,13 @@ var ProbeTargetConfigResultSchema = object({
14395
14917
  stateInstance: string().optional(),
14396
14918
  error: string().optional()
14397
14919
  });
14398
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14920
+ method(object({}), array(TargetKindSchema)), method(object({
14399
14921
  kind: string(),
14400
14922
  config: record(string(), unknown()).optional(),
14401
14923
  /**
14402
14924
  * When editing / duplicating, the blank password field never round-trips
14403
14925
  * the stored token. Pass the source target id so the provider can restore
14404
- * secrets server-side before dialling (same merge as upsert).
14926
+ * secrets server-side before dialling.
14405
14927
  */
14406
14928
  targetId: string().optional()
14407
14929
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14424,27 +14946,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14424
14946
  }), TestResultSchema, {
14425
14947
  kind: "mutation",
14426
14948
  auth: "admin"
14427
- }), method(object({
14428
- target: TargetSchema,
14429
- /**
14430
- * Duplicate flow: create a NEW target id while copying secrets from an
14431
- * existing row. The UI never holds the stored token; blank / redacted
14432
- * password fields are restored from this source (D634 still applies if
14433
- * destinations diverge without a re-typed secret).
14434
- */
14435
- cloneSecretsFrom: string().optional()
14436
- }), TargetSchema, {
14437
- kind: "mutation",
14438
- auth: "admin"
14439
- }), method(object({ targetId: string() }), _void(), {
14440
- kind: "mutation",
14441
- auth: "admin"
14442
- }), method(object({
14443
- targetId: string(),
14444
- enabled: boolean()
14445
- }), _void(), {
14446
- kind: "mutation",
14447
- auth: "admin"
14448
14949
  });
14449
14950
  looseObject({
14450
14951
  name: string().min(1),
@@ -22712,7 +23213,6 @@ var SYSTEM_CAP_VOCABULARY = {
22712
23213
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22713
23214
  "notification-rules": system("notification rules — authored on their own page"),
22714
23215
  "oauth-integration": system(IDENTITY),
22715
- "oidc-connections": system(IDENTITY),
22716
23216
  "osd-manager": system(PIPELINE),
22717
23217
  "pipeline-executor": system(PIPELINE),
22718
23218
  "pipeline-orchestrator": system(PIPELINE),
@@ -42733,6 +43233,7 @@ var ExpressionBindingSourceSchema = union([
42733
43233
  object({
42734
43234
  kind: literal("global"),
42735
43235
  sourceStableId: string(),
43236
+ addonId: string().optional(),
42736
43237
  cap: string(),
42737
43238
  fieldPath: string()
42738
43239
  })
@@ -42799,385 +43300,6 @@ var SourceInfoSchema = object({
42799
43300
  raw: record(string(), unknown()).optional()
42800
43301
  });
42801
43302
  /**
42802
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42803
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42804
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42805
- * decimals). K derives from `°C = K − 273.15`.
42806
- */
42807
- var UNIT_TABLE = {
42808
- "°C": {
42809
- dimension: "temperature",
42810
- factor: 1,
42811
- offset: 0
42812
- },
42813
- "°F": {
42814
- dimension: "temperature",
42815
- factor: 5 / 9,
42816
- offset: -160 / 9
42817
- },
42818
- K: {
42819
- dimension: "temperature",
42820
- factor: 1,
42821
- offset: -273.15
42822
- },
42823
- hPa: {
42824
- dimension: "pressure",
42825
- factor: 1,
42826
- offset: 0
42827
- },
42828
- kPa: {
42829
- dimension: "pressure",
42830
- factor: 10,
42831
- offset: 0
42832
- },
42833
- Pa: {
42834
- dimension: "pressure",
42835
- factor: .01,
42836
- offset: 0
42837
- },
42838
- mbar: {
42839
- dimension: "pressure",
42840
- factor: 1,
42841
- offset: 0
42842
- },
42843
- bar: {
42844
- dimension: "pressure",
42845
- factor: 1e3,
42846
- offset: 0
42847
- },
42848
- inHg: {
42849
- dimension: "pressure",
42850
- factor: 33.8639,
42851
- offset: 0
42852
- },
42853
- mmHg: {
42854
- dimension: "pressure",
42855
- factor: 1.33322,
42856
- offset: 0
42857
- },
42858
- psi: {
42859
- dimension: "pressure",
42860
- factor: 68.9476,
42861
- offset: 0
42862
- },
42863
- "m/s": {
42864
- dimension: "speed",
42865
- factor: 1,
42866
- offset: 0
42867
- },
42868
- "km/h": {
42869
- dimension: "speed",
42870
- factor: 1 / 3.6,
42871
- offset: 0
42872
- },
42873
- mph: {
42874
- dimension: "speed",
42875
- factor: .44704,
42876
- offset: 0
42877
- },
42878
- kn: {
42879
- dimension: "speed",
42880
- factor: .514444,
42881
- offset: 0
42882
- },
42883
- "mm/h": {
42884
- dimension: "precipitation-rate",
42885
- factor: 1,
42886
- offset: 0
42887
- },
42888
- "in/h": {
42889
- dimension: "precipitation-rate",
42890
- factor: 25.4,
42891
- offset: 0
42892
- },
42893
- m: {
42894
- dimension: "length",
42895
- factor: 1,
42896
- offset: 0
42897
- },
42898
- mm: {
42899
- dimension: "length",
42900
- factor: .001,
42901
- offset: 0
42902
- },
42903
- cm: {
42904
- dimension: "length",
42905
- factor: .01,
42906
- offset: 0
42907
- },
42908
- km: {
42909
- dimension: "length",
42910
- factor: 1e3,
42911
- offset: 0
42912
- },
42913
- in: {
42914
- dimension: "length",
42915
- factor: .0254,
42916
- offset: 0
42917
- },
42918
- ft: {
42919
- dimension: "length",
42920
- factor: .3048,
42921
- offset: 0
42922
- },
42923
- mi: {
42924
- dimension: "length",
42925
- factor: 1609.344,
42926
- offset: 0
42927
- },
42928
- "m²": {
42929
- dimension: "area",
42930
- factor: 1,
42931
- offset: 0
42932
- },
42933
- lx: {
42934
- dimension: "illuminance",
42935
- factor: 1,
42936
- offset: 0
42937
- },
42938
- "W/m²": {
42939
- dimension: "irradiance",
42940
- factor: 1,
42941
- offset: 0
42942
- },
42943
- W: {
42944
- dimension: "power",
42945
- factor: 1,
42946
- offset: 0
42947
- },
42948
- kW: {
42949
- dimension: "power",
42950
- factor: 1e3,
42951
- offset: 0
42952
- },
42953
- VA: {
42954
- dimension: "apparent-power",
42955
- factor: 1,
42956
- offset: 0
42957
- },
42958
- Wh: {
42959
- dimension: "energy",
42960
- factor: 1,
42961
- offset: 0
42962
- },
42963
- kWh: {
42964
- dimension: "energy",
42965
- factor: 1e3,
42966
- offset: 0
42967
- },
42968
- MWh: {
42969
- dimension: "energy",
42970
- factor: 1e6,
42971
- offset: 0
42972
- },
42973
- V: {
42974
- dimension: "voltage",
42975
- factor: 1,
42976
- offset: 0
42977
- },
42978
- mV: {
42979
- dimension: "voltage",
42980
- factor: .001,
42981
- offset: 0
42982
- },
42983
- A: {
42984
- dimension: "current",
42985
- factor: 1,
42986
- offset: 0
42987
- },
42988
- mA: {
42989
- dimension: "current",
42990
- factor: .001,
42991
- offset: 0
42992
- },
42993
- ppm: {
42994
- dimension: "concentration-volume",
42995
- factor: 1,
42996
- offset: 0
42997
- },
42998
- ppb: {
42999
- dimension: "concentration-volume",
43000
- factor: .001,
43001
- offset: 0
43002
- },
43003
- "µg/m³": {
43004
- dimension: "concentration-mass",
43005
- factor: 1,
43006
- offset: 0
43007
- },
43008
- "mg/m³": {
43009
- dimension: "concentration-mass",
43010
- factor: 1e3,
43011
- offset: 0
43012
- },
43013
- kg: {
43014
- dimension: "mass",
43015
- factor: 1,
43016
- offset: 0
43017
- },
43018
- g: {
43019
- dimension: "mass",
43020
- factor: .001,
43021
- offset: 0
43022
- },
43023
- lb: {
43024
- dimension: "mass",
43025
- factor: .453592,
43026
- offset: 0
43027
- },
43028
- oz: {
43029
- dimension: "mass",
43030
- factor: .0283495,
43031
- offset: 0
43032
- },
43033
- "%": {
43034
- dimension: "percentage",
43035
- factor: 1,
43036
- offset: 0
43037
- },
43038
- L: {
43039
- dimension: "volume",
43040
- factor: 1,
43041
- offset: 0
43042
- },
43043
- mL: {
43044
- dimension: "volume",
43045
- factor: .001,
43046
- offset: 0
43047
- },
43048
- gal: {
43049
- dimension: "volume",
43050
- factor: 3.785411784,
43051
- offset: 0
43052
- },
43053
- s: {
43054
- dimension: "duration",
43055
- factor: 1,
43056
- offset: 0
43057
- },
43058
- ms: {
43059
- dimension: "duration",
43060
- factor: .001,
43061
- offset: 0
43062
- },
43063
- min: {
43064
- dimension: "duration",
43065
- factor: 60,
43066
- offset: 0
43067
- },
43068
- h: {
43069
- dimension: "duration",
43070
- factor: 3600,
43071
- offset: 0
43072
- },
43073
- d: {
43074
- dimension: "duration",
43075
- factor: 86400,
43076
- offset: 0
43077
- },
43078
- B: {
43079
- dimension: "information",
43080
- factor: 1,
43081
- offset: 0
43082
- },
43083
- KiB: {
43084
- dimension: "information",
43085
- factor: 1024,
43086
- offset: 0
43087
- },
43088
- MiB: {
43089
- dimension: "information",
43090
- factor: 1024 ** 2,
43091
- offset: 0
43092
- },
43093
- GiB: {
43094
- dimension: "information",
43095
- factor: 1024 ** 3,
43096
- offset: 0
43097
- },
43098
- TiB: {
43099
- dimension: "information",
43100
- factor: 1024 ** 4,
43101
- offset: 0
43102
- },
43103
- kB: {
43104
- dimension: "information",
43105
- factor: 1e3,
43106
- offset: 0
43107
- },
43108
- MB: {
43109
- dimension: "information",
43110
- factor: 1e6,
43111
- offset: 0
43112
- },
43113
- GB: {
43114
- dimension: "information",
43115
- factor: 1e9,
43116
- offset: 0
43117
- },
43118
- "°": {
43119
- dimension: "angle",
43120
- factor: 1,
43121
- offset: 0
43122
- },
43123
- dBFS: {
43124
- dimension: "sound-level",
43125
- factor: 1,
43126
- offset: 0
43127
- },
43128
- dBm: {
43129
- dimension: "signal-level",
43130
- factor: 1,
43131
- offset: 0
43132
- },
43133
- mired: {
43134
- dimension: "colour-temperature",
43135
- factor: 1,
43136
- offset: 0
43137
- },
43138
- kbps: {
43139
- dimension: "bitrate",
43140
- factor: 1,
43141
- offset: 0
43142
- },
43143
- Mbps: {
43144
- dimension: "bitrate",
43145
- factor: 1e3,
43146
- offset: 0
43147
- },
43148
- fps: {
43149
- dimension: "rate",
43150
- factor: 1,
43151
- offset: 0
43152
- }
43153
- };
43154
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43155
- function specFor(unit) {
43156
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43157
- }
43158
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43159
- function toCanonical(value, spec) {
43160
- return value * spec.factor + spec.offset;
43161
- }
43162
- /** Canonical-unit value → the unit described by `spec`. */
43163
- function fromCanonical(canonical, spec) {
43164
- return (canonical - spec.offset) / spec.factor;
43165
- }
43166
- /**
43167
- * Graceful variant for the render path: returns the converted value, or
43168
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43169
- * Never throws — a render caller falls back to the raw value + source unit.
43170
- */
43171
- function tryConvertUnit(value, from, to) {
43172
- if (from === to) return value;
43173
- const fromSpec = specFor(from);
43174
- if (!fromSpec) return void 0;
43175
- const toSpec = specFor(to);
43176
- if (!toSpec) return void 0;
43177
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43178
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43179
- }
43180
- /**
43181
43303
  * A number a person types for a physical quantity — the input side of the
43182
43304
  * value + unit contract (D972).
43183
43305
  *
@@ -44854,15 +44976,28 @@ var SettingsSchemaWithValuesSchema = object({
44854
44976
  });
44855
44977
  /** Patch object — keys are field names, values are the new field values. */
44856
44978
  var SettingsPatchSchema = record(string(), unknown());
44979
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
44980
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
44981
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
44982
+ var ScopedSettingsDeclarationSchema = object({
44983
+ cap: string(),
44984
+ scope: ScopedSettingsScopeSchema,
44985
+ sectionIds: array(string())
44986
+ });
44857
44987
  /** Standard success response for update operations. */
44858
44988
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44859
44989
  method(object({
44860
44990
  addonId: string(),
44861
- nodeId: string().optional(),
44862
- overlay: record(string(), unknown()).optional(),
44863
- cap: string().optional()
44991
+ cap: string(),
44992
+ scope: ScopedSettingsScopeSchema,
44993
+ nodeId: string().optional()
44864
44994
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44865
44995
  addonId: string(),
44996
+ nodeId: string().optional()
44997
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
44998
+ addonId: string(),
44999
+ cap: string(),
45000
+ scope: ScopedSettingsScopeSchema,
44866
45001
  nodeId: string().optional(),
44867
45002
  patch: SettingsPatchSchema
44868
45003
  }), SettingsUpdateResultSchema, {
@@ -44870,8 +45005,10 @@ method(object({
44870
45005
  auth: "admin"
44871
45006
  }), method(object({
44872
45007
  addonId: string(),
45008
+ cap: string(),
45009
+ scope: ScopedSettingsScopeSchema,
44873
45010
  nodeId: string().optional()
44874
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45011
+ }), record(string(), unknown()).nullable()), method(object({
44875
45012
  addonId: string(),
44876
45013
  nodeId: string().optional(),
44877
45014
  instance: IntegrationInstanceRefSchema
@@ -46211,7 +46348,7 @@ var BackupDestinationInfoSchema = object({
46211
46348
  id: string(),
46212
46349
  /**
46213
46350
  * Manifest id of the owning addon. The Settings button on the
46214
- * destination card opens that addon's `globalSettingsSchema` panel.
46351
+ * destination card opens that addon's scoped settings panel.
46215
46352
  */
46216
46353
  addonId: string(),
46217
46354
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50681,6 +50818,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50681
50818
  kind: "mutation",
50682
50819
  auth: "admin"
50683
50820
  });
50821
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50684
50822
  /**
50685
50823
  * A SITE ZONE — a named part of the property that several locations belong to.
50686
50824
  *
@@ -51074,6 +51212,12 @@ var DeviceInfoSchema = object({
51074
51212
  addonId: string(),
51075
51213
  type: _enum(DeviceType),
51076
51214
  name: string(),
51215
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51216
+ * entity_id style. Generated once from the name at first registration, unique
51217
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51218
+ * Optional: a record produced by an older build omits it until the boot
51219
+ * backfill has run. */
51220
+ alias: DeviceAliasSchema.optional(),
51077
51221
  /** Operator-organisational location label. `null` when unset. */
51078
51222
  location: string().nullable(),
51079
51223
  /** Soft-disabled flag. */
@@ -51393,6 +51537,25 @@ method(object({
51393
51537
  }), _void(), {
51394
51538
  kind: "mutation",
51395
51539
  auth: "admin"
51540
+ }), method(object({
51541
+ deviceId: number(),
51542
+ alias: string()
51543
+ }), discriminatedUnion("ok", [object({
51544
+ ok: literal(true),
51545
+ alias: DeviceAliasSchema
51546
+ }), object({
51547
+ ok: literal(false),
51548
+ reason: _enum([
51549
+ "empty",
51550
+ "too-long",
51551
+ "grammar",
51552
+ "reserved",
51553
+ "taken"
51554
+ ]),
51555
+ takenBy: number().optional()
51556
+ })]), {
51557
+ kind: "mutation",
51558
+ auth: "admin"
51396
51559
  }), method(object({
51397
51560
  deviceId: number(),
51398
51561
  location: string().nullable()
@@ -54418,7 +54581,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54418
54581
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54419
54582
  * clears it.
54420
54583
  */
54421
- var ConfigSchemaPassthrough$3 = custom();
54584
+ var ConfigSchemaPassthrough$2 = custom();
54422
54585
  var IntegrationInstanceIdSchema = string().min(1);
54423
54586
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54424
54587
  /**
@@ -54464,7 +54627,16 @@ var IntegrationInstanceKindSchema = object({
54464
54627
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54465
54628
  connectPath: string().min(1).optional(),
54466
54629
  /** Relative hub path to disconnect that account. */
54467
- disconnectPath: string().min(1).optional()
54630
+ disconnectPath: string().min(1).optional(),
54631
+ /**
54632
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54633
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54634
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54635
+ */
54636
+ widgets: array(object({
54637
+ id: string().min(1),
54638
+ placement: _enum(["connection", "model"])
54639
+ })).optional()
54468
54640
  });
54469
54641
  /**
54470
54642
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54569,7 +54741,7 @@ var integrationInstancesCapability = {
54569
54741
  id: IntegrationInstanceIdSchema.nullable(),
54570
54742
  /** The kind of the create form (`id: null`) of a provider with several. */
54571
54743
  kind: string().min(1).optional()
54572
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54744
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54573
54745
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54574
54746
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54575
54747
  /**
@@ -55520,12 +55692,12 @@ var LlmModelInfoSchema = object({
55520
55692
  });
55521
55693
  /**
55522
55694
  * The provider-specific part of a profile, validated by the provider that owns
55523
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55695
+ * the kind (saving an instance refuses what it does not accept). A field its
55524
55696
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55525
55697
  * keychain, never in the row, and reads back as the redaction marker.
55526
55698
  */
55527
55699
  var LlmProviderConfigSchema = record(string(), unknown());
55528
- var LlmProfileSchema = object({
55700
+ object({
55529
55701
  id: string(),
55530
55702
  name: string(),
55531
55703
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55619,8 +55791,8 @@ var LlmProviderWidgetSchema = object({
55619
55791
  /** ConfigUISchema tree passed through untyped on the wire (the
55620
55792
  * notification-output `ConfigSchemaPassthrough` precedent at
55621
55793
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55622
- var ConfigSchemaPassthrough$2 = unknown();
55623
- var LlmProfileKindDescriptorSchema = object({
55794
+ var ConfigSchemaPassthrough$1 = unknown();
55795
+ object({
55624
55796
  kind: LlmProfileKindSchema,
55625
55797
  label: string(),
55626
55798
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55628,9 +55800,9 @@ var LlmProfileKindDescriptorSchema = object({
55628
55800
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55629
55801
  addonId: string(),
55630
55802
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55631
- connectionSchema: ConfigSchemaPassthrough$2,
55803
+ connectionSchema: ConfigSchemaPassthrough$1,
55632
55804
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55633
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55805
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55634
55806
  /** The kind's defaults; a model from `listModels` may override any of them. */
55635
55807
  features: LlmModelFeaturesSchema,
55636
55808
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55759,20 +55931,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55759
55931
  }), method(object({
55760
55932
  addonId: string().optional(),
55761
55933
  requestId: string()
55762
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55763
- kind: "mutation",
55764
- auth: "admin"
55765
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55766
- kind: "mutation",
55767
- auth: "admin"
55768
- }), method(ProfileRefInputSchema, _void(), {
55769
- kind: "mutation",
55770
- auth: "admin"
55771
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55772
- kind: "mutation",
55773
- auth: "admin",
55774
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55775
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55934
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55776
55935
  kind: "mutation",
55777
55936
  auth: "admin"
55778
55937
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56767,20 +56926,7 @@ method(_void(), MeshStatusSchema), method(object({
56767
56926
  /** Optional hostname override the host should advertise once joined. */
56768
56927
  hostname: string().optional() }), object({
56769
56928
  /** Authentication URL the operator should open in a browser. */
56770
- 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({
56771
- /** Optional auth key — when provided, probes the key validity
56772
- * against the provider's API. Omit when already joined to
56773
- * just ping the daemon. */
56774
- authKey: string().optional() }), object({
56775
- ok: boolean(),
56776
- /** Provider-side identifier resolved by the probe (tailnet
56777
- * name for Tailscale, network id for ZeroTier, etc.). */
56778
- tenant: string().optional(),
56779
- /** Daemon binary version, when reachable. */
56780
- daemonVersion: string().optional(),
56781
- /** Human-readable error when `ok: false`. */
56782
- error: string().optional()
56783
- }), { kind: "mutation" });
56929
+ 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() }));
56784
56930
  object({
56785
56931
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56786
56932
  lastFetchedAt: number()
@@ -64141,6 +64287,11 @@ var NcScheduleSchema = object({
64141
64287
  * as a STATE SUMMARY at the end of each window of `schedule`.
64142
64288
  */
64143
64289
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64290
+ _enum([
64291
+ "activation",
64292
+ "deactivation",
64293
+ "schedule"
64294
+ ]);
64144
64295
  /**
64145
64296
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64146
64297
  * every section; `section` = one notification per section with something to
@@ -64157,11 +64308,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64157
64308
  * midnight crossing); the send time is each window's END. Refused on the
64158
64309
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64159
64310
  */
64160
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64161
- mode: literal("summary"),
64162
- schedule: NcScheduleSchema,
64163
- split: NcStateSummarySplitSchema.optional()
64164
- })]);
64311
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64312
+ object({ mode: literal("instant") }),
64313
+ object({
64314
+ mode: literal("summary"),
64315
+ schedule: NcScheduleSchema,
64316
+ split: NcStateSummarySplitSchema.optional()
64317
+ }),
64318
+ object({
64319
+ mode: literal("modes"),
64320
+ /** The bad edge, and every kind that has a single edge. */
64321
+ onActivation: boolean(),
64322
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64323
+ onDeactivation: boolean(),
64324
+ /** A state summary at the end of each window; absent = no summary. */
64325
+ onSchedule: object({
64326
+ schedule: NcScheduleSchema,
64327
+ grouping: NcStateSummarySplitSchema
64328
+ }).optional()
64329
+ })
64330
+ ]);
64165
64331
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64166
64332
  var NcPlateMatcherSchema = object({
64167
64333
  /**
@@ -66438,61 +66604,6 @@ var OauthIntegrationDescriptorSchema = object({
66438
66604
  });
66439
66605
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66440
66606
  /**
66441
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66442
- *
66443
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66444
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66445
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66446
- * immutable after create.
66447
- */
66448
- var ConfigSchemaPassthrough$1 = unknown();
66449
- var OidcConnectionSchema = object({
66450
- id: string().min(1),
66451
- displayName: string(),
66452
- icon: string(),
66453
- issuerUrl: string(),
66454
- clientId: string(),
66455
- /** Client id + secret present — never the secret value. */
66456
- configured: boolean(),
66457
- /**
66458
- * Values for the connection form schema. `clientSecret` is a keychain
66459
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66460
- */
66461
- formValues: record(string(), unknown()).optional()
66462
- });
66463
- /**
66464
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66465
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66466
- */
66467
- var OidcConnectionUpdateInputSchema = object({
66468
- id: string().min(1),
66469
- displayName: string().min(1),
66470
- icon: string().optional(),
66471
- issuerUrl: string().optional(),
66472
- clientId: string().optional(),
66473
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66474
- redirectUri: string().optional(),
66475
- scopes: string().optional(),
66476
- usernameClaim: _enum([
66477
- "preferred_username",
66478
- "email",
66479
- "sub"
66480
- ]).optional()
66481
- });
66482
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66483
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66484
- displayName: string().min(1)
66485
- }), object({ id: string() }), {
66486
- kind: "mutation",
66487
- auth: "admin"
66488
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66489
- kind: "mutation",
66490
- auth: "admin"
66491
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66492
- kind: "mutation",
66493
- auth: "admin"
66494
- });
66495
- /**
66496
66607
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66497
66608
  * a firmware-defined list of overlays (channel name, timestamp,
66498
66609
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -72678,21 +72789,6 @@ method(object({
72678
72789
  */
72679
72790
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72680
72791
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72681
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72682
- providerId: string().min(1),
72683
- displayName: string().min(1),
72684
- configSchema: unknown(),
72685
- nodeLocal: boolean(),
72686
- shouldSaveDiskSpace: literal(true),
72687
- minFreePercent: number().min(0).max(100)
72688
- }), object({
72689
- providerId: string().min(1),
72690
- displayName: string().min(1),
72691
- configSchema: unknown(),
72692
- nodeLocal: boolean(),
72693
- shouldSaveDiskSpace: literal(false),
72694
- minFreePercent: literal(null)
72695
- })]);
72696
72792
  method(object({
72697
72793
  location: StorageLocationRefSchema,
72698
72794
  relativePath: string()
@@ -72754,42 +72850,7 @@ method(object({
72754
72850
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72755
72851
  kind: "mutation",
72756
72852
  auth: "admin"
72757
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72758
- createdAt: true,
72759
- updatedAt: true
72760
- }), StorageLocationSchema, {
72761
- kind: "mutation",
72762
- auth: "admin"
72763
- }), method(object({
72764
- id: string(),
72765
- force: boolean().optional()
72766
- }), _void(), {
72767
- kind: "mutation",
72768
- auth: "admin"
72769
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72770
- ok: boolean(),
72771
- error: string().optional()
72772
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72773
- providerId: string(),
72774
- /**
72775
- * The location this config is an UNSAVED edit of, when there is one.
72776
- *
72777
- * `listLocations` replaces every declared secret with the redaction
72778
- * sentinel, so the edit modal's form state holds the sentinel for any
72779
- * credential the operator did not retype — and posting that here
72780
- * without a way to resolve it makes the provider try to authenticate
72781
- * as `__camstack_redacted__` and report the operator's own working
72782
- * password as wrong. Given this id, the orchestrator restores each
72783
- * sentinel from the stored config (same rule as `upsertLocation`)
72784
- * before dispatching. Omitted by the "Add location" wizard, where
72785
- * every value was typed just now and nothing is stored yet.
72786
- */
72787
- locationId: string().optional(),
72788
- config: record(string(), unknown())
72789
- }), object({
72790
- ok: boolean(),
72791
- error: string().optional()
72792
- }), { auth: "admin" });
72853
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72793
72854
  /** One table of the settings database. */
72794
72855
  var StorageTableSchema = object({
72795
72856
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -74020,7 +74081,7 @@ var TerminalInstanceInfoSchema = object({
74020
74081
  /**
74021
74082
  * False while the node the instance is bound to is not in the cluster
74022
74083
  * topology (forgotten, renamed, re-paired). The instance is kept; the
74023
- * operator re-points it with `updateInstance({ targetNodeId })`.
74084
+ * operator re-points it on the Terminal page.
74024
74085
  */
74025
74086
  nodeInCluster: boolean(),
74026
74087
  profileId: string(),
@@ -74058,41 +74119,7 @@ var TerminalOutputBatchSchema = object({
74058
74119
  snapshot: string().optional(),
74059
74120
  events: array(TerminalOutputEventSchema).readonly()
74060
74121
  });
74061
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
74062
- targetNodeId: string().min(1),
74063
- profileId: string().min(1),
74064
- name: string().trim().min(1).max(160).optional(),
74065
- executable: string().max(1024).optional(),
74066
- args: array(string().max(2048)).max(64).optional(),
74067
- cwd: string().max(1024).optional(),
74068
- environment: array(string().max(4096)).max(64).optional(),
74069
- profileSettings: ProfileSettingsBagSchema.optional()
74070
- }), TerminalInstanceInfoSchema, {
74071
- kind: "mutation",
74072
- auth: "admin"
74073
- }), method(object({
74074
- instanceId: string().min(1),
74075
- /** Re-point the instance to another node; the profile must exist there. */
74076
- targetNodeId: string().min(1).max(256).optional(),
74077
- name: string().trim().min(1).max(160).optional(),
74078
- executable: string().max(1024).optional(),
74079
- args: array(string().max(2048)).max(64).optional(),
74080
- cwd: string().max(1024).optional(),
74081
- environment: array(string().max(4096)).max(64).optional(),
74082
- profileSettings: ProfileSettingsBagSchema.optional()
74083
- }), TerminalInstanceInfoSchema, {
74084
- kind: "mutation",
74085
- auth: "admin"
74086
- }), method(object({ instanceId: string().min(1) }), _void(), {
74087
- kind: "mutation",
74088
- auth: "admin"
74089
- }), method(object({
74090
- instanceId: string().min(1),
74091
- enabled: boolean()
74092
- }), TerminalInstanceInfoSchema, {
74093
- kind: "mutation",
74094
- auth: "admin"
74095
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74122
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74096
74123
  stableId: string().min(1),
74097
74124
  name: string().trim().min(1).max(160).optional()
74098
74125
  }), TerminalInstanceInfoSchema, {
@@ -74161,6 +74188,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74161
74188
  _enum(["switch", "pet-fountain"]);
74162
74189
  DeviceType.Heater, DeviceType.PetFountain;
74163
74190
  DeviceType.Heater, DeviceType.PetFountain;
74191
+ /** One duration, as the one-tap run asks for it. */
74192
+ var TimedRunQuickRunSchema = object({
74193
+ duration: number().positive().finite(),
74194
+ unit: TimedRunUnitSchema
74195
+ });
74164
74196
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74165
74197
  var TimedRunRefusalSchema = _enum([
74166
74198
  "unsupported-device",
@@ -74181,9 +74213,30 @@ object({
74181
74213
  activeUntil: number().nullable(),
74182
74214
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74183
74215
  requestedBy: string().nullable(),
74216
+ /**
74217
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74218
+ * the device is gone or its type has no timed run; a status from before this field
74219
+ * existed has none, and a surface falls back to the type's default.
74220
+ */
74221
+ quickRun: TimedRunQuickRunSchema.nullable(),
74184
74222
  lastFetchedAt: number()
74185
74223
  });
74186
- method(object({
74224
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74225
+ var TimedRunOptionsSchema = object({
74226
+ unit: TimedRunUnitSchema,
74227
+ /** The longest run, in `unit`. */
74228
+ maxDuration: number(),
74229
+ defaultDuration: number()
74230
+ });
74231
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74232
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74233
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74234
+ deviceId: number().int(),
74235
+ settings: TimedRunSettingsPatchSchema
74236
+ }), _void(), {
74237
+ kind: "mutation",
74238
+ auth: "admin"
74239
+ }), method(object({
74187
74240
  deviceId: number().int(),
74188
74241
  duration: number().positive().finite(),
74189
74242
  unit: TimedRunUnitSchema
@@ -76889,21 +76942,28 @@ Object.freeze({
76889
76942
  access: "view",
76890
76943
  role: "admin"
76891
76944
  },
76892
- "addonSettings.getGlobalSettings": {
76945
+ "addonSettings.getIntegrationFormSchema": {
76946
+ capName: "addon-settings",
76947
+ capScope: "system",
76948
+ addonId: null,
76949
+ access: "view",
76950
+ role: "admin"
76951
+ },
76952
+ "addonSettings.getScopedSettings": {
76893
76953
  capName: "addon-settings",
76894
76954
  capScope: "system",
76895
76955
  addonId: null,
76896
76956
  access: "view",
76897
76957
  role: "admin"
76898
76958
  },
76899
- "addonSettings.getIntegrationFormSchema": {
76959
+ "addonSettings.listScopedSettings": {
76900
76960
  capName: "addon-settings",
76901
76961
  capScope: "system",
76902
76962
  addonId: null,
76903
76963
  access: "view",
76904
76964
  role: "admin"
76905
76965
  },
76906
- "addonSettings.getIntegrationSettings": {
76966
+ "addonSettings.readScopedValues": {
76907
76967
  capName: "addon-settings",
76908
76968
  capScope: "system",
76909
76969
  addonId: null,
@@ -76924,7 +76984,7 @@ Object.freeze({
76924
76984
  access: "create",
76925
76985
  role: "admin"
76926
76986
  },
76927
- "addonSettings.updateGlobalSettings": {
76987
+ "addonSettings.updateScopedSettings": {
76928
76988
  capName: "addon-settings",
76929
76989
  capScope: "system",
76930
76990
  addonId: null,
@@ -79094,6 +79154,13 @@ Object.freeze({
79094
79154
  access: "create",
79095
79155
  role: "admin"
79096
79156
  },
79157
+ "deviceManager.setAlias": {
79158
+ capName: "device-manager",
79159
+ capScope: "system",
79160
+ addonId: null,
79161
+ access: "create",
79162
+ role: "admin"
79163
+ },
79097
79164
  "deviceManager.setChildLayout": {
79098
79165
  capName: "device-manager",
79099
79166
  capScope: "system",
@@ -80382,13 +80449,6 @@ Object.freeze({
80382
80449
  access: "delete",
80383
80450
  role: "admin"
80384
80451
  },
80385
- "llm.deleteProfile": {
80386
- capName: "llm",
80387
- capScope: "system",
80388
- addonId: null,
80389
- access: "delete",
80390
- role: "admin"
80391
- },
80392
80452
  "llm.generate": {
80393
80453
  capName: "llm",
80394
80454
  capScope: "system",
@@ -80452,20 +80512,6 @@ Object.freeze({
80452
80512
  access: "view",
80453
80513
  role: "admin"
80454
80514
  },
80455
- "llm.listProfileKinds": {
80456
- capName: "llm",
80457
- capScope: "system",
80458
- addonId: null,
80459
- access: "view",
80460
- role: "admin"
80461
- },
80462
- "llm.listProfiles": {
80463
- capName: "llm",
80464
- capScope: "system",
80465
- addonId: null,
80466
- access: "view",
80467
- role: "admin"
80468
- },
80469
80515
  "llm.listRuntimeNodes": {
80470
80516
  capName: "llm",
80471
80517
  capScope: "system",
@@ -80473,13 +80519,6 @@ Object.freeze({
80473
80519
  access: "view",
80474
80520
  role: "admin"
80475
80521
  },
80476
- "llm.migrateRawSecrets": {
80477
- capName: "llm",
80478
- capScope: "system",
80479
- addonId: null,
80480
- access: "create",
80481
- role: "admin"
80482
- },
80483
80522
  "llm.resolveModelRef": {
80484
80523
  capName: "llm",
80485
80524
  capScope: "system",
@@ -80508,20 +80547,6 @@ Object.freeze({
80508
80547
  access: "create",
80509
80548
  role: "admin"
80510
80549
  },
80511
- "llm.testProfile": {
80512
- capName: "llm",
80513
- capScope: "system",
80514
- addonId: null,
80515
- access: "create",
80516
- role: "admin"
80517
- },
80518
- "llm.upsertProfile": {
80519
- capName: "llm",
80520
- capScope: "system",
80521
- addonId: null,
80522
- access: "create",
80523
- role: "admin"
80524
- },
80525
80550
  "llmRuntime.appleAvailability": {
80526
80551
  capName: "llm-runtime",
80527
80552
  capScope: "system",
@@ -81026,13 +81051,6 @@ Object.freeze({
81026
81051
  access: "create",
81027
81052
  role: "admin"
81028
81053
  },
81029
- "meshNetwork.testConnection": {
81030
- capName: "mesh-network",
81031
- capScope: "system",
81032
- addonId: null,
81033
- access: "create",
81034
- role: "admin"
81035
- },
81036
81054
  "meter.getStatus": {
81037
81055
  capName: "meter",
81038
81056
  capScope: "device",
@@ -81691,13 +81709,6 @@ Object.freeze({
81691
81709
  access: "create",
81692
81710
  role: "admin"
81693
81711
  },
81694
- "notificationOutput.deleteTarget": {
81695
- capName: "notification-output",
81696
- capScope: "system",
81697
- addonId: null,
81698
- access: "delete",
81699
- role: "admin"
81700
- },
81701
81712
  "notificationOutput.discoverTargets": {
81702
81713
  capName: "notification-output",
81703
81714
  capScope: "system",
@@ -81705,13 +81716,6 @@ Object.freeze({
81705
81716
  access: "view",
81706
81717
  role: "admin"
81707
81718
  },
81708
- "notificationOutput.getTarget": {
81709
- capName: "notification-output",
81710
- capScope: "system",
81711
- addonId: null,
81712
- access: "view",
81713
- role: "admin"
81714
- },
81715
81719
  "notificationOutput.listTargetKinds": {
81716
81720
  capName: "notification-output",
81717
81721
  capScope: "system",
@@ -81719,13 +81723,6 @@ Object.freeze({
81719
81723
  access: "view",
81720
81724
  role: "admin"
81721
81725
  },
81722
- "notificationOutput.listTargets": {
81723
- capName: "notification-output",
81724
- capScope: "system",
81725
- addonId: null,
81726
- access: "view",
81727
- role: "admin"
81728
- },
81729
81726
  "notificationOutput.probeTargetConfig": {
81730
81727
  capName: "notification-output",
81731
81728
  capScope: "system",
@@ -81740,13 +81737,6 @@ Object.freeze({
81740
81737
  access: "create",
81741
81738
  role: "admin"
81742
81739
  },
81743
- "notificationOutput.setTargetEnabled": {
81744
- capName: "notification-output",
81745
- capScope: "system",
81746
- addonId: null,
81747
- access: "create",
81748
- role: "admin"
81749
- },
81750
81740
  "notificationOutput.testTarget": {
81751
81741
  capName: "notification-output",
81752
81742
  capScope: "system",
@@ -81754,13 +81744,6 @@ Object.freeze({
81754
81744
  access: "create",
81755
81745
  role: "admin"
81756
81746
  },
81757
- "notificationOutput.upsertTarget": {
81758
- capName: "notification-output",
81759
- capScope: "system",
81760
- addonId: null,
81761
- access: "create",
81762
- role: "admin"
81763
- },
81764
81747
  "notificationRules.activateAlarmProfile": {
81765
81748
  capName: "notification-rules",
81766
81749
  capScope: "system",
@@ -82020,41 +82003,6 @@ Object.freeze({
82020
82003
  access: "view",
82021
82004
  role: "admin"
82022
82005
  },
82023
- "oidcConnections.connectionFormSchema": {
82024
- capName: "oidc-connections",
82025
- capScope: "system",
82026
- addonId: null,
82027
- access: "view",
82028
- role: "admin"
82029
- },
82030
- "oidcConnections.createConnection": {
82031
- capName: "oidc-connections",
82032
- capScope: "system",
82033
- addonId: null,
82034
- access: "create",
82035
- role: "admin"
82036
- },
82037
- "oidcConnections.deleteConnection": {
82038
- capName: "oidc-connections",
82039
- capScope: "system",
82040
- addonId: null,
82041
- access: "delete",
82042
- role: "admin"
82043
- },
82044
- "oidcConnections.listConnections": {
82045
- capName: "oidc-connections",
82046
- capScope: "system",
82047
- addonId: null,
82048
- access: "view",
82049
- role: "admin"
82050
- },
82051
- "oidcConnections.updateConnection": {
82052
- capName: "oidc-connections",
82053
- capScope: "system",
82054
- addonId: null,
82055
- access: "create",
82056
- role: "admin"
82057
- },
82058
82006
  "osd.getStatus": {
82059
82007
  capName: "osd",
82060
82008
  capScope: "device",
@@ -85086,13 +85034,6 @@ Object.freeze({
85086
85034
  access: "delete",
85087
85035
  role: "admin"
85088
85036
  },
85089
- "storage.deleteLocation": {
85090
- capName: "storage",
85091
- capScope: "system",
85092
- addonId: null,
85093
- access: "delete",
85094
- role: "admin"
85095
- },
85096
85037
  "storage.endDownload": {
85097
85038
  capName: "storage",
85098
85039
  capScope: "system",
@@ -85149,13 +85090,6 @@ Object.freeze({
85149
85090
  access: "view",
85150
85091
  role: "admin"
85151
85092
  },
85152
- "storage.listProviders": {
85153
- capName: "storage",
85154
- capScope: "system",
85155
- addonId: null,
85156
- access: "view",
85157
- role: "admin"
85158
- },
85159
85093
  "storage.read": {
85160
85094
  capName: "storage",
85161
85095
  capScope: "system",
@@ -85177,27 +85111,6 @@ Object.freeze({
85177
85111
  access: "view",
85178
85112
  role: "admin"
85179
85113
  },
85180
- "storage.testConfig": {
85181
- capName: "storage",
85182
- capScope: "system",
85183
- addonId: null,
85184
- access: "view",
85185
- role: "admin"
85186
- },
85187
- "storage.testLocation": {
85188
- capName: "storage",
85189
- capScope: "system",
85190
- addonId: null,
85191
- access: "view",
85192
- role: "admin"
85193
- },
85194
- "storage.upsertLocation": {
85195
- capName: "storage",
85196
- capScope: "system",
85197
- addonId: null,
85198
- access: "create",
85199
- role: "admin"
85200
- },
85201
85114
  "storage.write": {
85202
85115
  capName: "storage",
85203
85116
  capScope: "system",
@@ -85975,27 +85888,6 @@ Object.freeze({
85975
85888
  access: "create",
85976
85889
  role: "admin"
85977
85890
  },
85978
- "terminalSession.createInstance": {
85979
- capName: "terminal-session",
85980
- capScope: "system",
85981
- addonId: null,
85982
- access: "create",
85983
- role: "admin"
85984
- },
85985
- "terminalSession.deleteInstance": {
85986
- capName: "terminal-session",
85987
- capScope: "system",
85988
- addonId: null,
85989
- access: "delete",
85990
- role: "admin"
85991
- },
85992
- "terminalSession.listInstances": {
85993
- capName: "terminal-session",
85994
- capScope: "system",
85995
- addonId: null,
85996
- access: "view",
85997
- role: "admin"
85998
- },
85999
85891
  "terminalSession.listLegacyCameras": {
86000
85892
  capName: "terminal-session",
86001
85893
  capScope: "system",
@@ -86038,20 +85930,6 @@ Object.freeze({
86038
85930
  access: "create",
86039
85931
  role: "admin"
86040
85932
  },
86041
- "terminalSession.setInstanceEnabled": {
86042
- capName: "terminal-session",
86043
- capScope: "system",
86044
- addonId: null,
86045
- access: "create",
86046
- role: "admin"
86047
- },
86048
- "terminalSession.updateInstance": {
86049
- capName: "terminal-session",
86050
- capScope: "system",
86051
- addonId: null,
86052
- access: "create",
86053
- role: "admin"
86054
- },
86055
85933
  "terminalSession.writeInput": {
86056
85934
  capName: "terminal-session",
86057
85935
  capScope: "system",
@@ -86066,6 +85944,13 @@ Object.freeze({
86066
85944
  access: "create",
86067
85945
  role: "viewer"
86068
85946
  },
85947
+ "timedRun.getOptions": {
85948
+ capName: "timed-run",
85949
+ capScope: "device",
85950
+ addonId: null,
85951
+ access: "view",
85952
+ role: "viewer"
85953
+ },
86069
85954
  "timedRun.getStatus": {
86070
85955
  capName: "timed-run",
86071
85956
  capScope: "device",
@@ -86080,6 +85965,13 @@ Object.freeze({
86080
85965
  access: "create",
86081
85966
  role: "viewer"
86082
85967
  },
85968
+ "timedRun.setSettings": {
85969
+ capName: "timed-run",
85970
+ capScope: "device",
85971
+ addonId: null,
85972
+ access: "create",
85973
+ role: "admin"
85974
+ },
86083
85975
  "toast.onToast": {
86084
85976
  capName: "toast",
86085
85977
  capScope: "system",
@@ -87010,6 +86902,7 @@ function offered(received) {
87010
86902
  };
87011
86903
  }
87012
86904
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86905
+ string().min(1).max(4096);
87013
86906
  Object.freeze({
87014
86907
  "accessories.getStatus": [{
87015
86908
  name: "deviceId",
@@ -87693,6 +87586,11 @@ Object.freeze({
87693
87586
  form: "single",
87694
87587
  optional: false
87695
87588
  }],
87589
+ "deviceManager.setAlias": [{
87590
+ name: "deviceId",
87591
+ form: "single",
87592
+ optional: false
87593
+ }],
87696
87594
  "deviceManager.setChildLayout": [{
87697
87595
  name: "deviceId",
87698
87596
  form: "single",
@@ -89757,6 +89655,11 @@ Object.freeze({
89757
89655
  form: "single",
89758
89656
  optional: false
89759
89657
  }],
89658
+ "timedRun.getOptions": [{
89659
+ name: "deviceId",
89660
+ form: "single",
89661
+ optional: false
89662
+ }],
89760
89663
  "timedRun.getStatus": [{
89761
89664
  name: "deviceId",
89762
89665
  form: "single",
@@ -89767,6 +89670,11 @@ Object.freeze({
89767
89670
  form: "single",
89768
89671
  optional: false
89769
89672
  }],
89673
+ "timedRun.setSettings": [{
89674
+ name: "deviceId",
89675
+ form: "single",
89676
+ optional: false
89677
+ }],
89770
89678
  "update.getStatus": [{
89771
89679
  name: "deviceId",
89772
89680
  form: "single",
@@ -90629,6 +90537,7 @@ var STATE_SUMMARY_VARS = [
90629
90537
  [...RULE_TEXT],
90630
90538
  [...RULE_TEXT],
90631
90539
  [...RULE_TEXT],
90540
+ [...RULE_TEXT],
90632
90541
  [...RULE_BODY_ONLY],
90633
90542
  [...RULE_TEXT],
90634
90543
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -145983,17 +145892,6 @@ function scrub(message, secret) {
145983
145892
  function mintRelayId() {
145984
145893
  return `relay-${randomUUID().slice(0, 8)}`;
145985
145894
  }
145986
- /** The keys the settings page edits: the relay's connection fields. */
145987
- var RELAY_FORM_KEYS = new Set([
145988
- "host",
145989
- "port",
145990
- "secure",
145991
- "username",
145992
- "password",
145993
- "fromAddress",
145994
- "fromName"
145995
- ]);
145996
- var DEFAULT_RELAY_NAME = "Email (SMTP)";
145997
145895
  var RELAY_LIST_MAX = 200;
145998
145896
  function hasText(value) {
145999
145897
  return typeof value === "string" && value.length > 0;
@@ -146246,44 +146144,10 @@ var SmtpNodemailerAddon = class extends BaseAddon {
146246
146144
  const rows = (await records.kind(smtpRelaysKind).query({ limit: RELAY_LIST_MAX })).records;
146247
146145
  return (rows.find((row) => row.data.host.length > 0) ?? rows[0])?.id;
146248
146146
  }
146249
- /** The relay form. Its values live on the relay record, never in `addon-settings`. */
146147
+ /** The relay form. */
146250
146148
  async integrationFormSchema(_instance) {
146251
146149
  return this.relayForm();
146252
146150
  }
146253
- async getGlobalSettings() {
146254
- const schema = this.relayForm();
146255
- if (this.instances === null) return hydrateSchema(schema, {}, { inlineSecretsAsViews: true });
146256
- const id = await this.firstRelayId();
146257
- return hydrateSchema(schema, (id === void 0 ? null : await this.instances.readResolved(id))?.body ?? {}, { inlineSecretsAsViews: true });
146258
- }
146259
- async updateGlobalSettingsAsOperator(patch) {
146260
- await this.writeDefaultRelay(patch);
146261
- }
146262
- async updateGlobalSettings(patch) {
146263
- await this.writeDefaultRelay(patch);
146264
- }
146265
- /** Write the form's values onto the default relay, creating it on the first save. */
146266
- async writeDefaultRelay(patch) {
146267
- if (this.instances === null) throw new Error("smtp-nodemailer is not initialised");
146268
- const values = Object.fromEntries(Object.entries(patch).filter(([key]) => RELAY_FORM_KEYS.has(key)));
146269
- const existing = await this.firstRelayId();
146270
- if (existing !== void 0) {
146271
- await this.instances.updateInstance({
146272
- id: existing,
146273
- values
146274
- });
146275
- return;
146276
- }
146277
- const id = mintRelayId();
146278
- await this.instances.createInstance({
146279
- id,
146280
- name: DEFAULT_RELAY_NAME
146281
- });
146282
- await this.instances.updateInstance({
146283
- id,
146284
- values
146285
- });
146286
- }
146287
146151
  /** Probe a relay (EHLO + AUTH) with the form's values, falling back to the stored ones. */
146288
146152
  async probe(id, values) {
146289
146153
  const stored = id !== null && this.instances !== null ? await this.instances.getResolved(id) : null;