@camstack/addon-provider-bosch 0.1.41 → 0.1.43

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.
@@ -1,5 +1,5 @@
1
1
  import "node:crypto";
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-CdKka2ri.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] } : {},
@@ -6750,10 +6827,33 @@ function rowsDepsOver(api) {
6750
6827
  };
6751
6828
  }
6752
6829
  var PREFIX = "records/";
6830
+ /**
6831
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6832
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6833
+ * instance of the host's classes, not of the addon's. The brand plus the code
6834
+ * make `instanceof` answer by identity of the failure, not of the copy.
6835
+ */
6836
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6837
+ function carriesBrand(value) {
6838
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6839
+ }
6753
6840
  /** The base of every records failure. `detail` is the human part of the message. */
6754
6841
  var RecordsError = class extends Error {
6842
+ /** The code a subclass stands for; the base stands for every code. */
6843
+ static code = void 0;
6844
+ /**
6845
+ * `instanceof` across copies of this module: any branded error is a
6846
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6847
+ * subclass's code. Same-copy answers are unchanged.
6848
+ */
6849
+ static [Symbol.hasInstance](value) {
6850
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6851
+ if (!carriesBrand(value)) return false;
6852
+ return this.code === void 0 || value.code === this.code;
6853
+ }
6755
6854
  code;
6756
6855
  detail;
6856
+ [RECORDS_ERROR_BRAND] = true;
6757
6857
  constructor(code, detail) {
6758
6858
  super(`${PREFIX}${code}: ${detail}`);
6759
6859
  this.name = "RecordsError";
@@ -6762,29 +6862,33 @@ var RecordsError = class extends Error {
6762
6862
  }
6763
6863
  };
6764
6864
  /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
6765
- var RevConflictError = class extends RecordsError {
6865
+ var RevConflictError = class RevConflictError extends RecordsError {
6866
+ static code = "rev-conflict";
6766
6867
  constructor(detail) {
6767
- super("rev-conflict", detail);
6868
+ super(RevConflictError.code, detail);
6768
6869
  this.name = "RevConflictError";
6769
6870
  }
6770
6871
  };
6771
6872
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6772
- var RecordSecretUnavailableError = class extends RecordsError {
6873
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6874
+ static code = "secret-unavailable";
6773
6875
  constructor(detail) {
6774
- super("secret-unavailable", detail);
6876
+ super(RecordSecretUnavailableError.code, detail);
6775
6877
  this.name = "RecordSecretUnavailableError";
6776
6878
  }
6777
6879
  };
6778
- var InvalidRecordError = class extends RecordsError {
6880
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6881
+ static code = "invalid-record";
6779
6882
  constructor(detail) {
6780
- super("invalid-record", detail);
6883
+ super(InvalidRecordError.code, detail);
6781
6884
  this.name = "InvalidRecordError";
6782
6885
  }
6783
6886
  };
6784
6887
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6785
- var DeclarationRefusedError = class extends RecordsError {
6888
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6889
+ static code = "declaration-refused";
6786
6890
  constructor(detail) {
6787
- super("declaration-refused", detail);
6891
+ super(DeclarationRefusedError.code, detail);
6788
6892
  this.name = "DeclarationRefusedError";
6789
6893
  }
6790
6894
  };
@@ -7186,6 +7290,7 @@ var RecordIntegrationInstances = class {
7186
7290
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
7187
7291
  this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
7188
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`);
7189
7294
  }
7190
7295
  /**
7191
7296
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -7227,10 +7332,21 @@ var RecordIntegrationInstances = class {
7227
7332
  }
7228
7333
  return out;
7229
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
+ }
7230
7346
  viewOf(record) {
7231
7347
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
7232
7348
  const rawName = record.data[this.nameField];
7233
- const kind = this.hooks.kindOf?.(record.data);
7349
+ const kind = this.kindOfBody(record.data);
7234
7350
  const health = this.hooks.health?.(record.data, record.id);
7235
7351
  return {
7236
7352
  id: record.id,
@@ -7238,7 +7354,7 @@ var RecordIntegrationInstances = class {
7238
7354
  configured: this.hooks.configured(record.data),
7239
7355
  enabled: isInstanceEnabled(record.data),
7240
7356
  formValues: shown,
7241
- ...kind !== void 0 ? { kind } : {},
7357
+ kind,
7242
7358
  ...health?.status !== void 0 ? { status: health.status } : {},
7243
7359
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
7244
7360
  };
@@ -7271,17 +7387,35 @@ var RecordIntegrationInstances = class {
7271
7387
  ...kind !== void 0 ? { kind } : {}
7272
7388
  });
7273
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
+ }));
7274
7399
  await this.handle.put({
7275
7400
  id: minted,
7276
- data: this.hooks.initialBody(name, kind)
7401
+ data: initial
7277
7402
  }, { ifRev: 0 });
7278
- const patch = this.patchOf(values ?? {});
7279
7403
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7280
7404
  await this.hooks.onChanged?.(minted, "created");
7281
7405
  return { id: minted };
7282
7406
  };
7283
7407
  updateInstance = async ({ id, name, values }) => {
7284
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
+ }
7285
7419
  if (name !== void 0) patch[this.nameField] = name;
7286
7420
  await this.handle.patch(id, patch);
7287
7421
  await this.hooks.onChanged?.(id, "updated");
@@ -7592,6 +7726,21 @@ var SCOPED_SETTINGS_SCOPES = [
7592
7726
  "device-defaults",
7593
7727
  "node"
7594
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
+ }
7595
7744
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7596
7745
  var KeychainWriteRefusedError = class extends Error {
7597
7746
  fieldPath;
@@ -8167,7 +8316,16 @@ function createHandle(raw, opts) {
8167
8316
  return blob;
8168
8317
  }
8169
8318
  },
8170
- 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
+ })
8171
8329
  };
8172
8330
  }
8173
8331
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8297,7 +8455,7 @@ function boundOf(list) {
8297
8455
  * Base class for CamStack addons. Eliminates settings boilerplate:
8298
8456
  *
8299
8457
  * - Typed `config` property with automatic resolution from store + defaults
8300
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8458
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8301
8459
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8302
8460
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8303
8461
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8307,7 +8465,7 @@ function boundOf(list) {
8307
8465
  * Subclasses override:
8308
8466
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8309
8467
  * - `onShutdown()` — cleanup
8310
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8468
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8311
8469
  *
8312
8470
  * @example
8313
8471
  * ```ts
@@ -8321,7 +8479,7 @@ function boundOf(list) {
8321
8479
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8322
8480
  * }
8323
8481
  *
8324
- * protected globalSettingsSchema() {
8482
+ * protected clusterSettingsSchema() {
8325
8483
  * return this.schema({
8326
8484
  * sections: [{
8327
8485
  * id: 'main', title: 'Settings',
@@ -8364,7 +8522,7 @@ var BaseAddon = class {
8364
8522
  defaults;
8365
8523
  /**
8366
8524
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8367
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8525
+ * itself (`state()`, the scoped writers) that no form field declares.
8368
8526
  * Stored in the keychain exactly like a `password` field. Syntax:
8369
8527
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8370
8528
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8374,7 +8532,7 @@ var BaseAddon = class {
8374
8532
  /**
8375
8533
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8376
8534
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8377
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8535
+ * see them. Empty until loaded.
8378
8536
  */
8379
8537
  _scopedByCap = /* @__PURE__ */ new Map();
8380
8538
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8560,7 +8718,7 @@ var BaseAddon = class {
8560
8718
  /** Addon-specific cleanup. Override if needed. */
8561
8719
  async onShutdown() {}
8562
8720
  /**
8563
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8721
+ * Called after config is resolved during a settings write.
8564
8722
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8565
8723
  * Not called during initialize() — use onInitialize() for initial setup.
8566
8724
  */
@@ -8589,82 +8747,12 @@ var BaseAddon = class {
8589
8747
  scopedSettingsCapabilities() {
8590
8748
  return [];
8591
8749
  }
8592
- /** Override to provide global-level settings UI schema. */
8593
- globalSettingsSchema(_cap) {
8594
- return null;
8595
- }
8596
8750
  /** Override to provide device-level settings UI schema. */
8597
8751
  deviceSettingsSchema() {
8598
8752
  return null;
8599
8753
  }
8600
8754
  /**
8601
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8602
- * ARE the configuration of its integration.
8603
- *
8604
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8605
- * operator should find on the addon's integration page (System →
8606
- * Integrations → <name>) rather than only in the cluster-wide list of every
8607
- * addon. Empty (the default) means the addon has no integration-level
8608
- * settings and no such surface is offered — this is opt-in, because whether
8609
- * an addon's configuration IS its integration's configuration depends on the
8610
- * nature of the integration.
8611
- *
8612
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8613
- * the ONE global schema, in the ONE addon store, written by the ONE
8614
- * `updateGlobalSettings` path. There is deliberately no
8615
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8616
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8617
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8618
- *
8619
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8620
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8621
- * removed with the reason recorded at
8622
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8623
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8624
- * marker sprinkled across sections also has to borrow a field that already
8625
- * means something else; borrowing `section.tab` put the literal word
8626
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8627
- * GROUP this visually" and cannot also mean "where this lives" (D269
8628
- * supersedes D268). One declaration, in one place, next to the schema whose
8629
- * ids it names.
8630
- */
8631
- integrationSettingSections() {
8632
- return [];
8633
- }
8634
- async getGlobalSettings(overlay, cap, nodeId) {
8635
- await this.loadScopedSchemas(cap);
8636
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8637
- if (!schema) return { sections: [] };
8638
- const projected = await this.readRawProjectedStore(nodeId, cap);
8639
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8640
- }
8641
- /**
8642
- * The integration-level view of this addon's settings: exactly the sections
8643
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8644
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8645
- *
8646
- * Returns `null` when the addon declared nothing — an addon that opts out has
8647
- * no integration settings surface at all, rather than an empty one that reads
8648
- * as a failed load.
8649
- *
8650
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8651
- * and not in whichever UI happens to render this:
8652
- *
8653
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8654
- * shown here is the same field, with the same bare key, that the addon's
8655
- * own page shows. There is no integration-specific writer — callers save
8656
- * through `updateGlobalSettings` — so a second store key is unreachable,
8657
- * not merely discouraged.
8658
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8659
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8660
- * such a field silently picked would be a wrong answer for the operator
8661
- * who opened the page (D266).
8662
- * 3. **No silent typo.** A declared id that names no section throws. The
8663
- * alternative — skip it — turns a rename into a surface that quietly
8664
- * empties, which looks exactly like an addon with nothing to configure.
8665
- */
8666
- /**
8667
- * 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.
8668
8756
  *
8669
8757
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8670
8758
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8712,37 +8800,178 @@ var BaseAddon = class {
8712
8800
  }
8713
8801
  }
8714
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
+ /**
8715
8961
  * What the create wizard and the integration edit page render for one instance: the addon's
8716
8962
  * per-instance schema when it declares one, else its global schema (what they always showed).
8717
8963
  */
8718
8964
  async getIntegrationFormSchema(instance, nodeId) {
8719
8965
  const scoped = await this.integrationFormSchema(instance);
8720
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8966
+ if (scoped === null) return this.scopedForm(nodeId);
8721
8967
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8722
8968
  }
8723
- async getIntegrationSettings(nodeId) {
8724
- const declared = this.integrationSettingSections();
8725
- if (declared.length === 0) return null;
8726
- const schema = this.globalSettingsSchema();
8727
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8728
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8729
- const sections = [];
8730
- for (const id of declared) {
8731
- const section = byId.get(id);
8732
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8733
- const fields = dropPerNodeFields(section.fields);
8734
- if (fields.length === 0) continue;
8735
- sections.push({
8736
- ...section,
8737
- fields
8738
- });
8739
- }
8740
- if (sections.length === 0) return null;
8741
- const projected = await this.readRawProjectedStore(nodeId);
8742
- return hydrateSchema({
8743
- ...schema,
8744
- sections
8745
- }, 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 });
8746
8975
  }
8747
8976
  /**
8748
8977
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8751,8 +8980,8 @@ var BaseAddon = class {
8751
8980
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8752
8981
  * A no-op passthrough when the schema declares no `perNode` field.
8753
8982
  *
8754
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8755
- * 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
8756
8985
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8757
8986
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8758
8987
  *
@@ -8804,7 +9033,7 @@ var BaseAddon = class {
8804
9033
  }
8805
9034
  /**
8806
9035
  * Whether ONE global key of this addon's store was written, read three-way
8807
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
9036
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8808
9037
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8809
9038
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8810
9039
  * not supported here: its stored name is scoped.
@@ -8838,7 +9067,7 @@ var BaseAddon = class {
8838
9067
  }
8839
9068
  /**
8840
9069
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8841
- * 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
8842
9071
  * the operator sends is stored as sent even when it equals a pre-keychain
8843
9072
  * inline value: the operator re-entering a password is a change, an addon
8844
9073
  * writing back what it read is not (D787).
@@ -8851,9 +9080,6 @@ var BaseAddon = class {
8851
9080
  this._operatorWriteDepth--;
8852
9081
  }
8853
9082
  }
8854
- async updateGlobalSettings(patch, nodeId) {
8855
- await this.applyGlobalPatch(patch, nodeId);
8856
- }
8857
9083
  /**
8858
9084
  * REMOVE stored secrets — their keychain values and their markers (D787).
8859
9085
  *
@@ -8863,6 +9089,9 @@ var BaseAddon = class {
8863
9089
  * a keychain (an older kernel) it writes `''`, as such code always did.
8864
9090
  */
8865
9091
  async clearSecrets(keys, nodeId) {
9092
+ await this.clearSecretKeys(keys, nodeId);
9093
+ }
9094
+ async clearSecretKeys(keys, nodeId, cap) {
8866
9095
  const tree = this.addonSecretTree();
8867
9096
  const keychain = this._ctx?.secrets !== void 0;
8868
9097
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8875,25 +9104,26 @@ var BaseAddon = class {
8875
9104
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8876
9105
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8877
9106
  }
8878
- await this.applyGlobalPatch(patch, nodeId);
9107
+ await this.applyGlobalPatch(patch, nodeId, cap);
8879
9108
  }
8880
- async applyGlobalPatch(barePatch, nodeId) {
8881
- await this.loadScopedSchemas();
8882
- 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);
8883
9113
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8884
9114
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8885
9115
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8886
9116
  const handle = this._secretSettings;
8887
9117
  if (handle !== null) {
8888
9118
  const { changed } = await handle.writeAddonStore(scoped);
8889
- if (!changed) return;
9119
+ if (!changed) return false;
8890
9120
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8891
- if (target !== localNode) return;
9121
+ if (target !== localNode) return true;
8892
9122
  await this.resolveConfig();
8893
9123
  await this.onConfigChanged();
8894
9124
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8895
- const legacy = this.globalSettingsSchema();
8896
- 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;
8897
9127
  }
8898
9128
  /**
8899
9129
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8930,10 +9160,7 @@ var BaseAddon = class {
8930
9160
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8931
9161
  this._addonSecretTree = null;
8932
9162
  }
8933
- /**
8934
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8935
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8936
- */
9163
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8937
9164
  scopedFormSchema(cap) {
8938
9165
  const byId = /* @__PURE__ */ new Map();
8939
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);
@@ -8945,8 +9172,7 @@ var BaseAddon = class {
8945
9172
  return [...this._scopedByCap.values()].flat();
8946
9173
  }
8947
9174
  /**
8948
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8949
- * 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
8950
9176
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8951
9177
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8952
9178
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8955,8 +9181,7 @@ var BaseAddon = class {
8955
9181
  const cacheKey = cap ?? "";
8956
9182
  const cached = this._nodeKeysByCap.get(cacheKey);
8957
9183
  if (cached) return cached;
8958
- const legacy = this.globalSettingsSchema(cap);
8959
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9184
+ const keys = /* @__PURE__ */ new Set();
8960
9185
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8961
9186
  if (scope !== "node") continue;
8962
9187
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9174,8 +9399,6 @@ var BaseAddon = class {
9174
9399
  }
9175
9400
  declaredKeySet() {
9176
9401
  const keys = new Set(Object.keys(this.defaults));
9177
- const schema = this.globalSettingsSchema();
9178
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9179
9402
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9180
9403
  for (const key of this._declaredStateKeys) keys.add(key);
9181
9404
  for (const path of this.secretStatePaths) {
@@ -9273,7 +9496,7 @@ var BaseAddon = class {
9273
9496
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9274
9497
  addonSecretTree() {
9275
9498
  if (this._addonSecretTree === null) {
9276
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9499
+ const tree = /* @__PURE__ */ new Map();
9277
9500
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9278
9501
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9279
9502
  }
@@ -9485,47 +9708,6 @@ function classifyStoreError(err) {
9485
9708
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9486
9709
  return "permanent";
9487
9710
  }
9488
- /**
9489
- * Collect the keys of every field marked `perNode: true`, recursing into
9490
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9491
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9492
- * don't declare `perNode` and are excluded by the `in` narrowing.
9493
- */
9494
- /**
9495
- * The same fields with every `perNode: true` one removed, recursing into layout
9496
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9497
- * with no child is dropped rather than rendered empty.
9498
- *
9499
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9500
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9501
- */
9502
- function dropPerNodeFields(fields) {
9503
- const kept = [];
9504
- for (const field of fields) {
9505
- if (field.type === "group") {
9506
- const inner = dropPerNodeFields(field.fields);
9507
- if (inner.length > 0) kept.push({
9508
- ...field,
9509
- fields: inner
9510
- });
9511
- continue;
9512
- }
9513
- if (field.type === "sub-tabs") {
9514
- const tabs = field.tabs.map((tab) => ({
9515
- ...tab,
9516
- fields: dropPerNodeFields(tab.fields)
9517
- })).filter((tab) => tab.fields.length > 0);
9518
- if (tabs.length > 0) kept.push({
9519
- ...field,
9520
- tabs
9521
- });
9522
- continue;
9523
- }
9524
- if ("perNode" in field && field.perNode === true) continue;
9525
- kept.push(field);
9526
- }
9527
- return kept;
9528
- }
9529
9711
  /** The keys of the value-holding fields, recursing into layout containers. */
9530
9712
  function collectValueFieldKeys(fields) {
9531
9713
  const collected = [];
@@ -9558,21 +9740,6 @@ function collectFieldKeys(fields) {
9558
9740
  }
9559
9741
  return collected;
9560
9742
  }
9561
- function collectPerNodeFieldKeys(fields) {
9562
- const collected = [];
9563
- for (const field of fields) {
9564
- if (field.type === "group") {
9565
- collected.push(...collectPerNodeFieldKeys(field.fields));
9566
- continue;
9567
- }
9568
- if (field.type === "sub-tabs") {
9569
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9570
- continue;
9571
- }
9572
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9573
- }
9574
- return collected;
9575
- }
9576
9743
  /**
9577
9744
  * Normalize an `ICamstackAddon.initialize()` return value into the
9578
9745
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10649,7 +10816,6 @@ var CAP_NAMES = Object.freeze([
10649
10816
  "notifier",
10650
10817
  "numeric-sensor",
10651
10818
  "oauth-integration",
10652
- "oidc-connections",
10653
10819
  "osd",
10654
10820
  "osd-manager",
10655
10821
  "pet-feeder",
@@ -12356,13 +12522,25 @@ Object.freeze({
12356
12522
  "path": "temperature",
12357
12523
  "valueType": "number"
12358
12524
  }],
12359
- "timed-run": [{
12360
- "path": "activeUntil",
12361
- "valueType": "number"
12362
- }, {
12363
- "path": "requestedBy",
12364
- "valueType": "string"
12365
- }],
12525
+ "timed-run": [
12526
+ {
12527
+ "path": "activeUntil",
12528
+ "valueType": "number"
12529
+ },
12530
+ {
12531
+ "path": "requestedBy",
12532
+ "valueType": "string"
12533
+ },
12534
+ {
12535
+ "path": "quickRun.duration",
12536
+ "valueType": "number"
12537
+ },
12538
+ {
12539
+ "path": "quickRun.unit",
12540
+ "valueType": "enum",
12541
+ "enumValues": ["s", "min"]
12542
+ }
12543
+ ],
12366
12544
  "update": [
12367
12545
  {
12368
12546
  "path": "currentVersion",
@@ -12541,8 +12719,7 @@ Object.freeze({
12541
12719
  "device-provider": { "reloadDevice": 18e4 },
12542
12720
  "llm": {
12543
12721
  "generate": 6e5,
12544
- "generateVision": 6e5,
12545
- "testProfile": 6e5
12722
+ "generateVision": 6e5
12546
12723
  },
12547
12724
  "llm-runtime": {
12548
12725
  "complete": 6e5,
@@ -12587,7 +12764,7 @@ object({
12587
12764
  params: record(string(), union([string(), number()])).optional()
12588
12765
  });
12589
12766
  //#endregion
12590
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12767
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12591
12768
  /**
12592
12769
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12593
12770
  *
@@ -13749,444 +13926,830 @@ var NodeColorIdSchema = _enum([
13749
13926
  "node-h"
13750
13927
  ]);
13751
13928
  //#endregion
13752
- //#region ../types/dist/index.mjs
13753
- var SMTP_INBOUND_DEFAULT_PORT = 2525;
13754
- var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13755
- var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13756
- /** The text excerpt a message carries on the bus. */
13757
- var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13758
- var PortSchema = number().int().min(1).max(65535);
13759
- /** A recipient local-part: what stands before the `@`. */
13760
- var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13761
- object({
13762
- port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13763
- bindHost: string().min(1).default("0.0.0.0"),
13764
- domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13765
- requireAuth: boolean().default(true),
13766
- username: string().default(""),
13767
- password: string().default("").meta({ secret: true }),
13768
- starttls: boolean().default(false),
13769
- maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13770
- });
13771
- object({
13772
- host: string().min(1).nullable(),
13773
- hostReason: string().nullable(),
13774
- port: PortSchema,
13775
- domain: string().min(1),
13776
- requireAuth: boolean(),
13777
- username: string(),
13778
- password: string(),
13779
- starttls: boolean()
13780
- });
13781
- object({
13782
- recipients: array(SmtpRecipientSchema).min(1),
13783
- /**
13784
- * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13785
- * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13786
- * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13787
- * Absent, the subscriber is anonymous and unique.
13788
- */
13789
- subscriberId: string().min(1).max(64).optional()
13790
- });
13791
- var SmtpInboundAttachmentSchema = object({
13792
- filename: string(),
13793
- contentType: string(),
13794
- bytes: number().int().nonnegative()
13795
- });
13796
- /** The payload of one `broker.message` (the key is the recipient local-part). */
13797
- var SmtpInboundMessageSchema = object({
13798
- /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13799
- seq: number().int().positive(),
13800
- messageId: string(),
13801
- receivedAt: number().int().nonnegative(),
13802
- from: string(),
13803
- to: string(),
13804
- subject: string(),
13805
- text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13806
- attachments: array(SmtpInboundAttachmentSchema)
13807
- });
13808
- object({
13809
- /** The newest seq issued so far (0 before any mail). */
13810
- lastSeq: number().int().nonnegative(),
13811
- /** The oldest seq still retained; messages below it are gone. */
13812
- oldestSeq: number().int().nonnegative(),
13813
- messages: array(object({
13814
- key: string(),
13815
- message: SmtpInboundMessageSchema
13816
- }))
13817
- });
13818
- new Set([
13819
- "track",
13820
- "face",
13821
- "identity",
13822
- "plate",
13823
- "vehicle",
13824
- "scene",
13825
- "motion",
13826
- "object",
13827
- "audio",
13828
- "mosaic"
13829
- ]);
13830
- /**
13831
- * The owner types that are event tables, in the order the event-media resolver
13832
- * should consider them. Exported so a consumer asking "is this key an event?"
13833
- * does not re-spell the list and drift from it.
13834
- */
13835
- var EVENT_OWNER_TYPES = [
13836
- "motion",
13837
- "object",
13838
- "audio"
13839
- ];
13840
- /**
13841
- * The same list as a Zod enum, for the cap inputs that must NAME the table
13842
- * (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
13843
- * `(table, id)`).
13844
- *
13845
- * Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
13846
- * table would otherwise be accepted by the codec and refused at the door, with
13847
- * nothing failing until a caller asked.
13848
- */
13849
- var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
13850
- /** The same list as a Zod enum, for the cap input that carries it. */
13851
- var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
13852
- new Set(EVENT_OWNER_TYPES);
13853
- /**
13854
- * What a published package needs from the rest of the install (D989).
13855
- *
13856
- * One published framework package (`@camstack/server`) carries the kernel, the
13857
- * builtins and the capability contracts, so "which addon runs on which
13858
- * framework" is a question of version, and the answer is DECLARED by the
13859
- * package that knows it:
13860
- *
13861
- * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
13862
- * - the server says the oldest addon it works with
13863
- * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
13864
- * around it (`requires.wrapper: { desktop, image }`);
13865
- * - a wrapper (the desktop app, the container starter) says which server
13866
- * range it can host, and reports its own kind and version.
13867
- *
13868
- * The publish workflow STAMPS these after the version bump
13869
- * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
13870
- *
13871
- * A requirement that cannot be checked is NOT met: a shell that reports no
13872
- * version counts as below the minimum, and an unreadable running version is
13873
- * named in the result (a failed read is never a pass).
13874
- */
13875
- /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
13876
- var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
13877
- object({
13878
- /** The oldest `@camstack/server` this package runs on. */
13879
- server: MinVersionSchema.optional(),
13880
- /** The oldest installed version of each named addon package. */
13881
- addons: record(string(), MinVersionSchema).optional(),
13882
- /** The oldest shell around the server, per shell kind. */
13883
- wrapper: object({
13884
- desktop: MinVersionSchema.optional(),
13885
- image: MinVersionSchema.optional()
13886
- }).optional()
13887
- });
13888
- /** One unmet requirement; each names the versions on both sides. */
13889
- var UnmetRequirementSchema = discriminatedUnion("kind", [
13890
- object({
13891
- kind: literal("server-too-old"),
13892
- required: string(),
13893
- /** `null` — the running server version could not be read. */
13894
- running: string().nullable()
13895
- }),
13896
- object({
13897
- kind: literal("addon-too-old"),
13898
- packageName: string(),
13899
- required: string(),
13900
- /** `null` — the addon is not installed (or its version is unreadable). */
13901
- running: string().nullable()
13902
- }),
13903
- object({
13904
- kind: literal("wrapper-too-old"),
13905
- wrapper: _enum(["desktop", "image"]),
13906
- required: string(),
13907
- /** `null` — the wrapper reported no version. */
13908
- running: string().nullable()
13909
- })
13910
- ]);
13911
- var RequiredAddonUpdateSchema = object({
13912
- packageName: string(),
13913
- /** The minimum the target server declares. */
13914
- required: string(),
13915
- /** What this node has; `null` when the addon is not installed. */
13916
- installed: string().nullable()
13917
- });
13918
- /**
13919
- * Which integration instance a settings form is for. An addon that serves several instances
13920
- * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
13921
- * needs its connection, the default integration needs the addon's own sections.
13922
- *
13923
- * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
13924
- * setting up a NEW instance.
13925
- * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
13926
- * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
13927
- */
13928
- var IntegrationInstanceRefSchema = object({
13929
- integrationId: string().nullable(),
13930
- mode: _enum([
13931
- "standalone",
13932
- "account",
13933
- "broker"
13934
- ]),
13935
- candidateStableId: string().optional()
13936
- });
13937
- /**
13938
- * notification-output — canonical, capability-gated notification delivery.
13939
- *
13940
- * Apprise-derived model (see
13941
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13942
- * callers emit ONE canonical `Notification`; each provider declares a
13943
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
13944
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13945
- * message to what the kind supports — callers never special-case a service.
13946
- *
13947
- * DESIGN DECISIONS (locked):
13948
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13949
- * `setTargetEnabled`), each provider persisting via the `settings-store`
13950
- * cap. Rationale: the admin UI needs one uniform surface across the
13951
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13952
- * alternative would fork the UI per addon and cannot host the
13953
- * discovery→adopt flow.
13954
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13955
- * the generated cap-mount auto-`concatCollection`-fans them across every
13956
- * registered provider (notifiers addon + HA addon) so one catalog is
13957
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13958
- * `addonId` the generated collection router extracts from the call input.
13959
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13960
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13961
- * `storage` / `storage-provider` / `recording` caps over the same path. No
13962
- * base64 fallback needed.
13963
- *
13964
- * TODO (deferred, closed-set change — separate decision): add
13965
- * `providerKind: 'notify'` so notification providers surface on the unified
13966
- * admin "Integrations" page.
13967
- */
13968
- /**
13969
- * Zentik-derived typed-media enum — the superset across every kind. Each
13970
- * adapter picks what it supports and the degrade engine filters the rest.
13971
- */
13972
- var AttachmentMediaTypeSchema = _enum([
13973
- "image",
13974
- "video",
13975
- "gif",
13976
- "audio",
13977
- "icon"
13978
- ]);
13979
- /**
13980
- * A single attachment. Exactly one of `url` (remote source, most adapters
13981
- * prefer this) or `bytes` (inline source; required for Pushover-style
13982
- * bytes-only kinds) MUST be present — the degrade engine expresses a
13983
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
13984
- */
13985
- var AttachmentSchema = object({
13986
- mediaType: AttachmentMediaTypeSchema,
13987
- url: string().optional(),
13988
- bytes: _instanceof(Uint8Array).optional(),
13989
- mime: string().optional(),
13990
- name: string().optional()
13991
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13992
- var NotificationFormatSchema = _enum([
13993
- "text",
13994
- "markdown",
13995
- "html"
13996
- ]);
13929
+ //#region ../types/dist/convert-BAzH15HU.mjs
13997
13930
  /**
13998
- * The CLOSED icon vocabulary an action button may use.
13999
- *
14000
- * A closed set, not a free string, and that is the whole point: an arbitrary
14001
- * icon name is one that ntfy renders, zentik silently drops, and nobody
14002
- * notices — the same class of gap as a zone vocabulary nothing produced
14003
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14004
- * declares `actionIcons: false` and the degrade engine strips the field.
14005
- *
14006
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14007
- * renderer's icon set; "acknowledge" survives an adapter that draws it
14008
- * differently.
14009
- *
14010
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14011
- *
14012
- * These names are for US. **No adapter may forward one verbatim.** Each maps
14013
- * the whole set onto its own renderer's vocabulary through a
14014
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14015
- * fallback, so adding a member here fails every adapter's build until someone
14016
- * decides its glyph, which is the only place that decision can be made
14017
- * honestly.
14018
- *
14019
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14020
- * `disarm` straight through; iOS feeds that string to
14021
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14022
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
14023
- * mapping, and "the field is documented" is not "the value renders".
14024
- *
14025
- * Adding a member is TRAIN-BOUND. The enum lives in the published
14026
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14027
- * so an addon that emits a token the running hub does not know does not lose an
14028
- * icon — its whole `send` fails Zod validation and the notification never
14029
- * arrives. Never emit a new token from an addon before the train carrying it.
13931
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13932
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13933
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13934
+ * decimals). K derives from `°C = K − 273.15`.
14030
13935
  */
14031
- var NotificationActionIconSchema = _enum([
14032
- "acknowledge",
14033
- "dismiss",
14034
- "silence",
14035
- "snooze",
14036
- "view",
14037
- "play",
14038
- "open",
14039
- "close",
14040
- "lock",
14041
- "unlock",
14042
- "arm",
14043
- "arm-home",
14044
- "arm-away",
14045
- "arm-night",
14046
- "disarm",
14047
- "light",
14048
- "alert",
14049
- "camera"
14050
- ]);
14051
- /** A single tap-through action button. */
14052
- var NotificationActionSchema = object({
14053
- id: string(),
14054
- label: string(),
14055
- url: string().optional(),
14056
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14057
- icon: NotificationActionIconSchema.optional(),
14058
- /**
14059
- * Renders in a warning style where the notifier supports it.
14060
- *
14061
- * A HINT, never a gate. The callback's authority is its token and nothing
14062
- * else — see `notification-center/action-token.ts` for what that does and
14063
- * does not buy.
14064
- */
14065
- destructive: boolean().optional(),
14066
- /**
14067
- * How the tap should REACH the url.
14068
- *
14069
- * `navigate` (absent, and every button authored before this field) opens it:
14070
- * the phone leaves the notification and shows whatever the callback returns.
14071
- * That is right for a button whose answer the operator wants to read.
14072
- *
14073
- * `background` fires it as a POST and stays put. It exists for the buttons
14074
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
14075
- * an answer to the notification, and being thrown into a browser tab to
14076
- * confirm it costs more attention than the notification did. A backend that
14077
- * cannot do a background call renders it as an ordinary link (the adapters
14078
- * fall back rather than dropping the button), so this is a preference, never
14079
- * a requirement.
14080
- */
14081
- mode: _enum(["navigate", "background"]).optional()
14082
- });
14083
- /** A delivery parameter value: what a notifier declared it takes. */
14084
- var DeliveryParamValueSchema = union([
14085
- string(),
14086
- number(),
14087
- boolean()
14088
- ]);
13936
+ var UNIT_TABLE = {
13937
+ "°C": {
13938
+ dimension: "temperature",
13939
+ factor: 1,
13940
+ offset: 0
13941
+ },
13942
+ "°F": {
13943
+ dimension: "temperature",
13944
+ factor: 5 / 9,
13945
+ offset: -160 / 9
13946
+ },
13947
+ K: {
13948
+ dimension: "temperature",
13949
+ factor: 1,
13950
+ offset: -273.15
13951
+ },
13952
+ hPa: {
13953
+ dimension: "pressure",
13954
+ factor: 1,
13955
+ offset: 0
13956
+ },
13957
+ kPa: {
13958
+ dimension: "pressure",
13959
+ factor: 10,
13960
+ offset: 0
13961
+ },
13962
+ Pa: {
13963
+ dimension: "pressure",
13964
+ factor: .01,
13965
+ offset: 0
13966
+ },
13967
+ mbar: {
13968
+ dimension: "pressure",
13969
+ factor: 1,
13970
+ offset: 0
13971
+ },
13972
+ bar: {
13973
+ dimension: "pressure",
13974
+ factor: 1e3,
13975
+ offset: 0
13976
+ },
13977
+ inHg: {
13978
+ dimension: "pressure",
13979
+ factor: 33.8639,
13980
+ offset: 0
13981
+ },
13982
+ mmHg: {
13983
+ dimension: "pressure",
13984
+ factor: 1.33322,
13985
+ offset: 0
13986
+ },
13987
+ psi: {
13988
+ dimension: "pressure",
13989
+ factor: 68.9476,
13990
+ offset: 0
13991
+ },
13992
+ "m/s": {
13993
+ dimension: "speed",
13994
+ factor: 1,
13995
+ offset: 0
13996
+ },
13997
+ "km/h": {
13998
+ dimension: "speed",
13999
+ factor: 1 / 3.6,
14000
+ offset: 0
14001
+ },
14002
+ mph: {
14003
+ dimension: "speed",
14004
+ factor: .44704,
14005
+ offset: 0
14006
+ },
14007
+ kn: {
14008
+ dimension: "speed",
14009
+ factor: .514444,
14010
+ offset: 0
14011
+ },
14012
+ "mm/h": {
14013
+ dimension: "precipitation-rate",
14014
+ factor: 1,
14015
+ offset: 0
14016
+ },
14017
+ "in/h": {
14018
+ dimension: "precipitation-rate",
14019
+ factor: 25.4,
14020
+ offset: 0
14021
+ },
14022
+ m: {
14023
+ dimension: "length",
14024
+ factor: 1,
14025
+ offset: 0
14026
+ },
14027
+ mm: {
14028
+ dimension: "length",
14029
+ factor: .001,
14030
+ offset: 0
14031
+ },
14032
+ cm: {
14033
+ dimension: "length",
14034
+ factor: .01,
14035
+ offset: 0
14036
+ },
14037
+ km: {
14038
+ dimension: "length",
14039
+ factor: 1e3,
14040
+ offset: 0
14041
+ },
14042
+ in: {
14043
+ dimension: "length",
14044
+ factor: .0254,
14045
+ offset: 0
14046
+ },
14047
+ ft: {
14048
+ dimension: "length",
14049
+ factor: .3048,
14050
+ offset: 0
14051
+ },
14052
+ mi: {
14053
+ dimension: "length",
14054
+ factor: 1609.344,
14055
+ offset: 0
14056
+ },
14057
+ "m²": {
14058
+ dimension: "area",
14059
+ factor: 1,
14060
+ offset: 0
14061
+ },
14062
+ lx: {
14063
+ dimension: "illuminance",
14064
+ factor: 1,
14065
+ offset: 0
14066
+ },
14067
+ "W/m²": {
14068
+ dimension: "irradiance",
14069
+ factor: 1,
14070
+ offset: 0
14071
+ },
14072
+ W: {
14073
+ dimension: "power",
14074
+ factor: 1,
14075
+ offset: 0
14076
+ },
14077
+ kW: {
14078
+ dimension: "power",
14079
+ factor: 1e3,
14080
+ offset: 0
14081
+ },
14082
+ VA: {
14083
+ dimension: "apparent-power",
14084
+ factor: 1,
14085
+ offset: 0
14086
+ },
14087
+ Wh: {
14088
+ dimension: "energy",
14089
+ factor: 1,
14090
+ offset: 0
14091
+ },
14092
+ kWh: {
14093
+ dimension: "energy",
14094
+ factor: 1e3,
14095
+ offset: 0
14096
+ },
14097
+ MWh: {
14098
+ dimension: "energy",
14099
+ factor: 1e6,
14100
+ offset: 0
14101
+ },
14102
+ V: {
14103
+ dimension: "voltage",
14104
+ factor: 1,
14105
+ offset: 0
14106
+ },
14107
+ mV: {
14108
+ dimension: "voltage",
14109
+ factor: .001,
14110
+ offset: 0
14111
+ },
14112
+ A: {
14113
+ dimension: "current",
14114
+ factor: 1,
14115
+ offset: 0
14116
+ },
14117
+ mA: {
14118
+ dimension: "current",
14119
+ factor: .001,
14120
+ offset: 0
14121
+ },
14122
+ ppm: {
14123
+ dimension: "concentration-volume",
14124
+ factor: 1,
14125
+ offset: 0
14126
+ },
14127
+ ppb: {
14128
+ dimension: "concentration-volume",
14129
+ factor: .001,
14130
+ offset: 0
14131
+ },
14132
+ "µg/m³": {
14133
+ dimension: "concentration-mass",
14134
+ factor: 1,
14135
+ offset: 0
14136
+ },
14137
+ "mg/m³": {
14138
+ dimension: "concentration-mass",
14139
+ factor: 1e3,
14140
+ offset: 0
14141
+ },
14142
+ kg: {
14143
+ dimension: "mass",
14144
+ factor: 1,
14145
+ offset: 0
14146
+ },
14147
+ g: {
14148
+ dimension: "mass",
14149
+ factor: .001,
14150
+ offset: 0
14151
+ },
14152
+ lb: {
14153
+ dimension: "mass",
14154
+ factor: .453592,
14155
+ offset: 0
14156
+ },
14157
+ oz: {
14158
+ dimension: "mass",
14159
+ factor: .0283495,
14160
+ offset: 0
14161
+ },
14162
+ "%": {
14163
+ dimension: "percentage",
14164
+ factor: 1,
14165
+ offset: 0
14166
+ },
14167
+ L: {
14168
+ dimension: "volume",
14169
+ factor: 1,
14170
+ offset: 0
14171
+ },
14172
+ mL: {
14173
+ dimension: "volume",
14174
+ factor: .001,
14175
+ offset: 0
14176
+ },
14177
+ gal: {
14178
+ dimension: "volume",
14179
+ factor: 3.785411784,
14180
+ offset: 0
14181
+ },
14182
+ s: {
14183
+ dimension: "duration",
14184
+ factor: 1,
14185
+ offset: 0
14186
+ },
14187
+ ms: {
14188
+ dimension: "duration",
14189
+ factor: .001,
14190
+ offset: 0
14191
+ },
14192
+ min: {
14193
+ dimension: "duration",
14194
+ factor: 60,
14195
+ offset: 0
14196
+ },
14197
+ h: {
14198
+ dimension: "duration",
14199
+ factor: 3600,
14200
+ offset: 0
14201
+ },
14202
+ d: {
14203
+ dimension: "duration",
14204
+ factor: 86400,
14205
+ offset: 0
14206
+ },
14207
+ B: {
14208
+ dimension: "information",
14209
+ factor: 1,
14210
+ offset: 0
14211
+ },
14212
+ KiB: {
14213
+ dimension: "information",
14214
+ factor: 1024,
14215
+ offset: 0
14216
+ },
14217
+ MiB: {
14218
+ dimension: "information",
14219
+ factor: 1024 ** 2,
14220
+ offset: 0
14221
+ },
14222
+ GiB: {
14223
+ dimension: "information",
14224
+ factor: 1024 ** 3,
14225
+ offset: 0
14226
+ },
14227
+ TiB: {
14228
+ dimension: "information",
14229
+ factor: 1024 ** 4,
14230
+ offset: 0
14231
+ },
14232
+ kB: {
14233
+ dimension: "information",
14234
+ factor: 1e3,
14235
+ offset: 0
14236
+ },
14237
+ MB: {
14238
+ dimension: "information",
14239
+ factor: 1e6,
14240
+ offset: 0
14241
+ },
14242
+ GB: {
14243
+ dimension: "information",
14244
+ factor: 1e9,
14245
+ offset: 0
14246
+ },
14247
+ "°": {
14248
+ dimension: "angle",
14249
+ factor: 1,
14250
+ offset: 0
14251
+ },
14252
+ dBFS: {
14253
+ dimension: "sound-level",
14254
+ factor: 1,
14255
+ offset: 0
14256
+ },
14257
+ dBm: {
14258
+ dimension: "signal-level",
14259
+ factor: 1,
14260
+ offset: 0
14261
+ },
14262
+ mired: {
14263
+ dimension: "colour-temperature",
14264
+ factor: 1,
14265
+ offset: 0
14266
+ },
14267
+ kbps: {
14268
+ dimension: "bitrate",
14269
+ factor: 1,
14270
+ offset: 0
14271
+ },
14272
+ Mbps: {
14273
+ dimension: "bitrate",
14274
+ factor: 1e3,
14275
+ offset: 0
14276
+ },
14277
+ fps: {
14278
+ dimension: "rate",
14279
+ factor: 1,
14280
+ offset: 0
14281
+ }
14282
+ };
14283
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14284
+ function specFor(unit) {
14285
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14286
+ }
14287
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
14288
+ function unitDimension(unit) {
14289
+ return specFor(unit)?.dimension;
14290
+ }
14291
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14292
+ function toCanonical(value, spec) {
14293
+ return value * spec.factor + spec.offset;
14294
+ }
14295
+ /** Canonical-unit value → the unit described by `spec`. */
14296
+ function fromCanonical(canonical, spec) {
14297
+ return (canonical - spec.offset) / spec.factor;
14298
+ }
14089
14299
  /**
14090
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
14091
- * addresses a target of that kind may set it; a kind that declares nothing is
14092
- * never handed one. The admin UI derives its control from this descriptor —
14093
- * no editor knows a service by name.
14094
- *
14095
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14096
- * run on the addon cap path).
14300
+ * Graceful variant for the render path: returns the converted value, or
14301
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14302
+ * Never throws — a render caller falls back to the raw value + source unit.
14097
14303
  */
14098
- var DeliveryParamSchema = object({
14099
- key: string().min(1),
14100
- label: string(),
14101
- /** i18n key of the label, resolved by the declaring addon's namespace. */
14102
- labelKey: string().optional(),
14103
- type: _enum([
14104
- "enum",
14105
- "number",
14106
- "boolean",
14107
- "string"
14108
- ]),
14109
- /** `enum` only: the closed set of accepted values. */
14110
- options: array(object({
14111
- value: string(),
14112
- label: string()
14113
- })).optional(),
14114
- /** `number` only. */
14115
- min: number().optional(),
14116
- max: number().optional(),
14117
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14118
- default: DeliveryParamValueSchema.optional()
14304
+ function tryConvertUnit(value, from, to) {
14305
+ if (from === to) return value;
14306
+ const fromSpec = specFor(from);
14307
+ if (!fromSpec) return void 0;
14308
+ const toSpec = specFor(to);
14309
+ if (!toSpec) return void 0;
14310
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14311
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14312
+ }
14313
+ //#endregion
14314
+ //#region ../types/dist/index.mjs
14315
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
14316
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
14317
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
14318
+ /** The text excerpt a message carries on the bus. */
14319
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
14320
+ var PortSchema = number().int().min(1).max(65535);
14321
+ /** A recipient local-part: what stands before the `@`. */
14322
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
14323
+ object({
14324
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
14325
+ bindHost: string().min(1).default("0.0.0.0"),
14326
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
14327
+ requireAuth: boolean().default(true),
14328
+ username: string().default(""),
14329
+ password: string().default("").meta({ secret: true }),
14330
+ starttls: boolean().default(false),
14331
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
14119
14332
  });
14120
- /**
14121
- * The canonical notification. `body` is the only hard field (Apprise model).
14122
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14123
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14124
- * the adapter maps this ordinal onto its native level. `level?` is an
14125
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
14126
- * `priority` for that one target.
14127
- */
14128
- var NotificationSchema = object({
14129
- body: string(),
14130
- title: string().optional(),
14131
- format: NotificationFormatSchema.default("text"),
14132
- priority: number().int().min(1).max(5).default(3),
14133
- level: string().optional(),
14134
- attachments: array(AttachmentSchema).optional(),
14135
- clickUrl: string().optional(),
14136
- actions: array(NotificationActionSchema).optional(),
14137
- sound: string().optional(),
14138
- ttl: number().optional(),
14139
- tag: string().optional(),
14140
- deviceId: number().optional(),
14141
- eventId: string().optional(),
14142
- metadata: record(string(), unknown()).optional(),
14333
+ object({
14334
+ host: string().min(1).nullable(),
14335
+ hostReason: string().nullable(),
14336
+ port: PortSchema,
14337
+ domain: string().min(1),
14338
+ requireAuth: boolean(),
14339
+ username: string(),
14340
+ password: string(),
14341
+ starttls: boolean()
14342
+ });
14343
+ object({
14344
+ recipients: array(SmtpRecipientSchema).min(1),
14143
14345
  /**
14144
- * Values for the parameters the target's kind DECLARES in
14145
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14146
- * declaration by the sender; a notifier reads only the keys it declared.
14346
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
14347
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
14348
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
14349
+ * Absent, the subscriber is anonymous and unique.
14147
14350
  */
14148
- delivery: record(string(), DeliveryParamValueSchema).optional()
14351
+ subscriberId: string().min(1).max(64).optional()
14149
14352
  });
14150
- /** One declared native severity/priority level for a kind. */
14151
- var TargetKindLevelSchema = object({
14152
- id: string(),
14153
- label: string(),
14154
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14155
- ordinal: number().int().min(1).max(5).nullable(),
14156
- flags: object({
14157
- critical: boolean().optional(),
14158
- silent: boolean().optional(),
14159
- noPush: boolean().optional()
14160
- }).optional(),
14161
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14162
- requires: array(string()).optional(),
14163
- description: string().optional()
14353
+ var SmtpInboundAttachmentSchema = object({
14354
+ filename: string(),
14355
+ contentType: string(),
14356
+ bytes: number().int().nonnegative()
14164
14357
  });
14165
- /** The full capability block consulted before dispatch. */
14166
- var TargetKindCapsSchema = object({
14167
- attachments: object({
14168
- mediaTypes: array(AttachmentMediaTypeSchema),
14169
- mode: _enum([
14170
- "url",
14171
- "bytes",
14172
- "both"
14173
- ]),
14174
- max: number().int().nonnegative(),
14175
- maxBytes: number().int().positive().optional()
14176
- }),
14177
- /** Max action buttons (0 = none). */
14178
- actions: number().int().nonnegative(),
14179
- /**
14180
- * Whether this kind renders a per-action ICON.
14181
- *
14182
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14183
- * run on the addon cap path — three production failures in one day taught
14184
- * this repo that once. Absent is read as false by the degrade engine, which
14185
- * is the safe direction: an icon that is not rendered costs nothing, an icon
14186
- * assumed and dropped costs the operator's trust in the field.
14187
- */
14188
- actionIcons: boolean().optional(),
14189
- levels: array(TargetKindLevelSchema),
14358
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
14359
+ var SmtpInboundMessageSchema = object({
14360
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
14361
+ seq: number().int().positive(),
14362
+ messageId: string(),
14363
+ receivedAt: number().int().nonnegative(),
14364
+ from: string(),
14365
+ to: string(),
14366
+ subject: string(),
14367
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
14368
+ attachments: array(SmtpInboundAttachmentSchema)
14369
+ });
14370
+ object({
14371
+ /** The newest seq issued so far (0 before any mail). */
14372
+ lastSeq: number().int().nonnegative(),
14373
+ /** The oldest seq still retained; messages below it are gone. */
14374
+ oldestSeq: number().int().nonnegative(),
14375
+ messages: array(object({
14376
+ key: string(),
14377
+ message: SmtpInboundMessageSchema
14378
+ }))
14379
+ });
14380
+ new Set([
14381
+ "track",
14382
+ "face",
14383
+ "identity",
14384
+ "plate",
14385
+ "vehicle",
14386
+ "scene",
14387
+ "motion",
14388
+ "object",
14389
+ "audio",
14390
+ "mosaic"
14391
+ ]);
14392
+ /**
14393
+ * The owner types that are event tables, in the order the event-media resolver
14394
+ * should consider them. Exported so a consumer asking "is this key an event?"
14395
+ * does not re-spell the list and drift from it.
14396
+ */
14397
+ var EVENT_OWNER_TYPES = [
14398
+ "motion",
14399
+ "object",
14400
+ "audio"
14401
+ ];
14402
+ /**
14403
+ * The same list as a Zod enum, for the cap inputs that must NAME the table
14404
+ * (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
14405
+ * `(table, id)`).
14406
+ *
14407
+ * Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
14408
+ * table would otherwise be accepted by the codec and refused at the door, with
14409
+ * nothing failing until a caller asked.
14410
+ */
14411
+ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
14412
+ /** The same list as a Zod enum, for the cap input that carries it. */
14413
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
14414
+ new Set(EVENT_OWNER_TYPES);
14415
+ /**
14416
+ * What a published package needs from the rest of the install (D989).
14417
+ *
14418
+ * One published framework package (`@camstack/server`) carries the kernel, the
14419
+ * builtins and the capability contracts, so "which addon runs on which
14420
+ * framework" is a question of version, and the answer is DECLARED by the
14421
+ * package that knows it:
14422
+ *
14423
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
14424
+ * - the server says the oldest addon it works with
14425
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
14426
+ * around it (`requires.wrapper: { desktop, image }`);
14427
+ * - a wrapper (the desktop app, the container starter) says which server
14428
+ * range it can host, and reports its own kind and version.
14429
+ *
14430
+ * The publish workflow STAMPS these after the version bump
14431
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
14432
+ *
14433
+ * A requirement that cannot be checked is NOT met: a shell that reports no
14434
+ * version counts as below the minimum, and an unreadable running version is
14435
+ * named in the result (a failed read is never a pass).
14436
+ */
14437
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
14438
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
14439
+ object({
14440
+ /** The oldest `@camstack/server` this package runs on. */
14441
+ server: MinVersionSchema.optional(),
14442
+ /** The oldest installed version of each named addon package. */
14443
+ addons: record(string(), MinVersionSchema).optional(),
14444
+ /** The oldest shell around the server, per shell kind. */
14445
+ wrapper: object({
14446
+ desktop: MinVersionSchema.optional(),
14447
+ image: MinVersionSchema.optional()
14448
+ }).optional()
14449
+ });
14450
+ /** One unmet requirement; each names the versions on both sides. */
14451
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
14452
+ object({
14453
+ kind: literal("server-too-old"),
14454
+ required: string(),
14455
+ /** `null` — the running server version could not be read. */
14456
+ running: string().nullable()
14457
+ }),
14458
+ object({
14459
+ kind: literal("addon-too-old"),
14460
+ packageName: string(),
14461
+ required: string(),
14462
+ /** `null` — the addon is not installed (or its version is unreadable). */
14463
+ running: string().nullable()
14464
+ }),
14465
+ object({
14466
+ kind: literal("wrapper-too-old"),
14467
+ wrapper: _enum(["desktop", "image"]),
14468
+ required: string(),
14469
+ /** `null` — the wrapper reported no version. */
14470
+ running: string().nullable()
14471
+ })
14472
+ ]);
14473
+ var RequiredAddonUpdateSchema = object({
14474
+ packageName: string(),
14475
+ /** The minimum the target server declares. */
14476
+ required: string(),
14477
+ /** What this node has; `null` when the addon is not installed. */
14478
+ installed: string().nullable()
14479
+ });
14480
+ /**
14481
+ * Which integration instance a settings form is for. An addon that serves several instances
14482
+ * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
14483
+ * needs its connection, the default integration needs the addon's own sections.
14484
+ *
14485
+ * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
14486
+ * setting up a NEW instance.
14487
+ * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
14488
+ * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
14489
+ */
14490
+ var IntegrationInstanceRefSchema = object({
14491
+ integrationId: string().nullable(),
14492
+ mode: _enum([
14493
+ "standalone",
14494
+ "account",
14495
+ "broker"
14496
+ ]),
14497
+ candidateStableId: string().optional()
14498
+ });
14499
+ /**
14500
+ * notification-output — canonical, capability-gated notification delivery.
14501
+ *
14502
+ * Apprise-derived model (see
14503
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
14504
+ * callers emit ONE canonical `Notification`; each provider declares a
14505
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
14506
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
14507
+ * message to what the kind supports — callers never special-case a service.
14508
+ *
14509
+ * DESIGN DECISIONS (locked):
14510
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14511
+ * providers serve their targets as `integration-instances` records, one
14512
+ * record kind per target kind, and the admin lists, adds, edits, switches
14513
+ * and deletes them through that cap. This cap keeps what an instance cannot
14514
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14515
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14516
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14517
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14518
+ * cap-mount auto-`concatCollection`-fans them across every registered
14519
+ * provider so one catalog is routable. `send` / `testTarget` /
14520
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14521
+ * collection router extracts from the call input.
14522
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14523
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14524
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
14525
+ * base64 fallback needed.
14526
+ *
14527
+ * TODO (deferred, closed-set change — separate decision): add
14528
+ * `providerKind: 'notify'` so notification providers surface on the unified
14529
+ * admin "Integrations" page.
14530
+ */
14531
+ /**
14532
+ * Zentik-derived typed-media enum — the superset across every kind. Each
14533
+ * adapter picks what it supports and the degrade engine filters the rest.
14534
+ */
14535
+ var AttachmentMediaTypeSchema = _enum([
14536
+ "image",
14537
+ "video",
14538
+ "gif",
14539
+ "audio",
14540
+ "icon"
14541
+ ]);
14542
+ /**
14543
+ * A single attachment. Exactly one of `url` (remote source, most adapters
14544
+ * prefer this) or `bytes` (inline source; required for Pushover-style
14545
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
14546
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
14547
+ */
14548
+ var AttachmentSchema = object({
14549
+ mediaType: AttachmentMediaTypeSchema,
14550
+ url: string().optional(),
14551
+ bytes: _instanceof(Uint8Array).optional(),
14552
+ mime: string().optional(),
14553
+ name: string().optional()
14554
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
14555
+ var NotificationFormatSchema = _enum([
14556
+ "text",
14557
+ "markdown",
14558
+ "html"
14559
+ ]);
14560
+ /**
14561
+ * The CLOSED icon vocabulary an action button may use.
14562
+ *
14563
+ * A closed set, not a free string, and that is the whole point: an arbitrary
14564
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
14565
+ * notices — the same class of gap as a zone vocabulary nothing produced
14566
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14567
+ * declares `actionIcons: false` and the degrade engine strips the field.
14568
+ *
14569
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14570
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
14571
+ * differently.
14572
+ *
14573
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14574
+ *
14575
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
14576
+ * the whole set onto its own renderer's vocabulary through a
14577
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14578
+ * fallback, so adding a member here fails every adapter's build until someone
14579
+ * decides its glyph, which is the only place that decision can be made
14580
+ * honestly.
14581
+ *
14582
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14583
+ * `disarm` straight through; iOS feeds that string to
14584
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14585
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
14586
+ * mapping, and "the field is documented" is not "the value renders".
14587
+ *
14588
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
14589
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14590
+ * so an addon that emits a token the running hub does not know does not lose an
14591
+ * icon — its whole `send` fails Zod validation and the notification never
14592
+ * arrives. Never emit a new token from an addon before the train carrying it.
14593
+ */
14594
+ var NotificationActionIconSchema = _enum([
14595
+ "acknowledge",
14596
+ "dismiss",
14597
+ "silence",
14598
+ "snooze",
14599
+ "view",
14600
+ "play",
14601
+ "open",
14602
+ "close",
14603
+ "lock",
14604
+ "unlock",
14605
+ "arm",
14606
+ "arm-home",
14607
+ "arm-away",
14608
+ "arm-night",
14609
+ "disarm",
14610
+ "light",
14611
+ "alert",
14612
+ "camera"
14613
+ ]);
14614
+ /** A single tap-through action button. */
14615
+ var NotificationActionSchema = object({
14616
+ id: string(),
14617
+ label: string(),
14618
+ url: string().optional(),
14619
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14620
+ icon: NotificationActionIconSchema.optional(),
14621
+ /**
14622
+ * Renders in a warning style where the notifier supports it.
14623
+ *
14624
+ * A HINT, never a gate. The callback's authority is its token and nothing
14625
+ * else — see `notification-center/action-token.ts` for what that does and
14626
+ * does not buy.
14627
+ */
14628
+ destructive: boolean().optional(),
14629
+ /**
14630
+ * How the tap should REACH the url.
14631
+ *
14632
+ * `navigate` (absent, and every button authored before this field) opens it:
14633
+ * the phone leaves the notification and shows whatever the callback returns.
14634
+ * That is right for a button whose answer the operator wants to read.
14635
+ *
14636
+ * `background` fires it as a POST and stays put. It exists for the buttons
14637
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14638
+ * an answer to the notification, and being thrown into a browser tab to
14639
+ * confirm it costs more attention than the notification did. A backend that
14640
+ * cannot do a background call renders it as an ordinary link (the adapters
14641
+ * fall back rather than dropping the button), so this is a preference, never
14642
+ * a requirement.
14643
+ */
14644
+ mode: _enum(["navigate", "background"]).optional()
14645
+ });
14646
+ /** A delivery parameter value: what a notifier declared it takes. */
14647
+ var DeliveryParamValueSchema = union([
14648
+ string(),
14649
+ number(),
14650
+ boolean()
14651
+ ]);
14652
+ /**
14653
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14654
+ * addresses a target of that kind may set it; a kind that declares nothing is
14655
+ * never handed one. The admin UI derives its control from this descriptor —
14656
+ * no editor knows a service by name.
14657
+ *
14658
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14659
+ * run on the addon cap path).
14660
+ */
14661
+ var DeliveryParamSchema = object({
14662
+ key: string().min(1),
14663
+ label: string(),
14664
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14665
+ labelKey: string().optional(),
14666
+ type: _enum([
14667
+ "enum",
14668
+ "number",
14669
+ "boolean",
14670
+ "string"
14671
+ ]),
14672
+ /** `enum` only: the closed set of accepted values. */
14673
+ options: array(object({
14674
+ value: string(),
14675
+ label: string()
14676
+ })).optional(),
14677
+ /** `number` only. */
14678
+ min: number().optional(),
14679
+ max: number().optional(),
14680
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14681
+ default: DeliveryParamValueSchema.optional()
14682
+ });
14683
+ /**
14684
+ * The canonical notification. `body` is the only hard field (Apprise model).
14685
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14686
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14687
+ * the adapter maps this ordinal onto its native level. `level?` is an
14688
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14689
+ * `priority` for that one target.
14690
+ */
14691
+ var NotificationSchema = object({
14692
+ body: string(),
14693
+ title: string().optional(),
14694
+ format: NotificationFormatSchema.default("text"),
14695
+ priority: number().int().min(1).max(5).default(3),
14696
+ level: string().optional(),
14697
+ attachments: array(AttachmentSchema).optional(),
14698
+ clickUrl: string().optional(),
14699
+ actions: array(NotificationActionSchema).optional(),
14700
+ sound: string().optional(),
14701
+ ttl: number().optional(),
14702
+ tag: string().optional(),
14703
+ deviceId: number().optional(),
14704
+ eventId: string().optional(),
14705
+ metadata: record(string(), unknown()).optional(),
14706
+ /**
14707
+ * Values for the parameters the target's kind DECLARES in
14708
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14709
+ * declaration by the sender; a notifier reads only the keys it declared.
14710
+ */
14711
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14712
+ });
14713
+ /** One declared native severity/priority level for a kind. */
14714
+ var TargetKindLevelSchema = object({
14715
+ id: string(),
14716
+ label: string(),
14717
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14718
+ ordinal: number().int().min(1).max(5).nullable(),
14719
+ flags: object({
14720
+ critical: boolean().optional(),
14721
+ silent: boolean().optional(),
14722
+ noPush: boolean().optional()
14723
+ }).optional(),
14724
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14725
+ requires: array(string()).optional(),
14726
+ description: string().optional()
14727
+ });
14728
+ /** The full capability block consulted before dispatch. */
14729
+ var TargetKindCapsSchema = object({
14730
+ attachments: object({
14731
+ mediaTypes: array(AttachmentMediaTypeSchema),
14732
+ mode: _enum([
14733
+ "url",
14734
+ "bytes",
14735
+ "both"
14736
+ ]),
14737
+ max: number().int().nonnegative(),
14738
+ maxBytes: number().int().positive().optional()
14739
+ }),
14740
+ /** Max action buttons (0 = none). */
14741
+ actions: number().int().nonnegative(),
14742
+ /**
14743
+ * Whether this kind renders a per-action ICON.
14744
+ *
14745
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14746
+ * run on the addon cap path — three production failures in one day taught
14747
+ * this repo that once. Absent is read as false by the degrade engine, which
14748
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14749
+ * assumed and dropped costs the operator's trust in the field.
14750
+ */
14751
+ actionIcons: boolean().optional(),
14752
+ levels: array(TargetKindLevelSchema),
14190
14753
  format: array(NotificationFormatSchema),
14191
14754
  clickUrl: boolean(),
14192
14755
  sound: boolean(),
@@ -14200,7 +14763,7 @@ var TargetKindCapsSchema = object({
14200
14763
  * the union is large and not meant for runtime validation here; the exported
14201
14764
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14202
14765
  */
14203
- var ConfigSchemaPassthrough$4 = unknown();
14766
+ var ConfigSchemaPassthrough$3 = unknown();
14204
14767
  var TargetKindSchema = object({
14205
14768
  kind: string(),
14206
14769
  label: string(),
@@ -14237,7 +14800,7 @@ var TargetKindSchema = object({
14237
14800
  * its raster path, which is the safe default for an unknown image.
14238
14801
  */
14239
14802
  iconMediaType: string().optional(),
14240
- configSchema: ConfigSchemaPassthrough$4,
14803
+ configSchema: ConfigSchemaPassthrough$3,
14241
14804
  /**
14242
14805
  * The config keys that decide WHERE this kind's secrets are sent (server,
14243
14806
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14259,9 +14822,11 @@ var TargetKindSchema = object({
14259
14822
  caps: TargetKindCapsSchema
14260
14823
  });
14261
14824
  /**
14262
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14263
- * (return a presence marker only) when serving `listTargets` — never
14264
- * round-trip a stored secret to the UI.
14825
+ * A target as a provider's own code sees it (the notification-center bridge, the
14826
+ * send path): the flat record body split into the name, the switch and the
14827
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14828
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14829
+ * secret to the UI.
14265
14830
  */
14266
14831
  var TargetSchema = object({
14267
14832
  id: string(),
@@ -14304,13 +14869,13 @@ var ProbeTargetConfigResultSchema = object({
14304
14869
  stateInstance: string().optional(),
14305
14870
  error: string().optional()
14306
14871
  });
14307
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14872
+ method(object({}), array(TargetKindSchema)), method(object({
14308
14873
  kind: string(),
14309
14874
  config: record(string(), unknown()).optional(),
14310
14875
  /**
14311
14876
  * When editing / duplicating, the blank password field never round-trips
14312
14877
  * the stored token. Pass the source target id so the provider can restore
14313
- * secrets server-side before dialling (same merge as upsert).
14878
+ * secrets server-side before dialling.
14314
14879
  */
14315
14880
  targetId: string().optional()
14316
14881
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14333,27 +14898,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14333
14898
  }), TestResultSchema, {
14334
14899
  kind: "mutation",
14335
14900
  auth: "admin"
14336
- }), method(object({
14337
- target: TargetSchema,
14338
- /**
14339
- * Duplicate flow: create a NEW target id while copying secrets from an
14340
- * existing row. The UI never holds the stored token; blank / redacted
14341
- * password fields are restored from this source (D634 still applies if
14342
- * destinations diverge without a re-typed secret).
14343
- */
14344
- cloneSecretsFrom: string().optional()
14345
- }), TargetSchema, {
14346
- kind: "mutation",
14347
- auth: "admin"
14348
- }), method(object({ targetId: string() }), _void(), {
14349
- kind: "mutation",
14350
- auth: "admin"
14351
- }), method(object({
14352
- targetId: string(),
14353
- enabled: boolean()
14354
- }), _void(), {
14355
- kind: "mutation",
14356
- auth: "admin"
14357
14901
  });
14358
14902
  looseObject({
14359
14903
  name: string().min(1),
@@ -22745,7 +23289,6 @@ var SYSTEM_CAP_VOCABULARY = {
22745
23289
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22746
23290
  "notification-rules": system("notification rules — authored on their own page"),
22747
23291
  "oauth-integration": system(IDENTITY),
22748
- "oidc-connections": system(IDENTITY),
22749
23292
  "osd-manager": system(PIPELINE),
22750
23293
  "pipeline-executor": system(PIPELINE),
22751
23294
  "pipeline-orchestrator": system(PIPELINE),
@@ -42628,726 +43171,344 @@ var LEGACY_ICON_ALIASES = { gauge2: "gauge" };
42628
43171
  * ## What is persisted
42629
43172
  *
42630
43173
  * The canonical kebab-case lucide id ({@link IconKey}). Spellings that a lucide
42631
- * package still accepts (`home` for `house`) and this repo's own legacy ones
42632
- * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
42633
- * stale, but they are never OFFERED by search.
42634
- *
42635
- * ## Search
42636
- *
42637
- * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
42638
- * the caller adds, and lucide's English tags — in that order of weight.
42639
- */
42640
- var KEY_SET$2 = new Set(ICON_KEYS);
42641
- new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
42642
- /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
42643
- function isIconKey(value) {
42644
- return typeof value === "string" && KEY_SET$2.has(value);
42645
- }
42646
- /**
42647
- * Wire/storage schema for one icon key.
42648
- *
42649
- * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
42650
- * into every cap router, type and API-reference row that carries an icon. The
42651
- * check is membership in the catalog and the type is {@link IconKey}.
42652
- */
42653
- var IconKeySchema = custom(isIconKey, "Not an icon of the catalog");
42654
- var ENTRIES = ICON_KEYS.map((key, index) => {
42655
- const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
42656
- const categories = categoryIndexes.flatMap((i) => {
42657
- const id = ICON_CATEGORY_IDS[i];
42658
- return id === void 0 ? [] : [id];
42659
- });
42660
- return {
42661
- key,
42662
- categories,
42663
- primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
42664
- tags,
42665
- synonyms: ICON_SYNONYMS[key] ?? []
42666
- };
42667
- });
42668
- new Map(ENTRIES.map((e) => [e.key, e]));
42669
- /** Categories a picker offers first: the ones this product's icons mostly live in. */
42670
- var LEADING_CATEGORIES = [
42671
- "devices",
42672
- "home",
42673
- "security",
42674
- "buildings",
42675
- "connectivity",
42676
- "multimedia",
42677
- "nature",
42678
- "weather",
42679
- "transportation",
42680
- "animals"
42681
- ];
42682
- var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
42683
- [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
42684
- /**
42685
- * One (location, icon) assignment as it travels.
42686
- *
42687
- * `locationKey` is ALWAYS {@link locationIconKey} of the label — never the
42688
- * operator's casing. The registry matches labels trimmed + case-insensitively
42689
- * everywhere (`addLocation`, `removeLocation`, `renameLocation`), and a
42690
- * case-only rename (`kitchen` → `Kitchen`) is a real edit this repo supports; a
42691
- * map keyed on the displayed casing would orphan its entry on exactly that
42692
- * edit. Consumers join by keying their own label through `locationIconKey`.
42693
- */
42694
- var LocationIconAssignmentSchema = object({
42695
- locationKey: string(),
42696
- icon: IconKeySchema
42697
- });
42698
- /**
42699
- * One (location, description) assignment as it travels.
42700
- *
42701
- * Same key discipline as {@link LocationIconAssignmentSchema}, and for the same
42702
- * reason: this lived for a while in the post-analysis addon's own settings,
42703
- * keyed by a HAND-TYPED label matched exactly and case-sensitively. A rename in
42704
- * the Locations page carried the icon and silently orphaned the description —
42705
- * two authorities for one thing, joined by two different rules. Now both hang
42706
- * off the location itself, on `locationIconKey`.
42707
- */
42708
- var LocationDescriptionAssignmentSchema = object({
42709
- locationKey: string(),
42710
- description: string()
42711
- });
42712
- /**
42713
- * WHO an action step acts on (D983) — one target shape for every place that
42714
- * performs an action (automation steps, Action entries, notification
42715
- * sequences, scheduler points):
42716
- *
42717
- * - `devices` — device ids the operator picked, frozen at save time;
42718
- * - `domain` — every device that offers the step's VOCABULARY action,
42719
- * optionally narrowed to one device type and one room, resolved WHEN THE
42720
- * STEP RUNS (a device added later is included). The action always comes
42721
- * from the vocabulary (`cover.open`), never from a raw cap name, so a
42722
- * `domain` target needs an `actionId` (enforced on the step schema);
42723
- * - `var` — a `$var` resolved from the run's scope.
42724
- *
42725
- * {@link ActionTargetResolver} turns a target into device ids. It is pure
42726
- * (a {@link DeviceDirectory} is handed in) so the hub's core-blocks service and
42727
- * a generated automation block share it.
42728
- *
42729
- * A room is the device's `location` label (`DeviceInfo.location`); two labels
42730
- * name the same room when `locationIconKey` agrees (trimmed, lower-cased).
42731
- */
42732
- var ActionTargetSchema = discriminatedUnion("kind", [
42733
- object({
42734
- kind: literal("devices"),
42735
- ids: array(number().int()).min(1)
42736
- }),
42737
- object({
42738
- kind: literal("domain"),
42739
- /** Narrow to one kind of device. */
42740
- deviceType: _enum(DeviceType).optional(),
42741
- /** Narrow to one room: the `location` label devices carry. */
42742
- roomId: string().min(1).optional()
42743
- }),
42744
- object({
42745
- kind: literal("var"),
42746
- name: string().min(1)
42747
- })
42748
- ]);
42749
- EventCategory.DeviceRegistered, EventCategory.DeviceUnregistered, EventCategory.DeviceUpdated, EventCategory.DeviceMetaChanged, EventCategory.DeviceBindingsChanged;
42750
- var ExpressionBindingSourceSchema = union([
42751
- object({
42752
- kind: literal("field").optional(),
42753
- sourceKey: string(),
42754
- cap: string(),
42755
- fieldPath: string()
42756
- }),
42757
- object({
42758
- kind: literal("literal"),
42759
- value: union([
42760
- string(),
42761
- number(),
42762
- boolean(),
42763
- _null()
42764
- ])
42765
- }),
42766
- object({
42767
- kind: literal("global"),
42768
- sourceStableId: string(),
42769
- cap: string(),
42770
- fieldPath: string()
42771
- })
42772
- ]);
42773
- object({
42774
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
42775
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
42776
- }).superRefine((src, ctx) => {
42777
- const err = validateExpressionSource(src);
42778
- if (err !== null) ctx.addIssue({
42779
- code: "custom",
42780
- message: err,
42781
- path: ["expr"]
42782
- });
42783
- });
42784
- /**
42785
- * Identity envelope for a device's upstream-system metadata.
42786
- *
42787
- * Two jobs:
42788
- *
42789
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
42790
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
42791
- * WS-Discovery endpoint UUID, …). Provider code keeps a
42792
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
42793
- * without round-tripping through `stableId` / `id` mappings.
42794
- *
42795
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
42796
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
42797
- * for vendors where MAC is the natural key). For systems where the
42798
- * live dispatch key can be renamed (HA `entity_id` can be edited),
42799
- * providers detect renames by matching `uniqueId` and patch `id`
42800
- * in-place so dispatch keeps working.
42801
- *
42802
- * **Display unit and precision are NOT stored here.** They are a read-time
42803
- * preference (D944). A polymorphic reading stores its `quantity` here once,
42804
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
42805
- * neither is repeated on each sample. The `device_class` tag is an
42806
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
42807
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
42808
- *
42809
- * The `raw` escape hatch carries provider-specific attributes that don't
42810
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
42811
- * details, …). Providers control what lands here.
42812
- */
42813
- var SourceInfoSchema = object({
42814
- /** Live dispatch key — mutable when the source system allows rename. */
42815
- id: string(),
42816
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
42817
- system: string(),
42818
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
42819
- * Used to detect rename when `id` changes. */
42820
- uniqueId: string().optional(),
42821
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
42822
- quantity: ReadingQuantitySchema.optional(),
42823
- /** Date / time / datetime shape of a string reading, written once at adopt. */
42824
- dateTimeShape: _enum([
42825
- "date",
42826
- "time",
42827
- "datetime"
42828
- ]).optional(),
42829
- /** Provider-specific extras that don't fit the structured slots.
42830
- * Whitelist what goes here per provider — do NOT dump entire HA
42831
- * attribute blobs (would explode the meta JSON column). */
42832
- raw: record(string(), unknown()).optional()
42833
- });
42834
- /**
42835
- * Build the synthetic SourceInfo every existing provider falls back to
42836
- * when no upstream value has been persisted. Keeps non-migrated providers
42837
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
42838
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
42839
- * id. Real upstream identity replaces this once a provider migrates and
42840
- * calls `updateSourceInfo()` with concrete values.
42841
- */
42842
- function synthesizeSourceInfo(input) {
42843
- return {
42844
- id: input.stableId,
42845
- system: input.addonId
42846
- };
42847
- }
42848
- /**
42849
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
42850
- * patch values are ignored (no field clobber); to clear a field
42851
- * explicitly, providers omit it from the patch and rebuild from scratch
42852
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
42853
- * bother with a `null`-means-delete sentinel.
42854
- *
42855
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
42856
- * merges over existing `raw` keys instead of replacing the whole bag.
42857
- */
42858
- function mergeSourceInfo(prev, patch) {
42859
- const next = { ...prev };
42860
- for (const [k, v] of Object.entries(patch)) {
42861
- if (v === void 0) continue;
42862
- if (k === "raw" && typeof v === "object" && v !== null) {
42863
- next.raw = {
42864
- ...prev.raw,
42865
- ...v
42866
- };
42867
- continue;
42868
- }
42869
- next[k] = v;
42870
- }
42871
- return next;
42872
- }
42873
- /**
42874
- * Coerce common unit-of-measurement string variants to a canonical form
42875
- * so the UI's display templates can rely on stable strings. Pure
42876
- * passthrough for unknown units — kept conservative on purpose, the goal
42877
- * is to normalise the long tail of HA `unit_of_measurement` values that
42878
- * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
42879
- *
42880
- * Extend the map as we encounter real-world data. Returning the raw
42881
- * input for unknown values is intentional: providers should never
42882
- * lose information silently here.
42883
- */
42884
- function normalizeUnit(raw) {
42885
- if (raw == null) return void 0;
42886
- const trimmed = raw.trim();
42887
- if (trimmed === "") return void 0;
42888
- const lower = trimmed.toLowerCase();
42889
- if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
42890
- if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
42891
- if (lower === "k" || lower === "kelvin") return "K";
42892
- if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
42893
- if (lower === "lx" || lower === "lux") return "lx";
42894
- if (lower === "hpa" || lower === "hectopascal") return "hPa";
42895
- if (lower === "kpa") return "kPa";
42896
- if (lower === "pa") return "Pa";
42897
- if (lower === "mbar" || lower === "millibar") return "mbar";
42898
- if (lower === "w" || lower === "watt") return "W";
42899
- if (lower === "kw" || lower === "kilowatt") return "kW";
42900
- if (lower === "wh" || lower === "watt-hour") return "Wh";
42901
- if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
42902
- if (lower === "v" || lower === "volt") return "V";
42903
- if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
42904
- if (lower === "ma" || lower === "milliampere") return "mA";
42905
- if (lower === "va") return "VA";
42906
- if (lower === "ppm") return "ppm";
42907
- if (lower === "ppb") return "ppb";
42908
- if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
42909
- if (lower === "km/h" || lower === "kph") return "km/h";
42910
- if (lower === "mph") return "mph";
42911
- if (lower === "m/s") return "m/s";
42912
- if (lower === "m") return "m";
42913
- if (lower === "m²" || lower === "m2") return "m²";
42914
- if (lower === "mm") return "mm";
42915
- if (lower === "cm") return "cm";
42916
- if (lower === "km") return "km";
42917
- if (lower === "in" || lower === "inch" || lower === "inches") return "in";
42918
- if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
42919
- if (lower === "kg") return "kg";
42920
- if (lower === "g") return "g";
42921
- if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
42922
- if (lower === "ml") return "mL";
42923
- if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
42924
- if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
42925
- if (lower === "ms") return "ms";
42926
- if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
42927
- if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
42928
- if (lower === "d" || lower === "day" || lower === "days") return "d";
42929
- if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
42930
- if (lower === "kib") return "KiB";
42931
- if (lower === "mib") return "MiB";
42932
- if (lower === "gib") return "GiB";
42933
- if (lower === "tib") return "TiB";
42934
- if (lower === "kb") return "kB";
42935
- if (lower === "mb") return "MB";
42936
- if (lower === "gb") return "GB";
42937
- if (lower === "dbm") return "dBm";
42938
- if (lower === "dbfs") return "dBFS";
42939
- if (lower === "inhg") return "inHg";
42940
- if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
42941
- if (lower === "in/h" || lower === "in/hr") return "in/h";
42942
- if (lower === "w/m²" || lower === "w/m2") return "W/m²";
42943
- if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
42944
- if (lower === "kbps") return "kbps";
42945
- if (lower === "fps") return "fps";
42946
- return trimmed;
42947
- }
42948
- /**
42949
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
42950
- * blob is a free-form `Record<string, unknown>` shared with hardware-
42951
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
42952
- * lives under a single nested key to keep it composable with those.
42953
- */
42954
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
42955
- /**
42956
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
42957
- * with Zod. Returns `null` when the blob is missing or the nested
42958
- * `sourceInfo` key fails validation — callers fall back to the
42959
- * synthetic default in that case (see `synthesizeSourceInfo`).
42960
- */
42961
- function extractSourceInfoFromMetadata(metadata) {
42962
- if (!metadata) return null;
42963
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
42964
- if (!raw) return null;
42965
- const parsed = SourceInfoSchema.safeParse(raw);
42966
- if (!parsed.success) return null;
42967
- return parsed.data;
42968
- }
42969
- /**
42970
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42971
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42972
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42973
- * decimals). K derives from `°C = K − 273.15`.
42974
- */
42975
- var UNIT_TABLE = {
42976
- "°C": {
42977
- dimension: "temperature",
42978
- factor: 1,
42979
- offset: 0
42980
- },
42981
- "°F": {
42982
- dimension: "temperature",
42983
- factor: 5 / 9,
42984
- offset: -160 / 9
42985
- },
42986
- K: {
42987
- dimension: "temperature",
42988
- factor: 1,
42989
- offset: -273.15
42990
- },
42991
- hPa: {
42992
- dimension: "pressure",
42993
- factor: 1,
42994
- offset: 0
42995
- },
42996
- kPa: {
42997
- dimension: "pressure",
42998
- factor: 10,
42999
- offset: 0
43000
- },
43001
- Pa: {
43002
- dimension: "pressure",
43003
- factor: .01,
43004
- offset: 0
43005
- },
43006
- mbar: {
43007
- dimension: "pressure",
43008
- factor: 1,
43009
- offset: 0
43010
- },
43011
- bar: {
43012
- dimension: "pressure",
43013
- factor: 1e3,
43014
- offset: 0
43015
- },
43016
- inHg: {
43017
- dimension: "pressure",
43018
- factor: 33.8639,
43019
- offset: 0
43020
- },
43021
- mmHg: {
43022
- dimension: "pressure",
43023
- factor: 1.33322,
43024
- offset: 0
43025
- },
43026
- psi: {
43027
- dimension: "pressure",
43028
- factor: 68.9476,
43029
- offset: 0
43030
- },
43031
- "m/s": {
43032
- dimension: "speed",
43033
- factor: 1,
43034
- offset: 0
43035
- },
43036
- "km/h": {
43037
- dimension: "speed",
43038
- factor: 1 / 3.6,
43039
- offset: 0
43040
- },
43041
- mph: {
43042
- dimension: "speed",
43043
- factor: .44704,
43044
- offset: 0
43045
- },
43046
- kn: {
43047
- dimension: "speed",
43048
- factor: .514444,
43049
- offset: 0
43050
- },
43051
- "mm/h": {
43052
- dimension: "precipitation-rate",
43053
- factor: 1,
43054
- offset: 0
43055
- },
43056
- "in/h": {
43057
- dimension: "precipitation-rate",
43058
- factor: 25.4,
43059
- offset: 0
43060
- },
43061
- m: {
43062
- dimension: "length",
43063
- factor: 1,
43064
- offset: 0
43065
- },
43066
- mm: {
43067
- dimension: "length",
43068
- factor: .001,
43069
- offset: 0
43070
- },
43071
- cm: {
43072
- dimension: "length",
43073
- factor: .01,
43074
- offset: 0
43075
- },
43076
- km: {
43077
- dimension: "length",
43078
- factor: 1e3,
43079
- offset: 0
43080
- },
43081
- in: {
43082
- dimension: "length",
43083
- factor: .0254,
43084
- offset: 0
43085
- },
43086
- ft: {
43087
- dimension: "length",
43088
- factor: .3048,
43089
- offset: 0
43090
- },
43091
- mi: {
43092
- dimension: "length",
43093
- factor: 1609.344,
43094
- offset: 0
43095
- },
43096
- "m²": {
43097
- dimension: "area",
43098
- factor: 1,
43099
- offset: 0
43100
- },
43101
- lx: {
43102
- dimension: "illuminance",
43103
- factor: 1,
43104
- offset: 0
43105
- },
43106
- "W/m²": {
43107
- dimension: "irradiance",
43108
- factor: 1,
43109
- offset: 0
43110
- },
43111
- W: {
43112
- dimension: "power",
43113
- factor: 1,
43114
- offset: 0
43115
- },
43116
- kW: {
43117
- dimension: "power",
43118
- factor: 1e3,
43119
- offset: 0
43120
- },
43121
- VA: {
43122
- dimension: "apparent-power",
43123
- factor: 1,
43124
- offset: 0
43125
- },
43126
- Wh: {
43127
- dimension: "energy",
43128
- factor: 1,
43129
- offset: 0
43130
- },
43131
- kWh: {
43132
- dimension: "energy",
43133
- factor: 1e3,
43134
- offset: 0
43135
- },
43136
- MWh: {
43137
- dimension: "energy",
43138
- factor: 1e6,
43139
- offset: 0
43140
- },
43141
- V: {
43142
- dimension: "voltage",
43143
- factor: 1,
43144
- offset: 0
43145
- },
43146
- mV: {
43147
- dimension: "voltage",
43148
- factor: .001,
43149
- offset: 0
43150
- },
43151
- A: {
43152
- dimension: "current",
43153
- factor: 1,
43154
- offset: 0
43155
- },
43156
- mA: {
43157
- dimension: "current",
43158
- factor: .001,
43159
- offset: 0
43160
- },
43161
- ppm: {
43162
- dimension: "concentration-volume",
43163
- factor: 1,
43164
- offset: 0
43165
- },
43166
- ppb: {
43167
- dimension: "concentration-volume",
43168
- factor: .001,
43169
- offset: 0
43170
- },
43171
- "µg/m³": {
43172
- dimension: "concentration-mass",
43173
- factor: 1,
43174
- offset: 0
43175
- },
43176
- "mg/m³": {
43177
- dimension: "concentration-mass",
43178
- factor: 1e3,
43179
- offset: 0
43180
- },
43181
- kg: {
43182
- dimension: "mass",
43183
- factor: 1,
43184
- offset: 0
43185
- },
43186
- g: {
43187
- dimension: "mass",
43188
- factor: .001,
43189
- offset: 0
43190
- },
43191
- lb: {
43192
- dimension: "mass",
43193
- factor: .453592,
43194
- offset: 0
43195
- },
43196
- oz: {
43197
- dimension: "mass",
43198
- factor: .0283495,
43199
- offset: 0
43200
- },
43201
- "%": {
43202
- dimension: "percentage",
43203
- factor: 1,
43204
- offset: 0
43205
- },
43206
- L: {
43207
- dimension: "volume",
43208
- factor: 1,
43209
- offset: 0
43210
- },
43211
- mL: {
43212
- dimension: "volume",
43213
- factor: .001,
43214
- offset: 0
43215
- },
43216
- gal: {
43217
- dimension: "volume",
43218
- factor: 3.785411784,
43219
- offset: 0
43220
- },
43221
- s: {
43222
- dimension: "duration",
43223
- factor: 1,
43224
- offset: 0
43225
- },
43226
- ms: {
43227
- dimension: "duration",
43228
- factor: .001,
43229
- offset: 0
43230
- },
43231
- min: {
43232
- dimension: "duration",
43233
- factor: 60,
43234
- offset: 0
43235
- },
43236
- h: {
43237
- dimension: "duration",
43238
- factor: 3600,
43239
- offset: 0
43240
- },
43241
- d: {
43242
- dimension: "duration",
43243
- factor: 86400,
43244
- offset: 0
43245
- },
43246
- B: {
43247
- dimension: "information",
43248
- factor: 1,
43249
- offset: 0
43250
- },
43251
- KiB: {
43252
- dimension: "information",
43253
- factor: 1024,
43254
- offset: 0
43255
- },
43256
- MiB: {
43257
- dimension: "information",
43258
- factor: 1024 ** 2,
43259
- offset: 0
43260
- },
43261
- GiB: {
43262
- dimension: "information",
43263
- factor: 1024 ** 3,
43264
- offset: 0
43265
- },
43266
- TiB: {
43267
- dimension: "information",
43268
- factor: 1024 ** 4,
43269
- offset: 0
43270
- },
43271
- kB: {
43272
- dimension: "information",
43273
- factor: 1e3,
43274
- offset: 0
43275
- },
43276
- MB: {
43277
- dimension: "information",
43278
- factor: 1e6,
43279
- offset: 0
43280
- },
43281
- GB: {
43282
- dimension: "information",
43283
- factor: 1e9,
43284
- offset: 0
43285
- },
43286
- "°": {
43287
- dimension: "angle",
43288
- factor: 1,
43289
- offset: 0
43290
- },
43291
- dBFS: {
43292
- dimension: "sound-level",
43293
- factor: 1,
43294
- offset: 0
43295
- },
43296
- dBm: {
43297
- dimension: "signal-level",
43298
- factor: 1,
43299
- offset: 0
43300
- },
43301
- mired: {
43302
- dimension: "colour-temperature",
43303
- factor: 1,
43304
- offset: 0
43305
- },
43306
- kbps: {
43307
- dimension: "bitrate",
43308
- factor: 1,
43309
- offset: 0
43310
- },
43311
- Mbps: {
43312
- dimension: "bitrate",
43313
- factor: 1e3,
43314
- offset: 0
43315
- },
43316
- fps: {
43317
- dimension: "rate",
43318
- factor: 1,
43319
- offset: 0
43320
- }
43321
- };
43322
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43323
- function specFor(unit) {
43324
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43174
+ * package still accepts (`home` for `house`) and this repo's own legacy ones
43175
+ * (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
43176
+ * stale, but they are never OFFERED by search.
43177
+ *
43178
+ * ## Search
43179
+ *
43180
+ * Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
43181
+ * the caller adds, and lucide's English tags — in that order of weight.
43182
+ */
43183
+ var KEY_SET$2 = new Set(ICON_KEYS);
43184
+ new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
43185
+ /** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
43186
+ function isIconKey(value) {
43187
+ return typeof value === "string" && KEY_SET$2.has(value);
43325
43188
  }
43326
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
43327
- function unitDimension(unit) {
43328
- return specFor(unit)?.dimension;
43189
+ /**
43190
+ * Wire/storage schema for one icon key.
43191
+ *
43192
+ * Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
43193
+ * into every cap router, type and API-reference row that carries an icon. The
43194
+ * check is membership in the catalog and the type is {@link IconKey}.
43195
+ */
43196
+ var IconKeySchema = custom(isIconKey, "Not an icon of the catalog");
43197
+ var ENTRIES = ICON_KEYS.map((key, index) => {
43198
+ const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
43199
+ const categories = categoryIndexes.flatMap((i) => {
43200
+ const id = ICON_CATEGORY_IDS[i];
43201
+ return id === void 0 ? [] : [id];
43202
+ });
43203
+ return {
43204
+ key,
43205
+ categories,
43206
+ primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
43207
+ tags,
43208
+ synonyms: ICON_SYNONYMS[key] ?? []
43209
+ };
43210
+ });
43211
+ new Map(ENTRIES.map((e) => [e.key, e]));
43212
+ /** Categories a picker offers first: the ones this product's icons mostly live in. */
43213
+ var LEADING_CATEGORIES = [
43214
+ "devices",
43215
+ "home",
43216
+ "security",
43217
+ "buildings",
43218
+ "connectivity",
43219
+ "multimedia",
43220
+ "nature",
43221
+ "weather",
43222
+ "transportation",
43223
+ "animals"
43224
+ ];
43225
+ var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
43226
+ [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
43227
+ /**
43228
+ * One (location, icon) assignment as it travels.
43229
+ *
43230
+ * `locationKey` is ALWAYS {@link locationIconKey} of the label — never the
43231
+ * operator's casing. The registry matches labels trimmed + case-insensitively
43232
+ * everywhere (`addLocation`, `removeLocation`, `renameLocation`), and a
43233
+ * case-only rename (`kitchen` → `Kitchen`) is a real edit this repo supports; a
43234
+ * map keyed on the displayed casing would orphan its entry on exactly that
43235
+ * edit. Consumers join by keying their own label through `locationIconKey`.
43236
+ */
43237
+ var LocationIconAssignmentSchema = object({
43238
+ locationKey: string(),
43239
+ icon: IconKeySchema
43240
+ });
43241
+ /**
43242
+ * One (location, description) assignment as it travels.
43243
+ *
43244
+ * Same key discipline as {@link LocationIconAssignmentSchema}, and for the same
43245
+ * reason: this lived for a while in the post-analysis addon's own settings,
43246
+ * keyed by a HAND-TYPED label matched exactly and case-sensitively. A rename in
43247
+ * the Locations page carried the icon and silently orphaned the description —
43248
+ * two authorities for one thing, joined by two different rules. Now both hang
43249
+ * off the location itself, on `locationIconKey`.
43250
+ */
43251
+ var LocationDescriptionAssignmentSchema = object({
43252
+ locationKey: string(),
43253
+ description: string()
43254
+ });
43255
+ /**
43256
+ * WHO an action step acts on (D983) — one target shape for every place that
43257
+ * performs an action (automation steps, Action entries, notification
43258
+ * sequences, scheduler points):
43259
+ *
43260
+ * - `devices` — device ids the operator picked, frozen at save time;
43261
+ * - `domain` — every device that offers the step's VOCABULARY action,
43262
+ * optionally narrowed to one device type and one room, resolved WHEN THE
43263
+ * STEP RUNS (a device added later is included). The action always comes
43264
+ * from the vocabulary (`cover.open`), never from a raw cap name, so a
43265
+ * `domain` target needs an `actionId` (enforced on the step schema);
43266
+ * - `var` — a `$var` resolved from the run's scope.
43267
+ *
43268
+ * {@link ActionTargetResolver} turns a target into device ids. It is pure
43269
+ * (a {@link DeviceDirectory} is handed in) so the hub's core-blocks service and
43270
+ * a generated automation block share it.
43271
+ *
43272
+ * A room is the device's `location` label (`DeviceInfo.location`); two labels
43273
+ * name the same room when `locationIconKey` agrees (trimmed, lower-cased).
43274
+ */
43275
+ var ActionTargetSchema = discriminatedUnion("kind", [
43276
+ object({
43277
+ kind: literal("devices"),
43278
+ ids: array(number().int()).min(1)
43279
+ }),
43280
+ object({
43281
+ kind: literal("domain"),
43282
+ /** Narrow to one kind of device. */
43283
+ deviceType: _enum(DeviceType).optional(),
43284
+ /** Narrow to one room: the `location` label devices carry. */
43285
+ roomId: string().min(1).optional()
43286
+ }),
43287
+ object({
43288
+ kind: literal("var"),
43289
+ name: string().min(1)
43290
+ })
43291
+ ]);
43292
+ EventCategory.DeviceRegistered, EventCategory.DeviceUnregistered, EventCategory.DeviceUpdated, EventCategory.DeviceMetaChanged, EventCategory.DeviceBindingsChanged;
43293
+ var ExpressionBindingSourceSchema = union([
43294
+ object({
43295
+ kind: literal("field").optional(),
43296
+ sourceKey: string(),
43297
+ cap: string(),
43298
+ fieldPath: string()
43299
+ }),
43300
+ object({
43301
+ kind: literal("literal"),
43302
+ value: union([
43303
+ string(),
43304
+ number(),
43305
+ boolean(),
43306
+ _null()
43307
+ ])
43308
+ }),
43309
+ object({
43310
+ kind: literal("global"),
43311
+ sourceStableId: string(),
43312
+ addonId: string().optional(),
43313
+ cap: string(),
43314
+ fieldPath: string()
43315
+ })
43316
+ ]);
43317
+ object({
43318
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
43319
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
43320
+ }).superRefine((src, ctx) => {
43321
+ const err = validateExpressionSource(src);
43322
+ if (err !== null) ctx.addIssue({
43323
+ code: "custom",
43324
+ message: err,
43325
+ path: ["expr"]
43326
+ });
43327
+ });
43328
+ /**
43329
+ * Identity envelope for a device's upstream-system metadata.
43330
+ *
43331
+ * Two jobs:
43332
+ *
43333
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
43334
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
43335
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
43336
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
43337
+ * without round-tripping through `stableId` / `id` mappings.
43338
+ *
43339
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
43340
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
43341
+ * for vendors where MAC is the natural key). For systems where the
43342
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
43343
+ * providers detect renames by matching `uniqueId` and patch `id`
43344
+ * in-place so dispatch keeps working.
43345
+ *
43346
+ * **Display unit and precision are NOT stored here.** They are a read-time
43347
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
43348
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
43349
+ * neither is repeated on each sample. The `device_class` tag is an
43350
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
43351
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
43352
+ *
43353
+ * The `raw` escape hatch carries provider-specific attributes that don't
43354
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
43355
+ * details, …). Providers control what lands here.
43356
+ */
43357
+ var SourceInfoSchema = object({
43358
+ /** Live dispatch key — mutable when the source system allows rename. */
43359
+ id: string(),
43360
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
43361
+ system: string(),
43362
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
43363
+ * Used to detect rename when `id` changes. */
43364
+ uniqueId: string().optional(),
43365
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
43366
+ quantity: ReadingQuantitySchema.optional(),
43367
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
43368
+ dateTimeShape: _enum([
43369
+ "date",
43370
+ "time",
43371
+ "datetime"
43372
+ ]).optional(),
43373
+ /** Provider-specific extras that don't fit the structured slots.
43374
+ * Whitelist what goes here per provider — do NOT dump entire HA
43375
+ * attribute blobs (would explode the meta JSON column). */
43376
+ raw: record(string(), unknown()).optional()
43377
+ });
43378
+ /**
43379
+ * Build the synthetic SourceInfo every existing provider falls back to
43380
+ * when no upstream value has been persisted. Keeps non-migrated providers
43381
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
43382
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
43383
+ * id. Real upstream identity replaces this once a provider migrates and
43384
+ * calls `updateSourceInfo()` with concrete values.
43385
+ */
43386
+ function synthesizeSourceInfo(input) {
43387
+ return {
43388
+ id: input.stableId,
43389
+ system: input.addonId
43390
+ };
43329
43391
  }
43330
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43331
- function toCanonical(value, spec) {
43332
- return value * spec.factor + spec.offset;
43392
+ /**
43393
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
43394
+ * patch values are ignored (no field clobber); to clear a field
43395
+ * explicitly, providers omit it from the patch and rebuild from scratch
43396
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
43397
+ * bother with a `null`-means-delete sentinel.
43398
+ *
43399
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
43400
+ * merges over existing `raw` keys instead of replacing the whole bag.
43401
+ */
43402
+ function mergeSourceInfo(prev, patch) {
43403
+ const next = { ...prev };
43404
+ for (const [k, v] of Object.entries(patch)) {
43405
+ if (v === void 0) continue;
43406
+ if (k === "raw" && typeof v === "object" && v !== null) {
43407
+ next.raw = {
43408
+ ...prev.raw,
43409
+ ...v
43410
+ };
43411
+ continue;
43412
+ }
43413
+ next[k] = v;
43414
+ }
43415
+ return next;
43333
43416
  }
43334
- /** Canonical-unit value → the unit described by `spec`. */
43335
- function fromCanonical(canonical, spec) {
43336
- return (canonical - spec.offset) / spec.factor;
43417
+ /**
43418
+ * Coerce common unit-of-measurement string variants to a canonical form
43419
+ * so the UI's display templates can rely on stable strings. Pure
43420
+ * passthrough for unknown units — kept conservative on purpose, the goal
43421
+ * is to normalise the long tail of HA `unit_of_measurement` values that
43422
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
43423
+ *
43424
+ * Extend the map as we encounter real-world data. Returning the raw
43425
+ * input for unknown values is intentional: providers should never
43426
+ * lose information silently here.
43427
+ */
43428
+ function normalizeUnit(raw) {
43429
+ if (raw == null) return void 0;
43430
+ const trimmed = raw.trim();
43431
+ if (trimmed === "") return void 0;
43432
+ const lower = trimmed.toLowerCase();
43433
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
43434
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
43435
+ if (lower === "k" || lower === "kelvin") return "K";
43436
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
43437
+ if (lower === "lx" || lower === "lux") return "lx";
43438
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
43439
+ if (lower === "kpa") return "kPa";
43440
+ if (lower === "pa") return "Pa";
43441
+ if (lower === "mbar" || lower === "millibar") return "mbar";
43442
+ if (lower === "w" || lower === "watt") return "W";
43443
+ if (lower === "kw" || lower === "kilowatt") return "kW";
43444
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
43445
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
43446
+ if (lower === "v" || lower === "volt") return "V";
43447
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
43448
+ if (lower === "ma" || lower === "milliampere") return "mA";
43449
+ if (lower === "va") return "VA";
43450
+ if (lower === "ppm") return "ppm";
43451
+ if (lower === "ppb") return "ppb";
43452
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
43453
+ if (lower === "km/h" || lower === "kph") return "km/h";
43454
+ if (lower === "mph") return "mph";
43455
+ if (lower === "m/s") return "m/s";
43456
+ if (lower === "m") return "m";
43457
+ if (lower === "m²" || lower === "m2") return "m²";
43458
+ if (lower === "mm") return "mm";
43459
+ if (lower === "cm") return "cm";
43460
+ if (lower === "km") return "km";
43461
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
43462
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
43463
+ if (lower === "kg") return "kg";
43464
+ if (lower === "g") return "g";
43465
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
43466
+ if (lower === "ml") return "mL";
43467
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
43468
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
43469
+ if (lower === "ms") return "ms";
43470
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
43471
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
43472
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
43473
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
43474
+ if (lower === "kib") return "KiB";
43475
+ if (lower === "mib") return "MiB";
43476
+ if (lower === "gib") return "GiB";
43477
+ if (lower === "tib") return "TiB";
43478
+ if (lower === "kb") return "kB";
43479
+ if (lower === "mb") return "MB";
43480
+ if (lower === "gb") return "GB";
43481
+ if (lower === "dbm") return "dBm";
43482
+ if (lower === "dbfs") return "dBFS";
43483
+ if (lower === "inhg") return "inHg";
43484
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
43485
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
43486
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
43487
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
43488
+ if (lower === "kbps") return "kbps";
43489
+ if (lower === "fps") return "fps";
43490
+ return trimmed;
43337
43491
  }
43338
43492
  /**
43339
- * Graceful variant for the render path: returns the converted value, or
43340
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43341
- * Never throws — a render caller falls back to the raw value + source unit.
43493
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
43494
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
43495
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
43496
+ * lives under a single nested key to keep it composable with those.
43342
43497
  */
43343
- function tryConvertUnit(value, from, to) {
43344
- if (from === to) return value;
43345
- const fromSpec = specFor(from);
43346
- if (!fromSpec) return void 0;
43347
- const toSpec = specFor(to);
43348
- if (!toSpec) return void 0;
43349
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43350
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43498
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
43499
+ /**
43500
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
43501
+ * with Zod. Returns `null` when the blob is missing or the nested
43502
+ * `sourceInfo` key fails validation — callers fall back to the
43503
+ * synthetic default in that case (see `synthesizeSourceInfo`).
43504
+ */
43505
+ function extractSourceInfoFromMetadata(metadata) {
43506
+ if (!metadata) return null;
43507
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
43508
+ if (!raw) return null;
43509
+ const parsed = SourceInfoSchema.safeParse(raw);
43510
+ if (!parsed.success) return null;
43511
+ return parsed.data;
43351
43512
  }
43352
43513
  /**
43353
43514
  * A number a person types for a physical quantity — the input side of the
@@ -45026,15 +45187,28 @@ var SettingsSchemaWithValuesSchema = object({
45026
45187
  });
45027
45188
  /** Patch object — keys are field names, values are the new field values. */
45028
45189
  var SettingsPatchSchema = record(string(), unknown());
45190
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45191
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45192
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
45193
+ var ScopedSettingsDeclarationSchema = object({
45194
+ cap: string(),
45195
+ scope: ScopedSettingsScopeSchema,
45196
+ sectionIds: array(string())
45197
+ });
45029
45198
  /** Standard success response for update operations. */
45030
45199
  var SettingsUpdateResultSchema = object({ success: literal(true) });
45031
45200
  method(object({
45032
45201
  addonId: string(),
45033
- nodeId: string().optional(),
45034
- overlay: record(string(), unknown()).optional(),
45035
- cap: string().optional()
45202
+ cap: string(),
45203
+ scope: ScopedSettingsScopeSchema,
45204
+ nodeId: string().optional()
45036
45205
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45037
45206
  addonId: string(),
45207
+ nodeId: string().optional()
45208
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
45209
+ addonId: string(),
45210
+ cap: string(),
45211
+ scope: ScopedSettingsScopeSchema,
45038
45212
  nodeId: string().optional(),
45039
45213
  patch: SettingsPatchSchema
45040
45214
  }), SettingsUpdateResultSchema, {
@@ -45042,8 +45216,10 @@ method(object({
45042
45216
  auth: "admin"
45043
45217
  }), method(object({
45044
45218
  addonId: string(),
45219
+ cap: string(),
45220
+ scope: ScopedSettingsScopeSchema,
45045
45221
  nodeId: string().optional()
45046
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45222
+ }), record(string(), unknown()).nullable()), method(object({
45047
45223
  addonId: string(),
45048
45224
  nodeId: string().optional(),
45049
45225
  instance: IntegrationInstanceRefSchema
@@ -46606,7 +46782,7 @@ var BackupDestinationInfoSchema = object({
46606
46782
  id: string(),
46607
46783
  /**
46608
46784
  * Manifest id of the owning addon. The Settings button on the
46609
- * destination card opens that addon's `globalSettingsSchema` panel.
46785
+ * destination card opens that addon's scoped settings panel.
46610
46786
  */
46611
46787
  addonId: string(),
46612
46788
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50020,7 +50196,7 @@ var mediaPlayerCapability = {
50020
50196
  isMuted: { kind: "boolean" }
50021
50197
  } }
50022
50198
  };
50023
- 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;
50199
+ 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;
50024
50200
  var NUMBER = {
50025
50201
  kind: "fixed",
50026
50202
  result: ["number"]
@@ -52038,6 +52214,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
52038
52214
  kind: "mutation",
52039
52215
  auth: "admin"
52040
52216
  });
52217
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
52041
52218
  /**
52042
52219
  * A SITE ZONE — a named part of the property that several locations belong to.
52043
52220
  *
@@ -52524,6 +52701,12 @@ var DeviceInfoSchema = object({
52524
52701
  addonId: string(),
52525
52702
  type: _enum(DeviceType),
52526
52703
  name: string(),
52704
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
52705
+ * entity_id style. Generated once from the name at first registration, unique
52706
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
52707
+ * Optional: a record produced by an older build omits it until the boot
52708
+ * backfill has run. */
52709
+ alias: DeviceAliasSchema.optional(),
52527
52710
  /** Operator-organisational location label. `null` when unset. */
52528
52711
  location: string().nullable(),
52529
52712
  /** Soft-disabled flag. */
@@ -52843,6 +53026,25 @@ method(object({
52843
53026
  }), _void(), {
52844
53027
  kind: "mutation",
52845
53028
  auth: "admin"
53029
+ }), method(object({
53030
+ deviceId: number(),
53031
+ alias: string()
53032
+ }), discriminatedUnion("ok", [object({
53033
+ ok: literal(true),
53034
+ alias: DeviceAliasSchema
53035
+ }), object({
53036
+ ok: literal(false),
53037
+ reason: _enum([
53038
+ "empty",
53039
+ "too-long",
53040
+ "grammar",
53041
+ "reserved",
53042
+ "taken"
53043
+ ]),
53044
+ takenBy: number().optional()
53045
+ })]), {
53046
+ kind: "mutation",
53047
+ auth: "admin"
52846
53048
  }), method(object({
52847
53049
  deviceId: number(),
52848
53050
  location: string().nullable()
@@ -56552,7 +56754,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56552
56754
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
56553
56755
  * clears it.
56554
56756
  */
56555
- var ConfigSchemaPassthrough$3 = custom();
56757
+ var ConfigSchemaPassthrough$2 = custom();
56556
56758
  var IntegrationInstanceIdSchema = string().min(1);
56557
56759
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56558
56760
  /**
@@ -56598,7 +56800,16 @@ var IntegrationInstanceKindSchema = object({
56598
56800
  /** Relative hub path to start an interactive account link of an instance of this kind. */
56599
56801
  connectPath: string().min(1).optional(),
56600
56802
  /** Relative hub path to disconnect that account. */
56601
- disconnectPath: string().min(1).optional()
56803
+ disconnectPath: string().min(1).optional(),
56804
+ /**
56805
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
56806
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
56807
+ * `connection` widgets write connection values, `model` widgets own the model choice.
56808
+ */
56809
+ widgets: array(object({
56810
+ id: string().min(1),
56811
+ placement: _enum(["connection", "model"])
56812
+ })).optional()
56602
56813
  });
56603
56814
  /**
56604
56815
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -56639,10 +56850,11 @@ var IntegrationInstanceDescriptionSchema = object({
56639
56850
  /** True when `testInstance` really probes the remote. */
56640
56851
  canTest: boolean(),
56641
56852
  /**
56642
- * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56643
- * Add flow asks which first and sends it as `createInstance.kind`.
56853
+ * The kinds an instance can be of (D1013 rule 1), each naming its `functionCap`. One entry:
56854
+ * nothing to choose; more than one: the Add flow asks which first and sends it as
56855
+ * `createInstance.kind`. Never empty: every instance belongs to exactly one kind.
56644
56856
  */
56645
- kinds: array(IntegrationInstanceKindSchema).optional(),
56857
+ kinds: array(IntegrationInstanceKindSchema).min(1),
56646
56858
  /**
56647
56859
  * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56648
56860
  * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
@@ -56678,8 +56890,8 @@ var IntegrationInstanceSchema = object({
56678
56890
  * the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
56679
56891
  */
56680
56892
  enabled: boolean().optional(),
56681
- /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56682
- kind: string().optional(),
56893
+ /** The kind of this instance: exactly one of `describe().kinds[].id` (D1013 rule 2). */
56894
+ kind: string().min(1),
56683
56895
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
56684
56896
  formValues: record(string(), unknown()),
56685
56897
  status: IntegrationInstanceStatusSchema.optional(),
@@ -56703,7 +56915,7 @@ var integrationInstancesCapability = {
56703
56915
  id: IntegrationInstanceIdSchema.nullable(),
56704
56916
  /** The kind of the create form (`id: null`) of a provider with several. */
56705
56917
  kind: string().min(1).optional()
56706
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56918
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
56707
56919
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56708
56920
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56709
56921
  /**
@@ -57805,12 +58017,12 @@ var LlmModelInfoSchema = object({
57805
58017
  });
57806
58018
  /**
57807
58019
  * The provider-specific part of a profile, validated by the provider that owns
57808
- * the kind (`upsertProfile` refuses what it does not accept). A field its
58020
+ * the kind (saving an instance refuses what it does not accept). A field its
57809
58021
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
57810
58022
  * keychain, never in the row, and reads back as the redaction marker.
57811
58023
  */
57812
58024
  var LlmProviderConfigSchema = record(string(), unknown());
57813
- var LlmProfileSchema = object({
58025
+ object({
57814
58026
  id: string(),
57815
58027
  name: string(),
57816
58028
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -57904,8 +58116,8 @@ var LlmProviderWidgetSchema = object({
57904
58116
  /** ConfigUISchema tree passed through untyped on the wire (the
57905
58117
  * notification-output `ConfigSchemaPassthrough` precedent at
57906
58118
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
57907
- var ConfigSchemaPassthrough$2 = unknown();
57908
- var LlmProfileKindDescriptorSchema = object({
58119
+ var ConfigSchemaPassthrough$1 = unknown();
58120
+ object({
57909
58121
  kind: LlmProfileKindSchema,
57910
58122
  label: string(),
57911
58123
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -57913,9 +58125,9 @@ var LlmProfileKindDescriptorSchema = object({
57913
58125
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
57914
58126
  addonId: string(),
57915
58127
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
57916
- connectionSchema: ConfigSchemaPassthrough$2,
58128
+ connectionSchema: ConfigSchemaPassthrough$1,
57917
58129
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
57918
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
58130
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
57919
58131
  /** The kind's defaults; a model from `listModels` may override any of them. */
57920
58132
  features: LlmModelFeaturesSchema,
57921
58133
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -58044,20 +58256,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
58044
58256
  }), method(object({
58045
58257
  addonId: string().optional(),
58046
58258
  requestId: string()
58047
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
58048
- kind: "mutation",
58049
- auth: "admin"
58050
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
58051
- kind: "mutation",
58052
- auth: "admin"
58053
- }), method(ProfileRefInputSchema, _void(), {
58054
- kind: "mutation",
58055
- auth: "admin"
58056
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
58057
- kind: "mutation",
58058
- auth: "admin",
58059
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
58060
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58259
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58061
58260
  kind: "mutation",
58062
58261
  auth: "admin"
58063
58262
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -59120,20 +59319,7 @@ method(_void(), MeshStatusSchema), method(object({
59120
59319
  /** Optional hostname override the host should advertise once joined. */
59121
59320
  hostname: string().optional() }), object({
59122
59321
  /** Authentication URL the operator should open in a browser. */
59123
- 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({
59124
- /** Optional auth key — when provided, probes the key validity
59125
- * against the provider's API. Omit when already joined to
59126
- * just ping the daemon. */
59127
- authKey: string().optional() }), object({
59128
- ok: boolean(),
59129
- /** Provider-side identifier resolved by the probe (tailnet
59130
- * name for Tailscale, network id for ZeroTier, etc.). */
59131
- tenant: string().optional(),
59132
- /** Daemon binary version, when reachable. */
59133
- daemonVersion: string().optional(),
59134
- /** Human-readable error when `ok: false`. */
59135
- error: string().optional()
59136
- }), { kind: "mutation" });
59322
+ 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() }));
59137
59323
  /**
59138
59324
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
59139
59325
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -66960,6 +67146,11 @@ var NcScheduleSchema = object({
66960
67146
  * as a STATE SUMMARY at the end of each window of `schedule`.
66961
67147
  */
66962
67148
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
67149
+ _enum([
67150
+ "activation",
67151
+ "deactivation",
67152
+ "schedule"
67153
+ ]);
66963
67154
  /**
66964
67155
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
66965
67156
  * every section; `section` = one notification per section with something to
@@ -66976,11 +67167,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
66976
67167
  * midnight crossing); the send time is each window's END. Refused on the
66977
67168
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
66978
67169
  */
66979
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
66980
- mode: literal("summary"),
66981
- schedule: NcScheduleSchema,
66982
- split: NcStateSummarySplitSchema.optional()
66983
- })]);
67170
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
67171
+ object({ mode: literal("instant") }),
67172
+ object({
67173
+ mode: literal("summary"),
67174
+ schedule: NcScheduleSchema,
67175
+ split: NcStateSummarySplitSchema.optional()
67176
+ }),
67177
+ object({
67178
+ mode: literal("modes"),
67179
+ /** The bad edge, and every kind that has a single edge. */
67180
+ onActivation: boolean(),
67181
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
67182
+ onDeactivation: boolean(),
67183
+ /** A state summary at the end of each window; absent = no summary. */
67184
+ onSchedule: object({
67185
+ schedule: NcScheduleSchema,
67186
+ grouping: NcStateSummarySplitSchema
67187
+ }).optional()
67188
+ })
67189
+ ]);
66984
67190
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
66985
67191
  var NcPlateMatcherSchema = object({
66986
67192
  /**
@@ -69340,61 +69546,6 @@ var OauthIntegrationDescriptorSchema = object({
69340
69546
  });
69341
69547
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
69342
69548
  /**
69343
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
69344
- *
69345
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
69346
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
69347
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
69348
- * immutable after create.
69349
- */
69350
- var ConfigSchemaPassthrough$1 = unknown();
69351
- var OidcConnectionSchema = object({
69352
- id: string().min(1),
69353
- displayName: string(),
69354
- icon: string(),
69355
- issuerUrl: string(),
69356
- clientId: string(),
69357
- /** Client id + secret present — never the secret value. */
69358
- configured: boolean(),
69359
- /**
69360
- * Values for the connection form schema. `clientSecret` is a keychain
69361
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
69362
- */
69363
- formValues: record(string(), unknown()).optional()
69364
- });
69365
- /**
69366
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
69367
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
69368
- */
69369
- var OidcConnectionUpdateInputSchema = object({
69370
- id: string().min(1),
69371
- displayName: string().min(1),
69372
- icon: string().optional(),
69373
- issuerUrl: string().optional(),
69374
- clientId: string().optional(),
69375
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
69376
- redirectUri: string().optional(),
69377
- scopes: string().optional(),
69378
- usernameClaim: _enum([
69379
- "preferred_username",
69380
- "email",
69381
- "sub"
69382
- ]).optional()
69383
- });
69384
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
69385
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
69386
- displayName: string().min(1)
69387
- }), object({ id: string() }), {
69388
- kind: "mutation",
69389
- auth: "admin"
69390
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
69391
- kind: "mutation",
69392
- auth: "admin"
69393
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
69394
- kind: "mutation",
69395
- auth: "admin"
69396
- });
69397
- /**
69398
69549
  * Generic on-screen-display (video overlay) cap. Each camera exposes
69399
69550
  * a firmware-defined list of overlays (channel name, timestamp,
69400
69551
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -75109,6 +75260,16 @@ var RemoteLayoutComponentSchema = object({
75109
75260
  method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
75110
75261
  var RUNNING_POWER_START_DELAY_MS = 3e4;
75111
75262
  var RUNNING_POWER_STOP_DELAY_MS = 18e4;
75263
+ /**
75264
+ * Per-type overrides of the dwell defaults. A media player draws power the
75265
+ * moment it is on and stops drawing the moment it is off, so it needs neither
75266
+ * a long dwell nor a second sample. A type absent here keeps the global values.
75267
+ */
75268
+ var RUNNING_POWER_DEFAULTS_BY_TYPE = { [DeviceType.MediaPlayer]: {
75269
+ startDwellMs: 1e4,
75270
+ stopDwellMs: 1e4,
75271
+ minSamples: 1
75272
+ } };
75112
75273
  /** Before any provider reading — all fields unknown (D933). */
75113
75274
  var RUNNING_STATE_COLD = {
75114
75275
  running: null,
@@ -75134,93 +75295,27 @@ var RunningFlipEventDataSchema = object({
75134
75295
  phase: string().nullable()
75135
75296
  });
75136
75297
  var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
75137
- /** Generic Customize target for the running boolean (D936). */
75138
- var runningCustomize = {
75139
- version: 1,
75140
- overridable: [{
75141
- path: "running",
75142
- by: "running"
75143
- }],
75144
- targets: [{
75145
- id: "running",
75146
- cardinality: "scalar",
75147
- vocabulary: {
75148
- cap: "running-state",
75149
- stateField: "running"
75150
- },
75151
- deviceTypes: [
75152
- DeviceType.Washer,
75153
- DeviceType.Dryer,
75154
- DeviceType.Dishwasher,
75155
- DeviceType.Vacuum,
75156
- DeviceType.LawnMower,
75157
- DeviceType.MediaPlayer,
75158
- DeviceType.WaterHeater,
75159
- DeviceType.Humidifier,
75160
- DeviceType.Fan,
75161
- DeviceType.Climate,
75162
- DeviceType.Thermostat,
75163
- DeviceType.PetFeeder,
75164
- DeviceType.Heater
75165
- ],
75166
- apply: "add",
75167
- path: "running",
75168
- sources: [
75169
- {
75170
- kind: "switch",
75171
- transforms: [{ kind: "identity" }]
75172
- },
75173
- {
75174
- kind: "binary",
75175
- transforms: [{ kind: "identity" }, { kind: "invert" }]
75176
- },
75177
- {
75178
- kind: "numeric",
75179
- requiredUnit: "W",
75180
- transforms: [{
75181
- kind: "threshold",
75182
- comparisons: ["gte", "lte"],
75183
- comparison: "gte",
75184
- defaultThreshold: 5,
75185
- defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75186
- defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75187
- thresholdUnit: "W"
75188
- }]
75189
- }
75190
- ],
75191
- companions: [
75192
- {
75193
- kind: "constant",
75194
- path: "source",
75195
- value: "assigned",
75196
- when: "always"
75197
- },
75198
- {
75199
- kind: "constant",
75200
- path: "phase",
75201
- value: null,
75202
- when: "add"
75203
- },
75204
- {
75205
- kind: "clock",
75206
- path: "lastFetchedAt",
75207
- when: "always"
75208
- },
75209
- {
75210
- kind: "transition-time",
75211
- path: "since",
75212
- sourceChangedAt: "prefer"
75213
- }
75214
- ],
75215
- sourceReport: {
75216
- status: "assignment-status",
75217
- runtimeField: "source",
75218
- nativeValue: "native",
75219
- assignedValue: "assigned"
75220
- },
75221
- advancedExpression: true
75222
- }]
75223
- };
75298
+ /**
75299
+ * Customize offers "running" only where a job makes sense (D933): a cycle,
75300
+ * a clean, a mow, playback, or another appliance that is on and working.
75301
+ * The cap itself stays on every type — a camera can still report running
75302
+ * natively; it just has no card.
75303
+ */
75304
+ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
75305
+ DeviceType.Washer,
75306
+ DeviceType.Dryer,
75307
+ DeviceType.Dishwasher,
75308
+ DeviceType.Vacuum,
75309
+ DeviceType.LawnMower,
75310
+ DeviceType.MediaPlayer,
75311
+ DeviceType.WaterHeater,
75312
+ DeviceType.Humidifier,
75313
+ DeviceType.Fan,
75314
+ DeviceType.Climate,
75315
+ DeviceType.Thermostat,
75316
+ DeviceType.PetFeeder,
75317
+ DeviceType.Heater
75318
+ ];
75224
75319
  var runningStateCapability = {
75225
75320
  name: "running-state",
75226
75321
  scope: "device",
@@ -75240,7 +75335,79 @@ var runningStateCapability = {
75240
75335
  durability: "restored",
75241
75336
  history: { fields: { running: { kind: "boolean" } } },
75242
75337
  volatileStateFields: ["lastFetchedAt"],
75243
- customize: runningCustomize
75338
+ customize: {
75339
+ version: 1,
75340
+ overridable: [{
75341
+ path: "running",
75342
+ by: "running"
75343
+ }],
75344
+ targets: [{
75345
+ id: "running",
75346
+ cardinality: "scalar",
75347
+ vocabulary: {
75348
+ cap: "running-state",
75349
+ stateField: "running"
75350
+ },
75351
+ deviceTypes: RUNNING_CUSTOMIZE_DEVICE_TYPES,
75352
+ apply: "add",
75353
+ path: "running",
75354
+ sources: [
75355
+ {
75356
+ kind: "switch",
75357
+ transforms: [{ kind: "identity" }]
75358
+ },
75359
+ {
75360
+ kind: "binary",
75361
+ transforms: [{ kind: "identity" }, { kind: "invert" }]
75362
+ },
75363
+ {
75364
+ kind: "numeric",
75365
+ requiredUnit: "W",
75366
+ transforms: [{
75367
+ kind: "threshold",
75368
+ comparisons: ["gte", "lte"],
75369
+ comparison: "gte",
75370
+ defaultThreshold: 5,
75371
+ defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75372
+ defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75373
+ deviceTypeDefaults: RUNNING_POWER_DEFAULTS_BY_TYPE,
75374
+ thresholdUnit: "W"
75375
+ }]
75376
+ }
75377
+ ],
75378
+ companions: [
75379
+ {
75380
+ kind: "constant",
75381
+ path: "source",
75382
+ value: "assigned",
75383
+ when: "always"
75384
+ },
75385
+ {
75386
+ kind: "constant",
75387
+ path: "phase",
75388
+ value: null,
75389
+ when: "add"
75390
+ },
75391
+ {
75392
+ kind: "clock",
75393
+ path: "lastFetchedAt",
75394
+ when: "always"
75395
+ },
75396
+ {
75397
+ kind: "transition-time",
75398
+ path: "since",
75399
+ sourceChangedAt: "prefer"
75400
+ }
75401
+ ],
75402
+ sourceReport: {
75403
+ status: "assignment-status",
75404
+ runtimeField: "source",
75405
+ nativeValue: "native",
75406
+ assignedValue: "assigned"
75407
+ },
75408
+ advancedExpression: true
75409
+ }]
75410
+ }
75244
75411
  };
75245
75412
  /**
75246
75413
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -76484,21 +76651,6 @@ method(object({
76484
76651
  */
76485
76652
  ttlSec: union([number().int().positive(), literal("never")]).optional()
76486
76653
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
76487
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
76488
- providerId: string().min(1),
76489
- displayName: string().min(1),
76490
- configSchema: unknown(),
76491
- nodeLocal: boolean(),
76492
- shouldSaveDiskSpace: literal(true),
76493
- minFreePercent: number().min(0).max(100)
76494
- }), object({
76495
- providerId: string().min(1),
76496
- displayName: string().min(1),
76497
- configSchema: unknown(),
76498
- nodeLocal: boolean(),
76499
- shouldSaveDiskSpace: literal(false),
76500
- minFreePercent: literal(null)
76501
- })]);
76502
76654
  method(object({
76503
76655
  location: StorageLocationRefSchema,
76504
76656
  relativePath: string()
@@ -76560,42 +76712,7 @@ method(object({
76560
76712
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
76561
76713
  kind: "mutation",
76562
76714
  auth: "admin"
76563
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
76564
- createdAt: true,
76565
- updatedAt: true
76566
- }), StorageLocationSchema, {
76567
- kind: "mutation",
76568
- auth: "admin"
76569
- }), method(object({
76570
- id: string(),
76571
- force: boolean().optional()
76572
- }), _void(), {
76573
- kind: "mutation",
76574
- auth: "admin"
76575
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
76576
- ok: boolean(),
76577
- error: string().optional()
76578
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
76579
- providerId: string(),
76580
- /**
76581
- * The location this config is an UNSAVED edit of, when there is one.
76582
- *
76583
- * `listLocations` replaces every declared secret with the redaction
76584
- * sentinel, so the edit modal's form state holds the sentinel for any
76585
- * credential the operator did not retype — and posting that here
76586
- * without a way to resolve it makes the provider try to authenticate
76587
- * as `__camstack_redacted__` and report the operator's own working
76588
- * password as wrong. Given this id, the orchestrator restores each
76589
- * sentinel from the stored config (same rule as `upsertLocation`)
76590
- * before dispatching. Omitted by the "Add location" wizard, where
76591
- * every value was typed just now and nothing is stored yet.
76592
- */
76593
- locationId: string().optional(),
76594
- config: record(string(), unknown())
76595
- }), object({
76596
- ok: boolean(),
76597
- error: string().optional()
76598
- }), { auth: "admin" });
76715
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
76599
76716
  /** One table of the settings database. */
76600
76717
  var StorageTableSchema = object({
76601
76718
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -78117,7 +78234,7 @@ var TerminalInstanceInfoSchema = object({
78117
78234
  /**
78118
78235
  * False while the node the instance is bound to is not in the cluster
78119
78236
  * topology (forgotten, renamed, re-paired). The instance is kept; the
78120
- * operator re-points it with `updateInstance({ targetNodeId })`.
78237
+ * operator re-points it on the Terminal page.
78121
78238
  */
78122
78239
  nodeInCluster: boolean(),
78123
78240
  profileId: string(),
@@ -78155,41 +78272,7 @@ var TerminalOutputBatchSchema = object({
78155
78272
  snapshot: string().optional(),
78156
78273
  events: array(TerminalOutputEventSchema).readonly()
78157
78274
  });
78158
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
78159
- targetNodeId: string().min(1),
78160
- profileId: string().min(1),
78161
- name: string().trim().min(1).max(160).optional(),
78162
- executable: string().max(1024).optional(),
78163
- args: array(string().max(2048)).max(64).optional(),
78164
- cwd: string().max(1024).optional(),
78165
- environment: array(string().max(4096)).max(64).optional(),
78166
- profileSettings: ProfileSettingsBagSchema.optional()
78167
- }), TerminalInstanceInfoSchema, {
78168
- kind: "mutation",
78169
- auth: "admin"
78170
- }), method(object({
78171
- instanceId: string().min(1),
78172
- /** Re-point the instance to another node; the profile must exist there. */
78173
- targetNodeId: string().min(1).max(256).optional(),
78174
- name: string().trim().min(1).max(160).optional(),
78175
- executable: string().max(1024).optional(),
78176
- args: array(string().max(2048)).max(64).optional(),
78177
- cwd: string().max(1024).optional(),
78178
- environment: array(string().max(4096)).max(64).optional(),
78179
- profileSettings: ProfileSettingsBagSchema.optional()
78180
- }), TerminalInstanceInfoSchema, {
78181
- kind: "mutation",
78182
- auth: "admin"
78183
- }), method(object({ instanceId: string().min(1) }), _void(), {
78184
- kind: "mutation",
78185
- auth: "admin"
78186
- }), method(object({
78187
- instanceId: string().min(1),
78188
- enabled: boolean()
78189
- }), TerminalInstanceInfoSchema, {
78190
- kind: "mutation",
78191
- auth: "admin"
78192
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78275
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78193
78276
  stableId: string().min(1),
78194
78277
  name: string().trim().min(1).max(160).optional()
78195
78278
  }), TerminalInstanceInfoSchema, {
@@ -78258,6 +78341,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
78258
78341
  _enum(["switch", "pet-fountain"]);
78259
78342
  DeviceType.Heater, DeviceType.PetFountain;
78260
78343
  var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
78344
+ /** One duration, as the one-tap run asks for it. */
78345
+ var TimedRunQuickRunSchema = object({
78346
+ duration: number().positive().finite(),
78347
+ unit: TimedRunUnitSchema
78348
+ });
78261
78349
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
78262
78350
  var TimedRunRefusalSchema = _enum([
78263
78351
  "unsupported-device",
@@ -78278,8 +78366,23 @@ var TimedRunStatusSchema = object({
78278
78366
  activeUntil: number().nullable(),
78279
78367
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
78280
78368
  requestedBy: string().nullable(),
78369
+ /**
78370
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
78371
+ * the device is gone or its type has no timed run; a status from before this field
78372
+ * existed has none, and a surface falls back to the type's default.
78373
+ */
78374
+ quickRun: TimedRunQuickRunSchema.nullable(),
78281
78375
  lastFetchedAt: number()
78282
78376
  });
78377
+ /** What the settings form needs: the unit, the bounds and the type's default. */
78378
+ var TimedRunOptionsSchema = object({
78379
+ unit: TimedRunUnitSchema,
78380
+ /** The longest run, in `unit`. */
78381
+ maxDuration: number(),
78382
+ defaultDuration: number()
78383
+ });
78384
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
78385
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
78283
78386
  var timedRunCapability = {
78284
78387
  name: "timed-run",
78285
78388
  scope: "device",
@@ -78287,7 +78390,24 @@ var timedRunCapability = {
78287
78390
  kind: "wrapper",
78288
78391
  defaultActive: true,
78289
78392
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
78393
+ deviceConfig: { ui: {
78394
+ kind: "derived-form",
78395
+ builderId: "timed-run",
78396
+ tab: "general"
78397
+ } },
78290
78398
  methods: {
78399
+ getOptions: method(object({ deviceId: number().int() }), TimedRunOptionsSchema),
78400
+ /**
78401
+ * The one-tap run of this device. A duration above the type's maximum is
78402
+ * REFUSED (thrown), like `runFor`: the maximum is a safety bound.
78403
+ */
78404
+ setSettings: method(object({
78405
+ deviceId: number().int(),
78406
+ settings: TimedRunSettingsPatchSchema
78407
+ }), _void(), {
78408
+ kind: "mutation",
78409
+ auth: "admin"
78410
+ }),
78291
78411
  /**
78292
78412
  * Turn the device on and arm its off. A run already armed is replaced (the
78293
78413
  * new deadline wins). Refuses with a typed reason above the type's maximum.
@@ -83019,21 +83139,28 @@ Object.freeze({
83019
83139
  access: "view",
83020
83140
  role: "admin"
83021
83141
  },
83022
- "addonSettings.getGlobalSettings": {
83142
+ "addonSettings.getIntegrationFormSchema": {
83023
83143
  capName: "addon-settings",
83024
83144
  capScope: "system",
83025
83145
  addonId: null,
83026
83146
  access: "view",
83027
83147
  role: "admin"
83028
83148
  },
83029
- "addonSettings.getIntegrationFormSchema": {
83149
+ "addonSettings.getScopedSettings": {
83150
+ capName: "addon-settings",
83151
+ capScope: "system",
83152
+ addonId: null,
83153
+ access: "view",
83154
+ role: "admin"
83155
+ },
83156
+ "addonSettings.listScopedSettings": {
83030
83157
  capName: "addon-settings",
83031
83158
  capScope: "system",
83032
83159
  addonId: null,
83033
83160
  access: "view",
83034
83161
  role: "admin"
83035
83162
  },
83036
- "addonSettings.getIntegrationSettings": {
83163
+ "addonSettings.readScopedValues": {
83037
83164
  capName: "addon-settings",
83038
83165
  capScope: "system",
83039
83166
  addonId: null,
@@ -83054,7 +83181,7 @@ Object.freeze({
83054
83181
  access: "create",
83055
83182
  role: "admin"
83056
83183
  },
83057
- "addonSettings.updateGlobalSettings": {
83184
+ "addonSettings.updateScopedSettings": {
83058
83185
  capName: "addon-settings",
83059
83186
  capScope: "system",
83060
83187
  addonId: null,
@@ -85224,6 +85351,13 @@ Object.freeze({
85224
85351
  access: "create",
85225
85352
  role: "admin"
85226
85353
  },
85354
+ "deviceManager.setAlias": {
85355
+ capName: "device-manager",
85356
+ capScope: "system",
85357
+ addonId: null,
85358
+ access: "create",
85359
+ role: "admin"
85360
+ },
85227
85361
  "deviceManager.setChildLayout": {
85228
85362
  capName: "device-manager",
85229
85363
  capScope: "system",
@@ -86512,13 +86646,6 @@ Object.freeze({
86512
86646
  access: "delete",
86513
86647
  role: "admin"
86514
86648
  },
86515
- "llm.deleteProfile": {
86516
- capName: "llm",
86517
- capScope: "system",
86518
- addonId: null,
86519
- access: "delete",
86520
- role: "admin"
86521
- },
86522
86649
  "llm.generate": {
86523
86650
  capName: "llm",
86524
86651
  capScope: "system",
@@ -86582,20 +86709,6 @@ Object.freeze({
86582
86709
  access: "view",
86583
86710
  role: "admin"
86584
86711
  },
86585
- "llm.listProfileKinds": {
86586
- capName: "llm",
86587
- capScope: "system",
86588
- addonId: null,
86589
- access: "view",
86590
- role: "admin"
86591
- },
86592
- "llm.listProfiles": {
86593
- capName: "llm",
86594
- capScope: "system",
86595
- addonId: null,
86596
- access: "view",
86597
- role: "admin"
86598
- },
86599
86712
  "llm.listRuntimeNodes": {
86600
86713
  capName: "llm",
86601
86714
  capScope: "system",
@@ -86603,13 +86716,6 @@ Object.freeze({
86603
86716
  access: "view",
86604
86717
  role: "admin"
86605
86718
  },
86606
- "llm.migrateRawSecrets": {
86607
- capName: "llm",
86608
- capScope: "system",
86609
- addonId: null,
86610
- access: "create",
86611
- role: "admin"
86612
- },
86613
86719
  "llm.resolveModelRef": {
86614
86720
  capName: "llm",
86615
86721
  capScope: "system",
@@ -86638,20 +86744,6 @@ Object.freeze({
86638
86744
  access: "create",
86639
86745
  role: "admin"
86640
86746
  },
86641
- "llm.testProfile": {
86642
- capName: "llm",
86643
- capScope: "system",
86644
- addonId: null,
86645
- access: "create",
86646
- role: "admin"
86647
- },
86648
- "llm.upsertProfile": {
86649
- capName: "llm",
86650
- capScope: "system",
86651
- addonId: null,
86652
- access: "create",
86653
- role: "admin"
86654
- },
86655
86747
  "llmRuntime.appleAvailability": {
86656
86748
  capName: "llm-runtime",
86657
86749
  capScope: "system",
@@ -87156,13 +87248,6 @@ Object.freeze({
87156
87248
  access: "create",
87157
87249
  role: "admin"
87158
87250
  },
87159
- "meshNetwork.testConnection": {
87160
- capName: "mesh-network",
87161
- capScope: "system",
87162
- addonId: null,
87163
- access: "create",
87164
- role: "admin"
87165
- },
87166
87251
  "meter.getStatus": {
87167
87252
  capName: "meter",
87168
87253
  capScope: "device",
@@ -87821,13 +87906,6 @@ Object.freeze({
87821
87906
  access: "create",
87822
87907
  role: "admin"
87823
87908
  },
87824
- "notificationOutput.deleteTarget": {
87825
- capName: "notification-output",
87826
- capScope: "system",
87827
- addonId: null,
87828
- access: "delete",
87829
- role: "admin"
87830
- },
87831
87909
  "notificationOutput.discoverTargets": {
87832
87910
  capName: "notification-output",
87833
87911
  capScope: "system",
@@ -87835,13 +87913,6 @@ Object.freeze({
87835
87913
  access: "view",
87836
87914
  role: "admin"
87837
87915
  },
87838
- "notificationOutput.getTarget": {
87839
- capName: "notification-output",
87840
- capScope: "system",
87841
- addonId: null,
87842
- access: "view",
87843
- role: "admin"
87844
- },
87845
87916
  "notificationOutput.listTargetKinds": {
87846
87917
  capName: "notification-output",
87847
87918
  capScope: "system",
@@ -87849,13 +87920,6 @@ Object.freeze({
87849
87920
  access: "view",
87850
87921
  role: "admin"
87851
87922
  },
87852
- "notificationOutput.listTargets": {
87853
- capName: "notification-output",
87854
- capScope: "system",
87855
- addonId: null,
87856
- access: "view",
87857
- role: "admin"
87858
- },
87859
87923
  "notificationOutput.probeTargetConfig": {
87860
87924
  capName: "notification-output",
87861
87925
  capScope: "system",
@@ -87870,13 +87934,6 @@ Object.freeze({
87870
87934
  access: "create",
87871
87935
  role: "admin"
87872
87936
  },
87873
- "notificationOutput.setTargetEnabled": {
87874
- capName: "notification-output",
87875
- capScope: "system",
87876
- addonId: null,
87877
- access: "create",
87878
- role: "admin"
87879
- },
87880
87937
  "notificationOutput.testTarget": {
87881
87938
  capName: "notification-output",
87882
87939
  capScope: "system",
@@ -87884,13 +87941,6 @@ Object.freeze({
87884
87941
  access: "create",
87885
87942
  role: "admin"
87886
87943
  },
87887
- "notificationOutput.upsertTarget": {
87888
- capName: "notification-output",
87889
- capScope: "system",
87890
- addonId: null,
87891
- access: "create",
87892
- role: "admin"
87893
- },
87894
87944
  "notificationRules.activateAlarmProfile": {
87895
87945
  capName: "notification-rules",
87896
87946
  capScope: "system",
@@ -88150,41 +88200,6 @@ Object.freeze({
88150
88200
  access: "view",
88151
88201
  role: "admin"
88152
88202
  },
88153
- "oidcConnections.connectionFormSchema": {
88154
- capName: "oidc-connections",
88155
- capScope: "system",
88156
- addonId: null,
88157
- access: "view",
88158
- role: "admin"
88159
- },
88160
- "oidcConnections.createConnection": {
88161
- capName: "oidc-connections",
88162
- capScope: "system",
88163
- addonId: null,
88164
- access: "create",
88165
- role: "admin"
88166
- },
88167
- "oidcConnections.deleteConnection": {
88168
- capName: "oidc-connections",
88169
- capScope: "system",
88170
- addonId: null,
88171
- access: "delete",
88172
- role: "admin"
88173
- },
88174
- "oidcConnections.listConnections": {
88175
- capName: "oidc-connections",
88176
- capScope: "system",
88177
- addonId: null,
88178
- access: "view",
88179
- role: "admin"
88180
- },
88181
- "oidcConnections.updateConnection": {
88182
- capName: "oidc-connections",
88183
- capScope: "system",
88184
- addonId: null,
88185
- access: "create",
88186
- role: "admin"
88187
- },
88188
88203
  "osd.getStatus": {
88189
88204
  capName: "osd",
88190
88205
  capScope: "device",
@@ -91216,13 +91231,6 @@ Object.freeze({
91216
91231
  access: "delete",
91217
91232
  role: "admin"
91218
91233
  },
91219
- "storage.deleteLocation": {
91220
- capName: "storage",
91221
- capScope: "system",
91222
- addonId: null,
91223
- access: "delete",
91224
- role: "admin"
91225
- },
91226
91234
  "storage.endDownload": {
91227
91235
  capName: "storage",
91228
91236
  capScope: "system",
@@ -91279,13 +91287,6 @@ Object.freeze({
91279
91287
  access: "view",
91280
91288
  role: "admin"
91281
91289
  },
91282
- "storage.listProviders": {
91283
- capName: "storage",
91284
- capScope: "system",
91285
- addonId: null,
91286
- access: "view",
91287
- role: "admin"
91288
- },
91289
91290
  "storage.read": {
91290
91291
  capName: "storage",
91291
91292
  capScope: "system",
@@ -91307,27 +91308,6 @@ Object.freeze({
91307
91308
  access: "view",
91308
91309
  role: "admin"
91309
91310
  },
91310
- "storage.testConfig": {
91311
- capName: "storage",
91312
- capScope: "system",
91313
- addonId: null,
91314
- access: "view",
91315
- role: "admin"
91316
- },
91317
- "storage.testLocation": {
91318
- capName: "storage",
91319
- capScope: "system",
91320
- addonId: null,
91321
- access: "view",
91322
- role: "admin"
91323
- },
91324
- "storage.upsertLocation": {
91325
- capName: "storage",
91326
- capScope: "system",
91327
- addonId: null,
91328
- access: "create",
91329
- role: "admin"
91330
- },
91331
91311
  "storage.write": {
91332
91312
  capName: "storage",
91333
91313
  capScope: "system",
@@ -92105,27 +92085,6 @@ Object.freeze({
92105
92085
  access: "create",
92106
92086
  role: "admin"
92107
92087
  },
92108
- "terminalSession.createInstance": {
92109
- capName: "terminal-session",
92110
- capScope: "system",
92111
- addonId: null,
92112
- access: "create",
92113
- role: "admin"
92114
- },
92115
- "terminalSession.deleteInstance": {
92116
- capName: "terminal-session",
92117
- capScope: "system",
92118
- addonId: null,
92119
- access: "delete",
92120
- role: "admin"
92121
- },
92122
- "terminalSession.listInstances": {
92123
- capName: "terminal-session",
92124
- capScope: "system",
92125
- addonId: null,
92126
- access: "view",
92127
- role: "admin"
92128
- },
92129
92088
  "terminalSession.listLegacyCameras": {
92130
92089
  capName: "terminal-session",
92131
92090
  capScope: "system",
@@ -92168,20 +92127,6 @@ Object.freeze({
92168
92127
  access: "create",
92169
92128
  role: "admin"
92170
92129
  },
92171
- "terminalSession.setInstanceEnabled": {
92172
- capName: "terminal-session",
92173
- capScope: "system",
92174
- addonId: null,
92175
- access: "create",
92176
- role: "admin"
92177
- },
92178
- "terminalSession.updateInstance": {
92179
- capName: "terminal-session",
92180
- capScope: "system",
92181
- addonId: null,
92182
- access: "create",
92183
- role: "admin"
92184
- },
92185
92130
  "terminalSession.writeInput": {
92186
92131
  capName: "terminal-session",
92187
92132
  capScope: "system",
@@ -92196,6 +92141,13 @@ Object.freeze({
92196
92141
  access: "create",
92197
92142
  role: "viewer"
92198
92143
  },
92144
+ "timedRun.getOptions": {
92145
+ capName: "timed-run",
92146
+ capScope: "device",
92147
+ addonId: null,
92148
+ access: "view",
92149
+ role: "viewer"
92150
+ },
92199
92151
  "timedRun.getStatus": {
92200
92152
  capName: "timed-run",
92201
92153
  capScope: "device",
@@ -92210,6 +92162,13 @@ Object.freeze({
92210
92162
  access: "create",
92211
92163
  role: "viewer"
92212
92164
  },
92165
+ "timedRun.setSettings": {
92166
+ capName: "timed-run",
92167
+ capScope: "device",
92168
+ addonId: null,
92169
+ access: "create",
92170
+ role: "admin"
92171
+ },
92213
92172
  "toast.onToast": {
92214
92173
  capName: "toast",
92215
92174
  capScope: "system",
@@ -93140,6 +93099,7 @@ function offered(received) {
93140
93099
  };
93141
93100
  }
93142
93101
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
93102
+ string().min(1).max(4096);
93143
93103
  Object.freeze({
93144
93104
  "accessories.getStatus": [{
93145
93105
  name: "deviceId",
@@ -93823,6 +93783,11 @@ Object.freeze({
93823
93783
  form: "single",
93824
93784
  optional: false
93825
93785
  }],
93786
+ "deviceManager.setAlias": [{
93787
+ name: "deviceId",
93788
+ form: "single",
93789
+ optional: false
93790
+ }],
93826
93791
  "deviceManager.setChildLayout": [{
93827
93792
  name: "deviceId",
93828
93793
  form: "single",
@@ -95887,6 +95852,11 @@ Object.freeze({
95887
95852
  form: "single",
95888
95853
  optional: false
95889
95854
  }],
95855
+ "timedRun.getOptions": [{
95856
+ name: "deviceId",
95857
+ form: "single",
95858
+ optional: false
95859
+ }],
95890
95860
  "timedRun.getStatus": [{
95891
95861
  name: "deviceId",
95892
95862
  form: "single",
@@ -95897,6 +95867,11 @@ Object.freeze({
95897
95867
  form: "single",
95898
95868
  optional: false
95899
95869
  }],
95870
+ "timedRun.setSettings": [{
95871
+ name: "deviceId",
95872
+ form: "single",
95873
+ optional: false
95874
+ }],
95900
95875
  "update.getStatus": [{
95901
95876
  name: "deviceId",
95902
95877
  form: "single",
@@ -96759,6 +96734,7 @@ var STATE_SUMMARY_VARS = [
96759
96734
  [...RULE_TEXT],
96760
96735
  [...RULE_TEXT],
96761
96736
  [...RULE_TEXT],
96737
+ [...RULE_TEXT],
96762
96738
  [...RULE_BODY_ONLY],
96763
96739
  [...RULE_TEXT],
96764
96740
  ([...RULE_TEXT], [...DEVICE_EVENTS]),