@camstack/addon-provider-enpal 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/addon.js +1599 -1763
  3. package/dist/addon.mjs +1599 -1763
  4. package/package.json +2 -2
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-BmPm98n0.mjs
1
+ //#region ../types/dist/event-category-78tcBemo.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -119,6 +119,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
119
119
  */
120
120
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
121
121
  /**
122
+ * An addon is RUNNING although its installed package is below what the
123
+ * running server requires (`camstack.requires.addons` of the server, or the
124
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
125
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
126
+ * operator alert per node and package. The addon is never held back for it.
127
+ *
128
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
129
+ */
130
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
131
+ /**
132
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
133
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
134
+ *
135
+ * Payload: `{ packageName, addonId, nodeId }`.
136
+ */
137
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
138
+ /**
122
139
  * Monitor scheduled the next retry for a failed addon. Transient —
123
140
  * surfaced to the UI for live-updating the "next retry in Ns"
124
141
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -229,7 +246,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
229
246
  EventCategory["ProcessRestarted"] = "process.restarted";
230
247
  /**
231
248
  * The SET of storage locations changed — one was created, edited, enabled,
232
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
249
+ * disabled or deleted through the location instances or the forced-delete action.
233
250
  *
234
251
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
235
252
  * it must also converge on its own periodic path, because a dropped event
@@ -784,7 +801,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
784
801
  return EventCategory;
785
802
  }({});
786
803
  //#endregion
787
- //#region ../types/dist/device-type-D1fH5kaC.mjs
804
+ //#region ../types/dist/device-type-DxCT0krO.mjs
788
805
  /**
789
806
  import { errMsg } from '@camstack/types'
790
807
  * Extract a human-readable message from an unknown error value.
@@ -946,6 +963,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
946
963
  * filters that want "all devices with on-motion behaviour".
947
964
  */
948
965
  DeviceFeature["MotionTrigger"] = "motion-trigger";
966
+ /**
967
+ * Device can be run for a chosen time and then stopped. Mirrors the
968
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
969
+ * a driver, so the flag is derived from the device's bindings
970
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
971
+ */
972
+ DeviceFeature["TimedRun"] = "timed-run";
949
973
  /** Light supports rgb-triplet color via `color` cap. */
950
974
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
951
975
  /** Light supports HSV color via `color` cap. */
@@ -5980,7 +6004,7 @@ var ZodIssueCode = {
5980
6004
  var ZodFirstPartyTypeKind;
5981
6005
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
6006
  //#endregion
5983
- //#region ../types/dist/sleep-CmjWurK5.mjs
6007
+ //#region ../types/dist/sleep-CGPZyzB5.mjs
5984
6008
  /**
5985
6009
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
6010
  * window to float samples (D455).
@@ -6344,6 +6368,59 @@ Object.fromEntries([
6344
6368
  function isValuelessField(field) {
6345
6369
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
6346
6370
  }
6371
+ /**
6372
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6373
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6374
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6375
+ * pre-resolving) lets callers both read the effective value AND detect
6376
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6377
+ * malformed input yields an empty map, never throws.
6378
+ */
6379
+ function collectHydratedFieldEntries(schema) {
6380
+ const out = /* @__PURE__ */ new Map();
6381
+ if (typeof schema !== "object" || schema === null) return out;
6382
+ const sections = schema.sections;
6383
+ if (!Array.isArray(sections)) return out;
6384
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6385
+ return out;
6386
+ }
6387
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6388
+ function collectFieldEntriesInto(fields, into) {
6389
+ if (!Array.isArray(fields)) return;
6390
+ for (const field of fields) {
6391
+ if (typeof field !== "object" || field === null) continue;
6392
+ const type = field.type;
6393
+ if (type === "group") {
6394
+ collectFieldEntriesInto(field.fields, into);
6395
+ continue;
6396
+ }
6397
+ if (type === "sub-tabs") {
6398
+ const tabs = field.tabs;
6399
+ if (Array.isArray(tabs)) {
6400
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6401
+ }
6402
+ continue;
6403
+ }
6404
+ const key = field.key;
6405
+ if (typeof key !== "string" || key.length === 0) continue;
6406
+ if (!("value" in field)) continue;
6407
+ into.set(key, {
6408
+ value: field.value,
6409
+ default: field.default
6410
+ });
6411
+ }
6412
+ }
6413
+ /**
6414
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6415
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6416
+ * common "just give me the resolved values" case. Built on
6417
+ * {@link collectHydratedFieldEntries}.
6418
+ */
6419
+ function collectHydratedFieldValues(schema) {
6420
+ const out = /* @__PURE__ */ new Map();
6421
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6422
+ return out;
6423
+ }
6347
6424
  function hydrateSchema(schema, values, options = {}) {
6348
6425
  return {
6349
6426
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6749,10 +6826,33 @@ function rowsDepsOver(api) {
6749
6826
  };
6750
6827
  }
6751
6828
  var PREFIX = "records/";
6829
+ /**
6830
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6831
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6832
+ * instance of the host's classes, not of the addon's. The brand plus the code
6833
+ * make `instanceof` answer by identity of the failure, not of the copy.
6834
+ */
6835
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6836
+ function carriesBrand(value) {
6837
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6838
+ }
6752
6839
  /** The base of every records failure. `detail` is the human part of the message. */
6753
6840
  var RecordsError = class extends Error {
6841
+ /** The code a subclass stands for; the base stands for every code. */
6842
+ static code = void 0;
6843
+ /**
6844
+ * `instanceof` across copies of this module: any branded error is a
6845
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6846
+ * subclass's code. Same-copy answers are unchanged.
6847
+ */
6848
+ static [Symbol.hasInstance](value) {
6849
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6850
+ if (!carriesBrand(value)) return false;
6851
+ return this.code === void 0 || value.code === this.code;
6852
+ }
6754
6853
  code;
6755
6854
  detail;
6855
+ [RECORDS_ERROR_BRAND] = true;
6756
6856
  constructor(code, detail) {
6757
6857
  super(`${PREFIX}${code}: ${detail}`);
6758
6858
  this.name = "RecordsError";
@@ -6761,29 +6861,33 @@ var RecordsError = class extends Error {
6761
6861
  }
6762
6862
  };
6763
6863
  /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
6764
- var RevConflictError = class extends RecordsError {
6864
+ var RevConflictError = class RevConflictError extends RecordsError {
6865
+ static code = "rev-conflict";
6765
6866
  constructor(detail) {
6766
- super("rev-conflict", detail);
6867
+ super(RevConflictError.code, detail);
6767
6868
  this.name = "RevConflictError";
6768
6869
  }
6769
6870
  };
6770
6871
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6771
- var RecordSecretUnavailableError = class extends RecordsError {
6872
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6873
+ static code = "secret-unavailable";
6772
6874
  constructor(detail) {
6773
- super("secret-unavailable", detail);
6875
+ super(RecordSecretUnavailableError.code, detail);
6774
6876
  this.name = "RecordSecretUnavailableError";
6775
6877
  }
6776
6878
  };
6777
- var InvalidRecordError = class extends RecordsError {
6879
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6880
+ static code = "invalid-record";
6778
6881
  constructor(detail) {
6779
- super("invalid-record", detail);
6882
+ super(InvalidRecordError.code, detail);
6780
6883
  this.name = "InvalidRecordError";
6781
6884
  }
6782
6885
  };
6783
6886
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6784
- var DeclarationRefusedError = class extends RecordsError {
6887
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6888
+ static code = "declaration-refused";
6785
6889
  constructor(detail) {
6786
- super("declaration-refused", detail);
6890
+ super(DeclarationRefusedError.code, detail);
6787
6891
  this.name = "DeclarationRefusedError";
6788
6892
  }
6789
6893
  };
@@ -7185,6 +7289,7 @@ var RecordIntegrationInstances = class {
7185
7289
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
7186
7290
  this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
7187
7291
  this.nameField = kind.declaration.entry?.nameField ?? "name";
7292
+ if (hooks.kindOf === void 0 && hooks.description.kinds.length !== 1) throw new Error(`record kind '${kind.name}': describe().kinds has ${hooks.description.kinds.length} entries, so kindOf is required to say which one an instance is`);
7188
7293
  }
7189
7294
  /**
7190
7295
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -7226,10 +7331,21 @@ var RecordIntegrationInstances = class {
7226
7331
  }
7227
7332
  return out;
7228
7333
  }
7334
+ /** The kind an instance body is of: the provider's `kindOf`, else its one declared kind. */
7335
+ kindOfBody(body) {
7336
+ if (this.hooks.kindOf !== void 0) {
7337
+ const id = this.hooks.kindOf(body);
7338
+ if (!this.hooks.description.kinds.some((declared) => declared.id === id)) throw new Error(`record kind '${this.kind.name}': kindOf returned '${id}', which describe().kinds does not declare (D1013)`);
7339
+ return id;
7340
+ }
7341
+ const only = this.hooks.description.kinds[0];
7342
+ if (only === void 0) throw new Error(`record kind '${this.kind.name}' declares no kinds`);
7343
+ return only.id;
7344
+ }
7229
7345
  viewOf(record) {
7230
7346
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
7231
7347
  const rawName = record.data[this.nameField];
7232
- const kind = this.hooks.kindOf?.(record.data);
7348
+ const kind = this.kindOfBody(record.data);
7233
7349
  const health = this.hooks.health?.(record.data, record.id);
7234
7350
  return {
7235
7351
  id: record.id,
@@ -7237,7 +7353,7 @@ var RecordIntegrationInstances = class {
7237
7353
  configured: this.hooks.configured(record.data),
7238
7354
  enabled: isInstanceEnabled(record.data),
7239
7355
  formValues: shown,
7240
- ...kind !== void 0 ? { kind } : {},
7356
+ kind,
7241
7357
  ...health?.status !== void 0 ? { status: health.status } : {},
7242
7358
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
7243
7359
  };
@@ -7270,17 +7386,35 @@ var RecordIntegrationInstances = class {
7270
7386
  ...kind !== void 0 ? { kind } : {}
7271
7387
  });
7272
7388
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7389
+ const initial = this.hooks.initialBody(name, kind);
7390
+ const patch = this.patchOf(values ?? {});
7391
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7392
+ kind,
7393
+ body: {
7394
+ ...initial,
7395
+ ...patch
7396
+ }
7397
+ }));
7273
7398
  await this.handle.put({
7274
7399
  id: minted,
7275
- data: this.hooks.initialBody(name, kind)
7400
+ data: initial
7276
7401
  }, { ifRev: 0 });
7277
- const patch = this.patchOf(values ?? {});
7278
7402
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7279
7403
  await this.hooks.onChanged?.(minted, "created");
7280
7404
  return { id: minted };
7281
7405
  };
7282
7406
  updateInstance = async ({ id, name, values }) => {
7283
7407
  const patch = this.patchOf(values);
7408
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7409
+ const stored = await this.handle.get(id);
7410
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7411
+ kind: void 0,
7412
+ body: {
7413
+ ...stored.data,
7414
+ ...patch
7415
+ }
7416
+ }));
7417
+ }
7284
7418
  if (name !== void 0) patch[this.nameField] = name;
7285
7419
  await this.handle.patch(id, patch);
7286
7420
  await this.hooks.onChanged?.(id, "updated");
@@ -7591,6 +7725,21 @@ var SCOPED_SETTINGS_SCOPES = [
7591
7725
  "device-defaults",
7592
7726
  "node"
7593
7727
  ];
7728
+ /** The one refusal of a key a scope did not declare. */
7729
+ function assertDeclaredKeys(keys, declared) {
7730
+ const undeclared = keys.filter((key) => !declared.has(key));
7731
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7732
+ }
7733
+ /** Build the handle over an addon's scoped read, write and unset. */
7734
+ function createScopedSettings(deps) {
7735
+ return {
7736
+ async get() {
7737
+ return deps.schema.parse(await deps.read());
7738
+ },
7739
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7740
+ unset: (keys) => deps.unset(keys)
7741
+ };
7742
+ }
7594
7743
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7595
7744
  var KeychainWriteRefusedError = class extends Error {
7596
7745
  fieldPath;
@@ -8166,7 +8315,16 @@ function createHandle(raw, opts) {
8166
8315
  return blob;
8167
8316
  }
8168
8317
  },
8169
- writeAddonStore: writeAddon
8318
+ writeAddonStore: writeAddon,
8319
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8320
+ const stored = await readStoredAddonBlob(raw);
8321
+ index.seen("addon", stored);
8322
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8323
+ if (removable.length === 0) return;
8324
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8325
+ await raw.writeAddonStore(patch);
8326
+ index.wrote("addon", patch);
8327
+ })
8170
8328
  };
8171
8329
  }
