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