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