8172
8330
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8296,7 +8454,7 @@ function boundOf(list) {
8296
8454
  * Base class for CamStack addons. Eliminates settings boilerplate:
8297
8455
  *
8298
8456
  * - Typed `config` property with automatic resolution from store + defaults
8299
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8457
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8300
8458
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8301
8459
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8302
8460
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8306,7 +8464,7 @@ function boundOf(list) {
8306
8464
  * Subclasses override:
8307
8465
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8308
8466
  * - `onShutdown()` — cleanup
8309
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8467
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8310
8468
  *
8311
8469
  * @example
8312
8470
  * ```ts
@@ -8320,7 +8478,7 @@ function boundOf(list) {
8320
8478
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8321
8479
  * }
8322
8480
  *
8323
- * protected globalSettingsSchema() {
8481
+ * protected clusterSettingsSchema() {
8324
8482
  * return this.schema({
8325
8483
  * sections: [{
8326
8484
  * id: 'main', title: 'Settings',
@@ -8363,7 +8521,7 @@ var BaseAddon = class {
8363
8521
  defaults;
8364
8522
  /**
8365
8523
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8366
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8524
+ * itself (`state()`, the scoped writers) that no form field declares.
8367
8525
  * Stored in the keychain exactly like a `password` field. Syntax:
8368
8526
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8369
8527
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8373,7 +8531,7 @@ var BaseAddon = class {
8373
8531
  /**
8374
8532
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8375
8533
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8376
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8534
+ * see them. Empty until loaded.
8377
8535
  */
8378
8536
  _scopedByCap = /* @__PURE__ */ new Map();
8379
8537
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8559,7 +8717,7 @@ var BaseAddon = class {
8559
8717
  /** Addon-specific cleanup. Override if needed. */
8560
8718
  async onShutdown() {}
8561
8719
  /**
8562
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8720
+ * Called after config is resolved during a settings write.
8563
8721
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8564
8722
  * Not called during initialize() — use onInitialize() for initial setup.
8565
8723
  */
@@ -8588,82 +8746,12 @@ var BaseAddon = class {
8588
8746
  scopedSettingsCapabilities() {
8589
8747
  return [];
8590
8748
  }
8591
- /** Override to provide global-level settings UI schema. */
8592
- globalSettingsSchema(_cap) {
8593
- return null;
8594
- }
8595
8749
  /** Override to provide device-level settings UI schema. */
8596
8750
  deviceSettingsSchema() {
8597
8751
  return null;
8598
8752
  }
8599
8753
  /**
8600
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8601
- * ARE the configuration of its integration.
8602
- *
8603
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8604
- * operator should find on the addon's integration page (System →
8605
- * Integrations → <name>) rather than only in the cluster-wide list of every
8606
- * addon. Empty (the default) means the addon has no integration-level
8607
- * settings and no such surface is offered — this is opt-in, because whether
8608
- * an addon's configuration IS its integration's configuration depends on the
8609
- * nature of the integration.
8610
- *
8611
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8612
- * the ONE global schema, in the ONE addon store, written by the ONE
8613
- * `updateGlobalSettings` path. There is deliberately no
8614
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8615
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8616
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8617
- *
8618
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8619
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8620
- * removed with the reason recorded at
8621
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8622
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8623
- * marker sprinkled across sections also has to borrow a field that already
8624
- * means something else; borrowing `section.tab` put the literal word
8625
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8626
- * GROUP this visually" and cannot also mean "where this lives" (D269
8627
- * supersedes D268). One declaration, in one place, next to the schema whose
8628
- * ids it names.
8629
- */
8630
- integrationSettingSections() {
8631
- return [];
8632
- }
8633
- async getGlobalSettings(overlay, cap, nodeId) {
8634
- await this.loadScopedSchemas(cap);
8635
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8636
- if (!schema) return { sections: [] };
8637
- const projected = await this.readRawProjectedStore(nodeId, cap);
8638
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8639
- }
8640
- /**
8641
- * The integration-level view of this addon's settings: exactly the sections
8642
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8643
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8644
- *
8645
- * Returns `null` when the addon declared nothing — an addon that opts out has
8646
- * no integration settings surface at all, rather than an empty one that reads
8647
- * as a failed load.
8648
- *
8649
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8650
- * and not in whichever UI happens to render this:
8651
- *
8652
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8653
- * shown here is the same field, with the same bare key, that the addon's
8654
- * own page shows. There is no integration-specific writer — callers save
8655
- * through `updateGlobalSettings` — so a second store key is unreachable,
8656
- * not merely discouraged.
8657
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8658
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8659
- * such a field silently picked would be a wrong answer for the operator
8660
- * who opened the page (D266).
8661
- * 3. **No silent typo.** A declared id that names no section throws. The
8662
- * alternative — skip it — turns a rename into a surface that quietly
8663
- * empties, which looks exactly like an addon with nothing to configure.
8664
- */
8665
- /**
8666
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8754
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8667
8755
  *
8668
8756
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8669
8757
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8711,37 +8799,178 @@ var BaseAddon = class {
8711
8799
  }
8712
8800
  }
8713
8801
  /**
8802
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8803
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8804
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8805
+ */
8806
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8807
+ return null;
8808
+ }
8809
+ /**
8810
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8811
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8812
+ */
8813
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8814
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8815
+ clusterSettings(cap, schema) {
8816
+ return this.scopedHandle("cluster", cap, schema);
8817
+ }
8818
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8819
+ deviceDefaults(cap, schema) {
8820
+ return this.scopedHandle("device-defaults", cap, schema);
8821
+ }
8822
+ /**
8823
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8824
+ * with no row yields the schema defaults — never a bare-key fallback.
8825
+ */
8826
+ nodeSettings(cap, schema, nodeId) {
8827
+ return this.scopedHandle("node", cap, schema, nodeId);
8828
+ }
8829
+ /**
8830
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8831
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8832
+ * central delete. `nodeId` omitted ⇒ the local node.
8833
+ */
8834
+ nodeState(key, schema, fallback, nodeId) {
8835
+ this._declaredStateKeys.add(key);
8836
+ return createDurableState({
8837
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8838
+ schema,
8839
+ fallback,
8840
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8841
+ write: async (patch) => {
8842
+ await this._ctx?.settings?.writeAddonStore(patch);
8843
+ },
8844
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8845
+ addonId: this._ctx?.id,
8846
+ key: k,
8847
+ error: String(e)
8848
+ } })
8849
+ });
8850
+ }
8851
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8852
+ async declaredScopedSchema(scope, cap) {
8853
+ await this.loadScopedSchemas(cap);
8854
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8855
+ }
8856
+ async requireScopedSchema(scope, cap) {
8857
+ const schema = await this.declaredScopedSchema(scope, cap);
8858
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8859
+ return schema;
8860
+ }
8861
+ scopedKeys(schema) {
8862
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8863
+ }
8864
+ /** `store` narrowed to the keys `schema` declares. */
8865
+ narrowToScope(store, schema) {
8866
+ const keys = this.scopedKeys(schema);
8867
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8868
+ }
8869
+ scopedHandle(scope, cap, schema, nodeId) {
8870
+ return createScopedSettings({
8871
+ schema,
8872
+ read: async () => {
8873
+ const declared = await this.requireScopedSchema(scope, cap);
8874
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8875
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8876
+ return {
8877
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8878
+ ...stored
8879
+ };
8880
+ },
8881
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8882
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8883
+ });
8884
+ }
8885
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8886
+ async writeScoped(scope, cap, patch, nodeId) {
8887
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8888
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8889
+ }
8890
+ /**
8891
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8892
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8893
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8894
+ */
8895
+ async unsetScoped(scope, cap, keys, nodeId) {
8896
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8897
+ const target = scope === "node" ? nodeId : void 0;
8898
+ const tree = this.addonSecretTree();
8899
+ const secretKeys = keys.filter((key) => tree.has(key));
8900
+ const plainKeys = keys.filter((key) => !tree.has(key));
8901
+ let changed = false;
8902
+ if (plainKeys.length > 0) {
8903
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8904
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8905
+ }
8906
+ if (secretKeys.length > 0) {
8907
+ await this.clearSecretKeys(secretKeys, target, cap);
8908
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8909
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8910
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8911
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8912
+ changed = true;
8913
+ }
8914
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8915
+ }
8916
+ /**
8917
+ * The hydrated form of one scope of one capability's settings (any
8918
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8919
+ */
8920
+ async getScopedSettings(scope, cap, nodeId) {
8921
+ const schema = await this.declaredScopedSchema(scope, cap);
8922
+ if (schema === null) return null;
8923
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8924
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8925
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8926
+ }
8927
+ /**
8928
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8929
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8930
+ * nothing.
8931
+ */
8932
+ async listScopedSettings() {
8933
+ await this.loadScopedSchemas();
8934
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8935
+ cap,
8936
+ scope: entry.scope,
8937
+ sectionIds: entry.schema.sections.map((section) => section.id)
8938
+ })));
8939
+ }
8940
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8941
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8942
+ this._operatorWriteDepth++;
8943
+ try {
8944
+ await this.writeScoped(scope, cap, patch, nodeId);
8945
+ } finally {
8946
+ this._operatorWriteDepth--;
8947
+ }
8948
+ }
8949
+ /**
8950
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8951
+ * its value. `null` when the capability declares nothing for the scope.
8952
+ */
8953
+ async readScopedValues(scope, cap, nodeId) {
8954
+ const schema = await this.declaredScopedSchema(scope, cap);
8955
+ if (schema === null) return null;
8956
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8957
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8958
+ }
8959
+ /**
8714
8960
  * What the create wizard and the integration edit page render for one instance: the addon's
8715
8961
  * per-instance schema when it declares one, else its global schema (what they always showed).
8716
8962
  */
8717
8963
  async getIntegrationFormSchema(instance, nodeId) {
8718
8964
  const scoped = await this.integrationFormSchema(instance);
8719
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8965
+ if (scoped === null) return this.scopedForm(nodeId);
8720
8966
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8721
8967
  }
8722
- async getIntegrationSettings(nodeId) {
8723
- const declared = this.integrationSettingSections();
8724
- if (declared.length === 0) return null;
8725
- const schema = this.globalSettingsSchema();
8726
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8727
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8728
- const sections = [];
8729
- for (const id of declared) {
8730
- const section = byId.get(id);
8731
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8732
- const fields = dropPerNodeFields(section.fields);
8733
- if (fields.length === 0) continue;
8734
- sections.push({
8735
- ...section,
8736
- fields
8737
- });
8738
- }
8739
- if (sections.length === 0) return null;
8740
- const projected = await this.readRawProjectedStore(nodeId);
8741
- return hydrateSchema({
8742
- ...schema,
8743
- sections
8744
- }, projected, { inlineSecretsAsViews: true });
8968
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8969
+ async scopedForm(nodeId) {
8970
+ await this.loadScopedSchemas();
8971
+ const schema = this.scopedFormSchema();
8972
+ if (!schema) return { sections: [] };
8973
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8745
8974
  }
8746
8975
  /**
8747
8976
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8750,8 +8979,8 @@ var BaseAddon = class {
8750
8979
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8751
8980
  * A no-op passthrough when the schema declares no `perNode` field.
8752
8981
  *
8753
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8754
- * the store for custom option logic (option narrowing, value snapping) to
8982
+ * This is the sanctioned way for an addon that needs
8983
+ * the store for custom logic (option narrowing, value snapping) to
8755
8984
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8756
8985
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8757
8986
  *
@@ -8803,7 +9032,7 @@ var BaseAddon = class {
8803
9032
  }
8804
9033
  /**
8805
9034
  * Whether ONE global key of this addon's store was written, read three-way
8806
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
9035
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8807
9036
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8808
9037
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8809
9038
  * not supported here: its stored name is scoped.
@@ -8837,7 +9066,7 @@ var BaseAddon = class {
8837
9066
  }
8838
9067
  /**
8839
9068
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8840
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9069
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8841
9070
  * the operator sends is stored as sent even when it equals a pre-keychain
8842
9071
  * inline value: the operator re-entering a password is a change, an addon
8843
9072
  * writing back what it read is not (D787).
@@ -8850,9 +9079,6 @@ var BaseAddon = class {
8850
9079
  this._operatorWriteDepth--;
8851
9080
  }
8852
9081
  }
8853
- async updateGlobalSettings(patch, nodeId) {
8854
- await this.applyGlobalPatch(patch, nodeId);
8855
- }
8856
9082
  /**
8857
9083
  * REMOVE stored secrets — their keychain values and their markers (D787).
8858
9084
  *
@@ -8862,6 +9088,9 @@ var BaseAddon = class {
8862
9088
  * a keychain (an older kernel) it writes `''`, as such code always did.
8863
9089
  */
8864
9090
  async clearSecrets(keys, nodeId) {
9091
+ await this.clearSecretKeys(keys, nodeId);
9092
+ }
9093
+ async clearSecretKeys(keys, nodeId, cap) {
8865
9094
  const tree = this.addonSecretTree();
8866
9095
  const keychain = this._ctx?.secrets !== void 0;
8867
9096
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8874,25 +9103,26 @@ var BaseAddon = class {
8874
9103
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8875
9104
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8876
9105
  }
8877
- await this.applyGlobalPatch(patch, nodeId);
9106
+ await this.applyGlobalPatch(patch, nodeId, cap);
8878
9107
  }
8879
- async applyGlobalPatch(barePatch, nodeId) {
8880
- await this.loadScopedSchemas();
8881
- const keys = this.perNodeKeys();
9108
+ /** Resolves `true` when the write changed stored values. */
9109
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9110
+ await this.loadScopedSchemas(cap);
9111
+ const keys = this.perNodeKeys(cap);
8882
9112
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8883
9113
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8884
9114
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8885
9115
  const handle = this._secretSettings;
8886
9116
  if (handle !== null) {
8887
9117
  const { changed } = await handle.writeAddonStore(scoped);
8888
- if (!changed) return;
9118
+ if (!changed) return false;
8889
9119
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8890
- if (target !== localNode) return;
9120
+ if (target !== localNode) return true;
8891
9121
  await this.resolveConfig();
8892
9122
  await this.onConfigChanged();
8893
9123
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8894
- const legacy = this.globalSettingsSchema();
8895
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9124
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9125
+ return true;
8896
9126
  }
8897
9127
  /**
8898
9128
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8929,10 +9159,7 @@ var BaseAddon = class {
8929
9159
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8930
9160
  this._addonSecretTree = null;
8931
9161
  }
8932
- /**
8933
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8934
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8935
- */
9162
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8936
9163
  scopedFormSchema(cap) {
8937
9164
  const byId = /* @__PURE__ */ new Map();
8938
9165
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8944,8 +9171,7 @@ var BaseAddon = class {
8944
9171
  return [...this._scopedByCap.values()].flat();
8945
9172
  }
8946
9173
  /**
8947
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8948
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9174
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8949
9175
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8950
9176
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8951
9177
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8954,8 +9180,7 @@ var BaseAddon = class {
8954
9180
  const cacheKey = cap ?? "";
8955
9181
  const cached = this._nodeKeysByCap.get(cacheKey);
8956
9182
  if (cached) return cached;
8957
- const legacy = this.globalSettingsSchema(cap);
8958
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9183
+ const keys = /* @__PURE__ */ new Set();
8959
9184
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8960
9185
  if (scope !== "node") continue;
8961
9186
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9173,8 +9398,6 @@ var BaseAddon = class {
9173
9398
  }
9174
9399
  declaredKeySet() {
9175
9400
  const keys = new Set(Object.keys(this.defaults));
9176
- const schema = this.globalSettingsSchema();
9177
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9178
9401
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9179
9402
  for (const key of this._declaredStateKeys) keys.add(key);
9180
9403
  for (const path of this.secretStatePaths) {
@@ -9272,7 +9495,7 @@ var BaseAddon = class {
9272
9495
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9273
9496
  addonSecretTree() {
9274
9497
  if (this._addonSecretTree === null) {
9275
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9498
+ const tree = /* @__PURE__ */ new Map();
9276
9499
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9277
9500
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9278
9501
  }
@@ -9484,47 +9707,6 @@ function classifyStoreError(err) {
9484
9707
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9485
9708
  return "permanent";
9486
9709
  }
9487
- /**
9488
- * Collect the keys of every field marked `perNode: true`, recursing into
9489
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9490
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9491
- * don't declare `perNode` and are excluded by the `in` narrowing.
9492
- */
9493
- /**
9494
- * The same fields with every `perNode: true` one removed, recursing into layout
9495
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9496
- * with no child is dropped rather than rendered empty.
9497
- *
9498
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9499
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9500
- */
9501
- function dropPerNodeFields(fields) {
9502
- const kept = [];
9503
- for (const field of fields) {
9504
- if (field.type === "group") {
9505
- const inner = dropPerNodeFields(field.fields);
9506
- if (inner.length > 0) kept.push({
9507
- ...field,
9508
- fields: inner
9509
- });
9510
- continue;
9511
- }
9512
- if (field.type === "sub-tabs") {
9513
- const tabs = field.tabs.map((tab) => ({
9514
- ...tab,
9515
- fields: dropPerNodeFields(tab.fields)
9516
- })).filter((tab) => tab.fields.length > 0);
9517
- if (tabs.length > 0) kept.push({
9518
- ...field,
9519
- tabs
9520
- });
9521
- continue;
9522
- }
9523
- if ("perNode" in field && field.perNode === true) continue;
9524
- kept.push(field);
9525
- }
9526
- return kept;
9527
- }
9528
9710
  /** The keys of the value-holding fields, recursing into layout containers. */
9529
9711
  function collectValueFieldKeys(fields) {
9530
9712
  const collected = [];
@@ -9557,21 +9739,6 @@ function collectFieldKeys(fields) {
9557
9739
  }
9558
9740
  return collected;
9559
9741
  }
9560
- function collectPerNodeFieldKeys(fields) {
9561
- const collected = [];
9562
- for (const field of fields) {
9563
- if (field.type === "group") {
9564
- collected.push(...collectPerNodeFieldKeys(field.fields));
9565
- continue;
9566
- }
9567
- if (field.type === "sub-tabs") {
9568
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9569
- continue;
9570
- }
9571
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9572
- }
9573
- return collected;
9574
- }
9575
9742
  /**
9576
9743
  * Normalize an `ICamstackAddon.initialize()` return value into the
9577
9744
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -9587,30 +9754,98 @@ function normalizeAddonInitResult(result) {
9587
9754
  if (Array.isArray(result)) return { providers: result };
9588
9755
  return result;
9589
9756
  }
9757
+ object({
9758
+ field: string().min(1),
9759
+ on: literal("advance")
9760
+ });
9590
9761
  /**
9591
- * The share-view scope kinds a share-link token can carry. The hub mints
9592
- * and validates them (`ShareViewGrantSchema` builds its
9593
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9762
+ * Output schema shared by the contribution + live methods.
9763
+ *
9764
+ * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9765
+ * tabs[]) without importing from `../interfaces/config-ui.js` — a
9766
+ * concrete-but-lenient Zod object keeps tRPC output inference happy
9767
+ * (using `z.unknown()` here collapses unrelated router branches to
9768
+ * `unknown` when the generator re-inlines the huge AppRouter type).
9769
+ *
9770
+ * `.passthrough()` on sections/fields accepts whatever FormBuilder
9771
+ * extensions the caller adds (showWhen, displayScale, …) without
9772
+ * rebuilding every time a new field kind is introduced.
9594
9773
  */
9595
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9596
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9597
- var PeerBytesTicketSchema = object({
9598
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9599
- url: string().min(1),
9600
- /**
9601
- * The HOST node this URL means something on — the hub or a named agent,
9602
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9603
- * refuses `cross-node` by name when they differ, without dialling.
9604
- */
9605
- hostNodeId: string().min(1),
9606
- expiresAtMs: number().int().nonnegative(),
9607
- /**
9608
- * What the producer DECLARED the body to be, when it knows — `null` when it
9609
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9610
- * must be able to tell "the producer did not say" from "the body is empty".
9611
- */
9612
- declaredBytes: number().int().nonnegative().nullable()
9774
+ var ContributionSectionSchema = object({
9775
+ id: string(),
9776
+ title: string(),
9777
+ description: string().optional(),
9778
+ style: _enum(["card", "accordion"]).optional(),
9779
+ defaultCollapsed: boolean().optional(),
9780
+ columns: union([
9781
+ literal(1),
9782
+ literal(2),
9783
+ literal(3),
9784
+ literal(4)
9785
+ ]).optional(),
9786
+ tab: string().optional(),
9787
+ location: _enum([
9788
+ "settings",
9789
+ "top-tab",
9790
+ "hero"
9791
+ ]).optional(),
9792
+ order: number().optional(),
9793
+ fields: array(any())
9613
9794
  });
9795
+ object({
9796
+ tabs: array(object({
9797
+ id: string(),
9798
+ label: string(),
9799
+ icon: string(),
9800
+ order: number().optional()
9801
+ })).optional(),
9802
+ sections: array(ContributionSectionSchema)
9803
+ }).nullable();
9804
+ object({ deviceId: number() }), object({ deviceId: number() }), object({
9805
+ deviceId: number(),
9806
+ patch: record(string(), unknown())
9807
+ }), object({ success: literal(true) });
9808
+ object({ deviceId: number() }), unknown().nullable();
9809
+ function method(input, output, options) {
9810
+ return {
9811
+ input,
9812
+ output,
9813
+ kind: options?.kind ?? "query",
9814
+ ...options?.auth !== void 0 ? { auth: options.auth } : {},
9815
+ ...options?.access !== void 0 ? { access: options.access } : {},
9816
+ ...options?.role !== void 0 ? { role: options.role } : {},
9817
+ ...options?.caller !== void 0 ? { caller: options.caller } : {},
9818
+ timeoutMs: options?.timeoutMs
9819
+ };
9820
+ }
9821
+ /**
9822
+ * A wrapper/system-only method: served exclusively by the cap's system-level
9823
+ * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9824
+ * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9825
+ * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9826
+ */
9827
+ function systemMethod(input, output, options) {
9828
+ return {
9829
+ ...method(input, output, options),
9830
+ systemOnly: true
9831
+ };
9832
+ }
9833
+ /**
9834
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9835
+ * on `InferProvider`. The `providerOptional: true` literal is what
9836
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9837
+ * for when this is the honest answer and when it is a soft stub.
9838
+ */
9839
+ function optionalMethod(input, output, options) {
9840
+ return {
9841
+ ...method(input, output, options),
9842
+ providerOptional: true
9843
+ };
9844
+ }
9845
+ /** Shorthand to define an event schema */
9846
+ function event(data) {
9847
+ return { data };
9848
+ }
9614
9849
  function defOf(schema) {
9615
9850
  const zodBag = Reflect.get(schema, "_zod");
9616
9851
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -9867,98 +10102,30 @@ function defineEntryKind(name, spec) {
9867
10102
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9868
10103
  return defined;
9869
10104
  }
9870
- object({
9871
- field: string().min(1),
9872
- on: literal("advance")
9873
- });
9874
10105
  /**
9875
- * Output schema shared by the contribution + live methods.
9876
- *
9877
- * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9878
- * tabs[]) without importing from `../interfaces/config-ui.js` — a
9879
- * concrete-but-lenient Zod object keeps tRPC output inference happy
9880
- * (using `z.unknown()` here collapses unrelated router branches to
9881
- * `unknown` when the generator re-inlines the huge AppRouter type).
9882
- *
9883
- * `.passthrough()` on sections/fields accepts whatever FormBuilder
9884
- * extensions the caller adds (showWhen, displayScale, …) without
9885
- * rebuilding every time a new field kind is introduced.
10106
+ * The share-view scope kinds a share-link token can carry. The hub mints
10107
+ * and validates them (`ShareViewGrantSchema` builds its
10108
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9886
10109
  */
9887
- var ContributionSectionSchema = object({
9888
- id: string(),
9889
- title: string(),
9890
- description: string().optional(),
9891
- style: _enum(["card", "accordion"]).optional(),
9892
- defaultCollapsed: boolean().optional(),
9893
- columns: union([
9894
- literal(1),
9895
- literal(2),
9896
- literal(3),
9897
- literal(4)
9898
- ]).optional(),
9899
- tab: string().optional(),
9900
- location: _enum([
9901
- "settings",
9902
- "top-tab",
9903
- "hero"
9904
- ]).optional(),
9905
- order: number().optional(),
9906
- fields: array(any())
10110
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
10111
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
10112
+ var PeerBytesTicketSchema = object({
10113
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
10114
+ url: string().min(1),
10115
+ /**
10116
+ * The HOST node this URL means something on — the hub or a named agent,
10117
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
10118
+ * refuses `cross-node` by name when they differ, without dialling.
10119
+ */
10120
+ hostNodeId: string().min(1),
10121
+ expiresAtMs: number().int().nonnegative(),
10122
+ /**
10123
+ * What the producer DECLARED the body to be, when it knows — `null` when it
10124
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
10125
+ * must be able to tell "the producer did not say" from "the body is empty".
10126
+ */
10127
+ declaredBytes: number().int().nonnegative().nullable()
9907
10128
  });
9908
- object({
9909
- tabs: array(object({
9910
- id: string(),
9911
- label: string(),
9912
- icon: string(),
9913
- order: number().optional()
9914
- })).optional(),
9915
- sections: array(ContributionSectionSchema)
9916
- }).nullable();
9917
- object({ deviceId: number() }), object({ deviceId: number() }), object({
9918
- deviceId: number(),
9919
- patch: record(string(), unknown())
9920
- }), object({ success: literal(true) });
9921
- object({ deviceId: number() }), unknown().nullable();
9922
- function method(input, output, options) {
9923
- return {
9924
- input,
9925
- output,
9926
- kind: options?.kind ?? "query",
9927
- ...options?.auth !== void 0 ? { auth: options.auth } : {},
9928
- ...options?.access !== void 0 ? { access: options.access } : {},
9929
- ...options?.role !== void 0 ? { role: options.role } : {},
9930
- ...options?.caller !== void 0 ? { caller: options.caller } : {},
9931
- timeoutMs: options?.timeoutMs
9932
- };
9933
- }
9934
- /**
9935
- * A wrapper/system-only method: served exclusively by the cap's system-level
9936
- * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9937
- * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9938
- * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9939
- */
9940
- function systemMethod(input, output, options) {
9941
- return {
9942
- ...method(input, output, options),
9943
- systemOnly: true
9944
- };
9945
- }
9946
- /**
9947
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9948
- * on `InferProvider`. The `providerOptional: true` literal is what
9949
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9950
- * for when this is the honest answer and when it is a soft stub.
9951
- */
9952
- function optionalMethod(input, output, options) {
9953
- return {
9954
- ...method(input, output, options),
9955
- providerOptional: true
9956
- };
9957
- }
9958
- /** Shorthand to define an event schema */
9959
- function event(data) {
9960
- return { data };
9961
- }
9962
10129
  var READING_QUANTITIES = [
9963
10130
  "temperature",
9964
10131
  "pressure",
@@ -10537,7 +10704,6 @@ var CAP_NAMES = Object.freeze([
10537
10704
  "climate-control",
10538
10705
  "color",
10539
10706
  "composer",
10540
- "connection-test",
10541
10707
  "connectivity",
10542
10708
  "consumables",
10543
10709
  "contact",
@@ -10627,7 +10793,6 @@ var CAP_NAMES = Object.freeze([
10627
10793
  "notifier",
10628
10794
  "numeric-sensor",
10629
10795
  "oauth-integration",
10630
- "oidc-connections",
10631
10796
  "osd",
10632
10797
  "osd-manager",
10633
10798
  "pet-feeder",
@@ -12334,13 +12499,25 @@ Object.freeze({
12334
12499
  "path": "temperature",
12335
12500
  "valueType": "number"
12336
12501
  }],
12337
- "timed-run": [{
12338
- "path": "activeUntil",
12339
- "valueType": "number"
12340
- }, {
12341
- "path": "requestedBy",
12342
- "valueType": "string"
12343
- }],
12502
+ "timed-run": [
12503
+ {
12504
+ "path": "activeUntil",
12505
+ "valueType": "number"
12506
+ },
12507
+ {
12508
+ "path": "requestedBy",
12509
+ "valueType": "string"
12510
+ },
12511
+ {
12512
+ "path": "quickRun.duration",
12513
+ "valueType": "number"
12514
+ },
12515
+ {
12516
+ "path": "quickRun.unit",
12517
+ "valueType": "enum",
12518
+ "enumValues": ["s", "min"]
12519
+ }
12520
+ ],
12344
12521
  "update": [
12345
12522
  {
12346
12523
  "path": "currentVersion",
@@ -12519,8 +12696,7 @@ Object.freeze({
12519
12696
  "device-provider": { "reloadDevice": 18e4 },
12520
12697
  "llm": {
12521
12698
  "generate": 6e5,
12522
- "generateVision": 6e5,
12523
- "testProfile": 6e5
12699
+ "generateVision": 6e5
12524
12700
  },
12525
12701
  "llm-runtime": {
12526
12702
  "complete": 6e5,
@@ -12557,8 +12733,7 @@ Object.freeze({
12557
12733
  "offerArtifact": 3e5,
12558
12734
  "readResult": 3e5,
12559
12735
  "submitJob": 18e5,
12560
- "uploadDataset": 72e5,
12561
- "validateCredentials": 3e4
12736
+ "uploadDataset": 72e5
12562
12737
  }
12563
12738
  });
12564
12739
  object({
@@ -12566,7 +12741,7 @@ object({
12566
12741
  params: record(string(), union([string(), number()])).optional()
12567
12742
  });
12568
12743
  //#endregion
12569
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12744
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12570
12745
  /**
12571
12746
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12572
12747
  *
@@ -13728,6 +13903,391 @@ var NodeColorIdSchema = _enum([
13728
13903
  "node-h"
13729
13904
  ]);
13730
13905
  //#endregion
13906
+ //#region ../types/dist/convert-BAzH15HU.mjs
13907
+ /**
13908
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13909
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13910
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13911
+ * decimals). K derives from `°C = K − 273.15`.
13912
+ */
13913
+ var UNIT_TABLE = {
13914
+ "°C": {
13915
+ dimension: "temperature",
13916
+ factor: 1,
13917
+ offset: 0
13918
+ },
13919
+ "°F": {
13920
+ dimension: "temperature",
13921
+ factor: 5 / 9,
13922
+ offset: -160 / 9
13923
+ },
13924
+ K: {
13925
+ dimension: "temperature",
13926
+ factor: 1,
13927
+ offset: -273.15
13928
+ },
13929
+ hPa: {
13930
+ dimension: "pressure",
13931
+ factor: 1,
13932
+ offset: 0
13933
+ },
13934
+ kPa: {
13935
+ dimension: "pressure",
13936
+ factor: 10,
13937
+ offset: 0
13938
+ },
13939
+ Pa: {
13940
+ dimension: "pressure",
13941
+ factor: .01,
13942
+ offset: 0
13943
+ },
13944
+ mbar: {
13945
+ dimension: "pressure",
13946
+ factor: 1,
13947
+ offset: 0
13948
+ },
13949
+ bar: {
13950
+ dimension: "pressure",
13951
+ factor: 1e3,
13952
+ offset: 0
13953
+ },
13954
+ inHg: {
13955
+ dimension: "pressure",
13956
+ factor: 33.8639,
13957
+ offset: 0
13958
+ },
13959
+ mmHg: {
13960
+ dimension: "pressure",
13961
+ factor: 1.33322,
13962
+ offset: 0
13963
+ },
13964
+ psi: {
13965
+ dimension: "pressure",
13966
+ factor: 68.9476,
13967
+ offset: 0
13968
+ },
13969
+ "m/s": {
13970
+ dimension: "speed",
13971
+ factor: 1,
13972
+ offset: 0
13973
+ },
13974
+ "km/h": {
13975
+ dimension: "speed",
13976
+ factor: 1 / 3.6,
13977
+ offset: 0
13978
+ },
13979
+ mph: {
13980
+ dimension: "speed",
13981
+ factor: .44704,
13982
+ offset: 0
13983
+ },
13984
+ kn: {
13985
+ dimension: "speed",
13986
+ factor: .514444,
13987
+ offset: 0
13988
+ },
13989
+ "mm/h": {
13990
+ dimension: "precipitation-rate",
13991
+ factor: 1,
13992
+ offset: 0
13993
+ },
13994
+ "in/h": {
13995
+ dimension: "precipitation-rate",
13996
+ factor: 25.4,
13997
+ offset: 0
13998
+ },
13999
+ m: {
14000
+ dimension: "length",
14001
+ factor: 1,
14002
+ offset: 0
14003
+ },
14004
+ mm: {
14005
+ dimension: "length",
14006
+ factor: .001,
14007
+ offset: 0
14008
+ },
14009
+ cm: {
14010
+ dimension: "length",
14011
+ factor: .01,
14012
+ offset: 0
14013
+ },
14014
+ km: {
14015
+ dimension: "length",
14016
+ factor: 1e3,
14017
+ offset: 0
14018
+ },
14019
+ in: {
14020
+ dimension: "length",
14021
+ factor: .0254,
14022
+ offset: 0
14023
+ },
14024
+ ft: {
14025
+ dimension: "length",
14026
+ factor: .3048,
14027
+ offset: 0
14028
+ },
14029
+ mi: {
14030
+ dimension: "length",
14031
+ factor: 1609.344,
14032
+ offset: 0
14033
+ },
14034
+ "m²": {
14035
+ dimension: "area",
14036
+ factor: 1,
14037
+ offset: 0
14038
+ },
14039
+ lx: {
14040
+ dimension: "illuminance",
14041
+ factor: 1,
14042
+ offset: 0
14043
+ },
14044
+ "W/m²": {
14045
+ dimension: "irradiance",
14046
+ factor: 1,
14047
+ offset: 0
14048
+ },
14049
+ W: {
14050
+ dimension: "power",
14051
+ factor: 1,
14052
+ offset: 0
14053
+ },
14054
+ kW: {
14055
+ dimension: "power",
14056
+ factor: 1e3,
14057
+ offset: 0
14058
+ },
14059
+ VA: {
14060
+ dimension: "apparent-power",
14061
+ factor: 1,
14062
+ offset: 0
14063
+ },
14064
+ Wh: {
14065
+ dimension: "energy",
14066
+ factor: 1,
14067
+ offset: 0
14068
+ },
14069
+ kWh: {
14070
+ dimension: "energy",
14071
+ factor: 1e3,
14072
+ offset: 0
14073
+ },
14074
+ MWh: {
14075
+ dimension: "energy",
14076
+ factor: 1e6,
14077
+ offset: 0
14078
+ },
14079
+ V: {
14080
+ dimension: "voltage",
14081
+ factor: 1,
14082
+ offset: 0
14083
+ },
14084
+ mV: {
14085
+ dimension: "voltage",
14086
+ factor: .001,
14087
+ offset: 0
14088
+ },
14089
+ A: {
14090
+ dimension: "current",
14091
+ factor: 1,
14092
+ offset: 0
14093
+ },
14094
+ mA: {
14095
+ dimension: "current",
14096
+ factor: .001,
14097
+ offset: 0
14098
+ },
14099
+ ppm: {
14100
+ dimension: "concentration-volume",
14101
+ factor: 1,
14102
+ offset: 0
14103
+ },
14104
+ ppb: {
14105
+ dimension: "concentration-volume",
14106
+ factor: .001,
14107
+ offset: 0
14108
+ },
14109
+ "µg/m³": {
14110
+ dimension: "concentration-mass",
14111
+ factor: 1,
14112
+ offset: 0
14113
+ },
14114
+ "mg/m³": {
14115
+ dimension: "concentration-mass",
14116
+ factor: 1e3,
14117
+ offset: 0
14118
+ },
14119
+ kg: {
14120
+ dimension: "mass",
14121
+ factor: 1,
14122
+ offset: 0
14123
+ },
14124
+ g: {
14125
+ dimension: "mass",
14126
+ factor: .001,
14127
+ offset: 0
14128
+ },
14129
+ lb: {
14130
+ dimension: "mass",
14131
+ factor: .453592,
14132
+ offset: 0
14133
+ },
14134
+ oz: {
14135
+ dimension: "mass",
14136
+ factor: .0283495,
14137
+ offset: 0
14138
+ },
14139
+ "%": {
14140
+ dimension: "percentage",
14141
+ factor: 1,
14142
+ offset: 0
14143
+ },
14144
+ L: {
14145
+ dimension: "volume",
14146
+ factor: 1,
14147
+ offset: 0
14148
+ },
14149
+ mL: {
14150
+ dimension: "volume",
14151
+ factor: .001,
14152
+ offset: 0
14153
+ },
14154
+ gal: {
14155
+ dimension: "volume",
14156
+ factor: 3.785411784,
14157
+ offset: 0
14158
+ },
14159
+ s: {
14160
+ dimension: "duration",
14161
+ factor: 1,
14162
+ offset: 0
14163
+ },
14164
+ ms: {
14165
+ dimension: "duration",
14166
+ factor: .001,
14167
+ offset: 0
14168
+ },
14169
+ min: {
14170
+ dimension: "duration",
14171
+ factor: 60,
14172
+ offset: 0
14173
+ },
14174
+ h: {
14175
+ dimension: "duration",
14176
+ factor: 3600,
14177
+ offset: 0
14178
+ },
14179
+ d: {
14180
+ dimension: "duration",
14181
+ factor: 86400,
14182
+ offset: 0
14183
+ },
14184
+ B: {
14185
+ dimension: "information",
14186
+ factor: 1,
14187
+ offset: 0
14188
+ },
14189
+ KiB: {
14190
+ dimension: "information",
14191
+ factor: 1024,
14192
+ offset: 0
14193
+ },
14194
+ MiB: {
14195
+ dimension: "information",
14196
+ factor: 1024 ** 2,
14197
+ offset: 0
14198
+ },
14199
+ GiB: {
14200
+ dimension: "information",
14201
+ factor: 1024 ** 3,
14202
+ offset: 0
14203
+ },
14204
+ TiB: {
14205
+ dimension: "information",
14206
+ factor: 1024 ** 4,
14207
+ offset: 0
14208
+ },
14209
+ kB: {
14210
+ dimension: "information",
14211
+ factor: 1e3,
14212
+ offset: 0
14213
+ },
14214
+ MB: {
14215
+ dimension: "information",
14216
+ factor: 1e6,
14217
+ offset: 0
14218
+ },
14219
+ GB: {
14220
+ dimension: "information",
14221
+ factor: 1e9,
14222
+ offset: 0
14223
+ },
14224
+ "°": {
14225
+ dimension: "angle",
14226
+ factor: 1,
14227
+ offset: 0
14228
+ },
14229
+ dBFS: {
14230
+ dimension: "sound-level",
14231
+ factor: 1,
14232
+ offset: 0
14233
+ },
14234
+ dBm: {
14235
+ dimension: "signal-level",
14236
+ factor: 1,
14237
+ offset: 0
14238
+ },
14239
+ mired: {
14240
+ dimension: "colour-temperature",
14241
+ factor: 1,
14242
+ offset: 0
14243
+ },
14244
+ kbps: {
14245
+ dimension: "bitrate",
14246
+ factor: 1,
14247
+ offset: 0
14248
+ },
14249
+ Mbps: {
14250
+ dimension: "bitrate",
14251
+ factor: 1e3,
14252
+ offset: 0
14253
+ },
14254
+ fps: {
14255
+ dimension: "rate",
14256
+ factor: 1,
14257
+ offset: 0
14258
+ }
14259
+ };
14260
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14261
+ function specFor(unit) {
14262
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14263
+ }
14264
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
14265
+ function unitDimension(unit) {
14266
+ return specFor(unit)?.dimension;
14267
+ }
14268
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14269
+ function toCanonical(value, spec) {
14270
+ return value * spec.factor + spec.offset;
14271
+ }
14272
+ /** Canonical-unit value → the unit described by `spec`. */
14273
+ function fromCanonical(canonical, spec) {
14274
+ return (canonical - spec.offset) / spec.factor;
14275
+ }
14276
+ /**
14277
+ * Graceful variant for the render path: returns the converted value, or
14278
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14279
+ * Never throws — a render caller falls back to the raw value + source unit.
14280
+ */
14281
+ function tryConvertUnit(value, from, to) {
14282
+ if (from === to) return value;
14283
+ const fromSpec = specFor(from);
14284
+ if (!fromSpec) return void 0;
14285
+ const toSpec = specFor(to);
14286
+ if (!toSpec) return void 0;
14287
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14288
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14289
+ }
14290
+ //#endregion
13731
14291
  //#region ../types/dist/index.mjs
13732
14292
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13733
14293
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -13913,6 +14473,414 @@ var IntegrationInstanceRefSchema = object({
13913
14473
  ]),
13914
14474
  candidateStableId: string().optional()
13915
14475
  });
14476
+ /**
14477
+ * notification-output — canonical, capability-gated notification delivery.
14478
+ *
14479
+ * Apprise-derived model (see
14480
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
14481
+ * callers emit ONE canonical `Notification`; each provider declares a
14482
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
14483
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
14484
+ * message to what the kind supports — callers never special-case a service.
14485
+ *
14486
+ * DESIGN DECISIONS (locked):
14487
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14488
+ * providers serve their targets as `integration-instances` records, one
14489
+ * record kind per target kind, and the admin lists, adds, edits, switches
14490
+ * and deletes them through that cap. This cap keeps what an instance cannot
14491
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14492
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14493
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14494
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14495
+ * cap-mount auto-`concatCollection`-fans them across every registered
14496
+ * provider so one catalog is routable. `send` / `testTarget` /
14497
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14498
+ * collection router extracts from the call input.
14499
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
14500
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
14501
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
14502
+ * base64 fallback needed.
14503
+ *
14504
+ * TODO (deferred, closed-set change — separate decision): add
14505
+ * `providerKind: 'notify'` so notification providers surface on the unified
14506
+ * admin "Integrations" page.
14507
+ */
14508
+ /**
14509
+ * Zentik-derived typed-media enum — the superset across every kind. Each
14510
+ * adapter picks what it supports and the degrade engine filters the rest.
14511
+ */
14512
+ var AttachmentMediaTypeSchema = _enum([
14513
+ "image",
14514
+ "video",
14515
+ "gif",
14516
+ "audio",
14517
+ "icon"
14518
+ ]);
14519
+ /**
14520
+ * A single attachment. Exactly one of `url` (remote source, most adapters
14521
+ * prefer this) or `bytes` (inline source; required for Pushover-style
14522
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
14523
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
14524
+ */
14525
+ var AttachmentSchema = object({
14526
+ mediaType: AttachmentMediaTypeSchema,
14527
+ url: string().optional(),
14528
+ bytes: _instanceof(Uint8Array).optional(),
14529
+ mime: string().optional(),
14530
+ name: string().optional()
14531
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
14532
+ var NotificationFormatSchema = _enum([
14533
+ "text",
14534
+ "markdown",
14535
+ "html"
14536
+ ]);
14537
+ /**
14538
+ * The CLOSED icon vocabulary an action button may use.
14539
+ *
14540
+ * A closed set, not a free string, and that is the whole point: an arbitrary
14541
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
14542
+ * notices — the same class of gap as a zone vocabulary nothing produced
14543
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
14544
+ * declares `actionIcons: false` and the degrade engine strips the field.
14545
+ *
14546
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
14547
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
14548
+ * differently.
14549
+ *
14550
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
14551
+ *
14552
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
14553
+ * the whole set onto its own renderer's vocabulary through a
14554
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
14555
+ * fallback, so adding a member here fails every adapter's build until someone
14556
+ * decides its glyph, which is the only place that decision can be made
14557
+ * honestly.
14558
+ *
14559
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
14560
+ * `disarm` straight through; iOS feeds that string to
14561
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
14562
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
14563
+ * mapping, and "the field is documented" is not "the value renders".
14564
+ *
14565
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
14566
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
14567
+ * so an addon that emits a token the running hub does not know does not lose an
14568
+ * icon — its whole `send` fails Zod validation and the notification never
14569
+ * arrives. Never emit a new token from an addon before the train carrying it.
14570
+ */
14571
+ var NotificationActionIconSchema = _enum([
14572
+ "acknowledge",
14573
+ "dismiss",
14574
+ "silence",
14575
+ "snooze",
14576
+ "view",
14577
+ "play",
14578
+ "open",
14579
+ "close",
14580
+ "lock",
14581
+ "unlock",
14582
+ "arm",
14583
+ "arm-home",
14584
+ "arm-away",
14585
+ "arm-night",
14586
+ "disarm",
14587
+ "light",
14588
+ "alert",
14589
+ "camera"
14590
+ ]);
14591
+ /** A single tap-through action button. */
14592
+ var NotificationActionSchema = object({
14593
+ id: string(),
14594
+ label: string(),
14595
+ url: string().optional(),
14596
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14597
+ icon: NotificationActionIconSchema.optional(),
14598
+ /**
14599
+ * Renders in a warning style where the notifier supports it.
14600
+ *
14601
+ * A HINT, never a gate. The callback's authority is its token and nothing
14602
+ * else — see `notification-center/action-token.ts` for what that does and
14603
+ * does not buy.
14604
+ */
14605
+ destructive: boolean().optional(),
14606
+ /**
14607
+ * How the tap should REACH the url.
14608
+ *
14609
+ * `navigate` (absent, and every button authored before this field) opens it:
14610
+ * the phone leaves the notification and shows whatever the callback returns.
14611
+ * That is right for a button whose answer the operator wants to read.
14612
+ *
14613
+ * `background` fires it as a POST and stays put. It exists for the buttons
14614
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14615
+ * an answer to the notification, and being thrown into a browser tab to
14616
+ * confirm it costs more attention than the notification did. A backend that
14617
+ * cannot do a background call renders it as an ordinary link (the adapters
14618
+ * fall back rather than dropping the button), so this is a preference, never
14619
+ * a requirement.
14620
+ */
14621
+ mode: _enum(["navigate", "background"]).optional()
14622
+ });
14623
+ /** A delivery parameter value: what a notifier declared it takes. */
14624
+ var DeliveryParamValueSchema = union([
14625
+ string(),
14626
+ number(),
14627
+ boolean()
14628
+ ]);
14629
+ /**
14630
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14631
+ * addresses a target of that kind may set it; a kind that declares nothing is
14632
+ * never handed one. The admin UI derives its control from this descriptor —
14633
+ * no editor knows a service by name.
14634
+ *
14635
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14636
+ * run on the addon cap path).
14637
+ */
14638
+ var DeliveryParamSchema = object({
14639
+ key: string().min(1),
14640
+ label: string(),
14641
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14642
+ labelKey: string().optional(),
14643
+ type: _enum([
14644
+ "enum",
14645
+ "number",
14646
+ "boolean",
14647
+ "string"
14648
+ ]),
14649
+ /** `enum` only: the closed set of accepted values. */
14650
+ options: array(object({
14651
+ value: string(),
14652
+ label: string()
14653
+ })).optional(),
14654
+ /** `number` only. */
14655
+ min: number().optional(),
14656
+ max: number().optional(),
14657
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14658
+ default: DeliveryParamValueSchema.optional()
14659
+ });
14660
+ /**
14661
+ * The canonical notification. `body` is the only hard field (Apprise model).
14662
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14663
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14664
+ * the adapter maps this ordinal onto its native level. `level?` is an
14665
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14666
+ * `priority` for that one target.
14667
+ */
14668
+ var NotificationSchema = object({
14669
+ body: string(),
14670
+ title: string().optional(),
14671
+ format: NotificationFormatSchema.default("text"),
14672
+ priority: number().int().min(1).max(5).default(3),
14673
+ level: string().optional(),
14674
+ attachments: array(AttachmentSchema).optional(),
14675
+ clickUrl: string().optional(),
14676
+ actions: array(NotificationActionSchema).optional(),
14677
+ sound: string().optional(),
14678
+ ttl: number().optional(),
14679
+ tag: string().optional(),
14680
+ deviceId: number().optional(),
14681
+ eventId: string().optional(),
14682
+ metadata: record(string(), unknown()).optional(),
14683
+ /**
14684
+ * Values for the parameters the target's kind DECLARES in
14685
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14686
+ * declaration by the sender; a notifier reads only the keys it declared.
14687
+ */
14688
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14689
+ });
14690
+ /** One declared native severity/priority level for a kind. */
14691
+ var TargetKindLevelSchema = object({
14692
+ id: string(),
14693
+ label: string(),
14694
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14695
+ ordinal: number().int().min(1).max(5).nullable(),
14696
+ flags: object({
14697
+ critical: boolean().optional(),
14698
+ silent: boolean().optional(),
14699
+ noPush: boolean().optional()
14700
+ }).optional(),
14701
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14702
+ requires: array(string()).optional(),
14703
+ description: string().optional()
14704
+ });
14705
+ /** The full capability block consulted before dispatch. */
14706
+ var TargetKindCapsSchema = object({
14707
+ attachments: object({
14708
+ mediaTypes: array(AttachmentMediaTypeSchema),
14709
+ mode: _enum([
14710
+ "url",
14711
+ "bytes",
14712
+ "both"
14713
+ ]),
14714
+ max: number().int().nonnegative(),
14715
+ maxBytes: number().int().positive().optional()
14716
+ }),
14717
+ /** Max action buttons (0 = none). */
14718
+ actions: number().int().nonnegative(),
14719
+ /**
14720
+ * Whether this kind renders a per-action ICON.
14721
+ *
14722
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14723
+ * run on the addon cap path — three production failures in one day taught
14724
+ * this repo that once. Absent is read as false by the degrade engine, which
14725
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14726
+ * assumed and dropped costs the operator's trust in the field.
14727
+ */
14728
+ actionIcons: boolean().optional(),
14729
+ levels: array(TargetKindLevelSchema),
14730
+ format: array(NotificationFormatSchema),
14731
+ clickUrl: boolean(),
14732
+ sound: boolean(),
14733
+ ttl: boolean(),
14734
+ bodyMaxLen: number().int().positive()
14735
+ });
14736
+ /**
14737
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14738
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14739
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14740
+ * the union is large and not meant for runtime validation here; the exported
14741
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14742
+ */
14743
+ var ConfigSchemaPassthrough$3 = unknown();
14744
+ var TargetKindSchema = object({
14745
+ kind: string(),
14746
+ label: string(),
14747
+ icon: string(),
14748
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14749
+ addonId: string(),
14750
+ /**
14751
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14752
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14753
+ * when the addon bundles no icon for that kind — the client then falls back
14754
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14755
+ *
14756
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14757
+ * client, and a native client joins it onto its own hub base.
14758
+ *
14759
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14760
+ * field that survived only because the runtime cap-router forwards provider
14761
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14762
+ * its own Zod parse from stripping it, and the whole arrangement would have
14763
+ * broken silently the moment output validation was tightened anywhere.
14764
+ */
14765
+ iconUrl: string().optional(),
14766
+ /**
14767
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14768
+ *
14769
+ * The server knows this and therefore says it, because the client cannot
14770
+ * safely guess: a React-Native client renders SVG and raster through two
14771
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14772
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14773
+ * placeholder glyph for every vector icon while the web build looked fine.
14774
+ *
14775
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14776
+ * not been updated — a client that cannot determine the type should prefer
14777
+ * its raster path, which is the safe default for an unknown image.
14778
+ */
14779
+ iconMediaType: string().optional(),
14780
+ configSchema: ConfigSchemaPassthrough$3,
14781
+ /**
14782
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14783
+ * base URL, instance, chat, recipient). A save that changes any of them
14784
+ * while leaving a secret redacted is refused by the provider (D634), so an
14785
+ * editor uses this to ask for the secret on exactly those edits instead of
14786
+ * on every edit.
14787
+ *
14788
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14789
+ * kinds) and from a provider older than this field — an editor that finds
14790
+ * it absent must assume ANY edit may need the secret.
14791
+ */
14792
+ destinationKeys: array(string()).optional(),
14793
+ supportsDiscovery: boolean(),
14794
+ /**
14795
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14796
+ * Absent = none; the rule editor then offers nothing.
14797
+ */
14798
+ deliveryParams: array(DeliveryParamSchema).optional(),
14799
+ caps: TargetKindCapsSchema
14800
+ });
14801
+ /**
14802
+ * A target as a provider's own code sees it (the notification-center bridge, the
14803
+ * send path): the flat record body split into the name, the switch and the
14804
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14805
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14806
+ * secret to the UI.
14807
+ */
14808
+ var TargetSchema = object({
14809
+ id: string(),
14810
+ name: string(),
14811
+ kind: string(),
14812
+ addonId: string(),
14813
+ enabled: boolean(),
14814
+ config: record(string(), unknown())
14815
+ });
14816
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14817
+ var DiscoveredTargetSchema = object({
14818
+ kind: string(),
14819
+ suggestedName: string(),
14820
+ config: record(string(), unknown())
14821
+ });
14822
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14823
+ var RenderedAsSchema = object({
14824
+ level: string(),
14825
+ format: NotificationFormatSchema,
14826
+ attachmentsSent: number().int().nonnegative(),
14827
+ actionsSent: number().int().nonnegative(),
14828
+ truncated: boolean(),
14829
+ dropped: array(string())
14830
+ });
14831
+ var SendResultSchema = object({
14832
+ success: boolean(),
14833
+ error: string().optional(),
14834
+ renderedAs: RenderedAsSchema.optional()
14835
+ });
14836
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14837
+ var TestResultSchema = SendResultSchema;
14838
+ /**
14839
+ * Pre-save credential probe against a partial (unsaved) target config.
14840
+ * `resolvedUrl` never includes secrets (token stripped from path).
14841
+ */
14842
+ var ProbeTargetConfigResultSchema = object({
14843
+ ok: boolean(),
14844
+ resolvedUrl: string(),
14845
+ httpStatus: number().int().optional(),
14846
+ stateInstance: string().optional(),
14847
+ error: string().optional()
14848
+ });
14849
+ method(object({}), array(TargetKindSchema)), method(object({
14850
+ kind: string(),
14851
+ config: record(string(), unknown()).optional(),
14852
+ /**
14853
+ * When editing / duplicating, the blank password field never round-trips
14854
+ * the stored token. Pass the source target id so the provider can restore
14855
+ * secrets server-side before dialling.
14856
+ */
14857
+ targetId: string().optional()
14858
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14859
+ kind: string(),
14860
+ config: record(string(), unknown()),
14861
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14862
+ targetId: string().optional()
14863
+ }), ProbeTargetConfigResultSchema, {
14864
+ kind: "mutation",
14865
+ auth: "admin"
14866
+ }), method(object({
14867
+ targetId: string(),
14868
+ notification: NotificationSchema
14869
+ }), SendResultSchema, {
14870
+ kind: "mutation",
14871
+ auth: "admin"
14872
+ }), method(object({
14873
+ targetId: string(),
14874
+ sample: NotificationSchema.optional()
14875
+ }), TestResultSchema, {
14876
+ kind: "mutation",
14877
+ auth: "admin"
14878
+ });
14879
+ looseObject({
14880
+ name: string().min(1),
14881
+ enabled: boolean().optional()
14882
+ });
14883
+ TargetSchema.omit({ id: true });
13916
14884
  var EncodeProfileSchema = object({
13917
14885
  video: object({
13918
14886
  codec: _enum([
@@ -22327,7 +23295,6 @@ var SYSTEM_CAP_VOCABULARY = {
22327
23295
  "calendar-source": system(INTERNAL),
22328
23296
  calendars: system("calendars feed schedules and conditions, not device actions"),
22329
23297
  composer: system("device composition — an authoring tool, not an action"),
22330
- "connection-test": system(INTERNAL),
22331
23298
  "core-blocks": system(INTERNAL),
22332
23299
  "custom-model-registry": system(ML),
22333
23300
  "data-store-provider": system(INTERNAL),
@@ -22372,7 +23339,6 @@ var SYSTEM_CAP_VOCABULARY = {
22372
23339
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22373
23340
  "notification-rules": system("notification rules — authored on their own page"),
22374
23341
  "oauth-integration": system(IDENTITY),
22375
- "oidc-connections": system(IDENTITY),
22376
23342
  "osd-manager": system(PIPELINE),
22377
23343
  "pipeline-executor": system(PIPELINE),
22378
23344
  "pipeline-orchestrator": system(PIPELINE),
@@ -42393,6 +43359,7 @@ var ExpressionBindingSourceSchema = union([
42393
43359
  object({
42394
43360
  kind: literal("global"),
42395
43361
  sourceStableId: string(),
43362
+ addonId: string().optional(),
42396
43363
  cap: string(),
42397
43364
  fieldPath: string()
42398
43365
  })
@@ -42594,389 +43561,6 @@ function extractSourceInfoFromMetadata(metadata) {
42594
43561
  return parsed.data;
42595
43562
  }
42596
43563
  /**
42597
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42598
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42599
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42600
- * decimals). K derives from `°C = K − 273.15`.
42601
- */
42602
- var UNIT_TABLE = {
42603
- "°C": {
42604
- dimension: "temperature",
42605
- factor: 1,
42606
- offset: 0
42607
- },
42608
- "°F": {
42609
- dimension: "temperature",
42610
- factor: 5 / 9,
42611
- offset: -160 / 9
42612
- },
42613
- K: {
42614
- dimension: "temperature",
42615
- factor: 1,
42616
- offset: -273.15
42617
- },
42618
- hPa: {
42619
- dimension: "pressure",
42620
- factor: 1,
42621
- offset: 0
42622
- },
42623
- kPa: {
42624
- dimension: "pressure",
42625
- factor: 10,
42626
- offset: 0
42627
- },
42628
- Pa: {
42629
- dimension: "pressure",
42630
- factor: .01,
42631
- offset: 0
42632
- },
42633
- mbar: {
42634
- dimension: "pressure",
42635
- factor: 1,
42636
- offset: 0
42637
- },
42638
- bar: {
42639
- dimension: "pressure",
42640
- factor: 1e3,
42641
- offset: 0
42642
- },
42643
- inHg: {
42644
- dimension: "pressure",
42645
- factor: 33.8639,
42646
- offset: 0
42647
- },
42648
- mmHg: {
42649
- dimension: "pressure",
42650
- factor: 1.33322,
42651
- offset: 0
42652
- },
42653
- psi: {
42654
- dimension: "pressure",
42655
- factor: 68.9476,
42656
- offset: 0
42657
- },
42658
- "m/s": {
42659
- dimension: "speed",
42660
- factor: 1,
42661
- offset: 0
42662
- },
42663
- "km/h": {
42664
- dimension: "speed",
42665
- factor: 1 / 3.6,
42666
- offset: 0
42667
- },
42668
- mph: {
42669
- dimension: "speed",
42670
- factor: .44704,
42671
- offset: 0
42672
- },
42673
- kn: {
42674
- dimension: "speed",
42675
- factor: .514444,
42676
- offset: 0
42677
- },
42678
- "mm/h": {
42679
- dimension: "precipitation-rate",
42680
- factor: 1,
42681
- offset: 0
42682
- },
42683
- "in/h": {
42684
- dimension: "precipitation-rate",
42685
- factor: 25.4,
42686
- offset: 0
42687
- },
42688
- m: {
42689
- dimension: "length",
42690
- factor: 1,
42691
- offset: 0
42692
- },
42693
- mm: {
42694
- dimension: "length",
42695
- factor: .001,
42696
- offset: 0
42697
- },
42698
- cm: {
42699
- dimension: "length",
42700
- factor: .01,
42701
- offset: 0
42702
- },
42703
- km: {
42704
- dimension: "length",
42705
- factor: 1e3,
42706
- offset: 0
42707
- },
42708
- in: {
42709
- dimension: "length",
42710
- factor: .0254,
42711
- offset: 0
42712
- },
42713
- ft: {
42714
- dimension: "length",
42715
- factor: .3048,
42716
- offset: 0
42717
- },
42718
- mi: {
42719
- dimension: "length",
42720
- factor: 1609.344,
42721
- offset: 0
42722
- },
42723
- "m²": {
42724
- dimension: "area",
42725
- factor: 1,
42726
- offset: 0
42727
- },
42728
- lx: {
42729
- dimension: "illuminance",
42730
- factor: 1,
42731
- offset: 0
42732
- },
42733
- "W/m²": {
42734
- dimension: "irradiance",
42735
- factor: 1,
42736
- offset: 0
42737
- },
42738
- W: {
42739
- dimension: "power",
42740
- factor: 1,
42741
- offset: 0
42742
- },
42743
- kW: {
42744
- dimension: "power",
42745
- factor: 1e3,
42746
- offset: 0
42747
- },
42748
- VA: {
42749
- dimension: "apparent-power",
42750
- factor: 1,
42751
- offset: 0
42752
- },
42753
- Wh: {
42754
- dimension: "energy",
42755
- factor: 1,
42756
- offset: 0
42757
- },
42758
- kWh: {
42759
- dimension: "energy",
42760
- factor: 1e3,
42761
- offset: 0
42762
- },
42763
- MWh: {
42764
- dimension: "energy",
42765
- factor: 1e6,
42766
- offset: 0
42767
- },
42768
- V: {
42769
- dimension: "voltage",
42770
- factor: 1,
42771
- offset: 0
42772
- },
42773
- mV: {
42774
- dimension: "voltage",
42775
- factor: .001,
42776
- offset: 0
42777
- },
42778
- A: {
42779
- dimension: "current",
42780
- factor: 1,
42781
- offset: 0
42782
- },
42783
- mA: {
42784
- dimension: "current",
42785
- factor: .001,
42786
- offset: 0
42787
- },
42788
- ppm: {
42789
- dimension: "concentration-volume",
42790
- factor: 1,
42791
- offset: 0
42792
- },
42793
- ppb: {
42794
- dimension: "concentration-volume",
42795
- factor: .001,
42796
- offset: 0
42797
- },
42798
- "µg/m³": {
42799
- dimension: "concentration-mass",
42800
- factor: 1,
42801
- offset: 0
42802
- },
42803
- "mg/m³": {
42804
- dimension: "concentration-mass",
42805
- factor: 1e3,
42806
- offset: 0
42807
- },
42808
- kg: {
42809
- dimension: "mass",
42810
- factor: 1,
42811
- offset: 0
42812
- },
42813
- g: {
42814
- dimension: "mass",
42815
- factor: .001,
42816
- offset: 0
42817
- },
42818
- lb: {
42819
- dimension: "mass",
42820
- factor: .453592,
42821
- offset: 0
42822
- },
42823
- oz: {
42824
- dimension: "mass",
42825
- factor: .0283495,
42826
- offset: 0
42827
- },
42828
- "%": {
42829
- dimension: "percentage",
42830
- factor: 1,
42831
- offset: 0
42832
- },
42833
- L: {
42834
- dimension: "volume",
42835
- factor: 1,
42836
- offset: 0
42837
- },
42838
- mL: {
42839
- dimension: "volume",
42840
- factor: .001,
42841
- offset: 0
42842
- },
42843
- gal: {
42844
- dimension: "volume",
42845
- factor: 3.785411784,
42846
- offset: 0
42847
- },
42848
- s: {
42849
- dimension: "duration",
42850
- factor: 1,
42851
- offset: 0
42852
- },
42853
- ms: {
42854
- dimension: "duration",
42855
- factor: .001,
42856
- offset: 0
42857
- },
42858
- min: {
42859
- dimension: "duration",
42860
- factor: 60,
42861
- offset: 0
42862
- },
42863
- h: {
42864
- dimension: "duration",
42865
- factor: 3600,
42866
- offset: 0
42867
- },
42868
- d: {
42869
- dimension: "duration",
42870
- factor: 86400,
42871
- offset: 0
42872
- },
42873
- B: {
42874
- dimension: "information",
42875
- factor: 1,
42876
- offset: 0
42877
- },
42878
- KiB: {
42879
- dimension: "information",
42880
- factor: 1024,
42881
- offset: 0
42882
- },
42883
- MiB: {
42884
- dimension: "information",
42885
- factor: 1024 ** 2,
42886
- offset: 0
42887
- },
42888
- GiB: {
42889
- dimension: "information",
42890
- factor: 1024 ** 3,
42891
- offset: 0
42892
- },
42893
- TiB: {
42894
- dimension: "information",
42895
- factor: 1024 ** 4,
42896
- offset: 0
42897
- },
42898
- kB: {
42899
- dimension: "information",
42900
- factor: 1e3,
42901
- offset: 0
42902
- },
42903
- MB: {
42904
- dimension: "information",
42905
- factor: 1e6,
42906
- offset: 0
42907
- },
42908
- GB: {
42909
- dimension: "information",
42910
- factor: 1e9,
42911
- offset: 0
42912
- },
42913
- "°": {
42914
- dimension: "angle",
42915
- factor: 1,
42916
- offset: 0
42917
- },
42918
- dBFS: {
42919
- dimension: "sound-level",
42920
- factor: 1,
42921
- offset: 0
42922
- },
42923
- dBm: {
42924
- dimension: "signal-level",
42925
- factor: 1,
42926
- offset: 0
42927
- },
42928
- mired: {
42929
- dimension: "colour-temperature",
42930
- factor: 1,
42931
- offset: 0
42932
- },
42933
- kbps: {
42934
- dimension: "bitrate",
42935
- factor: 1,
42936
- offset: 0
42937
- },
42938
- Mbps: {
42939
- dimension: "bitrate",
42940
- factor: 1e3,
42941
- offset: 0
42942
- },
42943
- fps: {
42944
- dimension: "rate",
42945
- factor: 1,
42946
- offset: 0
42947
- }
42948
- };
42949
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
42950
- function specFor(unit) {
42951
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
42952
- }
42953
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
42954
- function unitDimension(unit) {
42955
- return specFor(unit)?.dimension;
42956
- }
42957
- /** Value in `spec`'s unit → its dimension's canonical unit. */
42958
- function toCanonical(value, spec) {
42959
- return value * spec.factor + spec.offset;
42960
- }
42961
- /** Canonical-unit value → the unit described by `spec`. */
42962
- function fromCanonical(canonical, spec) {
42963
- return (canonical - spec.offset) / spec.factor;
42964
- }
42965
- /**
42966
- * Graceful variant for the render path: returns the converted value, or
42967
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
42968
- * Never throws — a render caller falls back to the raw value + source unit.
42969
- */
42970
- function tryConvertUnit(value, from, to) {
42971
- if (from === to) return value;
42972
- const fromSpec = specFor(from);
42973
- if (!fromSpec) return void 0;
42974
- const toSpec = specFor(to);
42975
- if (!toSpec) return void 0;
42976
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
42977
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
42978
- }
42979
- /**
42980
43564
  * A number a person types for a physical quantity — the input side of the
42981
43565
  * value + unit contract (D972).
42982
43566
  *
@@ -44383,427 +44967,6 @@ var UnitWarningSchema = object({
44383
44967
  assumedUnit: string(),
44384
44968
  message: string()
44385
44969
  });
44386
- /**
44387
- * notification-output — canonical, capability-gated notification delivery.
44388
- *
44389
- * Apprise-derived model (see
44390
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44391
- * callers emit ONE canonical `Notification`; each provider declares a
44392
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44393
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44394
- * message to what the kind supports — callers never special-case a service.
44395
- *
44396
- * DESIGN DECISIONS (locked):
44397
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44398
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44399
- * cap. Rationale: the admin UI needs one uniform surface across the
44400
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44401
- * alternative would fork the UI per addon and cannot host the
44402
- * discovery→adopt flow.
44403
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44404
- * the generated cap-mount auto-`concatCollection`-fans them across every
44405
- * registered provider (notifiers addon + HA addon) so one catalog is
44406
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44407
- * `addonId` the generated collection router extracts from the call input.
44408
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44409
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44410
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44411
- * base64 fallback needed.
44412
- *
44413
- * TODO (deferred, closed-set change — separate decision): add
44414
- * `providerKind: 'notify'` so notification providers surface on the unified
44415
- * admin "Integrations" page.
44416
- */
44417
- /**
44418
- * Zentik-derived typed-media enum — the superset across every kind. Each
44419
- * adapter picks what it supports and the degrade engine filters the rest.
44420
- */
44421
- var AttachmentMediaTypeSchema = _enum([
44422
- "image",
44423
- "video",
44424
- "gif",
44425
- "audio",
44426
- "icon"
44427
- ]);
44428
- /**
44429
- * A single attachment. Exactly one of `url` (remote source, most adapters
44430
- * prefer this) or `bytes` (inline source; required for Pushover-style
44431
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44432
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44433
- */
44434
- var AttachmentSchema = object({
44435
- mediaType: AttachmentMediaTypeSchema,
44436
- url: string().optional(),
44437
- bytes: _instanceof(Uint8Array).optional(),
44438
- mime: string().optional(),
44439
- name: string().optional()
44440
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44441
- var NotificationFormatSchema = _enum([
44442
- "text",
44443
- "markdown",
44444
- "html"
44445
- ]);
44446
- /**
44447
- * The CLOSED icon vocabulary an action button may use.
44448
- *
44449
- * A closed set, not a free string, and that is the whole point: an arbitrary
44450
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44451
- * notices — the same class of gap as a zone vocabulary nothing produced
44452
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44453
- * declares `actionIcons: false` and the degrade engine strips the field.
44454
- *
44455
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44456
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44457
- * differently.
44458
- *
44459
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44460
- *
44461
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44462
- * the whole set onto its own renderer's vocabulary through a
44463
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44464
- * fallback, so adding a member here fails every adapter's build until someone
44465
- * decides its glyph, which is the only place that decision can be made
44466
- * honestly.
44467
- *
44468
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44469
- * `disarm` straight through; iOS feeds that string to
44470
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44471
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44472
- * mapping, and "the field is documented" is not "the value renders".
44473
- *
44474
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44475
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44476
- * so an addon that emits a token the running hub does not know does not lose an
44477
- * icon — its whole `send` fails Zod validation and the notification never
44478
- * arrives. Never emit a new token from an addon before the train carrying it.
44479
- */
44480
- var NotificationActionIconSchema = _enum([
44481
- "acknowledge",
44482
- "dismiss",
44483
- "silence",
44484
- "snooze",
44485
- "view",
44486
- "play",
44487
- "open",
44488
- "close",
44489
- "lock",
44490
- "unlock",
44491
- "arm",
44492
- "arm-home",
44493
- "arm-away",
44494
- "arm-night",
44495
- "disarm",
44496
- "light",
44497
- "alert",
44498
- "camera"
44499
- ]);
44500
- /** A single tap-through action button. */
44501
- var NotificationActionSchema = object({
44502
- id: string(),
44503
- label: string(),
44504
- url: string().optional(),
44505
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44506
- icon: NotificationActionIconSchema.optional(),
44507
- /**
44508
- * Renders in a warning style where the notifier supports it.
44509
- *
44510
- * A HINT, never a gate. The callback's authority is its token and nothing
44511
- * else — see `notification-center/action-token.ts` for what that does and
44512
- * does not buy.
44513
- */
44514
- destructive: boolean().optional(),
44515
- /**
44516
- * How the tap should REACH the url.
44517
- *
44518
- * `navigate` (absent, and every button authored before this field) opens it:
44519
- * the phone leaves the notification and shows whatever the callback returns.
44520
- * That is right for a button whose answer the operator wants to read.
44521
- *
44522
- * `background` fires it as a POST and stays put. It exists for the buttons
44523
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44524
- * an answer to the notification, and being thrown into a browser tab to
44525
- * confirm it costs more attention than the notification did. A backend that
44526
- * cannot do a background call renders it as an ordinary link (the adapters
44527
- * fall back rather than dropping the button), so this is a preference, never
44528
- * a requirement.
44529
- */
44530
- mode: _enum(["navigate", "background"]).optional()
44531
- });
44532
- /** A delivery parameter value: what a notifier declared it takes. */
44533
- var DeliveryParamValueSchema = union([
44534
- string(),
44535
- number(),
44536
- boolean()
44537
- ]);
44538
- /**
44539
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44540
- * addresses a target of that kind may set it; a kind that declares nothing is
44541
- * never handed one. The admin UI derives its control from this descriptor —
44542
- * no editor knows a service by name.
44543
- *
44544
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44545
- * run on the addon cap path).
44546
- */
44547
- var DeliveryParamSchema = object({
44548
- key: string().min(1),
44549
- label: string(),
44550
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44551
- labelKey: string().optional(),
44552
- type: _enum([
44553
- "enum",
44554
- "number",
44555
- "boolean",
44556
- "string"
44557
- ]),
44558
- /** `enum` only: the closed set of accepted values. */
44559
- options: array(object({
44560
- value: string(),
44561
- label: string()
44562
- })).optional(),
44563
- /** `number` only. */
44564
- min: number().optional(),
44565
- max: number().optional(),
44566
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44567
- default: DeliveryParamValueSchema.optional()
44568
- });
44569
- /**
44570
- * The canonical notification. `body` is the only hard field (Apprise model).
44571
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44572
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44573
- * the adapter maps this ordinal onto its native level. `level?` is an
44574
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44575
- * `priority` for that one target.
44576
- */
44577
- var NotificationSchema = object({
44578
- body: string(),
44579
- title: string().optional(),
44580
- format: NotificationFormatSchema.default("text"),
44581
- priority: number().int().min(1).max(5).default(3),
44582
- level: string().optional(),
44583
- attachments: array(AttachmentSchema).optional(),
44584
- clickUrl: string().optional(),
44585
- actions: array(NotificationActionSchema).optional(),
44586
- sound: string().optional(),
44587
- ttl: number().optional(),
44588
- tag: string().optional(),
44589
- deviceId: number().optional(),
44590
- eventId: string().optional(),
44591
- metadata: record(string(), unknown()).optional(),
44592
- /**
44593
- * Values for the parameters the target's kind DECLARES in
44594
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44595
- * declaration by the sender; a notifier reads only the keys it declared.
44596
- */
44597
- delivery: record(string(), DeliveryParamValueSchema).optional()
44598
- });
44599
- /** One declared native severity/priority level for a kind. */
44600
- var TargetKindLevelSchema = object({
44601
- id: string(),
44602
- label: string(),
44603
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44604
- ordinal: number().int().min(1).max(5).nullable(),
44605
- flags: object({
44606
- critical: boolean().optional(),
44607
- silent: boolean().optional(),
44608
- noPush: boolean().optional()
44609
- }).optional(),
44610
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44611
- requires: array(string()).optional(),
44612
- description: string().optional()
44613
- });
44614
- /** The full capability block consulted before dispatch. */
44615
- var TargetKindCapsSchema = object({
44616
- attachments: object({
44617
- mediaTypes: array(AttachmentMediaTypeSchema),
44618
- mode: _enum([
44619
- "url",
44620
- "bytes",
44621
- "both"
44622
- ]),
44623
- max: number().int().nonnegative(),
44624
- maxBytes: number().int().positive().optional()
44625
- }),
44626
- /** Max action buttons (0 = none). */
44627
- actions: number().int().nonnegative(),
44628
- /**
44629
- * Whether this kind renders a per-action ICON.
44630
- *
44631
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44632
- * run on the addon cap path — three production failures in one day taught
44633
- * this repo that once. Absent is read as false by the degrade engine, which
44634
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44635
- * assumed and dropped costs the operator's trust in the field.
44636
- */
44637
- actionIcons: boolean().optional(),
44638
- levels: array(TargetKindLevelSchema),
44639
- format: array(NotificationFormatSchema),
44640
- clickUrl: boolean(),
44641
- sound: boolean(),
44642
- ttl: boolean(),
44643
- bodyMaxLen: number().int().positive()
44644
- });
44645
- /**
44646
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44647
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44648
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44649
- * the union is large and not meant for runtime validation here; the exported
44650
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44651
- */
44652
- var ConfigSchemaPassthrough$4 = unknown();
44653
- var TargetKindSchema = object({
44654
- kind: string(),
44655
- label: string(),
44656
- icon: string(),
44657
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44658
- addonId: string(),
44659
- /**
44660
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44661
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44662
- * when the addon bundles no icon for that kind — the client then falls back
44663
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44664
- *
44665
- * Root-relative on purpose: it resolves against whatever origin serves a web
44666
- * client, and a native client joins it onto its own hub base.
44667
- *
44668
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44669
- * field that survived only because the runtime cap-router forwards provider
44670
- * output verbatim — so every consumer had to re-declare it by hand to stop
44671
- * its own Zod parse from stripping it, and the whole arrangement would have
44672
- * broken silently the moment output validation was tightened anywhere.
44673
- */
44674
- iconUrl: string().optional(),
44675
- /**
44676
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44677
- *
44678
- * The server knows this and therefore says it, because the client cannot
44679
- * safely guess: a React-Native client renders SVG and raster through two
44680
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44681
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44682
- * placeholder glyph for every vector icon while the web build looked fine.
44683
- *
44684
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44685
- * not been updated — a client that cannot determine the type should prefer
44686
- * its raster path, which is the safe default for an unknown image.
44687
- */
44688
- iconMediaType: string().optional(),
44689
- configSchema: ConfigSchemaPassthrough$4,
44690
- /**
44691
- * The config keys that decide WHERE this kind's secrets are sent (server,
44692
- * base URL, instance, chat, recipient). A save that changes any of them
44693
- * while leaving a secret redacted is refused by the provider (D634), so an
44694
- * editor uses this to ask for the secret on exactly those edits instead of
44695
- * on every edit.
44696
- *
44697
- * Optional: absent from a provider that holds no secret (the Home Assistant
44698
- * kinds) and from a provider older than this field — an editor that finds
44699
- * it absent must assume ANY edit may need the secret.
44700
- */
44701
- destinationKeys: array(string()).optional(),
44702
- supportsDiscovery: boolean(),
44703
- /**
44704
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44705
- * Absent = none; the rule editor then offers nothing.
44706
- */
44707
- deliveryParams: array(DeliveryParamSchema).optional(),
44708
- caps: TargetKindCapsSchema
44709
- });
44710
- /**
44711
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44712
- * (return a presence marker only) when serving `listTargets` — never
44713
- * round-trip a stored secret to the UI.
44714
- */
44715
- var TargetSchema = object({
44716
- id: string(),
44717
- name: string(),
44718
- kind: string(),
44719
- addonId: string(),
44720
- enabled: boolean(),
44721
- config: record(string(), unknown())
44722
- });
44723
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44724
- var DiscoveredTargetSchema = object({
44725
- kind: string(),
44726
- suggestedName: string(),
44727
- config: record(string(), unknown())
44728
- });
44729
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44730
- var RenderedAsSchema = object({
44731
- level: string(),
44732
- format: NotificationFormatSchema,
44733
- attachmentsSent: number().int().nonnegative(),
44734
- actionsSent: number().int().nonnegative(),
44735
- truncated: boolean(),
44736
- dropped: array(string())
44737
- });
44738
- var SendResultSchema = object({
44739
- success: boolean(),
44740
- error: string().optional(),
44741
- renderedAs: RenderedAsSchema.optional()
44742
- });
44743
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44744
- var TestResultSchema = SendResultSchema;
44745
- /**
44746
- * Pre-save credential probe against a partial (unsaved) target config.
44747
- * `resolvedUrl` never includes secrets (token stripped from path).
44748
- */
44749
- var ProbeTargetConfigResultSchema = object({
44750
- ok: boolean(),
44751
- resolvedUrl: string(),
44752
- httpStatus: number().int().optional(),
44753
- stateInstance: string().optional(),
44754
- error: string().optional()
44755
- });
44756
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44757
- kind: string(),
44758
- config: record(string(), unknown()).optional(),
44759
- /**
44760
- * When editing / duplicating, the blank password field never round-trips
44761
- * the stored token. Pass the source target id so the provider can restore
44762
- * secrets server-side before dialling (same merge as upsert).
44763
- */
44764
- targetId: string().optional()
44765
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44766
- kind: string(),
44767
- config: record(string(), unknown()),
44768
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44769
- targetId: string().optional()
44770
- }), ProbeTargetConfigResultSchema, {
44771
- kind: "mutation",
44772
- auth: "admin"
44773
- }), method(object({
44774
- targetId: string(),
44775
- notification: NotificationSchema
44776
- }), SendResultSchema, {
44777
- kind: "mutation",
44778
- auth: "admin"
44779
- }), method(object({
44780
- targetId: string(),
44781
- sample: NotificationSchema.optional()
44782
- }), TestResultSchema, {
44783
- kind: "mutation",
44784
- auth: "admin"
44785
- }), method(object({
44786
- target: TargetSchema,
44787
- /**
44788
- * Duplicate flow: create a NEW target id while copying secrets from an
44789
- * existing row. The UI never holds the stored token; blank / redacted
44790
- * password fields are restored from this source (D634 still applies if
44791
- * destinations diverge without a re-typed secret).
44792
- */
44793
- cloneSecretsFrom: string().optional()
44794
- }), TargetSchema, {
44795
- kind: "mutation",
44796
- auth: "admin"
44797
- }), method(object({ targetId: string() }), _void(), {
44798
- kind: "mutation",
44799
- auth: "admin"
44800
- }), method(object({
44801
- targetId: string(),
44802
- enabled: boolean()
44803
- }), _void(), {
44804
- kind: "mutation",
44805
- auth: "admin"
44806
- });
44807
44970
  _enum([
44808
44971
  "automation",
44809
44972
  "action",
@@ -45074,15 +45237,28 @@ var SettingsSchemaWithValuesSchema = object({
45074
45237
  });
45075
45238
  /** Patch object — keys are field names, values are the new field values. */
45076
45239
  var SettingsPatchSchema = record(string(), unknown());
45240
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45241
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45242
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
45243
+ var ScopedSettingsDeclarationSchema = object({
45244
+ cap: string(),
45245
+ scope: ScopedSettingsScopeSchema,
45246
+ sectionIds: array(string())
45247
+ });
45077
45248
  /** Standard success response for update operations. */
45078
45249
  var SettingsUpdateResultSchema = object({ success: literal(true) });
45079
45250
  method(object({
45080
45251
  addonId: string(),
45081
- nodeId: string().optional(),
45082
- overlay: record(string(), unknown()).optional(),
45083
- cap: string().optional()
45252
+ cap: string(),
45253
+ scope: ScopedSettingsScopeSchema,
45254
+ nodeId: string().optional()
45084
45255
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45085
45256
  addonId: string(),
45257
+ nodeId: string().optional()
45258
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
45259
+ addonId: string(),
45260
+ cap: string(),
45261
+ scope: ScopedSettingsScopeSchema,
45086
45262
  nodeId: string().optional(),
45087
45263
  patch: SettingsPatchSchema
45088
45264
  }), SettingsUpdateResultSchema, {
@@ -45090,8 +45266,10 @@ method(object({
45090
45266
  auth: "admin"
45091
45267
  }), method(object({
45092
45268
  addonId: string(),
45269
+ cap: string(),
45270
+ scope: ScopedSettingsScopeSchema,
45093
45271
  nodeId: string().optional()
45094
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45272
+ }), record(string(), unknown()).nullable()), method(object({
45095
45273
  addonId: string(),
45096
45274
  nodeId: string().optional(),
45097
45275
  instance: IntegrationInstanceRefSchema
@@ -46654,7 +46832,7 @@ var BackupDestinationInfoSchema = object({
46654
46832
  id: string(),
46655
46833
  /**
46656
46834
  * Manifest id of the owning addon. The Settings button on the
46657
- * destination card opens that addon's `globalSettingsSchema` panel.
46835
+ * destination card opens that addon's scoped settings panel.
46658
46836
  */
46659
46837
  addonId: string(),
46660
46838
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -49343,37 +49521,6 @@ method(object({
49343
49521
  kind: "mutation",
49344
49522
  auth: "admin"
49345
49523
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
49346
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
49347
- object({
49348
- outcome: literal("validated"),
49349
- /** Round-trip of the sign-in, when the provider measured it. */
49350
- latencyMs: number().nonnegative().optional(),
49351
- /** Optional human detail worth showing next to the tick
49352
- * ("3 devices visible on this account"). */
49353
- detail: string().optional()
49354
- }).strict(),
49355
- object({
49356
- outcome: literal("rejected"),
49357
- error: string()
49358
- }).strict(),
49359
- object({
49360
- outcome: literal("inconclusive"),
49361
- error: string()
49362
- }).strict()
49363
- ]);
49364
- var ConnectionTestInputSchema = object({
49365
- /** Candidate integration settings, exactly as the create form collected them. */
49366
- settings: record(string(), unknown()) });
49367
- /**
49368
- * What the provider's test actually DOES, so the UI can say it in words before
49369
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
49370
- * descriptive — it never changes routing.
49371
- */
49372
- var ConnectionTestDescriptorSchema = object({ label: string() });
49373
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
49374
- kind: "mutation",
49375
- auth: "admin"
49376
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
49377
49524
  /**
49378
49525
  * Upstream-system connectivity sensor — distinct from `device-status`,
49379
49526
  * which is the kernel-managed online/offline flag for the device's
@@ -50099,7 +50246,7 @@ var mediaPlayerCapability = {
50099
50246
  isMuted: { kind: "boolean" }
50100
50247
  } }
50101
50248
  };
50102
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
50249
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.MediaPlayer;
50103
50250
  var NUMBER = {
50104
50251
  kind: "fixed",
50105
50252
  result: ["number"]
@@ -52138,6 +52285,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
52138
52285
  kind: "mutation",
52139
52286
  auth: "admin"
52140
52287
  });
52288
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
52141
52289
  /**
52142
52290
  * A SITE ZONE — a named part of the property that several locations belong to.
52143
52291
  *
@@ -52624,6 +52772,12 @@ var DeviceInfoSchema = object({
52624
52772
  addonId: string(),
52625
52773
  type: _enum(DeviceType),
52626
52774
  name: string(),
52775
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
52776
+ * entity_id style. Generated once from the name at first registration, unique
52777
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
52778
+ * Optional: a record produced by an older build omits it until the boot
52779
+ * backfill has run. */
52780
+ alias: DeviceAliasSchema.optional(),
52627
52781
  /** Operator-organisational location label. `null` when unset. */
52628
52782
  location: string().nullable(),
52629
52783
  /** Soft-disabled flag. */
@@ -52943,6 +53097,25 @@ method(object({
52943
53097
  }), _void(), {
52944
53098
  kind: "mutation",
52945
53099
  auth: "admin"
53100
+ }), method(object({
53101
+ deviceId: number(),
53102
+ alias: string()
53103
+ }), discriminatedUnion("ok", [object({
53104
+ ok: literal(true),
53105
+ alias: DeviceAliasSchema
53106
+ }), object({
53107
+ ok: literal(false),
53108
+ reason: _enum([
53109
+ "empty",
53110
+ "too-long",
53111
+ "grammar",
53112
+ "reserved",
53113
+ "taken"
53114
+ ]),
53115
+ takenBy: number().optional()
53116
+ })]), {
53117
+ kind: "mutation",
53118
+ auth: "admin"
52946
53119
  }), method(object({
52947
53120
  deviceId: number(),
52948
53121
  location: string().nullable()
@@ -56548,10 +56721,9 @@ var AvailableIntegrationTypeSchema = object({
56548
56721
  * locations" checkbox. Provider-declared in the addon manifest. */
56549
56722
  supportsLocationImport: boolean(),
56550
56723
  /**
56551
- * True when this integration DECLARES a pre-creation test (the
56552
- * `connection-test` cap, or a broker whose settings it stores). Drives the
56553
- * Test button: an integration that cannot be tested must say so up front
56554
- * rather than offering a button that always answers the same nonsense.
56724
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
56725
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
56726
+ * button that always answers the same nonsense.
56555
56727
  */
56556
56728
  canTest: boolean(),
56557
56729
  existingInstances: array(object({
@@ -56605,7 +56777,9 @@ var CreateIntegrationInputSchema = object({
56605
56777
  info: record(string(), unknown()).optional(),
56606
56778
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56607
56779
  originStableId: string().optional(),
56608
- settings: record(string(), unknown()).optional()
56780
+ settings: record(string(), unknown()).optional(),
56781
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
56782
+ kind: string().min(1).optional()
56609
56783
  });
56610
56784
  var UpdateIntegrationInputSchema = object({
56611
56785
  id: string(),
@@ -56631,13 +56805,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56631
56805
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56632
56806
  kind: "mutation",
56633
56807
  auth: "admin"
56634
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56635
- addonId: string(),
56636
- settings: record(string(), unknown())
56637
- }), TestConnectionResultSchema, {
56638
- kind: "mutation",
56639
- auth: "admin"
56640
- });
56808
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
56641
56809
  /**
56642
56810
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
56643
56811
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -56657,7 +56825,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56657
56825
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
56658
56826
  * clears it.
56659
56827
  */
56660
- var ConfigSchemaPassthrough$3 = custom();
56828
+ var ConfigSchemaPassthrough$2 = custom();
56661
56829
  var IntegrationInstanceIdSchema = string().min(1);
56662
56830
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56663
56831
  /**
@@ -56703,7 +56871,16 @@ var IntegrationInstanceKindSchema = object({
56703
56871
  /** Relative hub path to start an interactive account link of an instance of this kind. */
56704
56872
  connectPath: string().min(1).optional(),
56705
56873
  /** Relative hub path to disconnect that account. */
56706
- disconnectPath: string().min(1).optional()
56874
+ disconnectPath: string().min(1).optional(),
56875
+ /**
56876
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
56877
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
56878
+ * `connection` widgets write connection values, `model` widgets own the model choice.
56879
+ */
56880
+ widgets: array(object({
56881
+ id: string().min(1),
56882
+ placement: _enum(["connection", "model"])
56883
+ })).optional()
56707
56884
  });
56708
56885
  /**
56709
56886
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -56744,10 +56921,11 @@ var IntegrationInstanceDescriptionSchema = object({
56744
56921
  /** True when `testInstance` really probes the remote. */
56745
56922
  canTest: boolean(),
56746
56923
  /**
56747
- * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56748
- * Add flow asks which first and sends it as `createInstance.kind`.
56924
+ * The kinds an instance can be of (D1013 rule 1), each naming its `functionCap`. One entry:
56925
+ * nothing to choose; more than one: the Add flow asks which first and sends it as
56926
+ * `createInstance.kind`. Never empty: every instance belongs to exactly one kind.
56749
56927
  */
56750
- kinds: array(IntegrationInstanceKindSchema).optional(),
56928
+ kinds: array(IntegrationInstanceKindSchema).min(1),
56751
56929
  /**
56752
56930
  * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56753
56931
  * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
@@ -56783,8 +56961,8 @@ var IntegrationInstanceSchema = object({
56783
56961
  * the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
56784
56962
  */
56785
56963
  enabled: boolean().optional(),
56786
- /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56787
- kind: string().optional(),
56964
+ /** The kind of this instance: exactly one of `describe().kinds[].id` (D1013 rule 2). */
56965
+ kind: string().min(1),
56788
56966
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
56789
56967
  formValues: record(string(), unknown()),
56790
56968
  status: IntegrationInstanceStatusSchema.optional(),
@@ -56808,7 +56986,7 @@ var integrationInstancesCapability = {
56808
56986
  id: IntegrationInstanceIdSchema.nullable(),
56809
56987
  /** The kind of the create form (`id: null`) of a provider with several. */
56810
56988
  kind: string().min(1).optional()
56811
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56989
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
56812
56990
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56813
56991
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56814
56992
  /**
@@ -57910,12 +58088,12 @@ var LlmModelInfoSchema = object({
57910
58088
  });
57911
58089
  /**
57912
58090
  * The provider-specific part of a profile, validated by the provider that owns
57913
- * the kind (`upsertProfile` refuses what it does not accept). A field its
58091
+ * the kind (saving an instance refuses what it does not accept). A field its
57914
58092
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
57915
58093
  * keychain, never in the row, and reads back as the redaction marker.
57916
58094
  */
57917
58095
  var LlmProviderConfigSchema = record(string(), unknown());
57918
- var LlmProfileSchema = object({
58096
+ object({
57919
58097
  id: string(),
57920
58098
  name: string(),
57921
58099
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -58009,8 +58187,8 @@ var LlmProviderWidgetSchema = object({
58009
58187
  /** ConfigUISchema tree passed through untyped on the wire (the
58010
58188
  * notification-output `ConfigSchemaPassthrough` precedent at
58011
58189
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
58012
- var ConfigSchemaPassthrough$2 = unknown();
58013
- var LlmProfileKindDescriptorSchema = object({
58190
+ var ConfigSchemaPassthrough$1 = unknown();
58191
+ object({
58014
58192
  kind: LlmProfileKindSchema,
58015
58193
  label: string(),
58016
58194
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -58018,9 +58196,9 @@ var LlmProfileKindDescriptorSchema = object({
58018
58196
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
58019
58197
  addonId: string(),
58020
58198
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
58021
- connectionSchema: ConfigSchemaPassthrough$2,
58199
+ connectionSchema: ConfigSchemaPassthrough$1,
58022
58200
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
58023
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
58201
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
58024
58202
  /** The kind's defaults; a model from `listModels` may override any of them. */
58025
58203
  features: LlmModelFeaturesSchema,
58026
58204
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -58149,20 +58327,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
58149
58327
  }), method(object({
58150
58328
  addonId: string().optional(),
58151
58329
  requestId: string()
58152
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
58153
- kind: "mutation",
58154
- auth: "admin"
58155
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
58156
- kind: "mutation",
58157
- auth: "admin"
58158
- }), method(ProfileRefInputSchema, _void(), {
58159
- kind: "mutation",
58160
- auth: "admin"
58161
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
58162
- kind: "mutation",
58163
- auth: "admin",
58164
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
58165
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58330
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
58166
58331
  kind: "mutation",
58167
58332
  auth: "admin"
58168
58333
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -59225,20 +59390,7 @@ method(_void(), MeshStatusSchema), method(object({
59225
59390
  /** Optional hostname override the host should advertise once joined. */
59226
59391
  hostname: string().optional() }), object({
59227
59392
  /** Authentication URL the operator should open in a browser. */
59228
- 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({
59229
- /** Optional auth key — when provided, probes the key validity
59230
- * against the provider's API. Omit when already joined to
59231
- * just ping the daemon. */
59232
- authKey: string().optional() }), object({
59233
- ok: boolean(),
59234
- /** Provider-side identifier resolved by the probe (tailnet
59235
- * name for Tailscale, network id for ZeroTier, etc.). */
59236
- tenant: string().optional(),
59237
- /** Daemon binary version, when reachable. */
59238
- daemonVersion: string().optional(),
59239
- /** Human-readable error when `ok: false`. */
59240
- error: string().optional()
59241
- }), { kind: "mutation" });
59393
+ loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }));
59242
59394
  /**
59243
59395
  * A monotonic total between resets (D944). Energy uses `power-meter.energy`
59244
59396
  * so dashboards keep one series; every other instantaneous reading lands here.
@@ -65380,13 +65532,6 @@ var AddBrokerInputSchema = object({
65380
65532
  });
65381
65533
  var AddBrokerResultSchema = object({ id: string() });
65382
65534
  var IdInputSchema = object({ id: string() });
65383
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
65384
- ok: literal(true),
65385
- latencyMs: number()
65386
- }), object({
65387
- ok: literal(false),
65388
- error: string()
65389
- })]);
65390
65535
  var StartEmbeddedInputSchema = object({
65391
65536
  port: number().int().min(1).max(65535).default(1883),
65392
65537
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -65409,9 +65554,6 @@ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnection
65409
65554
  }), method(IdInputSchema, _void(), {
65410
65555
  kind: "mutation",
65411
65556
  auth: "admin"
65412
- }), method(IdInputSchema, TestResultSchema$1, {
65413
- kind: "mutation",
65414
- auth: "admin"
65415
65557
  }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
65416
65558
  kind: "mutation",
65417
65559
  auth: "admin"
@@ -67075,6 +67217,11 @@ var NcScheduleSchema = object({
67075
67217
  * as a STATE SUMMARY at the end of each window of `schedule`.
67076
67218
  */
67077
67219
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
67220
+ _enum([
67221
+ "activation",
67222
+ "deactivation",
67223
+ "schedule"
67224
+ ]);
67078
67225
  /**
67079
67226
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
67080
67227
  * every section; `section` = one notification per section with something to
@@ -67091,11 +67238,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
67091
67238
  * midnight crossing); the send time is each window's END. Refused on the
67092
67239
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
67093
67240
  */
67094
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
67095
- mode: literal("summary"),
67096
- schedule: NcScheduleSchema,
67097
- split: NcStateSummarySplitSchema.optional()
67098
- })]);
67241
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
67242
+ object({ mode: literal("instant") }),
67243
+ object({
67244
+ mode: literal("summary"),
67245
+ schedule: NcScheduleSchema,
67246
+ split: NcStateSummarySplitSchema.optional()
67247
+ }),
67248
+ object({
67249
+ mode: literal("modes"),
67250
+ /** The bad edge, and every kind that has a single edge. */
67251
+ onActivation: boolean(),
67252
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
67253
+ onDeactivation: boolean(),
67254
+ /** A state summary at the end of each window; absent = no summary. */
67255
+ onSchedule: object({
67256
+ schedule: NcScheduleSchema,
67257
+ grouping: NcStateSummarySplitSchema
67258
+ }).optional()
67259
+ })
67260
+ ]);
67099
67261
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
67100
67262
  var NcPlateMatcherSchema = object({
67101
67263
  /**
@@ -69455,61 +69617,6 @@ var OauthIntegrationDescriptorSchema = object({
69455
69617
  });
69456
69618
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
69457
69619
  /**
69458
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
69459
- *
69460
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
69461
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
69462
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
69463
- * immutable after create.
69464
- */
69465
- var ConfigSchemaPassthrough$1 = unknown();
69466
- var OidcConnectionSchema = object({
69467
- id: string().min(1),
69468
- displayName: string(),
69469
- icon: string(),
69470
- issuerUrl: string(),
69471
- clientId: string(),
69472
- /** Client id + secret present — never the secret value. */
69473
- configured: boolean(),
69474
- /**
69475
- * Values for the connection form schema. `clientSecret` is a keychain
69476
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
69477
- */
69478
- formValues: record(string(), unknown()).optional()
69479
- });
69480
- /**
69481
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
69482
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
69483
- */
69484
- var OidcConnectionUpdateInputSchema = object({
69485
- id: string().min(1),
69486
- displayName: string().min(1),
69487
- icon: string().optional(),
69488
- issuerUrl: string().optional(),
69489
- clientId: string().optional(),
69490
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
69491
- redirectUri: string().optional(),
69492
- scopes: string().optional(),
69493
- usernameClaim: _enum([
69494
- "preferred_username",
69495
- "email",
69496
- "sub"
69497
- ]).optional()
69498
- });
69499
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
69500
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
69501
- displayName: string().min(1)
69502
- }), object({ id: string() }), {
69503
- kind: "mutation",
69504
- auth: "admin"
69505
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
69506
- kind: "mutation",
69507
- auth: "admin"
69508
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
69509
- kind: "mutation",
69510
- auth: "admin"
69511
- });
69512
- /**
69513
69620
  * Generic on-screen-display (video overlay) cap. Each camera exposes
69514
69621
  * a firmware-defined list of overlays (channel name, timestamp,
69515
69622
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -75224,6 +75331,16 @@ var RemoteLayoutComponentSchema = object({
75224
75331
  method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
75225
75332
  var RUNNING_POWER_START_DELAY_MS = 3e4;
75226
75333
  var RUNNING_POWER_STOP_DELAY_MS = 18e4;
75334
+ /**
75335
+ * Per-type overrides of the dwell defaults. A media player draws power the
75336
+ * moment it is on and stops drawing the moment it is off, so it needs neither
75337
+ * a long dwell nor a second sample. A type absent here keeps the global values.
75338
+ */
75339
+ var RUNNING_POWER_DEFAULTS_BY_TYPE = { [DeviceType.MediaPlayer]: {
75340
+ startDwellMs: 1e4,
75341
+ stopDwellMs: 1e4,
75342
+ minSamples: 1
75343
+ } };
75227
75344
  var RunningStateSourceSchema = _enum(["native", "assigned"]);
75228
75345
  var RunningStateStatusSchema = object({
75229
75346
  /** null = unknown — never coerced to false (D393). */
@@ -75241,93 +75358,27 @@ var RunningFlipEventDataSchema = object({
75241
75358
  phase: string().nullable()
75242
75359
  });
75243
75360
  var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
75244
- /** Generic Customize target for the running boolean (D936). */
75245
- var runningCustomize = {
75246
- version: 1,
75247
- overridable: [{
75248
- path: "running",
75249
- by: "running"
75250
- }],
75251
- targets: [{
75252
- id: "running",
75253
- cardinality: "scalar",
75254
- vocabulary: {
75255
- cap: "running-state",
75256
- stateField: "running"
75257
- },
75258
- deviceTypes: [
75259
- DeviceType.Washer,
75260
- DeviceType.Dryer,
75261
- DeviceType.Dishwasher,
75262
- DeviceType.Vacuum,
75263
- DeviceType.LawnMower,
75264
- DeviceType.MediaPlayer,
75265
- DeviceType.WaterHeater,
75266
- DeviceType.Humidifier,
75267
- DeviceType.Fan,
75268
- DeviceType.Climate,
75269
- DeviceType.Thermostat,
75270
- DeviceType.PetFeeder,
75271
- DeviceType.Heater
75272
- ],
75273
- apply: "add",
75274
- path: "running",
75275
- sources: [
75276
- {
75277
- kind: "switch",
75278
- transforms: [{ kind: "identity" }]
75279
- },
75280
- {
75281
- kind: "binary",
75282
- transforms: [{ kind: "identity" }, { kind: "invert" }]
75283
- },
75284
- {
75285
- kind: "numeric",
75286
- requiredUnit: "W",
75287
- transforms: [{
75288
- kind: "threshold",
75289
- comparisons: ["gte", "lte"],
75290
- comparison: "gte",
75291
- defaultThreshold: 5,
75292
- defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75293
- defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75294
- thresholdUnit: "W"
75295
- }]
75296
- }
75297
- ],
75298
- companions: [
75299
- {
75300
- kind: "constant",
75301
- path: "source",
75302
- value: "assigned",
75303
- when: "always"
75304
- },
75305
- {
75306
- kind: "constant",
75307
- path: "phase",
75308
- value: null,
75309
- when: "add"
75310
- },
75311
- {
75312
- kind: "clock",
75313
- path: "lastFetchedAt",
75314
- when: "always"
75315
- },
75316
- {
75317
- kind: "transition-time",
75318
- path: "since",
75319
- sourceChangedAt: "prefer"
75320
- }
75321
- ],
75322
- sourceReport: {
75323
- status: "assignment-status",
75324
- runtimeField: "source",
75325
- nativeValue: "native",
75326
- assignedValue: "assigned"
75327
- },
75328
- advancedExpression: true
75329
- }]
75330
- };
75361
+ /**
75362
+ * Customize offers "running" only where a job makes sense (D933): a cycle,
75363
+ * a clean, a mow, playback, or another appliance that is on and working.
75364
+ * The cap itself stays on every type — a camera can still report running
75365
+ * natively; it just has no card.
75366
+ */
75367
+ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
75368
+ DeviceType.Washer,
75369
+ DeviceType.Dryer,
75370
+ DeviceType.Dishwasher,
75371
+ DeviceType.Vacuum,
75372
+ DeviceType.LawnMower,
75373
+ DeviceType.MediaPlayer,
75374
+ DeviceType.WaterHeater,
75375
+ DeviceType.Humidifier,
75376
+ DeviceType.Fan,
75377
+ DeviceType.Climate,
75378
+ DeviceType.Thermostat,
75379
+ DeviceType.PetFeeder,
75380
+ DeviceType.Heater
75381
+ ];
75331
75382
  var runningStateCapability = {
75332
75383
  name: "running-state",
75333
75384
  scope: "device",
@@ -75347,7 +75398,79 @@ var runningStateCapability = {
75347
75398
  durability: "restored",
75348
75399
  history: { fields: { running: { kind: "boolean" } } },
75349
75400
  volatileStateFields: ["lastFetchedAt"],
75350
- customize: runningCustomize
75401
+ customize: {
75402
+ version: 1,
75403
+ overridable: [{
75404
+ path: "running",
75405
+ by: "running"
75406
+ }],
75407
+ targets: [{
75408
+ id: "running",
75409
+ cardinality: "scalar",
75410
+ vocabulary: {
75411
+ cap: "running-state",
75412
+ stateField: "running"
75413
+ },
75414
+ deviceTypes: RUNNING_CUSTOMIZE_DEVICE_TYPES,
75415
+ apply: "add",
75416
+ path: "running",
75417
+ sources: [
75418
+ {
75419
+ kind: "switch",
75420
+ transforms: [{ kind: "identity" }]
75421
+ },
75422
+ {
75423
+ kind: "binary",
75424
+ transforms: [{ kind: "identity" }, { kind: "invert" }]
75425
+ },
75426
+ {
75427
+ kind: "numeric",
75428
+ requiredUnit: "W",
75429
+ transforms: [{
75430
+ kind: "threshold",
75431
+ comparisons: ["gte", "lte"],
75432
+ comparison: "gte",
75433
+ defaultThreshold: 5,
75434
+ defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
75435
+ defaultStopDwellMs: RUNNING_POWER_STOP_DELAY_MS,
75436
+ deviceTypeDefaults: RUNNING_POWER_DEFAULTS_BY_TYPE,
75437
+ thresholdUnit: "W"
75438
+ }]
75439
+ }
75440
+ ],
75441
+ companions: [
75442
+ {
75443
+ kind: "constant",
75444
+ path: "source",
75445
+ value: "assigned",
75446
+ when: "always"
75447
+ },
75448
+ {
75449
+ kind: "constant",
75450
+ path: "phase",
75451
+ value: null,
75452
+ when: "add"
75453
+ },
75454
+ {
75455
+ kind: "clock",
75456
+ path: "lastFetchedAt",
75457
+ when: "always"
75458
+ },
75459
+ {
75460
+ kind: "transition-time",
75461
+ path: "since",
75462
+ sourceChangedAt: "prefer"
75463
+ }
75464
+ ],
75465
+ sourceReport: {
75466
+ status: "assignment-status",
75467
+ runtimeField: "source",
75468
+ nativeValue: "native",
75469
+ assignedValue: "assigned"
75470
+ },
75471
+ advancedExpression: true
75472
+ }]
75473
+ }
75351
75474
  };
75352
75475
  /**
75353
75476
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -76591,21 +76714,6 @@ method(object({
76591
76714
  */
76592
76715
  ttlSec: union([number().int().positive(), literal("never")]).optional()
76593
76716
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
76594
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
76595
- providerId: string().min(1),
76596
- displayName: string().min(1),
76597
- configSchema: unknown(),
76598
- nodeLocal: boolean(),
76599
- shouldSaveDiskSpace: literal(true),
76600
- minFreePercent: number().min(0).max(100)
76601
- }), object({
76602
- providerId: string().min(1),
76603
- displayName: string().min(1),
76604
- configSchema: unknown(),
76605
- nodeLocal: boolean(),
76606
- shouldSaveDiskSpace: literal(false),
76607
- minFreePercent: literal(null)
76608
- })]);
76609
76717
  method(object({
76610
76718
  location: StorageLocationRefSchema,
76611
76719
  relativePath: string()
@@ -76667,42 +76775,7 @@ method(object({
76667
76775
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
76668
76776
  kind: "mutation",
76669
76777
  auth: "admin"
76670
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
76671
- createdAt: true,
76672
- updatedAt: true
76673
- }), StorageLocationSchema, {
76674
- kind: "mutation",
76675
- auth: "admin"
76676
- }), method(object({
76677
- id: string(),
76678
- force: boolean().optional()
76679
- }), _void(), {
76680
- kind: "mutation",
76681
- auth: "admin"
76682
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
76683
- ok: boolean(),
76684
- error: string().optional()
76685
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
76686
- providerId: string(),
76687
- /**
76688
- * The location this config is an UNSAVED edit of, when there is one.
76689
- *
76690
- * `listLocations` replaces every declared secret with the redaction
76691
- * sentinel, so the edit modal's form state holds the sentinel for any
76692
- * credential the operator did not retype — and posting that here
76693
- * without a way to resolve it makes the provider try to authenticate
76694
- * as `__camstack_redacted__` and report the operator's own working
76695
- * password as wrong. Given this id, the orchestrator restores each
76696
- * sentinel from the stored config (same rule as `upsertLocation`)
76697
- * before dispatching. Omitted by the "Add location" wizard, where
76698
- * every value was typed just now and nothing is stored yet.
76699
- */
76700
- locationId: string().optional(),
76701
- config: record(string(), unknown())
76702
- }), object({
76703
- ok: boolean(),
76704
- error: string().optional()
76705
- }), { auth: "admin" });
76778
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
76706
76779
  /** One table of the settings database. */
76707
76780
  var StorageTableSchema = object({
76708
76781
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -78224,7 +78297,7 @@ var TerminalInstanceInfoSchema = object({
78224
78297
  /**
78225
78298
  * False while the node the instance is bound to is not in the cluster
78226
78299
  * topology (forgotten, renamed, re-paired). The instance is kept; the
78227
- * operator re-points it with `updateInstance({ targetNodeId })`.
78300
+ * operator re-points it on the Terminal page.
78228
78301
  */
78229
78302
  nodeInCluster: boolean(),
78230
78303
  profileId: string(),
@@ -78262,41 +78335,7 @@ var TerminalOutputBatchSchema = object({
78262
78335
  snapshot: string().optional(),
78263
78336
  events: array(TerminalOutputEventSchema).readonly()
78264
78337
  });
78265
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
78266
- targetNodeId: string().min(1),
78267
- profileId: string().min(1),
78268
- name: string().trim().min(1).max(160).optional(),
78269
- executable: string().max(1024).optional(),
78270
- args: array(string().max(2048)).max(64).optional(),
78271
- cwd: string().max(1024).optional(),
78272
- environment: array(string().max(4096)).max(64).optional(),
78273
- profileSettings: ProfileSettingsBagSchema.optional()
78274
- }), TerminalInstanceInfoSchema, {
78275
- kind: "mutation",
78276
- auth: "admin"
78277
- }), method(object({
78278
- instanceId: string().min(1),
78279
- /** Re-point the instance to another node; the profile must exist there. */
78280
- targetNodeId: string().min(1).max(256).optional(),
78281
- name: string().trim().min(1).max(160).optional(),
78282
- executable: string().max(1024).optional(),
78283
- args: array(string().max(2048)).max(64).optional(),
78284
- cwd: string().max(1024).optional(),
78285
- environment: array(string().max(4096)).max(64).optional(),
78286
- profileSettings: ProfileSettingsBagSchema.optional()
78287
- }), TerminalInstanceInfoSchema, {
78288
- kind: "mutation",
78289
- auth: "admin"
78290
- }), method(object({ instanceId: string().min(1) }), _void(), {
78291
- kind: "mutation",
78292
- auth: "admin"
78293
- }), method(object({
78294
- instanceId: string().min(1),
78295
- enabled: boolean()
78296
- }), TerminalInstanceInfoSchema, {
78297
- kind: "mutation",
78298
- auth: "admin"
78299
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78338
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78300
78339
  stableId: string().min(1),
78301
78340
  name: string().trim().min(1).max(160).optional()
78302
78341
  }), TerminalInstanceInfoSchema, {
@@ -78365,6 +78404,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
78365
78404
  _enum(["switch", "pet-fountain"]);
78366
78405
  DeviceType.Heater, DeviceType.PetFountain;
78367
78406
  var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
78407
+ /** One duration, as the one-tap run asks for it. */
78408
+ var TimedRunQuickRunSchema = object({
78409
+ duration: number().positive().finite(),
78410
+ unit: TimedRunUnitSchema
78411
+ });
78368
78412
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
78369
78413
  var TimedRunRefusalSchema = _enum([
78370
78414
  "unsupported-device",
@@ -78385,8 +78429,23 @@ var TimedRunStatusSchema = object({
78385
78429
  activeUntil: number().nullable(),
78386
78430
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
78387
78431
  requestedBy: string().nullable(),
78432
+ /**
78433
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
78434
+ * the device is gone or its type has no timed run; a status from before this field
78435
+ * existed has none, and a surface falls back to the type's default.
78436
+ */
78437
+ quickRun: TimedRunQuickRunSchema.nullable(),
78388
78438
  lastFetchedAt: number()
78389
78439
  });
78440
+ /** What the settings form needs: the unit, the bounds and the type's default. */
78441
+ var TimedRunOptionsSchema = object({
78442
+ unit: TimedRunUnitSchema,
78443
+ /** The longest run, in `unit`. */
78444
+ maxDuration: number(),
78445
+ defaultDuration: number()
78446
+ });
78447
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
78448
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
78390
78449
  var timedRunCapability = {
78391
78450
  name: "timed-run",
78392
78451
  scope: "device",
@@ -78394,7 +78453,24 @@ var timedRunCapability = {
78394
78453
  kind: "wrapper",
78395
78454
  defaultActive: true,
78396
78455
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
78456
+ deviceConfig: { ui: {
78457
+ kind: "derived-form",
78458
+ builderId: "timed-run",
78459
+ tab: "general"
78460
+ } },
78397
78461
  methods: {
78462
+ getOptions: method(object({ deviceId: number().int() }), TimedRunOptionsSchema),
78463
+ /**
78464
+ * The one-tap run of this device. A duration above the type's maximum is
78465
+ * REFUSED (thrown), like `runFor`: the maximum is a safety bound.
78466
+ */
78467
+ setSettings: method(object({
78468
+ deviceId: number().int(),
78469
+ settings: TimedRunSettingsPatchSchema
78470
+ }), _void(), {
78471
+ kind: "mutation",
78472
+ auth: "admin"
78473
+ }),
78398
78474
  /**
78399
78475
  * Turn the device on and arm its off. A run already armed is replaced (the
78400
78476
  * new deadline wins). Refuses with a typed reason above the type's maximum.
@@ -78518,7 +78594,7 @@ var TrainingProviderTypeSchema = object({
78518
78594
  label: string(),
78519
78595
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
78520
78596
  iconAsset: string(),
78521
- /** Owning `training-provider` addon — collection selector for createEntry. */
78597
+ /** Owning `training-provider` addon. */
78522
78598
  addonId: string(),
78523
78599
  /**
78524
78600
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -78563,36 +78639,6 @@ var TrainingProviderDescriptorSchema = object({
78563
78639
  */
78564
78640
  formValues: record(string(), unknown()).optional()
78565
78641
  });
78566
- /**
78567
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
78568
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
78569
- * `{ $secretClear: true }` clears it.
78570
- */
78571
- var TrainingProviderUpdateEntryInputSchema = object({
78572
- entryId: string().min(1),
78573
- name: string().min(1),
78574
- tokenId: string().optional(),
78575
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
78576
- environment: string().optional(),
78577
- gpuType: string().optional(),
78578
- region: string().optional(),
78579
- costCapUsd: number().nonnegative().optional(),
78580
- maxRuntimeSec: number().int().positive().optional(),
78581
- failedDatasetGraceHours: number().nonnegative().optional()
78582
- });
78583
- var TrainingCredentialCheckSchema = object({
78584
- ok: boolean(),
78585
- /** Workspace / account name, when the provider says. */
78586
- account: string().optional(),
78587
- reason: _enum([
78588
- "missing",
78589
- "invalid",
78590
- "forbidden",
78591
- "network",
78592
- "quota"
78593
- ]).optional(),
78594
- detail: string().optional()
78595
- });
78596
78642
  var ProviderJobStateSchema = _enum([
78597
78643
  "pending",
78598
78644
  "starting",
@@ -78640,22 +78686,6 @@ var TrainingLogLineSchema = object({
78640
78686
  text: string()
78641
78687
  });
78642
78688
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
78643
- typeId: string().min(1),
78644
- name: string().min(1)
78645
- }), object({ entryId: string() }), {
78646
- kind: "mutation",
78647
- auth: "admin"
78648
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
78649
- kind: "mutation",
78650
- auth: "admin"
78651
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
78652
- kind: "mutation",
78653
- auth: "admin"
78654
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
78655
- kind: "mutation",
78656
- auth: "admin",
78657
- timeoutMs: 3e4
78658
- }), method(object({
78659
78689
  entryId: string().min(1),
78660
78690
  gpuType: string(),
78661
78691
  region: string(),
@@ -83233,21 +83263,28 @@ Object.freeze({
83233
83263
  access: "view",
83234
83264
  role: "admin"
83235
83265
  },
83236
- "addonSettings.getGlobalSettings": {
83266
+ "addonSettings.getIntegrationFormSchema": {
83237
83267
  capName: "addon-settings",
83238
83268
  capScope: "system",
83239
83269
  addonId: null,
83240
83270
  access: "view",
83241
83271
  role: "admin"
83242
83272
  },
83243
- "addonSettings.getIntegrationFormSchema": {
83273
+ "addonSettings.getScopedSettings": {
83244
83274
  capName: "addon-settings",
83245
83275
  capScope: "system",
83246
83276
  addonId: null,
83247
83277
  access: "view",
83248
83278
  role: "admin"
83249
83279
  },
83250
- "addonSettings.getIntegrationSettings": {
83280
+ "addonSettings.listScopedSettings": {
83281
+ capName: "addon-settings",
83282
+ capScope: "system",
83283
+ addonId: null,
83284
+ access: "view",
83285
+ role: "admin"
83286
+ },
83287
+ "addonSettings.readScopedValues": {
83251
83288
  capName: "addon-settings",
83252
83289
  capScope: "system",
83253
83290
  addonId: null,
@@ -83268,7 +83305,7 @@ Object.freeze({
83268
83305
  access: "create",
83269
83306
  role: "admin"
83270
83307
  },
83271
- "addonSettings.updateGlobalSettings": {
83308
+ "addonSettings.updateScopedSettings": {
83272
83309
  capName: "addon-settings",
83273
83310
  capScope: "system",
83274
83311
  addonId: null,
@@ -84108,20 +84145,6 @@ Object.freeze({
84108
84145
  access: "view",
84109
84146
  role: "admin"
84110
84147
  },
84111
- "connectionTest.describeTest": {
84112
- capName: "connection-test",
84113
- capScope: "system",
84114
- addonId: null,
84115
- access: "view",
84116
- role: "admin"
84117
- },
84118
- "connectionTest.testSettings": {
84119
- capName: "connection-test",
84120
- capScope: "system",
84121
- addonId: null,
84122
- access: "create",
84123
- role: "admin"
84124
- },
84125
84148
  "connectivity.getStatus": {
84126
84149
  capName: "connectivity",
84127
84150
  capScope: "device",
@@ -85452,6 +85475,13 @@ Object.freeze({
85452
85475
  access: "create",
85453
85476
  role: "admin"
85454
85477
  },
85478
+ "deviceManager.setAlias": {
85479
+ capName: "device-manager",
85480
+ capScope: "system",
85481
+ addonId: null,
85482
+ access: "create",
85483
+ role: "admin"
85484
+ },
85455
85485
  "deviceManager.setChildLayout": {
85456
85486
  capName: "device-manager",
85457
85487
  capScope: "system",
@@ -86488,13 +86518,6 @@ Object.freeze({
86488
86518
  access: "view",
86489
86519
  role: "admin"
86490
86520
  },
86491
- "integrations.testConnection": {
86492
- capName: "integrations",
86493
- capScope: "system",
86494
- addonId: null,
86495
- access: "create",
86496
- role: "admin"
86497
- },
86498
86521
  "integrations.update": {
86499
86522
  capName: "integrations",
86500
86523
  capScope: "system",
@@ -86747,13 +86770,6 @@ Object.freeze({
86747
86770
  access: "delete",
86748
86771
  role: "admin"
86749
86772
  },
86750
- "llm.deleteProfile": {
86751
- capName: "llm",
86752
- capScope: "system",
86753
- addonId: null,
86754
- access: "delete",
86755
- role: "admin"
86756
- },
86757
86773
  "llm.generate": {
86758
86774
  capName: "llm",
86759
86775
  capScope: "system",
@@ -86817,20 +86833,6 @@ Object.freeze({
86817
86833
  access: "view",
86818
86834
  role: "admin"
86819
86835
  },
86820
- "llm.listProfileKinds": {
86821
- capName: "llm",
86822
- capScope: "system",
86823
- addonId: null,
86824
- access: "view",
86825
- role: "admin"
86826
- },
86827
- "llm.listProfiles": {
86828
- capName: "llm",
86829
- capScope: "system",
86830
- addonId: null,
86831
- access: "view",
86832
- role: "admin"
86833
- },
86834
86836
  "llm.listRuntimeNodes": {
86835
86837
  capName: "llm",
86836
86838
  capScope: "system",
@@ -86838,13 +86840,6 @@ Object.freeze({
86838
86840
  access: "view",
86839
86841
  role: "admin"
86840
86842
  },
86841
- "llm.migrateRawSecrets": {
86842
- capName: "llm",
86843
- capScope: "system",
86844
- addonId: null,
86845
- access: "create",
86846
- role: "admin"
86847
- },
86848
86843
  "llm.resolveModelRef": {
86849
86844
  capName: "llm",
86850
86845
  capScope: "system",
@@ -86873,20 +86868,6 @@ Object.freeze({
86873
86868
  access: "create",
86874
86869
  role: "admin"
86875
86870
  },
86876
- "llm.testProfile": {
86877
- capName: "llm",
86878
- capScope: "system",
86879
- addonId: null,
86880
- access: "create",
86881
- role: "admin"
86882
- },
86883
- "llm.upsertProfile": {
86884
- capName: "llm",
86885
- capScope: "system",
86886
- addonId: null,
86887
- access: "create",
86888
- role: "admin"
86889
- },
86890
86871
  "llmRuntime.appleAvailability": {
86891
86872
  capName: "llm-runtime",
86892
86873
  capScope: "system",
@@ -87391,13 +87372,6 @@ Object.freeze({
87391
87372
  access: "create",
87392
87373
  role: "admin"
87393
87374
  },
87394
- "meshNetwork.testConnection": {
87395
- capName: "mesh-network",
87396
- capScope: "system",
87397
- addonId: null,
87398
- access: "create",
87399
- role: "admin"
87400
- },
87401
87375
  "meter.getStatus": {
87402
87376
  capName: "meter",
87403
87377
  capScope: "device",
@@ -87727,13 +87701,6 @@ Object.freeze({
87727
87701
  access: "create",
87728
87702
  role: "admin"
87729
87703
  },
87730
- "mqttBroker.testConnection": {
87731
- capName: "mqtt-broker",
87732
- capScope: "system",
87733
- addonId: null,
87734
- access: "create",
87735
- role: "admin"
87736
- },
87737
87704
  "nativeObjectDetection.getOptions": {
87738
87705
  capName: "native-object-detection",
87739
87706
  capScope: "device",
@@ -88063,13 +88030,6 @@ Object.freeze({
88063
88030
  access: "create",
88064
88031
  role: "admin"
88065
88032
  },
88066
- "notificationOutput.deleteTarget": {
88067
- capName: "notification-output",
88068
- capScope: "system",
88069
- addonId: null,
88070
- access: "delete",
88071
- role: "admin"
88072
- },
88073
88033
  "notificationOutput.discoverTargets": {
88074
88034
  capName: "notification-output",
88075
88035
  capScope: "system",
@@ -88077,13 +88037,6 @@ Object.freeze({
88077
88037
  access: "view",
88078
88038
  role: "admin"
88079
88039
  },
88080
- "notificationOutput.getTarget": {
88081
- capName: "notification-output",
88082
- capScope: "system",
88083
- addonId: null,
88084
- access: "view",
88085
- role: "admin"
88086
- },
88087
88040
  "notificationOutput.listTargetKinds": {
88088
88041
  capName: "notification-output",
88089
88042
  capScope: "system",
@@ -88091,13 +88044,6 @@ Object.freeze({
88091
88044
  access: "view",
88092
88045
  role: "admin"
88093
88046
  },
88094
- "notificationOutput.listTargets": {
88095
- capName: "notification-output",
88096
- capScope: "system",
88097
- addonId: null,
88098
- access: "view",
88099
- role: "admin"
88100
- },
88101
88047
  "notificationOutput.probeTargetConfig": {
88102
88048
  capName: "notification-output",
88103
88049
  capScope: "system",
@@ -88112,13 +88058,6 @@ Object.freeze({
88112
88058
  access: "create",
88113
88059
  role: "admin"
88114
88060
  },
88115
- "notificationOutput.setTargetEnabled": {
88116
- capName: "notification-output",
88117
- capScope: "system",
88118
- addonId: null,
88119
- access: "create",
88120
- role: "admin"
88121
- },
88122
88061
  "notificationOutput.testTarget": {
88123
88062
  capName: "notification-output",
88124
88063
  capScope: "system",
@@ -88126,13 +88065,6 @@ Object.freeze({
88126
88065
  access: "create",
88127
88066
  role: "admin"
88128
88067
  },
88129
- "notificationOutput.upsertTarget": {
88130
- capName: "notification-output",
88131
- capScope: "system",
88132
- addonId: null,
88133
- access: "create",
88134
- role: "admin"
88135
- },
88136
88068
  "notificationRules.activateAlarmProfile": {
88137
88069
  capName: "notification-rules",
88138
88070
  capScope: "system",
@@ -88392,41 +88324,6 @@ Object.freeze({
88392
88324
  access: "view",
88393
88325
  role: "admin"
88394
88326
  },
88395
- "oidcConnections.connectionFormSchema": {
88396
- capName: "oidc-connections",
88397
- capScope: "system",
88398
- addonId: null,
88399
- access: "view",
88400
- role: "admin"
88401
- },
88402
- "oidcConnections.createConnection": {
88403
- capName: "oidc-connections",
88404
- capScope: "system",
88405
- addonId: null,
88406
- access: "create",
88407
- role: "admin"
88408
- },
88409
- "oidcConnections.deleteConnection": {
88410
- capName: "oidc-connections",
88411
- capScope: "system",
88412
- addonId: null,
88413
- access: "delete",
88414
- role: "admin"
88415
- },
88416
- "oidcConnections.listConnections": {
88417
- capName: "oidc-connections",
88418
- capScope: "system",
88419
- addonId: null,
88420
- access: "view",
88421
- role: "admin"
88422
- },
88423
- "oidcConnections.updateConnection": {
88424
- capName: "oidc-connections",
88425
- capScope: "system",
88426
- addonId: null,
88427
- access: "create",
88428
- role: "admin"
88429
- },
88430
88327
  "osd.getStatus": {
88431
88328
  capName: "osd",
88432
88329
  capScope: "device",
@@ -91458,13 +91355,6 @@ Object.freeze({
91458
91355
  access: "delete",
91459
91356
  role: "admin"
91460
91357
  },
91461
- "storage.deleteLocation": {
91462
- capName: "storage",
91463
- capScope: "system",
91464
- addonId: null,
91465
- access: "delete",
91466
- role: "admin"
91467
- },
91468
91358
  "storage.endDownload": {
91469
91359
  capName: "storage",
91470
91360
  capScope: "system",
@@ -91521,13 +91411,6 @@ Object.freeze({
91521
91411
  access: "view",
91522
91412
  role: "admin"
91523
91413
  },
91524
- "storage.listProviders": {
91525
- capName: "storage",
91526
- capScope: "system",
91527
- addonId: null,
91528
- access: "view",
91529
- role: "admin"
91530
- },
91531
91414
  "storage.read": {
91532
91415
  capName: "storage",
91533
91416
  capScope: "system",
@@ -91549,27 +91432,6 @@ Object.freeze({
91549
91432
  access: "view",
91550
91433
  role: "admin"
91551
91434
  },
91552
- "storage.testConfig": {
91553
- capName: "storage",
91554
- capScope: "system",
91555
- addonId: null,
91556
- access: "view",
91557
- role: "admin"
91558
- },
91559
- "storage.testLocation": {
91560
- capName: "storage",
91561
- capScope: "system",
91562
- addonId: null,
91563
- access: "view",
91564
- role: "admin"
91565
- },
91566
- "storage.upsertLocation": {
91567
- capName: "storage",
91568
- capScope: "system",
91569
- addonId: null,
91570
- access: "create",
91571
- role: "admin"
91572
- },
91573
91435
  "storage.write": {
91574
91436
  capName: "storage",
91575
91437
  capScope: "system",
@@ -92347,27 +92209,6 @@ Object.freeze({
92347
92209
  access: "create",
92348
92210
  role: "admin"
92349
92211
  },
92350
- "terminalSession.createInstance": {
92351
- capName: "terminal-session",
92352
- capScope: "system",
92353
- addonId: null,
92354
- access: "create",
92355
- role: "admin"
92356
- },
92357
- "terminalSession.deleteInstance": {
92358
- capName: "terminal-session",
92359
- capScope: "system",
92360
- addonId: null,
92361
- access: "delete",
92362
- role: "admin"
92363
- },
92364
- "terminalSession.listInstances": {
92365
- capName: "terminal-session",
92366
- capScope: "system",
92367
- addonId: null,
92368
- access: "view",
92369
- role: "admin"
92370
- },
92371
92212
  "terminalSession.listLegacyCameras": {
92372
92213
  capName: "terminal-session",
92373
92214
  capScope: "system",
@@ -92410,20 +92251,6 @@ Object.freeze({
92410
92251
  access: "create",
92411
92252
  role: "admin"
92412
92253
  },
92413
- "terminalSession.setInstanceEnabled": {
92414
- capName: "terminal-session",
92415
- capScope: "system",
92416
- addonId: null,
92417
- access: "create",
92418
- role: "admin"
92419
- },
92420
- "terminalSession.updateInstance": {
92421
- capName: "terminal-session",
92422
- capScope: "system",
92423
- addonId: null,
92424
- access: "create",
92425
- role: "admin"
92426
- },
92427
92254
  "terminalSession.writeInput": {
92428
92255
  capName: "terminal-session",
92429
92256
  capScope: "system",
@@ -92438,6 +92265,13 @@ Object.freeze({
92438
92265
  access: "create",
92439
92266
  role: "viewer"
92440
92267
  },
92268
+ "timedRun.getOptions": {
92269
+ capName: "timed-run",
92270
+ capScope: "device",
92271
+ addonId: null,
92272
+ access: "view",
92273
+ role: "viewer"
92274
+ },
92441
92275
  "timedRun.getStatus": {
92442
92276
  capName: "timed-run",
92443
92277
  capScope: "device",
@@ -92452,6 +92286,13 @@ Object.freeze({
92452
92286
  access: "create",
92453
92287
  role: "viewer"
92454
92288
  },
92289
+ "timedRun.setSettings": {
92290
+ capName: "timed-run",
92291
+ capScope: "device",
92292
+ addonId: null,
92293
+ access: "create",
92294
+ role: "admin"
92295
+ },
92455
92296
  "toast.onToast": {
92456
92297
  capName: "toast",
92457
92298
  capScope: "system",
@@ -92564,20 +92405,6 @@ Object.freeze({
92564
92405
  access: "create",
92565
92406
  role: "admin"
92566
92407
  },
92567
- "trainingProvider.createEntry": {
92568
- capName: "training-provider",
92569
- capScope: "system",
92570
- addonId: null,
92571
- access: "create",
92572
- role: "admin"
92573
- },
92574
- "trainingProvider.deleteEntry": {
92575
- capName: "training-provider",
92576
- capScope: "system",
92577
- addonId: null,
92578
- access: "delete",
92579
- role: "admin"
92580
- },
92581
92408
  "trainingProvider.describe": {
92582
92409
  capName: "training-provider",
92583
92410
  capScope: "system",
@@ -92648,13 +92475,6 @@ Object.freeze({
92648
92475
  access: "create",
92649
92476
  role: "admin"
92650
92477
  },
92651
- "trainingProvider.updateEntry": {
92652
- capName: "training-provider",
92653
- capScope: "system",
92654
- addonId: null,
92655
- access: "create",
92656
- role: "admin"
92657
- },
92658
92478
  "trainingProvider.uploadDataset": {
92659
92479
  capName: "training-provider",
92660
92480
  capScope: "system",
@@ -92662,13 +92482,6 @@ Object.freeze({
92662
92482
  access: "create",
92663
92483
  role: "admin"
92664
92484
  },
92665
- "trainingProvider.validateCredentials": {
92666
- capName: "training-provider",
92667
- capScope: "system",
92668
- addonId: null,
92669
- access: "create",
92670
- role: "admin"
92671
- },
92672
92485
  "turnProvider.getTurnServers": {
92673
92486
  capName: "turn-provider",
92674
92487
  capScope: "system",
@@ -93410,6 +93223,7 @@ function offered(received) {
93410
93223
  };
93411
93224
  }
93412
93225
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
93226
+ string().min(1).max(4096);
93413
93227
  Object.freeze({
93414
93228
  "accessories.getStatus": [{
93415
93229
  name: "deviceId",
@@ -94093,6 +93907,11 @@ Object.freeze({
94093
93907
  form: "single",
94094
93908
  optional: false
94095
93909
  }],
93910
+ "deviceManager.setAlias": [{
93911
+ name: "deviceId",
93912
+ form: "single",
93913
+ optional: false
93914
+ }],
94096
93915
  "deviceManager.setChildLayout": [{
94097
93916
  name: "deviceId",
94098
93917
  form: "single",
@@ -96157,6 +95976,11 @@ Object.freeze({
96157
95976
  form: "single",
96158
95977
  optional: false
96159
95978
  }],
95979
+ "timedRun.getOptions": [{
95980
+ name: "deviceId",
95981
+ form: "single",
95982
+ optional: false
95983
+ }],
96160
95984
  "timedRun.getStatus": [{
96161
95985
  name: "deviceId",
96162
95986
  form: "single",
@@ -96167,6 +95991,11 @@ Object.freeze({
96167
95991
  form: "single",
96168
95992
  optional: false
96169
95993
  }],
95994
+ "timedRun.setSettings": [{
95995
+ name: "deviceId",
95996
+ form: "single",
95997
+ optional: false
95998
+ }],
96170
95999
  "update.getStatus": [{
96171
96000
  name: "deviceId",
96172
96001
  form: "single",
@@ -97029,6 +96858,7 @@ var STATE_SUMMARY_VARS = [
97029
96858
  [...RULE_TEXT],
97030
96859
  [...RULE_TEXT],
97031
96860
  [...RULE_TEXT],
96861
+ [...RULE_TEXT],
97032
96862
  [...RULE_BODY_ONLY],
97033
96863
  [...RULE_TEXT],
97034
96864
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -99396,7 +99226,13 @@ function buildEnpalInstances(records, deps) {
99396
99226
  },
99397
99227
  instanceMode: "multiple",
99398
99228
  kind: "server",
99399
- canTest: false
99229
+ canTest: false,
99230
+ kinds: [{
99231
+ id: "connection",
99232
+ label: "Enpal",
99233
+ functionCap: "device-provider",
99234
+ role: "devices"
99235
+ }]
99400
99236
  },
99401
99237
  formSchema: () => buildConnectionFormSchema(),
99402
99238
  initialBody: (name) => ({ name }),