@camstack/addon-provider-enpal 0.1.13 → 0.1.14

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/addon.js +1818 -1863
  3. package/dist/addon.mjs +1818 -1863
  4. package/package.json +2 -2
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-BmPm98n0.mjs
1
+ //#region ../types/dist/event-category-78tcBemo.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -119,6 +119,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
119
119
  */
120
120
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
121
121
  /**
122
+ * An addon is RUNNING although its installed package is below what the
123
+ * running server requires (`camstack.requires.addons` of the server, or the
124
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
125
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
126
+ * operator alert per node and package. The addon is never held back for it.
127
+ *
128
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
129
+ */
130
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
131
+ /**
132
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
133
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
134
+ *
135
+ * Payload: `{ packageName, addonId, nodeId }`.
136
+ */
137
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
138
+ /**
122
139
  * Monitor scheduled the next retry for a failed addon. Transient —
123
140
  * surfaced to the UI for live-updating the "next retry in Ns"
124
141
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -229,7 +246,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
229
246
  EventCategory["ProcessRestarted"] = "process.restarted";
230
247
  /**
231
248
  * The SET of storage locations changed — one was created, edited, enabled,
232
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
249
+ * disabled or deleted through the location instances or the forced-delete action.
233
250
  *
234
251
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
235
252
  * it must also converge on its own periodic path, because a dropped event
@@ -784,7 +801,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
784
801
  return EventCategory;
785
802
  }({});
786
803
  //#endregion
787
- //#region ../types/dist/device-type-D1fH5kaC.mjs
804
+ //#region ../types/dist/device-type-DxCT0krO.mjs
788
805
  /**
789
806
  import { errMsg } from '@camstack/types'
790
807
  * Extract a human-readable message from an unknown error value.
@@ -946,6 +963,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
946
963
  * filters that want "all devices with on-motion behaviour".
947
964
  */
948
965
  DeviceFeature["MotionTrigger"] = "motion-trigger";
966
+ /**
967
+ * Device can be run for a chosen time and then stopped. Mirrors the
968
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
969
+ * a driver, so the flag is derived from the device's bindings
970
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
971
+ */
972
+ DeviceFeature["TimedRun"] = "timed-run";
949
973
  /** Light supports rgb-triplet color via `color` cap. */
950
974
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
951
975
  /** Light supports HSV color via `color` cap. */
@@ -5980,7 +6004,7 @@ var ZodIssueCode = {
5980
6004
  var ZodFirstPartyTypeKind;
5981
6005
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
6006
  //#endregion
5983
- //#region ../types/dist/sleep-D7ZKm4nb.mjs
6007
+ //#region ../types/dist/sleep-CGPZyzB5.mjs
5984
6008
  /**
5985
6009
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
6010
  * window to float samples (D455).
@@ -6344,6 +6368,59 @@ Object.fromEntries([
6344
6368
  function isValuelessField(field) {
6345
6369
  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";
6346
6370
  }
6371
+ /**
6372
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6373
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6374
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6375
+ * pre-resolving) lets callers both read the effective value AND detect
6376
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6377
+ * malformed input yields an empty map, never throws.
6378
+ */
6379
+ function collectHydratedFieldEntries(schema) {
6380
+ const out = /* @__PURE__ */ new Map();
6381
+ if (typeof schema !== "object" || schema === null) return out;
6382
+ const sections = schema.sections;
6383
+ if (!Array.isArray(sections)) return out;
6384
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6385
+ return out;
6386
+ }
6387
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6388
+ function collectFieldEntriesInto(fields, into) {
6389
+ if (!Array.isArray(fields)) return;
6390
+ for (const field of fields) {
6391
+ if (typeof field !== "object" || field === null) continue;
6392
+ const type = field.type;
6393
+ if (type === "group") {
6394
+ collectFieldEntriesInto(field.fields, into);
6395
+ continue;
6396
+ }
6397
+ if (type === "sub-tabs") {
6398
+ const tabs = field.tabs;
6399
+ if (Array.isArray(tabs)) {
6400
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6401
+ }
6402
+ continue;
6403
+ }
6404
+ const key = field.key;
6405
+ if (typeof key !== "string" || key.length === 0) continue;
6406
+ if (!("value" in field)) continue;
6407
+ into.set(key, {
6408
+ value: field.value,
6409
+ default: field.default
6410
+ });
6411
+ }
6412
+ }
6413
+ /**
6414
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6415
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6416
+ * common "just give me the resolved values" case. Built on
6417
+ * {@link collectHydratedFieldEntries}.
6418
+ */
6419
+ function collectHydratedFieldValues(schema) {
6420
+ const out = /* @__PURE__ */ new Map();
6421
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6422
+ return out;
6423
+ }
6347
6424
  function hydrateSchema(schema, values, options = {}) {
6348
6425
  return {
6349
6426
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -7212,6 +7289,7 @@ var RecordIntegrationInstances = class {
7212
7289
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
7213
7290
  this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
7214
7291
  this.nameField = kind.declaration.entry?.nameField ?? "name";
7292
+ if (hooks.kindOf === void 0 && hooks.description.kinds.length !== 1) throw new Error(`record kind '${kind.name}': describe().kinds has ${hooks.description.kinds.length} entries, so kindOf is required to say which one an instance is`);
7215
7293
  }
7216
7294
  /**
7217
7295
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -7253,10 +7331,21 @@ var RecordIntegrationInstances = class {
7253
7331
  }
7254
7332
  return out;
7255
7333
  }
7334
+ /** The kind an instance body is of: the provider's `kindOf`, else its one declared kind. */
7335
+ kindOfBody(body) {
7336
+ if (this.hooks.kindOf !== void 0) {
7337
+ const id = this.hooks.kindOf(body);
7338
+ if (!this.hooks.description.kinds.some((declared) => declared.id === id)) throw new Error(`record kind '${this.kind.name}': kindOf returned '${id}', which describe().kinds does not declare (D1013)`);
7339
+ return id;
7340
+ }
7341
+ const only = this.hooks.description.kinds[0];
7342
+ if (only === void 0) throw new Error(`record kind '${this.kind.name}' declares no kinds`);
7343
+ return only.id;
7344
+ }
7256
7345
  viewOf(record) {
7257
7346
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
7258
7347
  const rawName = record.data[this.nameField];
7259
- const kind = this.hooks.kindOf?.(record.data);
7348
+ const kind = this.kindOfBody(record.data);
7260
7349
  const health = this.hooks.health?.(record.data, record.id);
7261
7350
  return {
7262
7351
  id: record.id,
@@ -7264,7 +7353,7 @@ var RecordIntegrationInstances = class {
7264
7353
  configured: this.hooks.configured(record.data),
7265
7354
  enabled: isInstanceEnabled(record.data),
7266
7355
  formValues: shown,
7267
- ...kind !== void 0 ? { kind } : {},
7356
+ kind,
7268
7357
  ...health?.status !== void 0 ? { status: health.status } : {},
7269
7358
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
7270
7359
  };
@@ -7297,17 +7386,35 @@ var RecordIntegrationInstances = class {
7297
7386
  ...kind !== void 0 ? { kind } : {}
7298
7387
  });
7299
7388
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7389
+ const initial = this.hooks.initialBody(name, kind);
7390
+ const patch = this.patchOf(values ?? {});
7391
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7392
+ kind,
7393
+ body: {
7394
+ ...initial,
7395
+ ...patch
7396
+ }
7397
+ }));
7300
7398
  await this.handle.put({
7301
7399
  id: minted,
7302
- data: this.hooks.initialBody(name, kind)
7400
+ data: initial
7303
7401
  }, { ifRev: 0 });
7304
- const patch = this.patchOf(values ?? {});
7305
7402
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7306
7403
  await this.hooks.onChanged?.(minted, "created");
7307
7404
  return { id: minted };
7308
7405
  };
7309
7406
  updateInstance = async ({ id, name, values }) => {
7310
7407
  const patch = this.patchOf(values);
7408
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7409
+ const stored = await this.handle.get(id);
7410
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7411
+ kind: void 0,
7412
+ body: {
7413
+ ...stored.data,
7414
+ ...patch
7415
+ }
7416
+ }));
7417
+ }
7311
7418
  if (name !== void 0) patch[this.nameField] = name;
7312
7419
  await this.handle.patch(id, patch);
7313
7420
  await this.hooks.onChanged?.(id, "updated");
@@ -7618,6 +7725,21 @@ var SCOPED_SETTINGS_SCOPES = [
7618
7725
  "device-defaults",
7619
7726
  "node"
7620
7727
  ];
7728
+ /** The one refusal of a key a scope did not declare. */
7729
+ function assertDeclaredKeys(keys, declared) {
7730
+ const undeclared = keys.filter((key) => !declared.has(key));
7731
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7732
+ }
7733
+ /** Build the handle over an addon's scoped read, write and unset. */
7734
+ function createScopedSettings(deps) {
7735
+ return {
7736
+ async get() {
7737
+ return deps.schema.parse(await deps.read());
7738
+ },
7739
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7740
+ unset: (keys) => deps.unset(keys)
7741
+ };
7742
+ }
7621
7743
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7622
7744
  var KeychainWriteRefusedError = class extends Error {
7623
7745
  fieldPath;
@@ -8193,7 +8315,16 @@ function createHandle(raw, opts) {
8193
8315
  return blob;
8194
8316
  }
8195
8317
  },
8196
- writeAddonStore: writeAddon
8318
+ writeAddonStore: writeAddon,
8319
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8320
+ const stored = await readStoredAddonBlob(raw);
8321
+ index.seen("addon", stored);
8322
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8323
+ if (removable.length === 0) return;
8324
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8325
+ await raw.writeAddonStore(patch);
8326
+ index.wrote("addon", patch);
8327
+ })
8197
8328
  };
8198
8329
  }
