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