8199
8330
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8323,7 +8454,7 @@ function boundOf(list) {
8323
8454
  * Base class for CamStack addons. Eliminates settings boilerplate:
8324
8455
  *
8325
8456
  * - Typed `config` property with automatic resolution from store + defaults
8326
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8457
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8327
8458
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8328
8459
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8329
8460
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8333,7 +8464,7 @@ function boundOf(list) {
8333
8464
  * Subclasses override:
8334
8465
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8335
8466
  * - `onShutdown()` — cleanup
8336
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8467
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8337
8468
  *
8338
8469
  * @example
8339
8470
  * ```ts
@@ -8347,7 +8478,7 @@ function boundOf(list) {
8347
8478
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8348
8479
  * }
8349
8480
  *
8350
- * protected globalSettingsSchema() {
8481
+ * protected clusterSettingsSchema() {
8351
8482
  * return this.schema({
8352
8483
  * sections: [{
8353
8484
  * id: 'main', title: 'Settings',
@@ -8390,7 +8521,7 @@ var BaseAddon = class {
8390
8521
  defaults;
8391
8522
  /**
8392
8523
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8393
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8524
+ * itself (`state()`, the scoped writers) that no form field declares.
8394
8525
  * Stored in the keychain exactly like a `password` field. Syntax:
8395
8526
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8396
8527
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8400,7 +8531,7 @@ var BaseAddon = class {
8400
8531
  /**
8401
8532
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8402
8533
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8403
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8534
+ * see them. Empty until loaded.
8404
8535
  */
8405
8536
  _scopedByCap = /* @__PURE__ */ new Map();
8406
8537
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8586,7 +8717,7 @@ var BaseAddon = class {
8586
8717
  /** Addon-specific cleanup. Override if needed. */
8587
8718
  async onShutdown() {}
8588
8719
  /**
8589
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8720
+ * Called after config is resolved during a settings write.
8590
8721
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8591
8722
  * Not called during initialize() — use onInitialize() for initial setup.
8592
8723
  */
@@ -8615,82 +8746,12 @@ var BaseAddon = class {
8615
8746
  scopedSettingsCapabilities() {
8616
8747
  return [];
8617
8748
  }
8618
- /** Override to provide global-level settings UI schema. */
8619
- globalSettingsSchema(_cap) {
8620
- return null;
8621
- }
8622
8749
  /** Override to provide device-level settings UI schema. */
8623
8750
  deviceSettingsSchema() {
8624
8751
  return null;
8625
8752
  }
8626
8753
  /**
8627
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8628
- * ARE the configuration of its integration.
8629
- *
8630
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8631
- * operator should find on the addon's integration page (System →
8632
- * Integrations → <name>) rather than only in the cluster-wide list of every
8633
- * addon. Empty (the default) means the addon has no integration-level
8634
- * settings and no such surface is offered — this is opt-in, because whether
8635
- * an addon's configuration IS its integration's configuration depends on the
8636
- * nature of the integration.
8637
- *
8638
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8639
- * the ONE global schema, in the ONE addon store, written by the ONE
8640
- * `updateGlobalSettings` path. There is deliberately no
8641
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8642
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8643
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8644
- *
8645
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8646
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8647
- * removed with the reason recorded at
8648
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8649
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8650
- * marker sprinkled across sections also has to borrow a field that already
8651
- * means something else; borrowing `section.tab` put the literal word
8652
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8653
- * GROUP this visually" and cannot also mean "where this lives" (D269
8654
- * supersedes D268). One declaration, in one place, next to the schema whose
8655
- * ids it names.
8656
- */
8657
- integrationSettingSections() {
8658
- return [];
8659
- }
8660
- async getGlobalSettings(overlay, cap, nodeId) {
8661
- await this.loadScopedSchemas(cap);
8662
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8663
- if (!schema) return { sections: [] };
8664
- const projected = await this.readRawProjectedStore(nodeId, cap);
8665
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8666
- }
8667
- /**
8668
- * The integration-level view of this addon's settings: exactly the sections
8669
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8670
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8671
- *
8672
- * Returns `null` when the addon declared nothing — an addon that opts out has
8673
- * no integration settings surface at all, rather than an empty one that reads
8674
- * as a failed load.
8675
- *
8676
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8677
- * and not in whichever UI happens to render this:
8678
- *
8679
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8680
- * shown here is the same field, with the same bare key, that the addon's
8681
- * own page shows. There is no integration-specific writer — callers save
8682
- * through `updateGlobalSettings` — so a second store key is unreachable,
8683
- * not merely discouraged.
8684
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8685
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8686
- * such a field silently picked would be a wrong answer for the operator
8687
- * who opened the page (D266).
8688
- * 3. **No silent typo.** A declared id that names no section throws. The
8689
- * alternative — skip it — turns a rename into a surface that quietly
8690
- * empties, which looks exactly like an addon with nothing to configure.
8691
- */
8692
- /**
8693
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8754
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8694
8755
  *
8695
8756
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8696
8757
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8738,37 +8799,178 @@ var BaseAddon = class {
8738
8799
  }
8739
8800
  }
8740
8801
  /**
8802
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8803
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8804
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8805
+ */
8806
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8807
+ return null;
8808
+ }
8809
+ /**
8810
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8811
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8812
+ */
8813
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8814
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8815
+ clusterSettings(cap, schema) {
8816
+ return this.scopedHandle("cluster", cap, schema);
8817
+ }
8818
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8819
+ deviceDefaults(cap, schema) {
8820
+ return this.scopedHandle("device-defaults", cap, schema);
8821
+ }
8822
+ /**
8823
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8824
+ * with no row yields the schema defaults — never a bare-key fallback.
8825
+ */
8826
+ nodeSettings(cap, schema, nodeId) {
8827
+ return this.scopedHandle("node", cap, schema, nodeId);
8828
+ }
8829
+ /**
8830
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8831
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8832
+ * central delete. `nodeId` omitted ⇒ the local node.
8833
+ */
8834
+ nodeState(key, schema, fallback, nodeId) {
8835
+ this._declaredStateKeys.add(key);
8836
+ return createDurableState({
8837
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8838
+ schema,
8839
+ fallback,
8840
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8841
+ write: async (patch) => {
8842
+ await this._ctx?.settings?.writeAddonStore(patch);
8843
+ },
8844
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8845
+ addonId: this._ctx?.id,
8846
+ key: k,
8847
+ error: String(e)
8848
+ } })
8849
+ });
8850
+ }
8851
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8852
+ async declaredScopedSchema(scope, cap) {
8853
+ await this.loadScopedSchemas(cap);
8854
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8855
+ }
8856
+ async requireScopedSchema(scope, cap) {
8857
+ const schema = await this.declaredScopedSchema(scope, cap);
8858
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8859
+ return schema;
8860
+ }
8861
+ scopedKeys(schema) {
8862
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8863
+ }
8864
+ /** `store` narrowed to the keys `schema` declares. */
8865
+ narrowToScope(store, schema) {
8866
+ const keys = this.scopedKeys(schema);
8867
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8868
+ }
8869
+ scopedHandle(scope, cap, schema, nodeId) {
8870
+ return createScopedSettings({
8871
+ schema,
8872
+ read: async () => {
8873
+ const declared = await this.requireScopedSchema(scope, cap);
8874
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8875
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8876
+ return {
8877
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8878
+ ...stored
8879
+ };
8880
+ },
8881
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8882
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8883
+ });
8884
+ }
8885
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8886
+ async writeScoped(scope, cap, patch, nodeId) {
8887
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8888
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8889
+ }
8890
+ /**
8891
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8892
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8893
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8894
+ */
8895
+ async unsetScoped(scope, cap, keys, nodeId) {
8896
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8897
+ const target = scope === "node" ? nodeId : void 0;
8898
+ const tree = this.addonSecretTree();
8899
+ const secretKeys = keys.filter((key) => tree.has(key));
8900
+ const plainKeys = keys.filter((key) => !tree.has(key));
8901
+ let changed = false;
8902
+ if (plainKeys.length > 0) {
8903
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8904
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8905
+ }
8906
+ if (secretKeys.length > 0) {
8907
+ await this.clearSecretKeys(secretKeys, target, cap);
8908
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8909
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8910
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8911
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8912
+ changed = true;
8913
+ }
8914
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8915
+ }
8916
+ /**
8917
+ * The hydrated form of one scope of one capability's settings (any
8918
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8919
+ */
8920
+ async getScopedSettings(scope, cap, nodeId) {
8921
+ const schema = await this.declaredScopedSchema(scope, cap);
8922
+ if (schema === null) return null;
8923
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8924
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8925
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8926
+ }
8927
+ /**
8928
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8929
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8930
+ * nothing.
8931
+ */
8932
+ async listScopedSettings() {
8933
+ await this.loadScopedSchemas();
8934
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8935
+ cap,
8936
+ scope: entry.scope,
8937
+ sectionIds: entry.schema.sections.map((section) => section.id)
8938
+ })));
8939
+ }
8940
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8941
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8942
+ this._operatorWriteDepth++;
8943
+ try {
8944
+ await this.writeScoped(scope, cap, patch, nodeId);
8945
+ } finally {
8946
+ this._operatorWriteDepth--;
8947
+ }
8948
+ }
8949
+ /**
8950
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8951
+ * its value. `null` when the capability declares nothing for the scope.
8952
+ */
8953
+ async readScopedValues(scope, cap, nodeId) {
8954
+ const schema = await this.declaredScopedSchema(scope, cap);
8955
+ if (schema === null) return null;
8956
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8957
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8958
+ }
8959
+ /**
8741
8960
  * What the create wizard and the integration edit page render for one instance: the addon's
8742
8961
  * per-instance schema when it declares one, else its global schema (what they always showed).
8743
8962
  */
8744
8963
  async getIntegrationFormSchema(instance, nodeId) {
8745
8964
  const scoped = await this.integrationFormSchema(instance);
8746
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8965
+ if (scoped === null) return this.scopedForm(nodeId);
8747
8966
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8748
8967
  }
8749
- async getIntegrationSettings(nodeId) {
8750
- const declared = this.integrationSettingSections();
8751
- if (declared.length === 0) return null;
8752
- const schema = this.globalSettingsSchema();
8753
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8754
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8755
- const sections = [];
8756
- for (const id of declared) {
8757
- const section = byId.get(id);
8758
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8759
- const fields = dropPerNodeFields(section.fields);
8760
- if (fields.length === 0) continue;
8761
- sections.push({
8762
- ...section,
8763
- fields
8764
- });
8765
- }
8766
- if (sections.length === 0) return null;
8767
- const projected = await this.readRawProjectedStore(nodeId);
8768
- return hydrateSchema({
8769
- ...schema,
8770
- sections
8771
- }, projected, { inlineSecretsAsViews: true });
8968
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8969
+ async scopedForm(nodeId) {
8970
+ await this.loadScopedSchemas();
8971
+ const schema = this.scopedFormSchema();
8972
+ if (!schema) return { sections: [] };
8973
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8772
8974
  }
8773
8975
  /**
8774
8976
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8777,8 +8979,8 @@ var BaseAddon = class {
8777
8979
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8778
8980
  * A no-op passthrough when the schema declares no `perNode` field.
8779
8981
  *
8780
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8781
- * the store for custom option logic (option narrowing, value snapping) to
8982
+ * This is the sanctioned way for an addon that needs
8983
+ * the store for custom logic (option narrowing, value snapping) to
8782
8984
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8783
8985
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8784
8986
  *
@@ -8830,7 +9032,7 @@ var BaseAddon = class {
8830
9032
  }
8831
9033
  /**
8832
9034
  * Whether ONE global key of this addon's store was written, read three-way
8833
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
9035
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8834
9036
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8835
9037
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8836
9038
  * not supported here: its stored name is scoped.
@@ -8864,7 +9066,7 @@ var BaseAddon = class {
8864
9066
  }
8865
9067
  /**
8866
9068
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8867
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9069
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8868
9070
  * the operator sends is stored as sent even when it equals a pre-keychain
8869
9071
  * inline value: the operator re-entering a password is a change, an addon
8870
9072
  * writing back what it read is not (D787).
@@ -8877,9 +9079,6 @@ var BaseAddon = class {
8877
9079
  this._operatorWriteDepth--;
8878
9080
  }
8879
9081
  }
8880
- async updateGlobalSettings(patch, nodeId) {
8881
- await this.applyGlobalPatch(patch, nodeId);
8882
- }
8883
9082
  /**
8884
9083
  * REMOVE stored secrets — their keychain values and their markers (D787).
8885
9084
  *
@@ -8889,6 +9088,9 @@ var BaseAddon = class {
8889
9088
  * a keychain (an older kernel) it writes `''`, as such code always did.
8890
9089
  */
8891
9090
  async clearSecrets(keys, nodeId) {
9091
+ await this.clearSecretKeys(keys, nodeId);
9092
+ }
9093
+ async clearSecretKeys(keys, nodeId, cap) {
8892
9094
  const tree = this.addonSecretTree();
8893
9095
  const keychain = this._ctx?.secrets !== void 0;
8894
9096
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8901,25 +9103,26 @@ var BaseAddon = class {
8901
9103
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8902
9104
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8903
9105
  }
8904
- await this.applyGlobalPatch(patch, nodeId);
9106
+ await this.applyGlobalPatch(patch, nodeId, cap);
8905
9107
  }
8906
- async applyGlobalPatch(barePatch, nodeId) {
8907
- await this.loadScopedSchemas();
8908
- const keys = this.perNodeKeys();
9108
+ /** Resolves `true` when the write changed stored values. */
9109
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9110
+ await this.loadScopedSchemas(cap);
9111
+ const keys = this.perNodeKeys(cap);
8909
9112
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8910
9113
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8911
9114
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8912
9115
  const handle = this._secretSettings;
8913
9116
  if (handle !== null) {
8914
9117
  const { changed } = await handle.writeAddonStore(scoped);
8915
- if (!changed) return;
9118
+ if (!changed) return false;
8916
9119
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8917
- if (target !== localNode) return;
9120
+ if (target !== localNode) return true;
8918
9121
  await this.resolveConfig();
8919
9122
  await this.onConfigChanged();
8920
9123
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8921
- const legacy = this.globalSettingsSchema();
8922
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9124
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9125
+ return true;
8923
9126
  }
8924
9127
  /**
8925
9128
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8956,10 +9159,7 @@ var BaseAddon = class {
8956
9159
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8957
9160
  this._addonSecretTree = null;
8958
9161
  }
8959
- /**
8960
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8961
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8962
- */
9162
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8963
9163
  scopedFormSchema(cap) {
8964
9164
  const byId = /* @__PURE__ */ new Map();
8965
9165
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8971,8 +9171,7 @@ var BaseAddon = class {
8971
9171
  return [...this._scopedByCap.values()].flat();
8972
9172
  }
8973
9173
  /**
8974
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8975
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9174
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8976
9175
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8977
9176
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8978
9177
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8981,8 +9180,7 @@ var BaseAddon = class {
8981
9180
  const cacheKey = cap ?? "";
8982
9181
  const cached = this._nodeKeysByCap.get(cacheKey);
8983
9182
  if (cached) return cached;
8984
- const legacy = this.globalSettingsSchema(cap);
8985
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9183
+ const keys = /* @__PURE__ */ new Set();
8986
9184
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8987
9185
  if (scope !== "node") continue;
8988
9186
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9200,8 +9398,6 @@ var BaseAddon = class {
9200
9398
  }
9201
9399
  declaredKeySet() {
9202
9400
  const keys = new Set(Object.keys(this.defaults));
9203
- const schema = this.globalSettingsSchema();
9204
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9205
9401
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9206
9402
  for (const key of this._declaredStateKeys) keys.add(key);
9207
9403
  for (const path of this.secretStatePaths) {
@@ -9299,7 +9495,7 @@ var BaseAddon = class {
9299
9495
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9300
9496
  addonSecretTree() {
9301
9497
  if (this._addonSecretTree === null) {
9302
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9498
+ const tree = /* @__PURE__ */ new Map();
9303
9499
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9304
9500
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9305
9501
  }
@@ -9511,47 +9707,6 @@ function classifyStoreError(err) {
9511
9707
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9512
9708
  return "permanent";
9513
9709
  }
9514
- /**
9515
- * Collect the keys of every field marked `perNode: true`, recursing into
9516
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9517
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9518
- * don't declare `perNode` and are excluded by the `in` narrowing.
9519
- */
9520
- /**
9521
- * The same fields with every `perNode: true` one removed, recursing into layout
9522
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9523
- * with no child is dropped rather than rendered empty.
9524
- *
9525
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9526
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9527
- */
9528
- function dropPerNodeFields(fields) {
9529
- const kept = [];
9530
- for (const field of fields) {
9531
- if (field.type === "group") {
9532
- const inner = dropPerNodeFields(field.fields);
9533
- if (inner.length > 0) kept.push({
9534
- ...field,
9535
- fields: inner
9536
- });
9537
- continue;
9538
- }
9539
- if (field.type === "sub-tabs") {
9540
- const tabs = field.tabs.map((tab) => ({
9541
- ...tab,
9542
- fields: dropPerNodeFields(tab.fields)
9543
- })).filter((tab) => tab.fields.length > 0);
9544
- if (tabs.length > 0) kept.push({
9545
- ...field,
9546
- tabs
9547
- });
9548
- continue;
9549
- }
9550
- if ("perNode" in field && field.perNode === true) continue;
9551
- kept.push(field);
9552
- }
9553
- return kept;
9554
- }
9555
9710
  /** The keys of the value-holding fields, recursing into layout containers. */
9556
9711
  function collectValueFieldKeys(fields) {
9557
9712
  const collected = [];
@@ -9584,21 +9739,6 @@ function collectFieldKeys(fields) {
9584
9739
  }
9585
9740
  return collected;
9586
9741
  }
9587
- function collectPerNodeFieldKeys(fields) {
9588
- const collected = [];
9589
- for (const field of fields) {
9590
- if (field.type === "group") {
9591
- collected.push(...collectPerNodeFieldKeys(field.fields));
9592
- continue;
9593
- }
9594
- if (field.type === "sub-tabs") {
9595
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9596
- continue;
9597
- }
9598
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9599
- }
9600
- return collected;
9601
- }
9602
9742
  /**
9603
9743
  * Normalize an `ICamstackAddon.initialize()` return value into the
9604
9744
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10653,7 +10793,6 @@ var CAP_NAMES = Object.freeze([
10653
10793
  "notifier",
10654
10794
  "numeric-sensor",
10655
10795
  "oauth-integration",
10656
- "oidc-connections",
10657
10796
  "osd",
10658
10797
  "osd-manager",
10659
10798
  "pet-feeder",
@@ -12360,13 +12499,25 @@ Object.freeze({
12360
12499
  "path": "temperature",
12361
12500
  "valueType": "number"
12362
12501
  }],
12363
- "timed-run": [{
12364
- "path": "activeUntil",
12365
- "valueType": "number"
12366
- }, {
12367
- "path": "requestedBy",
12368
- "valueType": "string"
12369
- }],
12502
+ "timed-run": [
12503
+ {
12504
+ "path": "activeUntil",
12505
+ "valueType": "number"
12506
+ },
12507
+ {
12508
+ "path": "requestedBy",
12509
+ "valueType": "string"
12510
+ },
12511
+ {
12512
+ "path": "quickRun.duration",
12513
+ "valueType": "number"
12514
+ },
12515
+ {
12516
+ "path": "quickRun.unit",
12517
+ "valueType": "enum",
12518
+ "enumValues": ["s", "min"]
12519
+ }
12520
+ ],
12370
12521
  "update": [
12371
12522
  {
12372
12523
  "path": "currentVersion",
@@ -12545,8 +12696,7 @@ Object.freeze({
12545
12696
  "device-provider": { "reloadDevice": 18e4 },
12546
12697
  "llm": {
12547
12698
  "generate": 6e5,
12548
- "generateVision": 6e5,
12549
- "testProfile": 6e5
12699
+ "generateVision": 6e5
12550
12700
  },
12551
12701
  "llm-runtime": {
12552
12702
  "complete": 6e5,
@@ -12591,7 +12741,7 @@ object({
12591
12741
  params: record(string(), union([string(), number()])).optional()
12592
12742
  });
12593
12743
  //#endregion
12594
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12744
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12595
12745
  /**
12596
12746
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12597
12747
  *
@@ -13753,458 +13903,844 @@ var NodeColorIdSchema = _enum([
13753
13903
  "node-h"
13754
13904
  ]);
13755
13905
  //#endregion
13756
- //#region ../types/dist/index.mjs
13757
- var SMTP_INBOUND_DEFAULT_PORT = 2525;
13758
- var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13759
- var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13760
- /** The text excerpt a message carries on the bus. */
13761
- var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13762
- var PortSchema = number().int().min(1).max(65535);
13763
- /** A recipient local-part: what stands before the `@`. */
13764
- var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13765
- object({
13766
- port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13767
- bindHost: string().min(1).default("0.0.0.0"),
13768
- domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13769
- requireAuth: boolean().default(true),
13770
- username: string().default(""),
13771
- password: string().default("").meta({ secret: true }),
13772
- starttls: boolean().default(false),
13773
- maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13774
- });
13775
- object({
13776
- host: string().min(1).nullable(),
13777
- hostReason: string().nullable(),
13778
- port: PortSchema,
13779
- domain: string().min(1),
13780
- requireAuth: boolean(),
13781
- username: string(),
13782
- password: string(),
13783
- starttls: boolean()
13784
- });
13785
- object({
13786
- recipients: array(SmtpRecipientSchema).min(1),
13787
- /**
13788
- * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13789
- * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13790
- * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13791
- * Absent, the subscriber is anonymous and unique.
13792
- */
13793
- subscriberId: string().min(1).max(64).optional()
13794
- });
13795
- var SmtpInboundAttachmentSchema = object({
13796
- filename: string(),
13797
- contentType: string(),
13798
- bytes: number().int().nonnegative()
13799
- });
13800
- /** The payload of one `broker.message` (the key is the recipient local-part). */
13801
- var SmtpInboundMessageSchema = object({
13802
- /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13803
- seq: number().int().positive(),
13804
- messageId: string(),
13805
- receivedAt: number().int().nonnegative(),
13806
- from: string(),
13807
- to: string(),
13808
- subject: string(),
13809
- text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13810
- attachments: array(SmtpInboundAttachmentSchema)
13811
- });
13812
- object({
13813
- /** The newest seq issued so far (0 before any mail). */
13814
- lastSeq: number().int().nonnegative(),
13815
- /** The oldest seq still retained; messages below it are gone. */
13816
- oldestSeq: number().int().nonnegative(),
13817
- messages: array(object({
13818
- key: string(),
13819
- message: SmtpInboundMessageSchema
13820
- }))
13821
- });
13822
- new Set([
13823
- "track",
13824
- "face",
13825
- "identity",
13826
- "plate",
13827
- "vehicle",
13828
- "scene",
13829
- "motion",
13830
- "object",
13831
- "audio",
13832
- "mosaic"
13833
- ]);
13834
- /**
13835
- * The owner types that are event tables, in the order the event-media resolver
13836
- * should consider them. Exported so a consumer asking "is this key an event?"
13837
- * does not re-spell the list and drift from it.
13838
- */
13839
- var EVENT_OWNER_TYPES = [
13840
- "motion",
13841
- "object",
13842
- "audio"
13843
- ];
13844
- /**
13845
- * The same list as a Zod enum, for the cap inputs that must NAME the table
13846
- * (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
13847
- * `(table, id)`).
13848
- *
13849
- * Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
13850
- * table would otherwise be accepted by the codec and refused at the door, with
13851
- * nothing failing until a caller asked.
13852
- */
13853
- var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
13854
- /** The same list as a Zod enum, for the cap input that carries it. */
13855
- var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
13856
- new Set(EVENT_OWNER_TYPES);
13857
- /**
13858
- * What a published package needs from the rest of the install (D989).
13859
- *
13860
- * One published framework package (`@camstack/server`) carries the kernel, the
13861
- * builtins and the capability contracts, so "which addon runs on which
13862
- * framework" is a question of version, and the answer is DECLARED by the
13863
- * package that knows it:
13864
- *
13865
- * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
13866
- * - the server says the oldest addon it works with
13867
- * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
13868
- * around it (`requires.wrapper: { desktop, image }`);
13869
- * - a wrapper (the desktop app, the container starter) says which server
13870
- * range it can host, and reports its own kind and version.
13871
- *
13872
- * The publish workflow STAMPS these after the version bump
13873
- * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
13874
- *
13875
- * A requirement that cannot be checked is NOT met: a shell that reports no
13876
- * version counts as below the minimum, and an unreadable running version is
13877
- * named in the result (a failed read is never a pass).
13878
- */
13879
- /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
13880
- var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
13881
- object({
13882
- /** The oldest `@camstack/server` this package runs on. */
13883
- server: MinVersionSchema.optional(),
13884
- /** The oldest installed version of each named addon package. */
13885
- addons: record(string(), MinVersionSchema).optional(),
13886
- /** The oldest shell around the server, per shell kind. */
13887
- wrapper: object({
13888
- desktop: MinVersionSchema.optional(),
13889
- image: MinVersionSchema.optional()
13890
- }).optional()
13891
- });
13892
- /** One unmet requirement; each names the versions on both sides. */
13893
- var UnmetRequirementSchema = discriminatedUnion("kind", [
13894
- object({
13895
- kind: literal("server-too-old"),
13896
- required: string(),
13897
- /** `null` — the running server version could not be read. */
13898
- running: string().nullable()
13899
- }),
13900
- object({
13901
- kind: literal("addon-too-old"),
13902
- packageName: string(),
13903
- required: string(),
13904
- /** `null` — the addon is not installed (or its version is unreadable). */
13905
- running: string().nullable()
13906
- }),
13907
- object({
13908
- kind: literal("wrapper-too-old"),
13909
- wrapper: _enum(["desktop", "image"]),
13910
- required: string(),
13911
- /** `null` — the wrapper reported no version. */
13912
- running: string().nullable()
13913
- })
13914
- ]);
13915
- var RequiredAddonUpdateSchema = object({
13916
- packageName: string(),
13917
- /** The minimum the target server declares. */
13918
- required: string(),
13919
- /** What this node has; `null` when the addon is not installed. */
13920
- installed: string().nullable()
13921
- });
13922
- /**
13923
- * Which integration instance a settings form is for. An addon that serves several instances
13924
- * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
13925
- * needs its connection, the default integration needs the addon's own sections.
13926
- *
13927
- * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
13928
- * setting up a NEW instance.
13929
- * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
13930
- * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
13931
- */
13932
- var IntegrationInstanceRefSchema = object({
13933
- integrationId: string().nullable(),
13934
- mode: _enum([
13935
- "standalone",
13936
- "account",
13937
- "broker"
13938
- ]),
13939
- candidateStableId: string().optional()
13940
- });
13941
- /**
13942
- * notification-output — canonical, capability-gated notification delivery.
13943
- *
13944
- * Apprise-derived model (see
13945
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13946
- * callers emit ONE canonical `Notification`; each provider declares a
13947
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
13948
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13949
- * message to what the kind supports — callers never special-case a service.
13950
- *
13951
- * DESIGN DECISIONS (locked):
13952
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13953
- * `setTargetEnabled`), each provider persisting via the `settings-store`
13954
- * cap. Rationale: the admin UI needs one uniform surface across the
13955
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13956
- * alternative would fork the UI per addon and cannot host the
13957
- * discovery→adopt flow.
13958
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13959
- * the generated cap-mount auto-`concatCollection`-fans them across every
13960
- * registered provider (notifiers addon + HA addon) so one catalog is
13961
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13962
- * `addonId` the generated collection router extracts from the call input.
13963
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13964
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13965
- * `storage` / `storage-provider` / `recording` caps over the same path. No
13966
- * base64 fallback needed.
13967
- *
13968
- * TODO (deferred, closed-set change — separate decision): add
13969
- * `providerKind: 'notify'` so notification providers surface on the unified
13970
- * admin "Integrations" page.
13971
- */
13972
- /**
13973
- * Zentik-derived typed-media enum — the superset across every kind. Each
13974
- * adapter picks what it supports and the degrade engine filters the rest.
13975
- */
13976
- var AttachmentMediaTypeSchema = _enum([
13977
- "image",
13978
- "video",
13979
- "gif",
13980
- "audio",
13981
- "icon"
13982
- ]);
13983
- /**
13984
- * A single attachment. Exactly one of `url` (remote source, most adapters
13985
- * prefer this) or `bytes` (inline source; required for Pushover-style
13986
- * bytes-only kinds) MUST be present — the degrade engine expresses a
13987
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
13988
- */
13989
- var AttachmentSchema = object({
13990
- mediaType: AttachmentMediaTypeSchema,
13991
- url: string().optional(),
13992
- bytes: _instanceof(Uint8Array).optional(),
13993
- mime: string().optional(),
13994
- name: string().optional()
13995
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13996
- var NotificationFormatSchema = _enum([
13997
- "text",
13998
- "markdown",
13999
- "html"
14000
- ]);
13906
+ //#region ../types/dist/convert-BAzH15HU.mjs
14001
13907
  /**
14002
- * The CLOSED icon vocabulary an action button may use.
14003
- *
14004
- * A closed set, not a free string, and that is the whole point: an arbitrary
14005
- * icon name is one that ntfy renders, zentik silently drops, and nobody
14006
- * notices — the same class of gap as a zone vocabulary nothing produced
14007
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14008
- * declares `actionIcons: false` and the degrade engine strips the field.
14009
- *
14010
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14011
- * renderer's icon set; "acknowledge" survives an adapter that draws it
14012
- * differently.
14013
- *
14014
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14015
- *
14016
- * These names are for US. **No adapter may forward one verbatim.** Each maps
14017
- * the whole set onto its own renderer's vocabulary through a
14018
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14019
- * fallback, so adding a member here fails every adapter's build until someone
14020
- * decides its glyph, which is the only place that decision can be made
14021
- * honestly.
14022
- *
14023
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14024
- * `disarm` straight through; iOS feeds that string to
14025
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14026
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
14027
- * mapping, and "the field is documented" is not "the value renders".
14028
- *
14029
- * Adding a member is TRAIN-BOUND. The enum lives in the published
14030
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14031
- * so an addon that emits a token the running hub does not know does not lose an
14032
- * icon — its whole `send` fails Zod validation and the notification never
14033
- * arrives. Never emit a new token from an addon before the train carrying it.
13908
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13909
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13910
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13911
+ * decimals). K derives from `°C = K − 273.15`.
14034
13912
  */
14035
- var NotificationActionIconSchema = _enum([
14036
- "acknowledge",
14037
- "dismiss",
14038
- "silence",
14039
- "snooze",
14040
- "view",
14041
- "play",
14042
- "open",
14043
- "close",
14044
- "lock",
14045
- "unlock",
14046
- "arm",
14047
- "arm-home",
14048
- "arm-away",
14049
- "arm-night",
14050
- "disarm",
14051
- "light",
14052
- "alert",
14053
- "camera"
14054
- ]);
14055
- /** A single tap-through action button. */
14056
- var NotificationActionSchema = object({
14057
- id: string(),
14058
- label: string(),
14059
- url: string().optional(),
14060
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14061
- icon: NotificationActionIconSchema.optional(),
14062
- /**
14063
- * Renders in a warning style where the notifier supports it.
14064
- *
14065
- * A HINT, never a gate. The callback's authority is its token and nothing
14066
- * else — see `notification-center/action-token.ts` for what that does and
14067
- * does not buy.
14068
- */
14069
- destructive: boolean().optional(),
14070
- /**
14071
- * How the tap should REACH the url.
14072
- *
14073
- * `navigate` (absent, and every button authored before this field) opens it:
14074
- * the phone leaves the notification and shows whatever the callback returns.
14075
- * That is right for a button whose answer the operator wants to read.
14076
- *
14077
- * `background` fires it as a POST and stays put. It exists for the buttons
14078
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
14079
- * an answer to the notification, and being thrown into a browser tab to
14080
- * confirm it costs more attention than the notification did. A backend that
14081
- * cannot do a background call renders it as an ordinary link (the adapters
14082
- * fall back rather than dropping the button), so this is a preference, never
14083
- * a requirement.
14084
- */
14085
- mode: _enum(["navigate", "background"]).optional()
14086
- });
14087
- /** A delivery parameter value: what a notifier declared it takes. */
14088
- var DeliveryParamValueSchema = union([
14089
- string(),
14090
- number(),
14091
- boolean()
14092
- ]);
14093
- /**
14094
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
14095
- * addresses a target of that kind may set it; a kind that declares nothing is
14096
- * never handed one. The admin UI derives its control from this descriptor —
14097
- * no editor knows a service by name.
14098
- *
14099
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14100
- * run on the addon cap path).
14101
- */
14102
- var DeliveryParamSchema = object({
14103
- key: string().min(1),
14104
- label: string(),
14105
- /** i18n key of the label, resolved by the declaring addon's namespace. */
14106
- labelKey: string().optional(),
14107
- type: _enum([
14108
- "enum",
14109
- "number",
14110
- "boolean",
14111
- "string"
14112
- ]),
14113
- /** `enum` only: the closed set of accepted values. */
14114
- options: array(object({
14115
- value: string(),
14116
- label: string()
14117
- })).optional(),
14118
- /** `number` only. */
14119
- min: number().optional(),
14120
- max: number().optional(),
14121
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14122
- default: DeliveryParamValueSchema.optional()
14123
- });
13913
+ var UNIT_TABLE = {
13914
+ "°C": {
13915
+ dimension: "temperature",
13916
+ factor: 1,
13917
+ offset: 0
13918
+ },
13919
+ "°F": {
13920
+ dimension: "temperature",
13921
+ factor: 5 / 9,
13922
+ offset: -160 / 9
13923
+ },
13924
+ K: {
13925
+ dimension: "temperature",
13926
+ factor: 1,
13927
+ offset: -273.15
13928
+ },
13929
+ hPa: {
13930
+ dimension: "pressure",
13931
+ factor: 1,
13932
+ offset: 0
13933
+ },
13934
+ kPa: {
13935
+ dimension: "pressure",
13936
+ factor: 10,
13937
+ offset: 0
13938
+ },
13939
+ Pa: {
13940
+ dimension: "pressure",
13941
+ factor: .01,
13942
+ offset: 0
13943
+ },
13944
+ mbar: {
13945
+ dimension: "pressure",
13946
+ factor: 1,
13947
+ offset: 0
13948
+ },
13949
+ bar: {
13950
+ dimension: "pressure",
13951
+ factor: 1e3,
13952
+ offset: 0
13953
+ },
13954
+ inHg: {
13955
+ dimension: "pressure",
13956
+ factor: 33.8639,
13957
+ offset: 0
13958
+ },
13959
+ mmHg: {
13960
+ dimension: "pressure",
13961
+ factor: 1.33322,
13962
+ offset: 0
13963
+ },
13964
+ psi: {
13965
+ dimension: "pressure",
13966
+ factor: 68.9476,
13967
+ offset: 0
13968
+ },
13969
+ "m/s": {
13970
+ dimension: "speed",
13971
+ factor: 1,
13972
+ offset: 0
13973
+ },
13974
+ "km/h": {
13975
+ dimension: "speed",
13976
+ factor: 1 / 3.6,
13977
+ offset: 0
13978
+ },
13979
+ mph: {
13980
+ dimension: "speed",
13981
+ factor: .44704,
13982
+ offset: 0
13983
+ },
13984
+ kn: {
13985
+ dimension: "speed",
13986
+ factor: .514444,
13987
+ offset: 0
13988
+ },
13989
+ "mm/h": {
13990
+ dimension: "precipitation-rate",
13991
+ factor: 1,
13992
+ offset: 0
13993
+ },
13994
+ "in/h": {
13995
+ dimension: "precipitation-rate",
13996
+ factor: 25.4,
13997
+ offset: 0
13998
+ },
13999
+ m: {
14000
+ dimension: "length",
14001
+ factor: 1,
14002
+ offset: 0
14003
+ },
14004
+ mm: {
14005
+ dimension: "length",
14006
+ factor: .001,
14007
+ offset: 0
14008
+ },
14009
+ cm: {
14010
+ dimension: "length",
14011
+ factor: .01,
14012
+ offset: 0
14013
+ },
14014
+ km: {
14015
+ dimension: "length",
14016
+ factor: 1e3,
14017
+ offset: 0
14018
+ },
14019
+ in: {
14020
+ dimension: "length",
14021
+ factor: .0254,
14022
+ offset: 0
14023
+ },
14024
+ ft: {
14025
+ dimension: "length",
14026
+ factor: .3048,
14027
+ offset: 0
14028
+ },
14029
+ mi: {
14030
+ dimension: "length",
14031
+ factor: 1609.344,
14032
+ offset: 0
14033
+ },
14034
+ "m²": {
14035
+ dimension: "area",
14036
+ factor: 1,
14037
+ offset: 0
14038
+ },
14039
+ lx: {
14040
+ dimension: "illuminance",
14041
+ factor: 1,
14042
+ offset: 0
14043
+ },
14044
+ "W/m²": {
14045
+ dimension: "irradiance",
14046
+ factor: 1,
14047
+ offset: 0
14048
+ },
14049
+ W: {
14050
+ dimension: "power",
14051
+ factor: 1,
14052
+ offset: 0
14053
+ },
14054
+ kW: {
14055
+ dimension: "power",
14056
+ factor: 1e3,
14057
+ offset: 0
14058
+ },
14059
+ VA: {
14060
+ dimension: "apparent-power",
14061
+ factor: 1,
14062
+ offset: 0
14063
+ },
14064
+ Wh: {
14065
+ dimension: "energy",
14066
+ factor: 1,
14067
+ offset: 0
14068
+ },
14069
+ kWh: {
14070
+ dimension: "energy",
14071
+ factor: 1e3,
14072
+ offset: 0
14073
+ },
14074
+ MWh: {
14075
+ dimension: "energy",
14076
+ factor: 1e6,
14077
+ offset: 0
14078
+ },
14079
+ V: {
14080
+ dimension: "voltage",
14081
+ factor: 1,
14082
+ offset: 0
14083
+ },
14084
+ mV: {
14085
+ dimension: "voltage",
14086
+ factor: .001,
14087
+ offset: 0
14088
+ },
14089
+ A: {
14090
+ dimension: "current",
14091
+ factor: 1,
14092
+ offset: 0
14093
+ },
14094
+ mA: {
14095
+ dimension: "current",
14096
+ factor: .001,
14097
+ offset: 0
14098
+ },
14099
+ ppm: {
14100
+ dimension: "concentration-volume",
14101
+ factor: 1,
14102
+ offset: 0
14103
+ },
14104
+ ppb: {
14105
+ dimension: "concentration-volume",
14106
+ factor: .001,
14107
+ offset: 0
14108
+ },
14109
+ "µg/m³": {
14110
+ dimension: "concentration-mass",
14111
+ factor: 1,
14112
+ offset: 0
14113
+ },
14114
+ "mg/m³": {
14115
+ dimension: "concentration-mass",
14116
+ factor: 1e3,
14117
+ offset: 0
14118
+ },
14119
+ kg: {
14120
+ dimension: "mass",
14121
+ factor: 1,
14122
+ offset: 0
14123
+ },
14124
+ g: {
14125
+ dimension: "mass",
14126
+ factor: .001,
14127
+ offset: 0
14128
+ },
14129
+ lb: {
14130
+ dimension: "mass",
14131
+ factor: .453592,
14132
+ offset: 0
14133
+ },
14134
+ oz: {
14135
+ dimension: "mass",
14136
+ factor: .0283495,
14137
+ offset: 0
14138
+ },
14139
+ "%": {
14140
+ dimension: "percentage",
14141
+ factor: 1,
14142
+ offset: 0
14143
+ },
14144
+ L: {
14145
+ dimension: "volume",
14146
+ factor: 1,
14147
+ offset: 0
14148
+ },
14149
+ mL: {
14150
+ dimension: "volume",
14151
+ factor: .001,
14152
+ offset: 0
14153
+ },
14154
+ gal: {
14155
+ dimension: "volume",
14156
+ factor: 3.785411784,
14157
+ offset: 0
14158
+ },
14159
+ s: {
14160
+ dimension: "duration",
14161
+ factor: 1,
14162
+ offset: 0
14163
+ },
14164
+ ms: {
14165
+ dimension: "duration",
14166
+ factor: .001,
14167
+ offset: 0
14168
+ },
14169
+ min: {
14170
+ dimension: "duration",
14171
+ factor: 60,
14172
+ offset: 0
14173
+ },
14174
+ h: {
14175
+ dimension: "duration",
14176
+ factor: 3600,
14177
+ offset: 0
14178
+ },
14179
+ d: {
14180
+ dimension: "duration",
14181
+ factor: 86400,
14182
+ offset: 0
14183
+ },
14184
+ B: {
14185
+ dimension: "information",
14186
+ factor: 1,
14187
+ offset: 0
14188
+ },
14189
+ KiB: {
14190
+ dimension: "information",
14191
+ factor: 1024,
14192
+ offset: 0
14193
+ },
14194
+ MiB: {
14195
+ dimension: "information",
14196
+ factor: 1024 ** 2,
14197
+ offset: 0
14198
+ },
14199
+ GiB: {
14200
+ dimension: "information",
14201
+ factor: 1024 ** 3,
14202
+ offset: 0
14203
+ },
14204
+ TiB: {
14205
+ dimension: "information",
14206
+ factor: 1024 ** 4,
14207
+ offset: 0
14208
+ },
14209
+ kB: {
14210
+ dimension: "information",
14211
+ factor: 1e3,
14212
+ offset: 0
14213
+ },
14214
+ MB: {
14215
+ dimension: "information",
14216
+ factor: 1e6,
14217
+ offset: 0
14218
+ },
14219
+ GB: {
14220
+ dimension: "information",
14221
+ factor: 1e9,
14222
+ offset: 0
14223
+ },
14224
+ "°": {
14225
+ dimension: "angle",
14226
+ factor: 1,
14227
+ offset: 0
14228
+ },
14229
+ dBFS: {
14230
+ dimension: "sound-level",
14231
+ factor: 1,
14232
+ offset: 0
14233
+ },
14234
+ dBm: {
14235
+ dimension: "signal-level",
14236
+ factor: 1,
14237
+ offset: 0
14238
+ },
14239
+ mired: {
14240
+ dimension: "colour-temperature",
14241
+ factor: 1,
14242
+ offset: 0
14243
+ },
14244
+ kbps: {
14245
+ dimension: "bitrate",
14246
+ factor: 1,
14247
+ offset: 0
14248
+ },
14249
+ Mbps: {
14250
+ dimension: "bitrate",
14251
+ factor: 1e3,
14252
+ offset: 0
14253
+ },
14254
+ fps: {
14255
+ dimension: "rate",
14256
+ factor: 1,
14257
+ offset: 0
14258
+ }
14259
+ };
14260
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14261
+ function specFor(unit) {
14262
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14263
+ }
14264
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
14265
+ function unitDimension(unit) {
14266
+ return specFor(unit)?.dimension;
14267
+ }
14268
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14269
+ function toCanonical(value, spec) {
14270
+ return value * spec.factor + spec.offset;
14271
+ }
14272
+ /** Canonical-unit value → the unit described by `spec`. */
14273
+ function fromCanonical(canonical, spec) {
14274
+ return (canonical - spec.offset) / spec.factor;
14275
+ }
14124
14276
  /**
14125
- * The canonical notification. `body` is the only hard field (Apprise model).
14126
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14127
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14128
- * the adapter maps this ordinal onto its native level. `level?` is an
14129
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
14130
- * `priority` for that one target.
14277
+ * Graceful variant for the render path: returns the converted value, or
14278
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14279
+ * Never throws — a render caller falls back to the raw value + source unit.
14131
14280
  */
14132
- var NotificationSchema = object({
14133
- body: string(),
14134
- title: string().optional(),
14135
- format: NotificationFormatSchema.default("text"),
14136
- priority: number().int().min(1).max(5).default(3),
14137
- level: string().optional(),
14138
- attachments: array(AttachmentSchema).optional(),
14139
- clickUrl: string().optional(),
14140
- actions: array(NotificationActionSchema).optional(),
14141
- sound: string().optional(),
14142
- ttl: number().optional(),
14143
- tag: string().optional(),
14144
- deviceId: number().optional(),
14145
- eventId: string().optional(),
14146
- metadata: record(string(), unknown()).optional(),
14147
- /**
14148
- * Values for the parameters the target's kind DECLARES in
14149
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14150
- * declaration by the sender; a notifier reads only the keys it declared.
14151
- */
14152
- delivery: record(string(), DeliveryParamValueSchema).optional()
14281
+ function tryConvertUnit(value, from, to) {
14282
+ if (from === to) return value;
14283
+ const fromSpec = specFor(from);
14284
+ if (!fromSpec) return void 0;
14285
+ const toSpec = specFor(to);
14286
+ if (!toSpec) return void 0;
14287
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14288
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14289
+ }
14290
+ //#endregion
14291
+ //#region ../types/dist/index.mjs
14292
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
14293
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
14294
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
14295
+ /** The text excerpt a message carries on the bus. */
14296
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
14297
+ var PortSchema = number().int().min(1).max(65535);
14298
+ /** A recipient local-part: what stands before the `@`. */
14299
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
14300
+ object({
14301
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
14302
+ bindHost: string().min(1).default("0.0.0.0"),
14303
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
14304
+ requireAuth: boolean().default(true),
14305
+ username: string().default(""),
14306
+ password: string().default("").meta({ secret: true }),
14307
+ starttls: boolean().default(false),
14308
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
14153
14309
  });
14154
- /** One declared native severity/priority level for a kind. */
14155
- var TargetKindLevelSchema = object({
14156
- id: string(),
14157
- label: string(),
14158
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14159
- ordinal: number().int().min(1).max(5).nullable(),
14160
- flags: object({
14161
- critical: boolean().optional(),
14162
- silent: boolean().optional(),
14163
- noPush: boolean().optional()
14164
- }).optional(),
14165
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14166
- requires: array(string()).optional(),
14167
- description: string().optional()
14310
+ object({
14311
+ host: string().min(1).nullable(),
14312
+ hostReason: string().nullable(),
14313
+ port: PortSchema,
14314
+ domain: string().min(1),
14315
+ requireAuth: boolean(),
14316
+ username: string(),
14317
+ password: string(),
14318
+ starttls: boolean()
14168
14319
  });
14169
- /** The full capability block consulted before dispatch. */
14170
- var TargetKindCapsSchema = object({
14171
- attachments: object({
14172
- mediaTypes: array(AttachmentMediaTypeSchema),
14173
- mode: _enum([
14174
- "url",
14175
- "bytes",
14176
- "both"
14177
- ]),
14178
- max: number().int().nonnegative(),
14179
- maxBytes: number().int().positive().optional()
14180
- }),
14181
- /** Max action buttons (0 = none). */
14182
- actions: number().int().nonnegative(),
14320
+ object({
14321
+ recipients: array(SmtpRecipientSchema).min(1),
14183
14322
  /**
14184
- * Whether this kind renders a per-action ICON.
14185
- *
14186
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14187
- * run on the addon cap path — three production failures in one day taught
14188
- * this repo that once. Absent is read as false by the degrade engine, which
14189
- * is the safe direction: an icon that is not rendered costs nothing, an icon
14190
- * assumed and dropped costs the operator's trust in the field.
14323
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
14324
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
14325
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
14326
+ * Absent, the subscriber is anonymous and unique.
14191
14327
  */
14192
- actionIcons: boolean().optional(),
14193
- levels: array(TargetKindLevelSchema),
14194
- format: array(NotificationFormatSchema),
14195
- clickUrl: boolean(),
14196
- sound: boolean(),
14197
- ttl: boolean(),
14198
- bodyMaxLen: number().int().positive()
14328
+ subscriberId: string().min(1).max(64).optional()
14199
14329
  });
14200
- /**
14201
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14202
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14203
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14204
- * the union is large and not meant for runtime validation here; the exported
14330
+ var SmtpInboundAttachmentSchema = object({
14331
+ filename: string(),
14332
+ contentType: string(),
14333
+ bytes: number().int().nonnegative()
14334
+ });
14335
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
14336
+ var SmtpInboundMessageSchema = object({
14337
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
14338
+ seq: number().int().positive(),
14339
+ messageId: string(),
14340
+ receivedAt: number().int().nonnegative(),
14341
+ from: string(),
14342
+ to: string(),
14343
+ subject: string(),
14344
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
14345
+ attachments: array(SmtpInboundAttachmentSchema)
14346
+ });
14347
+ object({
14348
+ /** The newest seq issued so far (0 before any mail). */
14349
+ lastSeq: number().int().nonnegative(),
14350
+ /** The oldest seq still retained; messages below it are gone. */
14351
+ oldestSeq: number().int().nonnegative(),
14352
+ messages: array(object({
14353
+ key: string(),
14354
+ message: SmtpInboundMessageSchema
14355
+ }))
14356
+ });
14357
+ new Set([
14358
+ "track",
14359
+ "face",
14360
+ "identity",
14361
+ "plate",
14362
+ "vehicle",
14363
+ "scene",
14364
+ "motion",
14365
+ "object",
14366
+ "audio",
14367
+ "mosaic"
14368
+ ]);
14369
+ /**
14370
+ * The owner types that are event tables, in the order the event-media resolver
14371
+ * should consider them. Exported so a consumer asking "is this key an event?"
14372
+ * does not re-spell the list and drift from it.
14373
+ */
14374
+ var EVENT_OWNER_TYPES = [
14375
+ "motion",
14376
+ "object",
14377
+ "audio"
14378
+ ];
14379
+ /**
14380
+ * The same list as a Zod enum, for the cap inputs that must NAME the table
14381
+ * (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
14382
+ * `(table, id)`).
14383
+ *
14384
+ * Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
14385
+ * table would otherwise be accepted by the codec and refused at the door, with
14386
+ * nothing failing until a caller asked.
14387
+ */
14388
+ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
14389
+ /** The same list as a Zod enum, for the cap input that carries it. */
14390
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
14391
+ new Set(EVENT_OWNER_TYPES);
14392
+ /**
14393
+ * What a published package needs from the rest of the install (D989).
14394
+ *
14395
+ * One published framework package (`@camstack/server`) carries the kernel, the
14396
+ * builtins and the capability contracts, so "which addon runs on which
14397
+ * framework" is a question of version, and the answer is DECLARED by the
14398
+ * package that knows it:
14399
+ *
14400
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
14401
+ * - the server says the oldest addon it works with
14402
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
14403
+ * around it (`requires.wrapper: { desktop, image }`);
14404
+ * - a wrapper (the desktop app, the container starter) says which server
14405
+ * range it can host, and reports its own kind and version.
14406
+ *
14407
+ * The publish workflow STAMPS these after the version bump
14408
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
14409
+ *
14410
+ * A requirement that cannot be checked is NOT met: a shell that reports no
14411
+ * version counts as below the minimum, and an unreadable running version is
14412
+ * named in the result (a failed read is never a pass).
14413
+ */
14414
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
14415
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
14416
+ object({
14417
+ /** The oldest `@camstack/server` this package runs on. */
14418
+ server: MinVersionSchema.optional(),
14419
+ /** The oldest installed version of each named addon package. */
14420
+ addons: record(string(), MinVersionSchema).optional(),
14421
+ /** The oldest shell around the server, per shell kind. */
14422
+ wrapper: object({
14423
+ desktop: MinVersionSchema.optional(),
14424
+ image: MinVersionSchema.optional()
14425
+ }).optional()
14426
+ });
14427
+ /** One unmet requirement; each names the versions on both sides. */
14428
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
14429
+ object({
14430
+ kind: literal("server-too-old"),
14431
+ required: string(),
14432
+ /** `null` — the running server version could not be read. */
14433
+ running: string().nullable()
14434
+ }),
14435
+ object({
14436
+ kind: literal("addon-too-old"),
14437
+ packageName: string(),
14438
+ required: string(),
14439
+ /** `null` — the addon is not installed (or its version is unreadable). */
14440
+ running: string().nullable()
14441
+ }),
14442
+ object({
14443
+ kind: literal("wrapper-too-old"),
14444
+ wrapper: _enum(["desktop", "image"]),
14445
+ required: string(),
14446
+ /** `null` — the wrapper reported no version. */
14447
+ running: string().nullable()
14448
+ })
14449
+ ]);
14450
+ var RequiredAddonUpdateSchema = object({
14451
+ packageName: string(),
14452
+ /** The minimum the target server declares. */
14453
+ required: string(),
14454
+ /** What this node has; `null` when the addon is not installed. */
14455
+ installed: string().nullable()
14456
+ });
14457
+ /**
14458
+ * Which integration instance a settings form is for. An addon that serves several instances
14459
+ * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
14460
+ * needs its connection, the default integration needs the addon's own sections.
14461
+ *
14462
+ * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
14463
+ * setting up a NEW instance.
14464
+ * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
14465
+ * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
14466
+ */
14467
+ var IntegrationInstanceRefSchema = object({
14468
+ integrationId: string().nullable(),
14469
+ mode: _enum([
14470
+ "standalone",
14471
+ "account",
14472
+ "broker"
14473
+ ]),
14474
+ candidateStableId: string().optional()
14475
+ });
14476
+ /**
14477
+ * notification-output — canonical, capability-gated notification delivery.
14478
+ *
14479
+ * Apprise-derived model (see
14480
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
14481
+ * callers emit ONE canonical `Notification`; each provider declares a
14482
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
14483
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
14484
+ * message to what the kind supports — callers never special-case a service.
14485
+ *
14486
+ * DESIGN DECISIONS (locked):
14487
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14488
+ * providers serve their targets as `integration-instances` records, one
14489
+ * record kind per target kind, and the admin lists, adds, edits, switches
14490
+ * and deletes them through that cap. This cap keeps what an instance cannot
14491
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14492
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14493
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14494
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14495
+ * cap-mount auto-`concatCollection`-fans them across every registered
14496
+ * provider so one catalog is routable. `send` / `testTarget` /
14497
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14498
+ * collection router extracts from the call input.
14499
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14500
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14501
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
14502
+ * base64 fallback needed.
14503
+ *
14504
+ * TODO (deferred, closed-set change — separate decision): add
14505
+ * `providerKind: 'notify'` so notification providers surface on the unified
14506
+ * admin "Integrations" page.
14507
+ */
14508
+ /**
14509
+ * Zentik-derived typed-media enum — the superset across every kind. Each
14510
+ * adapter picks what it supports and the degrade engine filters the rest.
14511
+ */
14512
+ var AttachmentMediaTypeSchema = _enum([
14513
+ "image",
14514
+ "video",
14515
+ "gif",
14516
+ "audio",
14517
+ "icon"
14518
+ ]);
14519
+ /**
14520
+ * A single attachment. Exactly one of `url` (remote source, most adapters
14521
+ * prefer this) or `bytes` (inline source; required for Pushover-style
14522
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
14523
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
14524
+ */
14525
+ var AttachmentSchema = object({
14526
+ mediaType: AttachmentMediaTypeSchema,
14527
+ url: string().optional(),
14528
+ bytes: _instanceof(Uint8Array).optional(),
14529
+ mime: string().optional(),
14530
+ name: string().optional()
14531
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
14532
+ var NotificationFormatSchema = _enum([
14533
+ "text",
14534
+ "markdown",
14535
+ "html"
14536
+ ]);
14537
+ /**
14538
+ * The CLOSED icon vocabulary an action button may use.
14539
+ *
14540
+ * A closed set, not a free string, and that is the whole point: an arbitrary
14541
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
14542
+ * notices — the same class of gap as a zone vocabulary nothing produced
14543
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14544
+ * declares `actionIcons: false` and the degrade engine strips the field.
14545
+ *
14546
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14547
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
14548
+ * differently.
14549
+ *
14550
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14551
+ *
14552
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
14553
+ * the whole set onto its own renderer's vocabulary through a
14554
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14555
+ * fallback, so adding a member here fails every adapter's build until someone
14556
+ * decides its glyph, which is the only place that decision can be made
14557
+ * honestly.
14558
+ *
14559
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14560
+ * `disarm` straight through; iOS feeds that string to
14561
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14562
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
14563
+ * mapping, and "the field is documented" is not "the value renders".
14564
+ *
14565
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
14566
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14567
+ * so an addon that emits a token the running hub does not know does not lose an
14568
+ * icon — its whole `send` fails Zod validation and the notification never
14569
+ * arrives. Never emit a new token from an addon before the train carrying it.
14570
+ */
14571
+ var NotificationActionIconSchema = _enum([
14572
+ "acknowledge",
14573
+ "dismiss",
14574
+ "silence",
14575
+ "snooze",
14576
+ "view",
14577
+ "play",
14578
+ "open",
14579
+ "close",
14580
+ "lock",
14581
+ "unlock",
14582
+ "arm",
14583
+ "arm-home",
14584
+ "arm-away",
14585
+ "arm-night",
14586
+ "disarm",
14587
+ "light",
14588
+ "alert",
14589
+ "camera"
14590
+ ]);
14591
+ /** A single tap-through action button. */
14592
+ var NotificationActionSchema = object({
14593
+ id: string(),
14594
+ label: string(),
14595
+ url: string().optional(),
14596
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14597
+ icon: NotificationActionIconSchema.optional(),
14598
+ /**
14599
+ * Renders in a warning style where the notifier supports it.
14600
+ *
14601
+ * A HINT, never a gate. The callback's authority is its token and nothing
14602
+ * else — see `notification-center/action-token.ts` for what that does and
14603
+ * does not buy.
14604
+ */
14605
+ destructive: boolean().optional(),
14606
+ /**
14607
+ * How the tap should REACH the url.
14608
+ *
14609
+ * `navigate` (absent, and every button authored before this field) opens it:
14610
+ * the phone leaves the notification and shows whatever the callback returns.
14611
+ * That is right for a button whose answer the operator wants to read.
14612
+ *
14613
+ * `background` fires it as a POST and stays put. It exists for the buttons
14614
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14615
+ * an answer to the notification, and being thrown into a browser tab to
14616
+ * confirm it costs more attention than the notification did. A backend that
14617
+ * cannot do a background call renders it as an ordinary link (the adapters
14618
+ * fall back rather than dropping the button), so this is a preference, never
14619
+ * a requirement.
14620
+ */
14621
+ mode: _enum(["navigate", "background"]).optional()
14622
+ });
14623
+ /** A delivery parameter value: what a notifier declared it takes. */
14624
+ var DeliveryParamValueSchema = union([
14625
+ string(),
14626
+ number(),
14627
+ boolean()
14628
+ ]);
14629
+ /**
14630
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14631
+ * addresses a target of that kind may set it; a kind that declares nothing is
14632
+ * never handed one. The admin UI derives its control from this descriptor —
14633
+ * no editor knows a service by name.
14634
+ *
14635
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14636
+ * run on the addon cap path).
14637
+ */
14638
+ var DeliveryParamSchema = object({
14639
+ key: string().min(1),
14640
+ label: string(),
14641
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14642
+ labelKey: string().optional(),
14643
+ type: _enum([
14644
+ "enum",
14645
+ "number",
14646
+ "boolean",
14647
+ "string"
14648
+ ]),
14649
+ /** `enum` only: the closed set of accepted values. */
14650
+ options: array(object({
14651
+ value: string(),
14652
+ label: string()
14653
+ })).optional(),
14654
+ /** `number` only. */
14655
+ min: number().optional(),
14656
+ max: number().optional(),
14657
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14658
+ default: DeliveryParamValueSchema.optional()
14659
+ });
14660
+ /**
14661
+ * The canonical notification. `body` is the only hard field (Apprise model).
14662
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14663
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14664
+ * the adapter maps this ordinal onto its native level. `level?` is an
14665
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14666
+ * `priority` for that one target.
14667
+ */
14668
+ var NotificationSchema = object({
14669
+ body: string(),
14670
+ title: string().optional(),
14671
+ format: NotificationFormatSchema.default("text"),
14672
+ priority: number().int().min(1).max(5).default(3),
14673
+ level: string().optional(),
14674
+ attachments: array(AttachmentSchema).optional(),
14675
+ clickUrl: string().optional(),
14676
+ actions: array(NotificationActionSchema).optional(),
14677
+ sound: string().optional(),
14678
+ ttl: number().optional(),
14679
+ tag: string().optional(),
14680
+ deviceId: number().optional(),
14681
+ eventId: string().optional(),
14682
+ metadata: record(string(), unknown()).optional(),
14683
+ /**
14684
+ * Values for the parameters the target's kind DECLARES in
14685
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14686
+ * declaration by the sender; a notifier reads only the keys it declared.
14687
+ */
14688
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14689
+ });
14690
+ /** One declared native severity/priority level for a kind. */
14691
+ var TargetKindLevelSchema = object({
14692
+ id: string(),
14693
+ label: string(),
14694
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14695
+ ordinal: number().int().min(1).max(5).nullable(),
14696
+ flags: object({
14697
+ critical: boolean().optional(),
14698
+ silent: boolean().optional(),
14699
+ noPush: boolean().optional()
14700
+ }).optional(),
14701
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14702
+ requires: array(string()).optional(),
14703
+ description: string().optional()
14704
+ });
14705
+ /** The full capability block consulted before dispatch. */
14706
+ var TargetKindCapsSchema = object({
14707
+ attachments: object({
14708
+ mediaTypes: array(AttachmentMediaTypeSchema),
14709
+ mode: _enum([
14710
+ "url",
14711
+ "bytes",
14712
+ "both"
14713
+ ]),
14714
+ max: number().int().nonnegative(),
14715
+ maxBytes: number().int().positive().optional()
14716
+ }),
14717
+ /** Max action buttons (0 = none). */
14718
+ actions: number().int().nonnegative(),
14719
+ /**
14720
+ * Whether this kind renders a per-action ICON.
14721
+ *
14722
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14723
+ * run on the addon cap path — three production failures in one day taught
14724
+ * this repo that once. Absent is read as false by the degrade engine, which
14725
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14726
+ * assumed and dropped costs the operator's trust in the field.
14727
+ */
14728
+ actionIcons: boolean().optional(),
14729
+ levels: array(TargetKindLevelSchema),
14730
+ format: array(NotificationFormatSchema),
14731
+ clickUrl: boolean(),
14732
+ sound: boolean(),
14733
+ ttl: boolean(),
14734
+ bodyMaxLen: number().int().positive()
14735
+ });
14736
+ /**
14737
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14738
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14739
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14740
+ * the union is large and not meant for runtime validation here; the exported
14205
14741
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14206
14742
  */
14207
- var ConfigSchemaPassthrough$4 = unknown();
14743
+ var ConfigSchemaPassthrough$3 = unknown();
14208
14744
  var TargetKindSchema = object({
14209
14745
  kind: string(),
14210
14746
  label: string(),
@@ -14241,7 +14777,7 @@ var TargetKindSchema = object({
14241
14777
  * its raster path, which is the safe default for an unknown image.
14242
14778
  */
14243
14779
  iconMediaType: string().optional(),
14244
- configSchema: ConfigSchemaPassthrough$4,
14780
+ configSchema: ConfigSchemaPassthrough$3,
14245
14781
  /**
14246
14782
  * The config keys that decide WHERE this kind's secrets are sent (server,
14247
14783
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14263,9 +14799,11 @@ var TargetKindSchema = object({
14263
14799
  caps: TargetKindCapsSchema
14264
14800
  });
14265
14801
  /**
14266
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14267
- * (return a presence marker only) when serving `listTargets` — never
14268
- * round-trip a stored secret to the UI.
14802
+ * A target as a provider's own code sees it (the notification-center bridge, the
14803
+ * send path): the flat record body split into the name, the switch and the
14804
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14805
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14806
+ * secret to the UI.
14269
14807
  */
14270
14808
  var TargetSchema = object({
14271
14809
  id: string(),
@@ -14308,13 +14846,13 @@ var ProbeTargetConfigResultSchema = object({
14308
14846
  stateInstance: string().optional(),
14309
14847
  error: string().optional()
14310
14848
  });
14311
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14849
+ method(object({}), array(TargetKindSchema)), method(object({
14312
14850
  kind: string(),
14313
14851
  config: record(string(), unknown()).optional(),
14314
14852
  /**
14315
14853
  * When editing / duplicating, the blank password field never round-trips
14316
14854
  * the stored token. Pass the source target id so the provider can restore
14317
- * secrets server-side before dialling (same merge as upsert).
14855
+ * secrets server-side before dialling.
14318
14856
  */
14319
14857
  targetId: string().optional()
14320
14858
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14337,27 +14875,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14337
14875
  }), TestResultSchema, {
14338
14876
  kind: "mutation",
14339
14877
  auth: "admin"
14340
- }), method(object({
14341
- target: TargetSchema,
14342
- /**
14343
- * Duplicate flow: create a NEW target id while copying secrets from an
14344
- * existing row. The UI never holds the stored token; blank / redacted
14345
- * password fields are restored from this source (D634 still applies if
14346
- * destinations diverge without a re-typed secret).
14347
- */
14348
- cloneSecretsFrom: string().optional()
14349
- }), TargetSchema, {
14350
- kind: "mutation",
14351
- auth: "admin"
14352
- }), method(object({ targetId: string() }), _void(), {
14353
- kind: "mutation",
14354
- auth: "admin"
14355
- }), method(object({
14356
- targetId: string(),
14357
- enabled: boolean()
14358
- }), _void(), {
14359
- kind: "mutation",
14360
- auth: "admin"
14361
14878
  });
14362
14879
  looseObject({
14363
14880
  name: string().min(1),
@@ -22822,7 +23339,6 @@ var SYSTEM_CAP_VOCABULARY = {
22822
23339
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22823
23340
  "notification-rules": system("notification rules — authored on their own page"),
22824
23341
  "oauth-integration": system(IDENTITY),
22825
- "oidc-connections": system(IDENTITY),
22826
23342
  "osd-manager": system(PIPELINE),
22827
23343
  "pipeline-executor": system(PIPELINE),
22828
23344
  "pipeline-orchestrator": system(PIPELINE),
@@ -42705,726 +43221,344 @@ var LEGACY_ICON_ALIASES = { gauge2: "gauge" };
42705
43221
  * ## What is persisted
42706
43222
  *
42707
43223
  * The canonical kebab-case lucide id ({@link IconKey}). Spellings that a lucide
42708
- * package still accepts (`home` for `house`) and this repo's own legacy ones
42709
- * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
42710
- * stale, but they are never OFFERED by search.
42711
- *
42712
- * ## Search
42713
- *
42714
- * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
42715
- * the caller adds, and lucide's English tags — in that order of weight.
42716
- */
42717
- var KEY_SET$2 = new Set(ICON_KEYS);
42718
- new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
42719
- /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
42720
- function isIconKey(value) {
42721
- return typeof value === "string" && KEY_SET$2.has(value);
42722
- }
42723
- /**
42724
- * Wire/storage schema for one icon key.
42725
- *
42726
- * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
42727
- * into every cap router, type and API-reference row that carries an icon. The
42728
- * check is membership in the catalog and the type is {@link IconKey}.
42729
- */
42730
- var IconKeySchema = custom(isIconKey, "Not an icon of the catalog");
42731
- var ENTRIES = ICON_KEYS.map((key, index) => {
42732
- const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
42733
- const categories = categoryIndexes.flatMap((i) => {
42734
- const id = ICON_CATEGORY_IDS[i];
42735
- return id === void 0 ? [] : [id];
42736
- });
42737
- return {
42738
- key,
42739
- categories,
42740
- primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
42741
- tags,
42742
- synonyms: ICON_SYNONYMS[key] ?? []
42743
- };
42744
- });
42745
- new Map(ENTRIES.map((e) => [e.key, e]));
42746
- /** Categories a picker offers first: the ones this product's icons mostly live in. */
42747
- var LEADING_CATEGORIES = [
42748
- "devices",
42749
- "home",
42750
- "security",
42751
- "buildings",
42752
- "connectivity",
42753
- "multimedia",
42754
- "nature",
42755
- "weather",
42756
- "transportation",
42757
- "animals"
42758
- ];
42759
- var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
42760
- [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
42761
- /**
42762
- * One (location, icon) assignment as it travels.
42763
- *
42764
- * `locationKey` is ALWAYS {@link locationIconKey} of the label — never the
42765
- * operator's casing. The registry matches labels trimmed + case-insensitively
42766
- * everywhere (`addLocation`, `removeLocation`, `renameLocation`), and a
42767
- * case-only rename (`kitchen` → `Kitchen`) is a real edit this repo supports; a
42768
- * map keyed on the displayed casing would orphan its entry on exactly that
42769
- * edit. Consumers join by keying their own label through `locationIconKey`.
42770
- */
42771
- var LocationIconAssignmentSchema = object({
42772
- locationKey: string(),
42773
- icon: IconKeySchema
42774
- });
42775
- /**
42776
- * One (location, description) assignment as it travels.
42777
- *
42778
- * Same key discipline as {@link LocationIconAssignmentSchema}, and for the same
42779
- * reason: this lived for a while in the post-analysis addon's own settings,
42780
- * keyed by a HAND-TYPED label matched exactly and case-sensitively. A rename in
42781
- * the Locations page carried the icon and silently orphaned the description —
42782
- * two authorities for one thing, joined by two different rules. Now both hang
42783
- * off the location itself, on `locationIconKey`.
42784
- */
42785
- var LocationDescriptionAssignmentSchema = object({
42786
- locationKey: string(),
42787
- description: string()
42788
- });
42789
- /**
42790
- * WHO an action step acts on (D983) — one target shape for every place that
42791
- * performs an action (automation steps, Action entries, notification
42792
- * sequences, scheduler points):
42793
- *
42794
- * - `devices` — device ids the operator picked, frozen at save time;
42795
- * - `domain` — every device that offers the step's VOCABULARY action,
42796
- * optionally narrowed to one device type and one room, resolved WHEN THE
42797
- * STEP RUNS (a device added later is included). The action always comes
42798
- * from the vocabulary (`cover.open`), never from a raw cap name, so a
42799
- * `domain` target needs an `actionId` (enforced on the step schema);
42800
- * - `var` — a `$var` resolved from the run's scope.
42801
- *
42802
- * {@link ActionTargetResolver} turns a target into device ids. It is pure
42803
- * (a {@link DeviceDirectory} is handed in) so the hub's core-blocks service and
42804
- * a generated automation block share it.
42805
- *
42806
- * A room is the device's `location` label (`DeviceInfo.location`); two labels
42807
- * name the same room when `locationIconKey` agrees (trimmed, lower-cased).
42808
- */
42809
- var ActionTargetSchema = discriminatedUnion("kind", [
42810
- object({
42811
- kind: literal("devices"),
42812
- ids: array(number().int()).min(1)
42813
- }),
42814
- object({
42815
- kind: literal("domain"),
42816
- /** Narrow to one kind of device. */
42817
- deviceType: _enum(DeviceType).optional(),
42818
- /** Narrow to one room: the `location` label devices carry. */
42819
- roomId: string().min(1).optional()
42820
- }),
42821
- object({
42822
- kind: literal("var"),
42823
- name: string().min(1)
42824
- })
42825
- ]);
42826
- EventCategory.DeviceRegistered, EventCategory.DeviceUnregistered, EventCategory.DeviceUpdated, EventCategory.DeviceMetaChanged, EventCategory.DeviceBindingsChanged;
42827
- var ExpressionBindingSourceSchema = union([
42828
- object({
42829
- kind: literal("field").optional(),
42830
- sourceKey: string(),
42831
- cap: string(),
42832
- fieldPath: string()
42833
- }),
42834
- object({
42835
- kind: literal("literal"),
42836
- value: union([
42837
- string(),
42838
- number(),
42839
- boolean(),
42840
- _null()
42841
- ])
42842
- }),
42843
- object({
42844
- kind: literal("global"),
42845
- sourceStableId: string(),
42846
- cap: string(),
42847
- fieldPath: string()
42848
- })
42849
- ]);
42850
- object({
42851
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
42852
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
42853
- }).superRefine((src, ctx) => {
42854
- const err = validateExpressionSource(src);
42855
- if (err !== null) ctx.addIssue({
42856
- code: "custom",
42857
- message: err,
42858
- path: ["expr"]
42859
- });
42860
- });
42861
- /**
42862
- * Identity envelope for a device's upstream-system metadata.
42863
- *
42864
- * Two jobs:
42865
- *
42866
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
42867
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
42868
- * WS-Discovery endpoint UUID, …). Provider code keeps a
42869
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
42870
- * without round-tripping through `stableId` / `id` mappings.
42871
- *
42872
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
42873
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
42874
- * for vendors where MAC is the natural key). For systems where the
42875
- * live dispatch key can be renamed (HA `entity_id` can be edited),
42876
- * providers detect renames by matching `uniqueId` and patch `id`
42877
- * in-place so dispatch keeps working.
42878
- *
42879
- * **Display unit and precision are NOT stored here.** They are a read-time
42880
- * preference (D944). A polymorphic reading stores its `quantity` here once,
42881
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
42882
- * neither is repeated on each sample. The `device_class` tag is an
42883
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
42884
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
42885
- *
42886
- * The `raw` escape hatch carries provider-specific attributes that don't
42887
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
42888
- * details, …). Providers control what lands here.
42889
- */
42890
- var SourceInfoSchema = object({
42891
- /** Live dispatch key — mutable when the source system allows rename. */
42892
- id: string(),
42893
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
42894
- system: string(),
42895
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
42896
- * Used to detect rename when `id` changes. */
42897
- uniqueId: string().optional(),
42898
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
42899
- quantity: ReadingQuantitySchema.optional(),
42900
- /** Date / time / datetime shape of a string reading, written once at adopt. */
42901
- dateTimeShape: _enum([
42902
- "date",
42903
- "time",
42904
- "datetime"
42905
- ]).optional(),
42906
- /** Provider-specific extras that don't fit the structured slots.
42907
- * Whitelist what goes here per provider — do NOT dump entire HA
42908
- * attribute blobs (would explode the meta JSON column). */
42909
- raw: record(string(), unknown()).optional()
42910
- });
42911
- /**
42912
- * Build the synthetic SourceInfo every existing provider falls back to
42913
- * when no upstream value has been persisted. Keeps non-migrated providers
42914
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
42915
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
42916
- * id. Real upstream identity replaces this once a provider migrates and
42917
- * calls `updateSourceInfo()` with concrete values.
42918
- */
42919
- function synthesizeSourceInfo(input) {
42920
- return {
42921
- id: input.stableId,
42922
- system: input.addonId
42923
- };
42924
- }
42925
- /**
42926
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
42927
- * patch values are ignored (no field clobber); to clear a field
42928
- * explicitly, providers omit it from the patch and rebuild from scratch
42929
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
42930
- * bother with a `null`-means-delete sentinel.
42931
- *
42932
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
42933
- * merges over existing `raw` keys instead of replacing the whole bag.
42934
- */
42935
- function mergeSourceInfo(prev, patch) {
42936
- const next = { ...prev };
42937
- for (const [k, v] of Object.entries(patch)) {
42938
- if (v === void 0) continue;
42939
- if (k === "raw" && typeof v === "object" && v !== null) {
42940
- next.raw = {
42941
- ...prev.raw,
42942
- ...v
42943
- };
42944
- continue;
42945
- }
42946
- next[k] = v;
42947
- }
42948
- return next;
42949
- }
42950
- /**
42951
- * Coerce common unit-of-measurement string variants to a canonical form
42952
- * so the UI's display templates can rely on stable strings. Pure
42953
- * passthrough for unknown units — kept conservative on purpose, the goal
42954
- * is to normalise the long tail of HA `unit_of_measurement` values that
42955
- * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
42956
- *
42957
- * Extend the map as we encounter real-world data. Returning the raw
42958
- * input for unknown values is intentional: providers should never
42959
- * lose information silently here.
42960
- */
42961
- function normalizeUnit(raw) {
42962
- if (raw == null) return void 0;
42963
- const trimmed = raw.trim();
42964
- if (trimmed === "") return void 0;
42965
- const lower = trimmed.toLowerCase();
42966
- if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
42967
- if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
42968
- if (lower === "k" || lower === "kelvin") return "K";
42969
- if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
42970
- if (lower === "lx" || lower === "lux") return "lx";
42971
- if (lower === "hpa" || lower === "hectopascal") return "hPa";
42972
- if (lower === "kpa") return "kPa";
42973
- if (lower === "pa") return "Pa";
42974
- if (lower === "mbar" || lower === "millibar") return "mbar";
42975
- if (lower === "w" || lower === "watt") return "W";
42976
- if (lower === "kw" || lower === "kilowatt") return "kW";
42977
- if (lower === "wh" || lower === "watt-hour") return "Wh";
42978
- if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
42979
- if (lower === "v" || lower === "volt") return "V";
42980
- if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
42981
- if (lower === "ma" || lower === "milliampere") return "mA";
42982
- if (lower === "va") return "VA";
42983
- if (lower === "ppm") return "ppm";
42984
- if (lower === "ppb") return "ppb";
42985
- if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
42986
- if (lower === "km/h" || lower === "kph") return "km/h";
42987
- if (lower === "mph") return "mph";
42988
- if (lower === "m/s") return "m/s";
42989
- if (lower === "m") return "m";
42990
- if (lower === "m²" || lower === "m2") return "m²";
42991
- if (lower === "mm") return "mm";
42992
- if (lower === "cm") return "cm";
42993
- if (lower === "km") return "km";
42994
- if (lower === "in" || lower === "inch" || lower === "inches") return "in";
42995
- if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
42996
- if (lower === "kg") return "kg";
42997
- if (lower === "g") return "g";
42998
- if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
42999
- if (lower === "ml") return "mL";
43000
- if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
43001
- if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
43002
- if (lower === "ms") return "ms";
43003
- if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
43004
- if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
43005
- if (lower === "d" || lower === "day" || lower === "days") return "d";
43006
- if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
43007
- if (lower === "kib") return "KiB";
43008
- if (lower === "mib") return "MiB";
43009
- if (lower === "gib") return "GiB";
43010
- if (lower === "tib") return "TiB";
43011
- if (lower === "kb") return "kB";
43012
- if (lower === "mb") return "MB";
43013
- if (lower === "gb") return "GB";
43014
- if (lower === "dbm") return "dBm";
43015
- if (lower === "dbfs") return "dBFS";
43016
- if (lower === "inhg") return "inHg";
43017
- if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
43018
- if (lower === "in/h" || lower === "in/hr") return "in/h";
43019
- if (lower === "w/m²" || lower === "w/m2") return "W/m²";
43020
- if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
43021
- if (lower === "kbps") return "kbps";
43022
- if (lower === "fps") return "fps";
43023
- return trimmed;
43024
- }
43025
- /**
43026
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
43027
- * blob is a free-form `Record<string, unknown>` shared with hardware-
43028
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
43029
- * lives under a single nested key to keep it composable with those.
43030
- */
43031
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
43032
- /**
43033
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
43034
- * with Zod. Returns `null` when the blob is missing or the nested
43035
- * `sourceInfo` key fails validation — callers fall back to the
43036
- * synthetic default in that case (see `synthesizeSourceInfo`).
43037
- */
43038
- function extractSourceInfoFromMetadata(metadata) {
43039
- if (!metadata) return null;
43040
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
43041
- if (!raw) return null;
43042
- const parsed = SourceInfoSchema.safeParse(raw);
43043
- if (!parsed.success) return null;
43044
- return parsed.data;
43045
- }
43046
- /**
43047
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
43048
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
43049
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
43050
- * decimals). K derives from `°C = K − 273.15`.
43051
- */
43052
- var UNIT_TABLE = {
43053
- "°C": {
43054
- dimension: "temperature",
43055
- factor: 1,
43056
- offset: 0
43057
- },
43058
- "°F": {
43059
- dimension: "temperature",
43060
- factor: 5 / 9,
43061
- offset: -160 / 9
43062
- },
43063
- K: {
43064
- dimension: "temperature",
43065
- factor: 1,
43066
- offset: -273.15
43067
- },
43068
- hPa: {
43069
- dimension: "pressure",
43070
- factor: 1,
43071
- offset: 0
43072
- },
43073
- kPa: {
43074
- dimension: "pressure",
43075
- factor: 10,
43076
- offset: 0
43077
- },
43078
- Pa: {
43079
- dimension: "pressure",
43080
- factor: .01,
43081
- offset: 0
43082
- },
43083
- mbar: {
43084
- dimension: "pressure",
43085
- factor: 1,
43086
- offset: 0
43087
- },
43088
- bar: {
43089
- dimension: "pressure",
43090
- factor: 1e3,
43091
- offset: 0
43092
- },
43093
- inHg: {
43094
- dimension: "pressure",
43095
- factor: 33.8639,
43096
- offset: 0
43097
- },
43098
- mmHg: {
43099
- dimension: "pressure",
43100
- factor: 1.33322,
43101
- offset: 0
43102
- },
43103
- psi: {
43104
- dimension: "pressure",
43105
- factor: 68.9476,
43106
- offset: 0
43107
- },
43108
- "m/s": {
43109
- dimension: "speed",
43110
- factor: 1,
43111
- offset: 0
43112
- },
43113
- "km/h": {
43114
- dimension: "speed",
43115
- factor: 1 / 3.6,
43116
- offset: 0
43117
- },
43118
- mph: {
43119
- dimension: "speed",
43120
- factor: .44704,
43121
- offset: 0
43122
- },
43123
- kn: {
43124
- dimension: "speed",
43125
- factor: .514444,
43126
- offset: 0
43127
- },
43128
- "mm/h": {
43129
- dimension: "precipitation-rate",
43130
- factor: 1,
43131
- offset: 0
43132
- },
43133
- "in/h": {
43134
- dimension: "precipitation-rate",
43135
- factor: 25.4,
43136
- offset: 0
43137
- },
43138
- m: {
43139
- dimension: "length",
43140
- factor: 1,
43141
- offset: 0
43142
- },
43143
- mm: {
43144
- dimension: "length",
43145
- factor: .001,
43146
- offset: 0
43147
- },
43148
- cm: {
43149
- dimension: "length",
43150
- factor: .01,
43151
- offset: 0
43152
- },
43153
- km: {
43154
- dimension: "length",
43155
- factor: 1e3,
43156
- offset: 0
43157
- },
43158
- in: {
43159
- dimension: "length",
43160
- factor: .0254,
43161
- offset: 0
43162
- },
43163
- ft: {
43164
- dimension: "length",
43165
- factor: .3048,
43166
- offset: 0
43167
- },
43168
- mi: {
43169
- dimension: "length",
43170
- factor: 1609.344,
43171
- offset: 0
43172
- },
43173
- "m²": {
43174
- dimension: "area",
43175
- factor: 1,
43176
- offset: 0
43177
- },
43178
- lx: {
43179
- dimension: "illuminance",
43180
- factor: 1,
43181
- offset: 0
43182
- },
43183
- "W/m²": {
43184
- dimension: "irradiance",
43185
- factor: 1,
43186
- offset: 0
43187
- },
43188
- W: {
43189
- dimension: "power",
43190
- factor: 1,
43191
- offset: 0
43192
- },
43193
- kW: {
43194
- dimension: "power",
43195
- factor: 1e3,
43196
- offset: 0
43197
- },
43198
- VA: {
43199
- dimension: "apparent-power",
43200
- factor: 1,
43201
- offset: 0
43202
- },
43203
- Wh: {
43204
- dimension: "energy",
43205
- factor: 1,
43206
- offset: 0
43207
- },
43208
- kWh: {
43209
- dimension: "energy",
43210
- factor: 1e3,
43211
- offset: 0
43212
- },
43213
- MWh: {
43214
- dimension: "energy",
43215
- factor: 1e6,
43216
- offset: 0
43217
- },
43218
- V: {
43219
- dimension: "voltage",
43220
- factor: 1,
43221
- offset: 0
43222
- },
43223
- mV: {
43224
- dimension: "voltage",
43225
- factor: .001,
43226
- offset: 0
43227
- },
43228
- A: {
43229
- dimension: "current",
43230
- factor: 1,
43231
- offset: 0
43232
- },
43233
- mA: {
43234
- dimension: "current",
43235
- factor: .001,
43236
- offset: 0
43237
- },
43238
- ppm: {
43239
- dimension: "concentration-volume",
43240
- factor: 1,
43241
- offset: 0
43242
- },
43243
- ppb: {
43244
- dimension: "concentration-volume",
43245
- factor: .001,
43246
- offset: 0
43247
- },
43248
- "µg/m³": {
43249
- dimension: "concentration-mass",
43250
- factor: 1,
43251
- offset: 0
43252
- },
43253
- "mg/m³": {
43254
- dimension: "concentration-mass",
43255
- factor: 1e3,
43256
- offset: 0
43257
- },
43258
- kg: {
43259
- dimension: "mass",
43260
- factor: 1,
43261
- offset: 0
43262
- },
43263
- g: {
43264
- dimension: "mass",
43265
- factor: .001,
43266
- offset: 0
43267
- },
43268
- lb: {
43269
- dimension: "mass",
43270
- factor: .453592,
43271
- offset: 0
43272
- },
43273
- oz: {
43274
- dimension: "mass",
43275
- factor: .0283495,
43276
- offset: 0
43277
- },
43278
- "%": {
43279
- dimension: "percentage",
43280
- factor: 1,
43281
- offset: 0
43282
- },
43283
- L: {
43284
- dimension: "volume",
43285
- factor: 1,
43286
- offset: 0
43287
- },
43288
- mL: {
43289
- dimension: "volume",
43290
- factor: .001,
43291
- offset: 0
43292
- },
43293
- gal: {
43294
- dimension: "volume",
43295
- factor: 3.785411784,
43296
- offset: 0
43297
- },
43298
- s: {
43299
- dimension: "duration",
43300
- factor: 1,
43301
- offset: 0
43302
- },
43303
- ms: {
43304
- dimension: "duration",
43305
- factor: .001,
43306
- offset: 0
43307
- },
43308
- min: {
43309
- dimension: "duration",
43310
- factor: 60,
43311
- offset: 0
43312
- },
43313
- h: {
43314
- dimension: "duration",
43315
- factor: 3600,
43316
- offset: 0
43317
- },
43318
- d: {
43319
- dimension: "duration",
43320
- factor: 86400,
43321
- offset: 0
43322
- },
43323
- B: {
43324
- dimension: "information",
43325
- factor: 1,
43326
- offset: 0
43327
- },
43328
- KiB: {
43329
- dimension: "information",
43330
- factor: 1024,
43331
- offset: 0
43332
- },
43333
- MiB: {
43334
- dimension: "information",
43335
- factor: 1024 ** 2,
43336
- offset: 0
43337
- },
43338
- GiB: {
43339
- dimension: "information",
43340
- factor: 1024 ** 3,
43341
- offset: 0
43342
- },
43343
- TiB: {
43344
- dimension: "information",
43345
- factor: 1024 ** 4,
43346
- offset: 0
43347
- },
43348
- kB: {
43349
- dimension: "information",
43350
- factor: 1e3,
43351
- offset: 0
43352
- },
43353
- MB: {
43354
- dimension: "information",
43355
- factor: 1e6,
43356
- offset: 0
43357
- },
43358
- GB: {
43359
- dimension: "information",
43360
- factor: 1e9,
43361
- offset: 0
43362
- },
43363
- "°": {
43364
- dimension: "angle",
43365
- factor: 1,
43366
- offset: 0
43367
- },
43368
- dBFS: {
43369
- dimension: "sound-level",
43370
- factor: 1,
43371
- offset: 0
43372
- },
43373
- dBm: {
43374
- dimension: "signal-level",
43375
- factor: 1,
43376
- offset: 0
43377
- },
43378
- mired: {
43379
- dimension: "colour-temperature",
43380
- factor: 1,
43381
- offset: 0
43382
- },
43383
- kbps: {
43384
- dimension: "bitrate",
43385
- factor: 1,
43386
- offset: 0
43387
- },
43388
- Mbps: {
43389
- dimension: "bitrate",
43390
- factor: 1e3,
43391
- offset: 0
43392
- },
43393
- fps: {
43394
- dimension: "rate",
43395
- factor: 1,
43396
- offset: 0
43397
- }
43398
- };
43399
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43400
- function specFor(unit) {
43401
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43224
+ * package still accepts (`home` for `house`) and this repo's own legacy ones
43225
+ * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
43226
+ * stale, but they are never OFFERED by search.
43227
+ *
43228
+ * ## Search
43229
+ *
43230
+ * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
43231
+ * the caller adds, and lucide's English tags — in that order of weight.
43232
+ */
43233
+ var KEY_SET$2 = new Set(ICON_KEYS);
43234
+ new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
43235
+ /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
43236
+ function isIconKey(value) {
43237
+ return typeof value === "string" && KEY_SET$2.has(value);
43402
43238
  }
43403
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
43404
- function unitDimension(unit) {
43405
- return specFor(unit)?.dimension;
43239
+ /**
43240
+ * Wire/storage schema for one icon key.
43241
+ *
43242
+ * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
43243
+ * into every cap router, type and API-reference row that carries an icon. The
43244
+ * check is membership in the catalog and the type is {@link IconKey}.
43245
+ */
43246
+ var IconKeySchema = custom(isIconKey, "Not an icon of the catalog");
43247
+ var ENTRIES = ICON_KEYS.map((key, index) => {
43248
+ const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
43249
+ const categories = categoryIndexes.flatMap((i) => {
43250
+ const id = ICON_CATEGORY_IDS[i];
43251
+ return id === void 0 ? [] : [id];
43252
+ });
43253
+ return {
43254
+ key,
43255
+ categories,
43256
+ primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
43257
+ tags,
43258
+ synonyms: ICON_SYNONYMS[key] ?? []
43259
+ };
43260
+ });
43261
+ new Map(ENTRIES.map((e) => [e.key, e]));
43262
+ /** Categories a picker offers first: the ones this product's icons mostly live in. */
43263
+ var LEADING_CATEGORIES = [
43264
+ "devices",
43265
+ "home",
43266
+ "security",
43267
+ "buildings",
43268
+ "connectivity",
43269
+ "multimedia",
43270
+ "nature",
43271
+ "weather",
43272
+ "transportation",
43273
+ "animals"
43274
+ ];
43275
+ var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
43276
+ [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
43277
+ /**
43278
+ * One (location, icon) assignment as it travels.
43279
+ *
43280
+ * `locationKey` is ALWAYS {@link locationIconKey} of the label — never the
43281
+ * operator's casing. The registry matches labels trimmed + case-insensitively
43282
+ * everywhere (`addLocation`, `removeLocation`, `renameLocation`), and a
43283
+ * case-only rename (`kitchen` → `Kitchen`) is a real edit this repo supports; a
43284
+ * map keyed on the displayed casing would orphan its entry on exactly that
43285
+ * edit. Consumers join by keying their own label through `locationIconKey`.
43286
+ */
43287
+ var LocationIconAssignmentSchema = object({
43288
+ locationKey: string(),
43289
+ icon: IconKeySchema
43290
+ });
43291
+ /**
43292
+ * One (location, description) assignment as it travels.
43293
+ *
43294
+ * Same key discipline as {@link LocationIconAssignmentSchema}, and for the same
43295
+ * reason: this lived for a while in the post-analysis addon's own settings,
43296
+ * keyed by a HAND-TYPED label matched exactly and case-sensitively. A rename in
43297
+ * the Locations page carried the icon and silently orphaned the description —
43298
+ * two authorities for one thing, joined by two different rules. Now both hang
43299
+ * off the location itself, on `locationIconKey`.
43300
+ */
43301
+ var LocationDescriptionAssignmentSchema = object({
43302
+ locationKey: string(),
43303
+ description: string()
43304
+ });
43305
+ /**
43306
+ * WHO an action step acts on (D983) — one target shape for every place that
43307
+ * performs an action (automation steps, Action entries, notification
43308
+ * sequences, scheduler points):
43309
+ *
43310
+ * - `devices` — device ids the operator picked, frozen at save time;
43311
+ * - `domain` — every device that offers the step's VOCABULARY action,
43312
+ * optionally narrowed to one device type and one room, resolved WHEN THE
43313
+ * STEP RUNS (a device added later is included). The action always comes
43314
+ * from the vocabulary (`cover.open`), never from a raw cap name, so a
43315
+ * `domain` target needs an `actionId` (enforced on the step schema);
43316
+ * - `var` — a `$var` resolved from the run's scope.
43317
+ *
43318
+ * {@link ActionTargetResolver} turns a target into device ids. It is pure
43319
+ * (a {@link DeviceDirectory} is handed in) so the hub's core-blocks service and
43320
+ * a generated automation block share it.
43321
+ *
43322
+ * A room is the device's `location` label (`DeviceInfo.location`); two labels
43323
+ * name the same room when `locationIconKey` agrees (trimmed, lower-cased).
43324
+ */
43325
+ var ActionTargetSchema = discriminatedUnion("kind", [
43326
+ object({
43327
+ kind: literal("devices"),
43328
+ ids: array(number().int()).min(1)
43329
+ }),
43330
+ object({
43331
+ kind: literal("domain"),
43332
+ /** Narrow to one kind of device. */
43333
+ deviceType: _enum(DeviceType).optional(),
43334
+ /** Narrow to one room: the `location` label devices carry. */
43335
+ roomId: string().min(1).optional()
43336
+ }),
43337
+ object({
43338
+ kind: literal("var"),
43339
+ name: string().min(1)
43340
+ })
43341
+ ]);
43342
+ EventCategory.DeviceRegistered, EventCategory.DeviceUnregistered, EventCategory.DeviceUpdated, EventCategory.DeviceMetaChanged, EventCategory.DeviceBindingsChanged;
43343
+ var ExpressionBindingSourceSchema = union([
43344
+ object({
43345
+ kind: literal("field").optional(),
43346
+ sourceKey: string(),
43347
+ cap: string(),
43348
+ fieldPath: string()
43349
+ }),
43350
+ object({
43351
+ kind: literal("literal"),
43352
+ value: union([
43353
+ string(),
43354
+ number(),
43355
+ boolean(),
43356
+ _null()
43357
+ ])
43358
+ }),
43359
+ object({
43360
+ kind: literal("global"),
43361
+ sourceStableId: string(),
43362
+ addonId: string().optional(),
43363
+ cap: string(),
43364
+ fieldPath: string()
43365
+ })
43366
+ ]);
43367
+ object({
43368
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
43369
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
43370
+ }).superRefine((src, ctx) => {
43371
+ const err = validateExpressionSource(src);
43372
+ if (err !== null) ctx.addIssue({
43373
+ code: "custom",
43374
+ message: err,
43375
+ path: ["expr"]
43376
+ });
43377
+ });
43378
+ /**
43379
+ * Identity envelope for a device's upstream-system metadata.
43380
+ *
43381
+ * Two jobs:
43382
+ *
43383
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
43384
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
43385
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
43386
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
43387
+ * without round-tripping through `stableId` / `id` mappings.
43388
+ *
43389
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
43390
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
43391
+ * for vendors where MAC is the natural key). For systems where the
43392
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
43393
+ * providers detect renames by matching `uniqueId` and patch `id`
43394
+ * in-place so dispatch keeps working.
43395
+ *
43396
+ * **Display unit and precision are NOT stored here.** They are a read-time
43397
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
43398
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
43399
+ * neither is repeated on each sample. The `device_class` tag is an
43400
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
43401
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
43402
+ *
43403
+ * The `raw` escape hatch carries provider-specific attributes that don't
43404
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
43405
+ * details, …). Providers control what lands here.
43406
+ */
43407
+ var SourceInfoSchema = object({
43408
+ /** Live dispatch key — mutable when the source system allows rename. */
43409
+ id: string(),
43410
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
43411
+ system: string(),
43412
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
43413
+ * Used to detect rename when `id` changes. */
43414
+ uniqueId: string().optional(),
43415
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
43416
+ quantity: ReadingQuantitySchema.optional(),
43417
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
43418
+ dateTimeShape: _enum([
43419
+ "date",
43420
+ "time",
43421
+ "datetime"
43422
+ ]).optional(),
43423
+ /** Provider-specific extras that don't fit the structured slots.
43424
+ * Whitelist what goes here per provider — do NOT dump entire HA
43425
+ * attribute blobs (would explode the meta JSON column). */
43426
+ raw: record(string(), unknown()).optional()
43427
+ });
43428
+ /**
43429
+ * Build the synthetic SourceInfo every existing provider falls back to
43430
+ * when no upstream value has been persisted. Keeps non-migrated providers
43431
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
43432
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
43433
+ * id. Real upstream identity replaces this once a provider migrates and
43434
+ * calls `updateSourceInfo()` with concrete values.
43435
+ */
43436
+ function synthesizeSourceInfo(input) {
43437
+ return {
43438
+ id: input.stableId,
43439
+ system: input.addonId
43440
+ };
43406
43441
  }
43407
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43408
- function toCanonical(value, spec) {
43409
- return value * spec.factor + spec.offset;
43442
+ /**
43443
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
43444
+ * patch values are ignored (no field clobber); to clear a field
43445
+ * explicitly, providers omit it from the patch and rebuild from scratch
43446
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
43447
+ * bother with a `null`-means-delete sentinel.
43448
+ *
43449
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
43450
+ * merges over existing `raw` keys instead of replacing the whole bag.
43451
+ */
43452
+ function mergeSourceInfo(prev, patch) {
43453
+ const next = { ...prev };
43454
+ for (const [k, v] of Object.entries(patch)) {
43455
+ if (v === void 0) continue;
43456
+ if (k === "raw" && typeof v === "object" && v !== null) {
43457
+ next.raw = {
43458
+ ...prev.raw,
43459
+ ...v
43460
+ };
43461
+ continue;
43462
+ }
43463
+ next[k] = v;
43464
+ }
43465
+ return next;
43410
43466
  }
43411
- /** Canonical-unit value → the unit described by `spec`. */
43412
- function fromCanonical(canonical, spec) {
43413
- return (canonical - spec.offset) / spec.factor;
43467
+ /**
43468
+ * Coerce common unit-of-measurement string variants to a canonical form
43469
+ * so the UI's display templates can rely on stable strings. Pure
43470
+ * passthrough for unknown units — kept conservative on purpose, the goal
43471
+ * is to normalise the long tail of HA `unit_of_measurement` values that
43472
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
43473
+ *
43474
+ * Extend the map as we encounter real-world data. Returning the raw
43475
+ * input for unknown values is intentional: providers should never
43476
+ * lose information silently here.
43477
+ */
43478
+ function normalizeUnit(raw) {
43479
+ if (raw == null) return void 0;
43480
+ const trimmed = raw.trim();
43481
+ if (trimmed === "") return void 0;
43482
+ const lower = trimmed.toLowerCase();
43483
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
43484
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
43485
+ if (lower === "k" || lower === "kelvin") return "K";
43486
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
43487
+ if (lower === "lx" || lower === "lux") return "lx";
43488
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
43489
+ if (lower === "kpa") return "kPa";
43490
+ if (lower === "pa") return "Pa";
43491
+ if (lower === "mbar" || lower === "millibar") return "mbar";
43492
+ if (lower === "w" || lower === "watt") return "W";
43493
+ if (lower === "kw" || lower === "kilowatt") return "kW";
43494
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
43495
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
43496
+ if (lower === "v" || lower === "volt") return "V";
43497
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
43498
+ if (lower === "ma" || lower === "milliampere") return "mA";
43499
+ if (lower === "va") return "VA";
43500
+ if (lower === "ppm") return "ppm";
43501
+ if (lower === "ppb") return "ppb";
43502
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
43503
+ if (lower === "km/h" || lower === "kph") return "km/h";
43504
+ if (lower === "mph") return "mph";
43505
+ if (lower === "m/s") return "m/s";
43506
+ if (lower === "m") return "m";
43507
+ if (lower === "m²" || lower === "m2") return "m²";
43508
+ if (lower === "mm") return "mm";
43509
+ if (lower === "cm") return "cm";
43510
+ if (lower === "km") return "km";
43511
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
43512
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
43513
+ if (lower === "kg") return "kg";
43514
+ if (lower === "g") return "g";
43515
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
43516
+ if (lower === "ml") return "mL";
43517
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
43518
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
43519
+ if (lower === "ms") return "ms";
43520
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
43521
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
43522
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
43523
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
43524
+ if (lower === "kib") return "KiB";
43525
+ if (lower === "mib") return "MiB";
43526
+ if (lower === "gib") return "GiB";
43527
+ if (lower === "tib") return "TiB";
43528
+ if (lower === "kb") return "kB";
43529
+ if (lower === "mb") return "MB";
43530
+ if (lower === "gb") return "GB";
43531
+ if (lower === "dbm") return "dBm";
43532
+ if (lower === "dbfs") return "dBFS";
43533
+ if (lower === "inhg") return "inHg";
43534
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
43535
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
43536
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
43537
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
43538
+ if (lower === "kbps") return "kbps";
43539
+ if (lower === "fps") return "fps";
43540
+ return trimmed;
43414
43541
  }
43415
43542
  /**
43416
- * Graceful variant for the render path: returns the converted value, or
43417
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43418
- * Never throws — a render caller falls back to the raw value + source unit.
43543
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
43544
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
43545
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
43546
+ * lives under a single nested key to keep it composable with those.
43419
43547
  */
43420
- function tryConvertUnit(value, from, to) {
43421
- if (from === to) return value;
43422
- const fromSpec = specFor(from);
43423
- if (!fromSpec) return void 0;
43424
- const toSpec = specFor(to);
43425
- if (!toSpec) return void 0;
43426
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43427
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43548
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
43549
+ /**
43550
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
43551
+ * with Zod. Returns `null` when the blob is missing or the nested
43552
+ * `sourceInfo` key fails validation — callers fall back to the
43553
+ * synthetic default in that case (see `synthesizeSourceInfo`).
43554
+ */
43555
+ function extractSourceInfoFromMetadata(metadata) {
43556
+ if (!metadata) return null;
43557
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
43558
+ if (!raw) return null;
43559
+ const parsed = SourceInfoSchema.safeParse(raw);
43560
+ if (!parsed.success) return null;
43561
+ return parsed.data;
43428
43562
  }
43429
43563
  /**
43430
43564
  * A number a person types for a physical quantity — the input side of the
@@ -45103,15 +45237,28 @@ var SettingsSchemaWithValuesSchema = object({
45103
45237
  });
45104
45238
  /** Patch object — keys are field names, values are the new field values. */
45105
45239
  var SettingsPatchSchema = record(string(), unknown());
45240
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45241
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45242
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
45243
+ var ScopedSettingsDeclarationSchema = object({
45244
+ cap: string(),
45245
+ scope: ScopedSettingsScopeSchema,
45246
+ sectionIds: array(string())
45247
+ });
45106
45248
  /** Standard success response for update operations. */
45107
45249
  var SettingsUpdateResultSchema = object({ success: literal(true) });
45108
45250
  method(object({
45109
45251
  addonId: string(),
45110
- nodeId: string().optional(),
45111
- overlay: record(string(), unknown()).optional(),
45112
- cap: string().optional()
45252
+ cap: string(),
45253
+ scope: ScopedSettingsScopeSchema,
45254
+ nodeId: string().optional()
45113
45255
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45114
45256
  addonId: string(),
45257
+ nodeId: string().optional()
45258
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
45259
+ addonId: string(),
45260
+ cap: string(),
45261
+ scope: ScopedSettingsScopeSchema,
45115
45262
  nodeId: string().optional(),
45116
45263
  patch: SettingsPatchSchema
45117
45264
  }), SettingsUpdateResultSchema, {
@@ -45119,8 +45266,10 @@ method(object({
45119
45266
  auth: "admin"
45120
45267
  }), method(object({
45121
45268
  addonId: string(),
45269
+ cap: string(),
45270
+ scope: ScopedSettingsScopeSchema,
45122
45271
  nodeId: string().optional()
45123
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45272
+ }), record(string(), unknown()).nullable()), method(object({
45124
45273
  addonId: string(),
45125
45274
  nodeId: string().optional(),
45126
45275
  instance: IntegrationInstanceRefSchema
@@ -46683,7 +46832,7 @@ var BackupDestinationInfoSchema = object({
46683
46832
  id: string(),
46684
46833
  /**
46685
46834
  * Manifest id of the owning addon. The Settings button on the
46686
- * destination card opens that addon's `globalSettingsSchema` panel.
46835
+ * destination card opens that addon's scoped settings panel.
46687
46836
  */
46688
46837
  addonId: string(),
46689
46838
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50097,7 +50246,7 @@ var mediaPlayerCapability = {
50097
50246
  isMuted: { kind: "boolean" }
50098
50247
  } }
50099
50248
  };
50100
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
50249
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.MediaPlayer;
50101
50250
  var NUMBER = {
50102
50251
  kind: "fixed",
50103
50252
  result: ["number"]
@@ -52136,6 +52285,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
52136
52285
  kind: "mutation",
52137
52286
  auth: "admin"
52138
52287
  });
52288
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
52139
52289
  /**
52140
52290
  * A SITE ZONE — a named part of the property that several locations belong to.
52141
52291
  *
@@ -52622,6 +52772,12 @@ var DeviceInfoSchema = object({
52622
52772
  addonId: string(),
52623
52773
  type: _enum(DeviceType),
52624
52774
  name: string(),
52775
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
52776
+ * entity_id style. Generated once from the name at first registration, unique
52777
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
52778
+ * Optional: a record produced by an older build omits it until the boot
52779
+ * backfill has run. */
52780
+ alias: DeviceAliasSchema.optional(),
52625
52781
  /** Operator-organisational location label. `null` when unset. */
52626
52782
  location: string().nullable(),
52627
52783
  /** Soft-disabled flag. */
@@ -52941,6 +53097,25 @@ method(object({
52941
53097
  }), _void(), {
52942
53098
  kind: "mutation",
52943
53099
  auth: "admin"
53100
+ }), method(object({
53101
+ deviceId: number(),
53102
+ alias: string()
53103
+ }), discriminatedUnion("ok", [object({
53104
+ ok: literal(true),
53105
+ alias: DeviceAliasSchema
53106
+ }), object({
53107
+ ok: literal(false),
53108
+ reason: _enum([
53109
+ "empty",
53110
+ "too-long",
53111
+ "grammar",
53112
+ "reserved",
53113
+ "taken"
53114
+ ]),
53115
+ takenBy: number().optional()
53116
+ })]), {
53117
+ kind: "mutation",
53118
+ auth: "admin"
52944
53119
  }), method(object({
52945
53120
  deviceId: number(),
52946
53121
  location: string().nullable()
@@ -56650,7 +56825,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56650
56825
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
56651
56826
  * clears it.
56652
56827
  */
56653
- var ConfigSchemaPassthrough$3 = custom();
56828
+ var ConfigSchemaPassthrough$2 = custom();
56654
56829
  var IntegrationInstanceIdSchema = string().min(1);
56655
56830
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56656
56831
  /**
@@ -56696,7 +56871,16 @@ var IntegrationInstanceKindSchema = object({
56696
56871
  /** Relative hub path to start an interactive account link of an instance of this kind. */
56697
56872
  connectPath: string().min(1).optional(),
56698
56873
  /** Relative hub path to disconnect that account. */
56699
- disconnectPath: string().min(1).optional()
56874
+ disconnectPath: string().min(1).optional(),
56875
+ /**
56876
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
56877
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
56878
+ * `connection` widgets write connection values, `model` widgets own the model choice.
56879
+ */
56880
+ widgets: array(object({
56881
+ id: string().min(1),
56882
+ placement: _enum(["connection", "model"])
56883
+ })).optional()
56700
56884
  });
56701
56885
  /**
56702
56886
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -56737,10 +56921,11 @@ var IntegrationInstanceDescriptionSchema = object({
56737
56921
  /** True when `testInstance` really probes the remote. */
56738
56922
  canTest: boolean(),
56739
56923
  /**
56740
- * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56741
- * Add flow asks which first and sends it as `createInstance.kind`.
56924
+ * The kinds an instance can be of (D1013 rule 1), each naming its `functionCap`. One entry:
56925
+ * nothing to choose; more than one: the Add flow asks which first and sends it as
56926
+ * `createInstance.kind`. Never empty: every instance belongs to exactly one kind.
56742
56927
  */
56743
- kinds: array(IntegrationInstanceKindSchema).optional(),
56928
+ kinds: array(IntegrationInstanceKindSchema).min(1),
56744
56929
  /**
56745
56930
  * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56746
56931
  * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
@@ -56776,8 +56961,8 @@ var IntegrationInstanceSchema = object({
56776
56961
  * the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
56777
56962
  */
56778
56963
  enabled: boolean().optional(),
56779
- /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56780
- kind: string().optional(),
56964
+ /** The kind of this instance: exactly one of `describe().kinds[].id` (D1013 rule 2). */
56965
+ kind: string().min(1),
56781
56966
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
56782
56967
  formValues: record(string(), unknown()),
56783
56968
  status: IntegrationInstanceStatusSchema.optional(),
@@ -56801,7 +56986,7 @@ var integrationInstancesCapability = {
56801
56986
  id: IntegrationInstanceIdSchema.nullable(),
56802
56987
  /** The kind of the create form (`id: null`) of a provider with several. */
56803
56988
  kind: string().min(1).optional()
56804
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56989
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
56805
56990
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56806
56991
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56807
56992
  /**
@@ -57903,12 +58088,12 @@ var LlmModelInfoSchema = object({
57903
58088
  });
57904
58089
  /**
57905
58090
  * The provider-specific part of a profile, validated by the provider that owns
57906
- * the kind (`upsertProfile` refuses what it does not accept). A field its
58091
+ * the kind (saving an instance refuses what it does not accept). A field its
57907
58092
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
57908
58093
  * keychain, never in the row, and reads back as the redaction marker.
57909
58094
  */
57910
58095
  var LlmProviderConfigSchema = record(string(), unknown());
57911
- var LlmProfileSchema = object({
58096
+ object({
57912
58097
  id: string(),
57913
58098
  name: string(),
57914
58099
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -58002,8 +58187,8 @@ var LlmProviderWidgetSchema = object({
58002
58187
  /** ConfigUISchema tree passed through untyped on the wire (the
58003
58188
  * notification-output `ConfigSchemaPassthrough` precedent at
58004
58189
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
58005
- var ConfigSchemaPassthrough$2 = unknown();
58006
- var LlmProfileKindDescriptorSchema = object({
58190
+ var ConfigSchemaPassthrough$1 = unknown();
58191
+ object({
58007
58192
  kind: LlmProfileKindSchema,
58008
58193
  label: string(),
58009
58194
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -58011,9 +58196,9 @@ var LlmProfileKindDescriptorSchema = object({
58011
58196
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
58012
58197
  addonId: string(),
58013
58198
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
58014
- connectionSchema: ConfigSchemaPassthrough$2,
58199
+ connectionSchema: ConfigSchemaPassthrough$1,
58015
58200
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
58016
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
58201
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
58017
58202
  /** The kind's defaults; a model from `listModels` may override any of them. */
58018
58203
  features: LlmModelFeaturesSchema,
58019
58204
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -58142,20 +58327,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
58142
58327
  }), method(object({
58143
58328
  addonId: string().optional(),
58144
58329
  requestId: string()
58145
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
58146
- kind: "mutation",
58147
- auth: "admin"
58148
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
58149
- kind: "mutation",
58150
- auth: "admin"
58151
- }), method(ProfileRefInputSchema, _void(), {
58152
- kind: "mutation",
58153
- auth: "admin"
58154
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
58155
- kind: "mutation",
58156
- auth: "admin",
58157
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
58158
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58330
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58159
58331
  kind: "mutation",
58160
58332
  auth: "admin"
58161
58333
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -59218,20 +59390,7 @@ method(_void(), MeshStatusSchema), method(object({
59218
59390
  /** Optional hostname override the host should advertise once joined. */
59219
59391
  hostname: string().optional() }), object({
59220
59392
  /** Authentication URL the operator should open in a browser. */
59221
- 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({
59222
- /** Optional auth key — when provided, probes the key validity
59223
- * against the provider's API. Omit when already joined to
59224
- * just ping the daemon. */
59225
- authKey: string().optional() }), object({
59226
- ok: boolean(),
59227
- /** Provider-side identifier resolved by the probe (tailnet
59228
- * name for Tailscale, network id for ZeroTier, etc.). */
59229
- tenant: string().optional(),
59230
- /** Daemon binary version, when reachable. */
59231
- daemonVersion: string().optional(),
59232
- /** Human-readable error when `ok: false`. */
59233
- error: string().optional()
59234
- }), { kind: "mutation" });
59393
+ 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() }));
59235
59394
  /**
59236
59395
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
59237
59396
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -67058,6 +67217,11 @@ var NcScheduleSchema = object({
67058
67217
  * as a STATE SUMMARY at the end of each window of `schedule`.
67059
67218
  */
67060
67219
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
67220
+ _enum([
67221
+ "activation",
67222
+ "deactivation",
67223
+ "schedule"
67224
+ ]);
67061
67225
  /**
67062
67226
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
67063
67227
  * every section; `section` = one notification per section with something to
@@ -67074,11 +67238,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
67074
67238
  * midnight crossing); the send time is each window's END. Refused on the
67075
67239
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
67076
67240
  */
67077
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
67078
- mode: literal("summary"),
67079
- schedule: NcScheduleSchema,
67080
- split: NcStateSummarySplitSchema.optional()
67081
- })]);
67241
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
67242
+ object({ mode: literal("instant") }),
67243
+ object({
67244
+ mode: literal("summary"),
67245
+ schedule: NcScheduleSchema,
67246
+ split: NcStateSummarySplitSchema.optional()
67247
+ }),
67248
+ object({
67249
+ mode: literal("modes"),
67250
+ /** The bad edge, and every kind that has a single edge. */
67251
+ onActivation: boolean(),
67252
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
67253
+ onDeactivation: boolean(),
67254
+ /** A state summary at the end of each window; absent = no summary. */
67255
+ onSchedule: object({
67256
+ schedule: NcScheduleSchema,
67257
+ grouping: NcStateSummarySplitSchema
67258
+ }).optional()
67259
+ })
67260
+ ]);
67082
67261
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
67083
67262
  var NcPlateMatcherSchema = object({
67084
67263
  /**
@@ -69438,61 +69617,6 @@ var OauthIntegrationDescriptorSchema = object({
69438
69617
  });
69439
69618
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
69440
69619
  /**
69441
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
69442
- *
69443
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
69444
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
69445
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
69446
- * immutable after create.
69447
- */
69448
- var ConfigSchemaPassthrough$1 = unknown();
69449
- var OidcConnectionSchema = object({
69450
- id: string().min(1),
69451
- displayName: string(),
69452
- icon: string(),
69453
- issuerUrl: string(),
69454
- clientId: string(),
69455
- /** Client id + secret present — never the secret value. */
69456
- configured: boolean(),
69457
- /**
69458
- * Values for the connection form schema. `clientSecret` is a keychain
69459
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
69460
- */
69461
- formValues: record(string(), unknown()).optional()
69462
- });
69463
- /**
69464
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
69465
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
69466
- */
69467
- var OidcConnectionUpdateInputSchema = object({
69468
- id: string().min(1),
69469
- displayName: string().min(1),
69470
- icon: string().optional(),
69471
- issuerUrl: string().optional(),
69472
- clientId: string().optional(),
69473
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
69474
- redirectUri: string().optional(),
69475
- scopes: string().optional(),
69476
- usernameClaim: _enum([
69477
- "preferred_username",
69478
- "email",
69479
- "sub"
69480
- ]).optional()
69481
- });
69482
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
69483
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
69484
- displayName: string().min(1)
69485
- }), object({ id: string() }), {
69486
- kind: "mutation",
69487
- auth: "admin"
69488
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
69489
- kind: "mutation",
69490
- auth: "admin"
69491
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
69492
- kind: "mutation",
69493
- auth: "admin"
69494
- });
69495
- /**
69496
69620
  * Generic on-screen-display (video overlay) cap. Each camera exposes
69497
69621
  * a firmware-defined list of overlays (channel name, timestamp,
69498
69622
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -75207,6 +75331,16 @@ var RemoteLayoutComponentSchema = object({
75207
75331
  method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
75208
75332
  var RUNNING_POWER_START_DELAY_MS = 3e4;
75209
75333
  var RUNNING_POWER_STOP_DELAY_MS = 18e4;
75334
+ /**
75335
+ * Per-type overrides of the dwell defaults. A media player draws power the
75336
+ * moment it is on and stops drawing the moment it is off, so it needs neither
75337
+ * a long dwell nor a second sample. A type absent here keeps the global values.
75338
+ */
75339
+ var RUNNING_POWER_DEFAULTS_BY_TYPE = { [DeviceType.MediaPlayer]: {
75340
+ startDwellMs: 1e4,
75341
+ stopDwellMs: 1e4,
75342
+ minSamples: 1
75343
+ } };
75210
75344
  var RunningStateSourceSchema = _enum(["native", "assigned"]);
75211
75345
  var RunningStateStatusSchema = object({
75212
75346
  /** null = unknown — never coerced to false (D393). */
@@ -75224,93 +75358,27 @@ var RunningFlipEventDataSchema = object({
75224
75358
  phase: string().nullable()
75225
75359
  });
75226
75360
  var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
75227
- /** Generic Customize target for the running boolean (D936). */
75228
- var runningCustomize = {
75229
- version: 1,
75230
- overridable: [{
75231
- path: "running",
75232
- by: "running"
75233
- }],
75234
- targets: [{
75235
- id: "running",
75236
- cardinality: "scalar",
75237
- vocabulary: {
75238
- cap: "running-state",
75239
- stateField: "running"
75240
- },
75241
- deviceTypes: [
75242
- DeviceType.Washer,
75243
- DeviceType.Dryer,
75244
- DeviceType.Dishwasher,
75245
- DeviceType.Vacuum,
75246
- DeviceType.LawnMower,
75247
- DeviceType.MediaPlayer,
75248
- DeviceType.WaterHeater,
75249
- DeviceType.Humidifier,
75250
- DeviceType.Fan,
75251
- DeviceType.Climate,
75252
- DeviceType.Thermostat,
75253
- DeviceType.PetFeeder,
75254
- DeviceType.Heater
75255
- ],
75256
- apply: "add",
75257
- path: "running",
75258
- sources: [
75259
- {
75260
- kind: "switch",
75261
- transforms: [{ kind: "identity" }]
75262
- },
75263
- {
75264
- kind: "binary",
75265
- transforms: [{ kind: "identity" }, { kind: "invert" }]
75266
- },
75267
- {
75268
- kind: "numeric",
75269
- requiredUnit: "W",
75270
- transforms: [{
75271
- kind: "threshold",
75272
- comparisons: ["gte", "lte"],
75273
- comparison: "gte",
75274
- defaultThreshold: 5,
75275
- defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75276
- defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75277
- thresholdUnit: "W"
75278
- }]
75279
- }
75280
- ],
75281
- companions: [
75282
- {
75283
- kind: "constant",
75284
- path: "source",
75285
- value: "assigned",
75286
- when: "always"
75287
- },
75288
- {
75289
- kind: "constant",
75290
- path: "phase",
75291
- value: null,
75292
- when: "add"
75293
- },
75294
- {
75295
- kind: "clock",
75296
- path: "lastFetchedAt",
75297
- when: "always"
75298
- },
75299
- {
75300
- kind: "transition-time",
75301
- path: "since",
75302
- sourceChangedAt: "prefer"
75303
- }
75304
- ],
75305
- sourceReport: {
75306
- status: "assignment-status",
75307
- runtimeField: "source",
75308
- nativeValue: "native",
75309
- assignedValue: "assigned"
75310
- },
75311
- advancedExpression: true
75312
- }]
75313
- };
75361
+ /**
75362
+ * Customize offers "running" only where a job makes sense (D933): a cycle,
75363
+ * a clean, a mow, playback, or another appliance that is on and working.
75364
+ * The cap itself stays on every type — a camera can still report running
75365
+ * natively; it just has no card.
75366
+ */
75367
+ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
75368
+ DeviceType.Washer,
75369
+ DeviceType.Dryer,
75370
+ DeviceType.Dishwasher,
75371
+ DeviceType.Vacuum,
75372
+ DeviceType.LawnMower,
75373
+ DeviceType.MediaPlayer,
75374
+ DeviceType.WaterHeater,
75375
+ DeviceType.Humidifier,
75376
+ DeviceType.Fan,
75377
+ DeviceType.Climate,
75378
+ DeviceType.Thermostat,
75379
+ DeviceType.PetFeeder,
75380
+ DeviceType.Heater
75381
+ ];
75314
75382
  var runningStateCapability = {
75315
75383
  name: "running-state",
75316
75384
  scope: "device",
@@ -75330,7 +75398,79 @@ var runningStateCapability = {
75330
75398
  durability: "restored",
75331
75399
  history: { fields: { running: { kind: "boolean" } } },
75332
75400
  volatileStateFields: ["lastFetchedAt"],
75333
- customize: runningCustomize
75401
+ customize: {
75402
+ version: 1,
75403
+ overridable: [{
75404
+ path: "running",
75405
+ by: "running"
75406
+ }],
75407
+ targets: [{
75408
+ id: "running",
75409
+ cardinality: "scalar",
75410
+ vocabulary: {
75411
+ cap: "running-state",
75412
+ stateField: "running"
75413
+ },
75414
+ deviceTypes: RUNNING_CUSTOMIZE_DEVICE_TYPES,
75415
+ apply: "add",
75416
+ path: "running",
75417
+ sources: [
75418
+ {
75419
+ kind: "switch",
75420
+ transforms: [{ kind: "identity" }]
75421
+ },
75422
+ {
75423
+ kind: "binary",
75424
+ transforms: [{ kind: "identity" }, { kind: "invert" }]
75425
+ },
75426
+ {
75427
+ kind: "numeric",
75428
+ requiredUnit: "W",
75429
+ transforms: [{
75430
+ kind: "threshold",
75431
+ comparisons: ["gte", "lte"],
75432
+ comparison: "gte",
75433
+ defaultThreshold: 5,
75434
+ defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75435
+ defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75436
+ deviceTypeDefaults: RUNNING_POWER_DEFAULTS_BY_TYPE,
75437
+ thresholdUnit: "W"
75438
+ }]
75439
+ }
75440
+ ],
75441
+ companions: [
75442
+ {
75443
+ kind: "constant",
75444
+ path: "source",
75445
+ value: "assigned",
75446
+ when: "always"
75447
+ },
75448
+ {
75449
+ kind: "constant",
75450
+ path: "phase",
75451
+ value: null,
75452
+ when: "add"
75453
+ },
75454
+ {
75455
+ kind: "clock",
75456
+ path: "lastFetchedAt",
75457
+ when: "always"
75458
+ },
75459
+ {
75460
+ kind: "transition-time",
75461
+ path: "since",
75462
+ sourceChangedAt: "prefer"
75463
+ }
75464
+ ],
75465
+ sourceReport: {
75466
+ status: "assignment-status",
75467
+ runtimeField: "source",
75468
+ nativeValue: "native",
75469
+ assignedValue: "assigned"
75470
+ },
75471
+ advancedExpression: true
75472
+ }]
75473
+ }
75334
75474
  };
75335
75475
  /**
75336
75476
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -76574,21 +76714,6 @@ method(object({
76574
76714
  */
76575
76715
  ttlSec: union([number().int().positive(), literal("never")]).optional()
76576
76716
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
76577
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
76578
- providerId: string().min(1),
76579
- displayName: string().min(1),
76580
- configSchema: unknown(),
76581
- nodeLocal: boolean(),
76582
- shouldSaveDiskSpace: literal(true),
76583
- minFreePercent: number().min(0).max(100)
76584
- }), object({
76585
- providerId: string().min(1),
76586
- displayName: string().min(1),
76587
- configSchema: unknown(),
76588
- nodeLocal: boolean(),
76589
- shouldSaveDiskSpace: literal(false),
76590
- minFreePercent: literal(null)
76591
- })]);
76592
76717
  method(object({
76593
76718
  location: StorageLocationRefSchema,
76594
76719
  relativePath: string()
@@ -76650,42 +76775,7 @@ method(object({
76650
76775
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
76651
76776
  kind: "mutation",
76652
76777
  auth: "admin"
76653
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
76654
- createdAt: true,
76655
- updatedAt: true
76656
- }), StorageLocationSchema, {
76657
- kind: "mutation",
76658
- auth: "admin"
76659
- }), method(object({
76660
- id: string(),
76661
- force: boolean().optional()
76662
- }), _void(), {
76663
- kind: "mutation",
76664
- auth: "admin"
76665
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
76666
- ok: boolean(),
76667
- error: string().optional()
76668
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
76669
- providerId: string(),
76670
- /**
76671
- * The location this config is an UNSAVED edit of, when there is one.
76672
- *
76673
- * `listLocations` replaces every declared secret with the redaction
76674
- * sentinel, so the edit modal's form state holds the sentinel for any
76675
- * credential the operator did not retype — and posting that here
76676
- * without a way to resolve it makes the provider try to authenticate
76677
- * as `__camstack_redacted__` and report the operator's own working
76678
- * password as wrong. Given this id, the orchestrator restores each
76679
- * sentinel from the stored config (same rule as `upsertLocation`)
76680
- * before dispatching. Omitted by the "Add location" wizard, where
76681
- * every value was typed just now and nothing is stored yet.
76682
- */
76683
- locationId: string().optional(),
76684
- config: record(string(), unknown())
76685
- }), object({
76686
- ok: boolean(),
76687
- error: string().optional()
76688
- }), { auth: "admin" });
76778
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
76689
76779
  /** One table of the settings database. */
76690
76780
  var StorageTableSchema = object({
76691
76781
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -78207,7 +78297,7 @@ var TerminalInstanceInfoSchema = object({
78207
78297
  /**
78208
78298
  * False while the node the instance is bound to is not in the cluster
78209
78299
  * topology (forgotten, renamed, re-paired). The instance is kept; the
78210
- * operator re-points it with `updateInstance({ targetNodeId })`.
78300
+ * operator re-points it on the Terminal page.
78211
78301
  */
78212
78302
  nodeInCluster: boolean(),
78213
78303
  profileId: string(),
@@ -78245,41 +78335,7 @@ var TerminalOutputBatchSchema = object({
78245
78335
  snapshot: string().optional(),
78246
78336
  events: array(TerminalOutputEventSchema).readonly()
78247
78337
  });
78248
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
78249
- targetNodeId: string().min(1),
78250
- profileId: string().min(1),
78251
- name: string().trim().min(1).max(160).optional(),
78252
- executable: string().max(1024).optional(),
78253
- args: array(string().max(2048)).max(64).optional(),
78254
- cwd: string().max(1024).optional(),
78255
- environment: array(string().max(4096)).max(64).optional(),
78256
- profileSettings: ProfileSettingsBagSchema.optional()
78257
- }), TerminalInstanceInfoSchema, {
78258
- kind: "mutation",
78259
- auth: "admin"
78260
- }), method(object({
78261
- instanceId: string().min(1),
78262
- /** Re-point the instance to another node; the profile must exist there. */
78263
- targetNodeId: string().min(1).max(256).optional(),
78264
- name: string().trim().min(1).max(160).optional(),
78265
- executable: string().max(1024).optional(),
78266
- args: array(string().max(2048)).max(64).optional(),
78267
- cwd: string().max(1024).optional(),
78268
- environment: array(string().max(4096)).max(64).optional(),
78269
- profileSettings: ProfileSettingsBagSchema.optional()
78270
- }), TerminalInstanceInfoSchema, {
78271
- kind: "mutation",
78272
- auth: "admin"
78273
- }), method(object({ instanceId: string().min(1) }), _void(), {
78274
- kind: "mutation",
78275
- auth: "admin"
78276
- }), method(object({
78277
- instanceId: string().min(1),
78278
- enabled: boolean()
78279
- }), TerminalInstanceInfoSchema, {
78280
- kind: "mutation",
78281
- auth: "admin"
78282
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78338
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78283
78339
  stableId: string().min(1),
78284
78340
  name: string().trim().min(1).max(160).optional()
78285
78341
  }), TerminalInstanceInfoSchema, {
@@ -78348,6 +78404,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
78348
78404
  _enum(["switch", "pet-fountain"]);
78349
78405
  DeviceType.Heater, DeviceType.PetFountain;
78350
78406
  var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
78407
+ /** One duration, as the one-tap run asks for it. */
78408
+ var TimedRunQuickRunSchema = object({
78409
+ duration: number().positive().finite(),
78410
+ unit: TimedRunUnitSchema
78411
+ });
78351
78412
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
78352
78413
  var TimedRunRefusalSchema = _enum([
78353
78414
  "unsupported-device",
@@ -78368,8 +78429,23 @@ var TimedRunStatusSchema = object({
78368
78429
  activeUntil: number().nullable(),
78369
78430
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
78370
78431
  requestedBy: string().nullable(),
78432
+ /**
78433
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
78434
+ * the device is gone or its type has no timed run; a status from before this field
78435
+ * existed has none, and a surface falls back to the type's default.
78436
+ */
78437
+ quickRun: TimedRunQuickRunSchema.nullable(),
78371
78438
  lastFetchedAt: number()
78372
78439
  });
78440
+ /** What the settings form needs: the unit, the bounds and the type's default. */
78441
+ var TimedRunOptionsSchema = object({
78442
+ unit: TimedRunUnitSchema,
78443
+ /** The longest run, in `unit`. */
78444
+ maxDuration: number(),
78445
+ defaultDuration: number()
78446
+ });
78447
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
78448
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
78373
78449
  var timedRunCapability = {
78374
78450
  name: "timed-run",
78375
78451
  scope: "device",
@@ -78377,7 +78453,24 @@ var timedRunCapability = {
78377
78453
  kind: "wrapper",
78378
78454
  defaultActive: true,
78379
78455
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
78456
+ deviceConfig: { ui: {
78457
+ kind: "derived-form",
78458
+ builderId: "timed-run",
78459
+ tab: "general"
78460
+ } },
78380
78461
  methods: {
78462
+ getOptions: method(object({ deviceId: number().int() }), TimedRunOptionsSchema),
78463
+ /**
78464
+ * The one-tap run of this device. A duration above the type's maximum is
78465
+ * REFUSED (thrown), like `runFor`: the maximum is a safety bound.
78466
+ */
78467
+ setSettings: method(object({
78468
+ deviceId: number().int(),
78469
+ settings: TimedRunSettingsPatchSchema
78470
+ }), _void(), {
78471
+ kind: "mutation",
78472
+ auth: "admin"
78473
+ }),
78381
78474
  /**
78382
78475
  * Turn the device on and arm its off. A run already armed is replaced (the
78383
78476
  * new deadline wins). Refuses with a typed reason above the type's maximum.
@@ -83170,21 +83263,28 @@ Object.freeze({
83170
83263
  access: "view",
83171
83264
  role: "admin"
83172
83265
  },
83173
- "addonSettings.getGlobalSettings": {
83266
+ "addonSettings.getIntegrationFormSchema": {
83174
83267
  capName: "addon-settings",
83175
83268
  capScope: "system",
83176
83269
  addonId: null,
83177
83270
  access: "view",
83178
83271
  role: "admin"
83179
83272
  },
83180
- "addonSettings.getIntegrationFormSchema": {
83273
+ "addonSettings.getScopedSettings": {
83274
+ capName: "addon-settings",
83275
+ capScope: "system",
83276
+ addonId: null,
83277
+ access: "view",
83278
+ role: "admin"
83279
+ },
83280
+ "addonSettings.listScopedSettings": {
83181
83281
  capName: "addon-settings",
83182
83282
  capScope: "system",
83183
83283
  addonId: null,
83184
83284
  access: "view",
83185
83285
  role: "admin"
83186
83286
  },
83187
- "addonSettings.getIntegrationSettings": {
83287
+ "addonSettings.readScopedValues": {
83188
83288
  capName: "addon-settings",
83189
83289
  capScope: "system",
83190
83290
  addonId: null,
@@ -83205,7 +83305,7 @@ Object.freeze({
83205
83305
  access: "create",
83206
83306
  role: "admin"
83207
83307
  },
83208
- "addonSettings.updateGlobalSettings": {
83308
+ "addonSettings.updateScopedSettings": {
83209
83309
  capName: "addon-settings",
83210
83310
  capScope: "system",
83211
83311
  addonId: null,
@@ -85375,6 +85475,13 @@ Object.freeze({
85375
85475
  access: "create",
85376
85476
  role: "admin"
85377
85477
  },
85478
+ "deviceManager.setAlias": {
85479
+ capName: "device-manager",
85480
+ capScope: "system",
85481
+ addonId: null,
85482
+ access: "create",
85483
+ role: "admin"
85484
+ },
85378
85485
  "deviceManager.setChildLayout": {
85379
85486
  capName: "device-manager",
85380
85487
  capScope: "system",
@@ -86663,13 +86770,6 @@ Object.freeze({
86663
86770
  access: "delete",
86664
86771
  role: "admin"
86665
86772
  },
86666
- "llm.deleteProfile": {
86667
- capName: "llm",
86668
- capScope: "system",
86669
- addonId: null,
86670
- access: "delete",
86671
- role: "admin"
86672
- },
86673
86773
  "llm.generate": {
86674
86774
  capName: "llm",
86675
86775
  capScope: "system",
@@ -86733,20 +86833,6 @@ Object.freeze({
86733
86833
  access: "view",
86734
86834
  role: "admin"
86735
86835
  },
86736
- "llm.listProfileKinds": {
86737
- capName: "llm",
86738
- capScope: "system",
86739
- addonId: null,
86740
- access: "view",
86741
- role: "admin"
86742
- },
86743
- "llm.listProfiles": {
86744
- capName: "llm",
86745
- capScope: "system",
86746
- addonId: null,
86747
- access: "view",
86748
- role: "admin"
86749
- },
86750
86836
  "llm.listRuntimeNodes": {
86751
86837
  capName: "llm",
86752
86838
  capScope: "system",
@@ -86754,13 +86840,6 @@ Object.freeze({
86754
86840
  access: "view",
86755
86841
  role: "admin"
86756
86842
  },
86757
- "llm.migrateRawSecrets": {
86758
- capName: "llm",
86759
- capScope: "system",
86760
- addonId: null,
86761
- access: "create",
86762
- role: "admin"
86763
- },
86764
86843
  "llm.resolveModelRef": {
86765
86844
  capName: "llm",
86766
86845
  capScope: "system",
@@ -86789,20 +86868,6 @@ Object.freeze({
86789
86868
  access: "create",
86790
86869
  role: "admin"
86791
86870
  },
86792
- "llm.testProfile": {
86793
- capName: "llm",
86794
- capScope: "system",
86795
- addonId: null,
86796
- access: "create",
86797
- role: "admin"
86798
- },
86799
- "llm.upsertProfile": {
86800
- capName: "llm",
86801
- capScope: "system",
86802
- addonId: null,
86803
- access: "create",
86804
- role: "admin"
86805
- },
86806
86871
  "llmRuntime.appleAvailability": {
86807
86872
  capName: "llm-runtime",
86808
86873
  capScope: "system",
@@ -87307,13 +87372,6 @@ Object.freeze({
87307
87372
  access: "create",
87308
87373
  role: "admin"
87309
87374
  },
87310
- "meshNetwork.testConnection": {
87311
- capName: "mesh-network",
87312
- capScope: "system",
87313
- addonId: null,
87314
- access: "create",
87315
- role: "admin"
87316
- },
87317
87375
  "meter.getStatus": {
87318
87376
  capName: "meter",
87319
87377
  capScope: "device",
@@ -87972,13 +88030,6 @@ Object.freeze({
87972
88030
  access: "create",
87973
88031
  role: "admin"
87974
88032
  },
87975
- "notificationOutput.deleteTarget": {
87976
- capName: "notification-output",
87977
- capScope: "system",
87978
- addonId: null,
87979
- access: "delete",
87980
- role: "admin"
87981
- },
87982
88033
  "notificationOutput.discoverTargets": {
87983
88034
  capName: "notification-output",
87984
88035
  capScope: "system",
@@ -87986,13 +88037,6 @@ Object.freeze({
87986
88037
  access: "view",
87987
88038
  role: "admin"
87988
88039
  },
87989
- "notificationOutput.getTarget": {
87990
- capName: "notification-output",
87991
- capScope: "system",
87992
- addonId: null,
87993
- access: "view",
87994
- role: "admin"
87995
- },
87996
88040
  "notificationOutput.listTargetKinds": {
87997
88041
  capName: "notification-output",
87998
88042
  capScope: "system",
@@ -88000,13 +88044,6 @@ Object.freeze({
88000
88044
  access: "view",
88001
88045
  role: "admin"
88002
88046
  },
88003
- "notificationOutput.listTargets": {
88004
- capName: "notification-output",
88005
- capScope: "system",
88006
- addonId: null,
88007
- access: "view",
88008
- role: "admin"
88009
- },
88010
88047
  "notificationOutput.probeTargetConfig": {
88011
88048
  capName: "notification-output",
88012
88049
  capScope: "system",
@@ -88021,13 +88058,6 @@ Object.freeze({
88021
88058
  access: "create",
88022
88059
  role: "admin"
88023
88060
  },
88024
- "notificationOutput.setTargetEnabled": {
88025
- capName: "notification-output",
88026
- capScope: "system",
88027
- addonId: null,
88028
- access: "create",
88029
- role: "admin"
88030
- },
88031
88061
  "notificationOutput.testTarget": {
88032
88062
  capName: "notification-output",
88033
88063
  capScope: "system",
@@ -88035,13 +88065,6 @@ Object.freeze({
88035
88065
  access: "create",
88036
88066
  role: "admin"
88037
88067
  },
88038
- "notificationOutput.upsertTarget": {
88039
- capName: "notification-output",
88040
- capScope: "system",
88041
- addonId: null,
88042
- access: "create",
88043
- role: "admin"
88044
- },
88045
88068
  "notificationRules.activateAlarmProfile": {
88046
88069
  capName: "notification-rules",
88047
88070
  capScope: "system",
@@ -88301,41 +88324,6 @@ Object.freeze({
88301
88324
  access: "view",
88302
88325
  role: "admin"
88303
88326
  },
88304
- "oidcConnections.connectionFormSchema": {
88305
- capName: "oidc-connections",
88306
- capScope: "system",
88307
- addonId: null,
88308
- access: "view",
88309
- role: "admin"
88310
- },
88311
- "oidcConnections.createConnection": {
88312
- capName: "oidc-connections",
88313
- capScope: "system",
88314
- addonId: null,
88315
- access: "create",
88316
- role: "admin"
88317
- },
88318
- "oidcConnections.deleteConnection": {
88319
- capName: "oidc-connections",
88320
- capScope: "system",
88321
- addonId: null,
88322
- access: "delete",
88323
- role: "admin"
88324
- },
88325
- "oidcConnections.listConnections": {
88326
- capName: "oidc-connections",
88327
- capScope: "system",
88328
- addonId: null,
88329
- access: "view",
88330
- role: "admin"
88331
- },
88332
- "oidcConnections.updateConnection": {
88333
- capName: "oidc-connections",
88334
- capScope: "system",
88335
- addonId: null,
88336
- access: "create",
88337
- role: "admin"
88338
- },
88339
88327
  "osd.getStatus": {
88340
88328
  capName: "osd",
88341
88329
  capScope: "device",
@@ -91367,13 +91355,6 @@ Object.freeze({
91367
91355
  access: "delete",
91368
91356
  role: "admin"
91369
91357
  },
91370
- "storage.deleteLocation": {
91371
- capName: "storage",
91372
- capScope: "system",
91373
- addonId: null,
91374
- access: "delete",
91375
- role: "admin"
91376
- },
91377
91358
  "storage.endDownload": {
91378
91359
  capName: "storage",
91379
91360
  capScope: "system",
@@ -91430,13 +91411,6 @@ Object.freeze({
91430
91411
  access: "view",
91431
91412
  role: "admin"
91432
91413
  },
91433
- "storage.listProviders": {
91434
- capName: "storage",
91435
- capScope: "system",
91436
- addonId: null,
91437
- access: "view",
91438
- role: "admin"
91439
- },
91440
91414
  "storage.read": {
91441
91415
  capName: "storage",
91442
91416
  capScope: "system",
@@ -91458,27 +91432,6 @@ Object.freeze({
91458
91432
  access: "view",
91459
91433
  role: "admin"
91460
91434
  },
91461
- "storage.testConfig": {
91462
- capName: "storage",
91463
- capScope: "system",
91464
- addonId: null,
91465
- access: "view",
91466
- role: "admin"
91467
- },
91468
- "storage.testLocation": {
91469
- capName: "storage",
91470
- capScope: "system",
91471
- addonId: null,
91472
- access: "view",
91473
- role: "admin"
91474
- },
91475
- "storage.upsertLocation": {
91476
- capName: "storage",
91477
- capScope: "system",
91478
- addonId: null,
91479
- access: "create",
91480
- role: "admin"
91481
- },
91482
91435
  "storage.write": {
91483
91436
  capName: "storage",
91484
91437
  capScope: "system",
@@ -92256,27 +92209,6 @@ Object.freeze({
92256
92209
  access: "create",
92257
92210
  role: "admin"
92258
92211
  },
92259
- "terminalSession.createInstance": {
92260
- capName: "terminal-session",
92261
- capScope: "system",
92262
- addonId: null,
92263
- access: "create",
92264
- role: "admin"
92265
- },
92266
- "terminalSession.deleteInstance": {
92267
- capName: "terminal-session",
92268
- capScope: "system",
92269
- addonId: null,
92270
- access: "delete",
92271
- role: "admin"
92272
- },
92273
- "terminalSession.listInstances": {
92274
- capName: "terminal-session",
92275
- capScope: "system",
92276
- addonId: null,
92277
- access: "view",
92278
- role: "admin"
92279
- },
92280
92212
  "terminalSession.listLegacyCameras": {
92281
92213
  capName: "terminal-session",
92282
92214
  capScope: "system",
@@ -92319,20 +92251,6 @@ Object.freeze({
92319
92251
  access: "create",
92320
92252
  role: "admin"
92321
92253
  },
92322
- "terminalSession.setInstanceEnabled": {
92323
- capName: "terminal-session",
92324
- capScope: "system",
92325
- addonId: null,
92326
- access: "create",
92327
- role: "admin"
92328
- },
92329
- "terminalSession.updateInstance": {
92330
- capName: "terminal-session",
92331
- capScope: "system",
92332
- addonId: null,
92333
- access: "create",
92334
- role: "admin"
92335
- },
92336
92254
  "terminalSession.writeInput": {
92337
92255
  capName: "terminal-session",
92338
92256
  capScope: "system",
@@ -92347,6 +92265,13 @@ Object.freeze({
92347
92265
  access: "create",
92348
92266
  role: "viewer"
92349
92267
  },
92268
+ "timedRun.getOptions": {
92269
+ capName: "timed-run",
92270
+ capScope: "device",
92271
+ addonId: null,
92272
+ access: "view",
92273
+ role: "viewer"
92274
+ },
92350
92275
  "timedRun.getStatus": {
92351
92276
  capName: "timed-run",
92352
92277
  capScope: "device",
@@ -92361,6 +92286,13 @@ Object.freeze({
92361
92286
  access: "create",
92362
92287
  role: "viewer"
92363
92288
  },
92289
+ "timedRun.setSettings": {
92290
+ capName: "timed-run",
92291
+ capScope: "device",
92292
+ addonId: null,
92293
+ access: "create",
92294
+ role: "admin"
92295
+ },
92364
92296
  "toast.onToast": {
92365
92297
  capName: "toast",
92366
92298
  capScope: "system",
@@ -93291,6 +93223,7 @@ function offered(received) {
93291
93223
  };
93292
93224
  }
93293
93225
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
93226
+ string().min(1).max(4096);
93294
93227
  Object.freeze({
93295
93228
  "accessories.getStatus": [{
93296
93229
  name: "deviceId",
@@ -93974,6 +93907,11 @@ Object.freeze({
93974
93907
  form: "single",
93975
93908
  optional: false
93976
93909
  }],
93910
+ "deviceManager.setAlias": [{
93911
+ name: "deviceId",
93912
+ form: "single",
93913
+ optional: false
93914
+ }],
93977
93915
  "deviceManager.setChildLayout": [{
93978
93916
  name: "deviceId",
93979
93917
  form: "single",
@@ -96038,6 +95976,11 @@ Object.freeze({
96038
95976
  form: "single",
96039
95977
  optional: false
96040
95978
  }],
95979
+ "timedRun.getOptions": [{
95980
+ name: "deviceId",
95981
+ form: "single",
95982
+ optional: false
95983
+ }],
96041
95984
  "timedRun.getStatus": [{
96042
95985
  name: "deviceId",
96043
95986
  form: "single",
@@ -96048,6 +95991,11 @@ Object.freeze({
96048
95991
  form: "single",
96049
95992
  optional: false
96050
95993
  }],
95994
+ "timedRun.setSettings": [{
95995
+ name: "deviceId",
95996
+ form: "single",
95997
+ optional: false
95998
+ }],
96051
95999
  "update.getStatus": [{
96052
96000
  name: "deviceId",
96053
96001
  form: "single",
@@ -96910,6 +96858,7 @@ var STATE_SUMMARY_VARS = [
96910
96858
  [...RULE_TEXT],
96911
96859
  [...RULE_TEXT],
96912
96860
  [...RULE_TEXT],
96861
+ [...RULE_TEXT],
96913
96862
  [...RULE_BODY_ONLY],
96914
96863
  [...RULE_TEXT],
96915
96864
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -99277,7 +99226,13 @@ function buildEnpalInstances(records, deps) {
99277
99226
  },
99278
99227
  instanceMode: "multiple",
99279
99228
  kind: "server",
99280
- canTest: false
99229
+ canTest: false,
99230
+ kinds: [{
99231
+ id: "connection",
99232
+ label: "Enpal",
99233
+ functionCap: "device-provider",
99234
+ role: "devices"
99235
+ }]
99281
99236
  },
99282
99237
  formSchema: () => buildConnectionFormSchema(),
99283
99238
  initialBody: (name) => ({ name }),