@camstack/addon-import-alexa 0.2.194 → 0.2.196
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 +13 -0
- package/dist/addon.js +492 -489
- package/dist/addon.mjs +492 -489
- package/package.json +2 -2
- package/python/alexa_gateway.py +6 -1
package/dist/addon.js
CHANGED
|
@@ -48,7 +48,7 @@ node_fs = __toESM(node_fs, 1);
|
|
|
48
48
|
let node_child_process = require("node:child_process");
|
|
49
49
|
require("node:zlib");
|
|
50
50
|
let node_url = require("node:url");
|
|
51
|
-
//#region ../types/dist/event-category-
|
|
51
|
+
//#region ../types/dist/event-category-78tcBemo.mjs
|
|
52
52
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
53
53
|
EventCategory["SystemBoot"] = "system.boot";
|
|
54
54
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -169,6 +169,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
169
169
|
*/
|
|
170
170
|
EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
|
|
171
171
|
/**
|
|
172
|
+
* An addon is RUNNING although its installed package is below what the
|
|
173
|
+
* running server requires (`camstack.requires.addons` of the server, or the
|
|
174
|
+
* addon's own `requires.server`). Raised once per streak by the unsupported-
|
|
175
|
+
* addon tracker, on the hub and on agents; AlertCenter turns it into one
|
|
176
|
+
* operator alert per node and package. The addon is never held back for it.
|
|
177
|
+
*
|
|
178
|
+
* Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
|
|
179
|
+
*/
|
|
180
|
+
EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
|
|
181
|
+
/**
|
|
182
|
+
* The package of an {@link AddonUnsupportedVersion} alert reached a supported
|
|
183
|
+
* version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
|
|
184
|
+
*
|
|
185
|
+
* Payload: `{ packageName, addonId, nodeId }`.
|
|
186
|
+
*/
|
|
187
|
+
EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
|
|
188
|
+
/**
|
|
172
189
|
* Monitor scheduled the next retry for a failed addon. Transient —
|
|
173
190
|
* surfaced to the UI for live-updating the "next retry in Ns"
|
|
174
191
|
* countdown on the Addons page row, NOT persisted as an alert.
|
|
@@ -279,7 +296,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
279
296
|
EventCategory["ProcessRestarted"] = "process.restarted";
|
|
280
297
|
/**
|
|
281
298
|
* The SET of storage locations changed — one was created, edited, enabled,
|
|
282
|
-
* disabled or deleted through
|
|
299
|
+
* disabled or deleted through the location instances or the forced-delete action.
|
|
283
300
|
*
|
|
284
301
|
* Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
|
|
285
302
|
* it must also converge on its own periodic path, because a dropped event
|
|
@@ -6044,7 +6061,7 @@ function number(params) {
|
|
|
6044
6061
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6045
6062
|
}
|
|
6046
6063
|
//#endregion
|
|
6047
|
-
//#region ../types/dist/sleep-
|
|
6064
|
+
//#region ../types/dist/sleep-CQP-REJp.mjs
|
|
6048
6065
|
/**
|
|
6049
6066
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6050
6067
|
* window to float samples (D455).
|
|
@@ -6408,6 +6425,59 @@ Object.fromEntries([
|
|
|
6408
6425
|
function isValuelessField(field) {
|
|
6409
6426
|
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";
|
|
6410
6427
|
}
|
|
6428
|
+
/**
|
|
6429
|
+
* Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
|
|
6430
|
+
* `key → { value, default }`, recursing through `group` and `sub-tabs`
|
|
6431
|
+
* layout fields. Keeping `value` and `default` SEPARATE (rather than
|
|
6432
|
+
* pre-resolving) lets callers both read the effective value AND detect
|
|
6433
|
+
* overrides (`value` differs from `default`). Structural over `unknown`;
|
|
6434
|
+
* malformed input yields an empty map, never throws.
|
|
6435
|
+
*/
|
|
6436
|
+
function collectHydratedFieldEntries(schema) {
|
|
6437
|
+
const out = /* @__PURE__ */ new Map();
|
|
6438
|
+
if (typeof schema !== "object" || schema === null) return out;
|
|
6439
|
+
const sections = schema.sections;
|
|
6440
|
+
if (!Array.isArray(sections)) return out;
|
|
6441
|
+
for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
|
|
6442
|
+
return out;
|
|
6443
|
+
}
|
|
6444
|
+
/** Recursive field walker for {@link collectHydratedFieldEntries}. */
|
|
6445
|
+
function collectFieldEntriesInto(fields, into) {
|
|
6446
|
+
if (!Array.isArray(fields)) return;
|
|
6447
|
+
for (const field of fields) {
|
|
6448
|
+
if (typeof field !== "object" || field === null) continue;
|
|
6449
|
+
const type = field.type;
|
|
6450
|
+
if (type === "group") {
|
|
6451
|
+
collectFieldEntriesInto(field.fields, into);
|
|
6452
|
+
continue;
|
|
6453
|
+
}
|
|
6454
|
+
if (type === "sub-tabs") {
|
|
6455
|
+
const tabs = field.tabs;
|
|
6456
|
+
if (Array.isArray(tabs)) {
|
|
6457
|
+
for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
|
|
6458
|
+
}
|
|
6459
|
+
continue;
|
|
6460
|
+
}
|
|
6461
|
+
const key = field.key;
|
|
6462
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
6463
|
+
if (!("value" in field)) continue;
|
|
6464
|
+
into.set(key, {
|
|
6465
|
+
value: field.value,
|
|
6466
|
+
default: field.default
|
|
6467
|
+
});
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
/**
|
|
6471
|
+
* Flatten a hydrated settings payload into `key → EFFECTIVE value`
|
|
6472
|
+
* (`value ?? default`). The read-side inverse of `hydrateSchema` for the
|
|
6473
|
+
* common "just give me the resolved values" case. Built on
|
|
6474
|
+
* {@link collectHydratedFieldEntries}.
|
|
6475
|
+
*/
|
|
6476
|
+
function collectHydratedFieldValues(schema) {
|
|
6477
|
+
const out = /* @__PURE__ */ new Map();
|
|
6478
|
+
for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
|
|
6479
|
+
return out;
|
|
6480
|
+
}
|
|
6411
6481
|
function hydrateSchema(schema, values, options = {}) {
|
|
6412
6482
|
return {
|
|
6413
6483
|
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
@@ -7308,17 +7378,35 @@ var RecordIntegrationInstances = class {
|
|
|
7308
7378
|
...kind !== void 0 ? { kind } : {}
|
|
7309
7379
|
});
|
|
7310
7380
|
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
7381
|
+
const initial = this.hooks.initialBody(name, kind);
|
|
7382
|
+
const patch = this.patchOf(values ?? {});
|
|
7383
|
+
if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
|
|
7384
|
+
kind,
|
|
7385
|
+
body: {
|
|
7386
|
+
...initial,
|
|
7387
|
+
...patch
|
|
7388
|
+
}
|
|
7389
|
+
}));
|
|
7311
7390
|
await this.handle.put({
|
|
7312
7391
|
id: minted,
|
|
7313
|
-
data:
|
|
7392
|
+
data: initial
|
|
7314
7393
|
}, { ifRev: 0 });
|
|
7315
|
-
const patch = this.patchOf(values ?? {});
|
|
7316
7394
|
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
7317
7395
|
await this.hooks.onChanged?.(minted, "created");
|
|
7318
7396
|
return { id: minted };
|
|
7319
7397
|
};
|
|
7320
7398
|
updateInstance = async ({ id, name, values }) => {
|
|
7321
7399
|
const patch = this.patchOf(values);
|
|
7400
|
+
if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
|
|
7401
|
+
const stored = await this.handle.get(id);
|
|
7402
|
+
if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
|
|
7403
|
+
kind: void 0,
|
|
7404
|
+
body: {
|
|
7405
|
+
...stored.data,
|
|
7406
|
+
...patch
|
|
7407
|
+
}
|
|
7408
|
+
}));
|
|
7409
|
+
}
|
|
7322
7410
|
if (name !== void 0) patch[this.nameField] = name;
|
|
7323
7411
|
await this.handle.patch(id, patch);
|
|
7324
7412
|
await this.hooks.onChanged?.(id, "updated");
|
|
@@ -7629,6 +7717,21 @@ var SCOPED_SETTINGS_SCOPES = [
|
|
|
7629
7717
|
"device-defaults",
|
|
7630
7718
|
"node"
|
|
7631
7719
|
];
|
|
7720
|
+
/** The one refusal of a key a scope did not declare. */
|
|
7721
|
+
function assertDeclaredKeys(keys, declared) {
|
|
7722
|
+
const undeclared = keys.filter((key) => !declared.has(key));
|
|
7723
|
+
if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
|
|
7724
|
+
}
|
|
7725
|
+
/** Build the handle over an addon's scoped read, write and unset. */
|
|
7726
|
+
function createScopedSettings(deps) {
|
|
7727
|
+
return {
|
|
7728
|
+
async get() {
|
|
7729
|
+
return deps.schema.parse(await deps.read());
|
|
7730
|
+
},
|
|
7731
|
+
patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
|
|
7732
|
+
unset: (keys) => deps.unset(keys)
|
|
7733
|
+
};
|
|
7734
|
+
}
|
|
7632
7735
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
7633
7736
|
var KeychainWriteRefusedError = class extends Error {
|
|
7634
7737
|
fieldPath;
|
|
@@ -8204,7 +8307,16 @@ function createHandle(raw, opts) {
|
|
|
8204
8307
|
return blob;
|
|
8205
8308
|
}
|
|
8206
8309
|
},
|
|
8207
|
-
writeAddonStore: writeAddon
|
|
8310
|
+
writeAddonStore: writeAddon,
|
|
8311
|
+
removeClearedKeys: (keys) => serialize("addon", async () => {
|
|
8312
|
+
const stored = await readStoredAddonBlob(raw);
|
|
8313
|
+
index.seen("addon", stored);
|
|
8314
|
+
const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
|
|
8315
|
+
if (removable.length === 0) return;
|
|
8316
|
+
const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
|
|
8317
|
+
await raw.writeAddonStore(patch);
|
|
8318
|
+
index.wrote("addon", patch);
|
|
8319
|
+
})
|
|
8208
8320
|
};
|
|
8209
8321
|
}
|
|
8210
8322
|
/** The context the addon sees, plus the private raw view and write outcome. */
|
|
@@ -8749,6 +8861,151 @@ var BaseAddon = class {
|
|
|
8749
8861
|
}
|
|
8750
8862
|
}
|
|
8751
8863
|
/**
|
|
8864
|
+
* Hook: values layered over the stored ones when the FORM of one scope is served
|
|
8865
|
+
* ({@link getScopedSettings}) — a preview that derives dependent options without writing.
|
|
8866
|
+
* Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
|
|
8867
|
+
*/
|
|
8868
|
+
async scopedSettingsOverlay(_scope, _cap, _nodeId) {
|
|
8869
|
+
return null;
|
|
8870
|
+
}
|
|
8871
|
+
/**
|
|
8872
|
+
* Hook: a write of one scope changed stored values (not called when the store reports no change,
|
|
8873
|
+
* e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
|
|
8874
|
+
*/
|
|
8875
|
+
async onScopedSettingsChanged(_scope, _cap, _keys) {}
|
|
8876
|
+
/** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
|
|
8877
|
+
clusterSettings(cap, schema) {
|
|
8878
|
+
return this.scopedHandle("cluster", cap, schema);
|
|
8879
|
+
}
|
|
8880
|
+
/** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
|
|
8881
|
+
deviceDefaults(cap, schema) {
|
|
8882
|
+
return this.scopedHandle("device-defaults", cap, schema);
|
|
8883
|
+
}
|
|
8884
|
+
/**
|
|
8885
|
+
* Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
|
|
8886
|
+
* with no row yields the schema defaults — never a bare-key fallback.
|
|
8887
|
+
*/
|
|
8888
|
+
nodeSettings(cap, schema, nodeId) {
|
|
8889
|
+
return this.scopedHandle("node", cap, schema, nodeId);
|
|
8890
|
+
}
|
|
8891
|
+
/**
|
|
8892
|
+
* {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
|
|
8893
|
+
* settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
|
|
8894
|
+
* central delete. `nodeId` omitted ⇒ the local node.
|
|
8895
|
+
*/
|
|
8896
|
+
nodeState(key, schema, fallback, nodeId) {
|
|
8897
|
+
this._declaredStateKeys.add(key);
|
|
8898
|
+
return createDurableState({
|
|
8899
|
+
key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
|
|
8900
|
+
schema,
|
|
8901
|
+
fallback,
|
|
8902
|
+
read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
|
|
8903
|
+
write: async (patch) => {
|
|
8904
|
+
await this._ctx?.settings?.writeAddonStore(patch);
|
|
8905
|
+
},
|
|
8906
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
|
|
8907
|
+
addonId: this._ctx?.id,
|
|
8908
|
+
key: k,
|
|
8909
|
+
error: String(e)
|
|
8910
|
+
} })
|
|
8911
|
+
});
|
|
8912
|
+
}
|
|
8913
|
+
/** The loaded schema `cap` declares for `scope`, or `null`. */
|
|
8914
|
+
async declaredScopedSchema(scope, cap) {
|
|
8915
|
+
await this.loadScopedSchemas(cap);
|
|
8916
|
+
return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
|
|
8917
|
+
}
|
|
8918
|
+
async requireScopedSchema(scope, cap) {
|
|
8919
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
8920
|
+
if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
|
|
8921
|
+
return schema;
|
|
8922
|
+
}
|
|
8923
|
+
scopedKeys(schema) {
|
|
8924
|
+
return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
|
|
8925
|
+
}
|
|
8926
|
+
/** `store` narrowed to the keys `schema` declares. */
|
|
8927
|
+
narrowToScope(store, schema) {
|
|
8928
|
+
const keys = this.scopedKeys(schema);
|
|
8929
|
+
return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
|
|
8930
|
+
}
|
|
8931
|
+
scopedHandle(scope, cap, schema, nodeId) {
|
|
8932
|
+
return createScopedSettings({
|
|
8933
|
+
schema,
|
|
8934
|
+
read: async () => {
|
|
8935
|
+
const declared = await this.requireScopedSchema(scope, cap);
|
|
8936
|
+
const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
|
|
8937
|
+
const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
|
|
8938
|
+
return {
|
|
8939
|
+
...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
|
|
8940
|
+
...stored
|
|
8941
|
+
};
|
|
8942
|
+
},
|
|
8943
|
+
write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
|
|
8944
|
+
unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
|
|
8945
|
+
});
|
|
8946
|
+
}
|
|
8947
|
+
/** The one write path of the scoped API: declared keys only, then the global patch. */
|
|
8948
|
+
async writeScoped(scope, cap, patch, nodeId) {
|
|
8949
|
+
assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
8950
|
+
if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
|
|
8954
|
+
* value is released exactly as there — and the emptied row is then removed; every other key is
|
|
8955
|
+
* removed by an `undefined` patch (which a secret transform would read as "keep").
|
|
8956
|
+
*/
|
|
8957
|
+
async unsetScoped(scope, cap, keys, nodeId) {
|
|
8958
|
+
assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
8959
|
+
const target = scope === "node" ? nodeId : void 0;
|
|
8960
|
+
const tree = this.addonSecretTree();
|
|
8961
|
+
const secretKeys = keys.filter((key) => tree.has(key));
|
|
8962
|
+
const plainKeys = keys.filter((key) => !tree.has(key));
|
|
8963
|
+
let changed = false;
|
|
8964
|
+
if (plainKeys.length > 0) {
|
|
8965
|
+
const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
|
|
8966
|
+
changed = await this.applyGlobalPatch(patch, target, cap) || changed;
|
|
8967
|
+
}
|
|
8968
|
+
if (secretKeys.length > 0) {
|
|
8969
|
+
await this.clearSecretKeys(secretKeys, target, cap);
|
|
8970
|
+
const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
|
|
8971
|
+
const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
|
|
8972
|
+
if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
|
|
8973
|
+
else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
|
|
8974
|
+
changed = true;
|
|
8975
|
+
}
|
|
8976
|
+
if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
|
|
8977
|
+
}
|
|
8978
|
+
/**
|
|
8979
|
+
* The hydrated form of one scope of one capability's settings (any
|
|
8980
|
+
* {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
|
|
8981
|
+
*/
|
|
8982
|
+
async getScopedSettings(scope, cap, nodeId) {
|
|
8983
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
8984
|
+
if (schema === null) return null;
|
|
8985
|
+
const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
|
|
8986
|
+
const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
|
|
8987
|
+
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
|
|
8988
|
+
}
|
|
8989
|
+
/** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
|
|
8990
|
+
async updateScopedSettings(scope, cap, patch, nodeId) {
|
|
8991
|
+
this._operatorWriteDepth++;
|
|
8992
|
+
try {
|
|
8993
|
+
await this.writeScoped(scope, cap, patch, nodeId);
|
|
8994
|
+
} finally {
|
|
8995
|
+
this._operatorWriteDepth--;
|
|
8996
|
+
}
|
|
8997
|
+
}
|
|
8998
|
+
/**
|
|
8999
|
+
* The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
|
|
9000
|
+
* its value. `null` when the capability declares nothing for the scope.
|
|
9001
|
+
*/
|
|
9002
|
+
async readScopedValues(scope, cap, nodeId) {
|
|
9003
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9004
|
+
if (schema === null) return null;
|
|
9005
|
+
const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
|
|
9006
|
+
return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
|
|
9007
|
+
}
|
|
9008
|
+
/**
|
|
8752
9009
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
8753
9010
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8754
9011
|
*/
|
|
@@ -8900,6 +9157,9 @@ var BaseAddon = class {
|
|
|
8900
9157
|
* a keychain (an older kernel) it writes `''`, as such code always did.
|
|
8901
9158
|
*/
|
|
8902
9159
|
async clearSecrets(keys, nodeId) {
|
|
9160
|
+
await this.clearSecretKeys(keys, nodeId);
|
|
9161
|
+
}
|
|
9162
|
+
async clearSecretKeys(keys, nodeId, cap) {
|
|
8903
9163
|
const tree = this.addonSecretTree();
|
|
8904
9164
|
const keychain = this._ctx?.secrets !== void 0;
|
|
8905
9165
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
@@ -8912,25 +9172,27 @@ var BaseAddon = class {
|
|
|
8912
9172
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
8913
9173
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
8914
9174
|
}
|
|
8915
|
-
await this.applyGlobalPatch(patch, nodeId);
|
|
9175
|
+
await this.applyGlobalPatch(patch, nodeId, cap);
|
|
8916
9176
|
}
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
9177
|
+
/** Resolves `true` when the write changed stored values. */
|
|
9178
|
+
async applyGlobalPatch(barePatch, nodeId, cap) {
|
|
9179
|
+
await this.loadScopedSchemas(cap);
|
|
9180
|
+
const keys = this.perNodeKeys(cap);
|
|
8920
9181
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
8921
9182
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
8922
9183
|
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
8923
9184
|
const handle = this._secretSettings;
|
|
8924
9185
|
if (handle !== null) {
|
|
8925
9186
|
const { changed } = await handle.writeAddonStore(scoped);
|
|
8926
|
-
if (!changed) return;
|
|
9187
|
+
if (!changed) return false;
|
|
8927
9188
|
} else await this._ctx?.settings?.writeAddonStore(scoped);
|
|
8928
|
-
if (target !== localNode) return;
|
|
9189
|
+
if (target !== localNode) return true;
|
|
8929
9190
|
await this.resolveConfig();
|
|
8930
9191
|
await this.onConfigChanged();
|
|
8931
9192
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
8932
9193
|
const legacy = this.globalSettingsSchema();
|
|
8933
9194
|
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
9195
|
+
return true;
|
|
8934
9196
|
}
|
|
8935
9197
|
/**
|
|
8936
9198
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -10664,7 +10926,6 @@ var CAP_NAMES = Object.freeze([
|
|
|
10664
10926
|
"notifier",
|
|
10665
10927
|
"numeric-sensor",
|
|
10666
10928
|
"oauth-integration",
|
|
10667
|
-
"oidc-connections",
|
|
10668
10929
|
"osd",
|
|
10669
10930
|
"osd-manager",
|
|
10670
10931
|
"pet-feeder",
|
|
@@ -12556,8 +12817,7 @@ Object.freeze({
|
|
|
12556
12817
|
"device-provider": { "reloadDevice": 18e4 },
|
|
12557
12818
|
"llm": {
|
|
12558
12819
|
"generate": 6e5,
|
|
12559
|
-
"generateVision": 6e5
|
|
12560
|
-
"testProfile": 6e5
|
|
12820
|
+
"generateVision": 6e5
|
|
12561
12821
|
},
|
|
12562
12822
|
"llm-runtime": {
|
|
12563
12823
|
"complete": 6e5,
|
|
@@ -14168,17 +14428,18 @@ function composeInstanceKinds(parts, base) {
|
|
|
14168
14428
|
* message to what the kind supports — callers never special-case a service.
|
|
14169
14429
|
*
|
|
14170
14430
|
* DESIGN DECISIONS (locked):
|
|
14171
|
-
* -
|
|
14172
|
-
*
|
|
14173
|
-
*
|
|
14174
|
-
*
|
|
14175
|
-
*
|
|
14176
|
-
*
|
|
14177
|
-
*
|
|
14178
|
-
*
|
|
14179
|
-
*
|
|
14180
|
-
* routable. `send` / `testTarget` /
|
|
14181
|
-
* `
|
|
14431
|
+
* - A target is an INSTANCE of this function cap (epic #440, step 6): the
|
|
14432
|
+
* providers serve their targets as `integration-instances` records, one
|
|
14433
|
+
* record kind per target kind, and the admin lists, adds, edits, switches
|
|
14434
|
+
* and deletes them through that cap. This cap keeps what an instance cannot
|
|
14435
|
+
* say: the kind catalogue (`listTargetKinds`: capabilities, delivery
|
|
14436
|
+
* parameters, brand icon), dispatch (`send`, `testTarget`) and the form
|
|
14437
|
+
* helpers (`discoverTargets`, `probeTargetConfig`).
|
|
14438
|
+
* - `listTargetKinds` / `discoverTargets` return arrays → the generated
|
|
14439
|
+
* cap-mount auto-`concatCollection`-fans them across every registered
|
|
14440
|
+
* provider so one catalog is routable. `send` / `testTarget` /
|
|
14441
|
+
* `probeTargetConfig` route to ONE provider by the `addonId` the generated
|
|
14442
|
+
* collection router extracts from the call input.
|
|
14182
14443
|
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
14183
14444
|
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
14184
14445
|
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
@@ -14423,7 +14684,7 @@ var TargetKindCapsSchema = object({
|
|
|
14423
14684
|
* the union is large and not meant for runtime validation here; the exported
|
|
14424
14685
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
14425
14686
|
*/
|
|
14426
|
-
var ConfigSchemaPassthrough$
|
|
14687
|
+
var ConfigSchemaPassthrough$3 = unknown();
|
|
14427
14688
|
var TargetKindSchema = object({
|
|
14428
14689
|
kind: string(),
|
|
14429
14690
|
label: string(),
|
|
@@ -14460,7 +14721,7 @@ var TargetKindSchema = object({
|
|
|
14460
14721
|
* its raster path, which is the safe default for an unknown image.
|
|
14461
14722
|
*/
|
|
14462
14723
|
iconMediaType: string().optional(),
|
|
14463
|
-
configSchema: ConfigSchemaPassthrough$
|
|
14724
|
+
configSchema: ConfigSchemaPassthrough$3,
|
|
14464
14725
|
/**
|
|
14465
14726
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
14466
14727
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -14482,9 +14743,11 @@ var TargetKindSchema = object({
|
|
|
14482
14743
|
caps: TargetKindCapsSchema
|
|
14483
14744
|
});
|
|
14484
14745
|
/**
|
|
14485
|
-
* A
|
|
14486
|
-
*
|
|
14487
|
-
*
|
|
14746
|
+
* A target as a provider's own code sees it (the notification-center bridge, the
|
|
14747
|
+
* send path): the flat record body split into the name, the switch and the
|
|
14748
|
+
* kind's fields. `config` holds secrets; a reader that serves it to a client
|
|
14749
|
+
* REDACTS secret fields (a presence marker only) — never round-trip a stored
|
|
14750
|
+
* secret to the UI.
|
|
14488
14751
|
*/
|
|
14489
14752
|
var TargetSchema = object({
|
|
14490
14753
|
id: string(),
|
|
@@ -14533,21 +14796,13 @@ var notificationOutputCapability = {
|
|
|
14533
14796
|
mode: "collection",
|
|
14534
14797
|
methods: {
|
|
14535
14798
|
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
14536
|
-
listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
|
|
14537
|
-
/**
|
|
14538
|
-
* One target WITH secrets — admin edit / duplicate hydration only.
|
|
14539
|
-
* `listTargets` stays redacted; the editor loads this so password fields
|
|
14540
|
-
* are filled (masked in the input) and a save can round-trip the token
|
|
14541
|
-
* when a destination changes (Duplicate with a new chatId, etc.).
|
|
14542
|
-
*/
|
|
14543
|
-
getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
|
|
14544
14799
|
discoverTargets: method(object({
|
|
14545
14800
|
kind: string(),
|
|
14546
14801
|
config: record(string(), unknown()).optional(),
|
|
14547
14802
|
/**
|
|
14548
14803
|
* When editing / duplicating, the blank password field never round-trips
|
|
14549
14804
|
* the stored token. Pass the source target id so the provider can restore
|
|
14550
|
-
* secrets server-side before dialling
|
|
14805
|
+
* secrets server-side before dialling.
|
|
14551
14806
|
*/
|
|
14552
14807
|
targetId: string().optional()
|
|
14553
14808
|
}), array(DiscoveredTargetSchema), { auth: "admin" }),
|
|
@@ -14573,30 +14828,6 @@ var notificationOutputCapability = {
|
|
|
14573
14828
|
}), TestResultSchema, {
|
|
14574
14829
|
kind: "mutation",
|
|
14575
14830
|
auth: "admin"
|
|
14576
|
-
}),
|
|
14577
|
-
upsertTarget: method(object({
|
|
14578
|
-
target: TargetSchema,
|
|
14579
|
-
/**
|
|
14580
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
14581
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
14582
|
-
* password fields are restored from this source (D634 still applies if
|
|
14583
|
-
* destinations diverge without a re-typed secret).
|
|
14584
|
-
*/
|
|
14585
|
-
cloneSecretsFrom: string().optional()
|
|
14586
|
-
}), TargetSchema, {
|
|
14587
|
-
kind: "mutation",
|
|
14588
|
-
auth: "admin"
|
|
14589
|
-
}),
|
|
14590
|
-
deleteTarget: method(object({ targetId: string() }), _void(), {
|
|
14591
|
-
kind: "mutation",
|
|
14592
|
-
auth: "admin"
|
|
14593
|
-
}),
|
|
14594
|
-
setTargetEnabled: method(object({
|
|
14595
|
-
targetId: string(),
|
|
14596
|
-
enabled: boolean()
|
|
14597
|
-
}), _void(), {
|
|
14598
|
-
kind: "mutation",
|
|
14599
|
-
auth: "admin"
|
|
14600
14831
|
})
|
|
14601
14832
|
}
|
|
14602
14833
|
};
|
|
@@ -14612,8 +14843,9 @@ var notificationOutputCapability = {
|
|
|
14612
14843
|
*
|
|
14613
14844
|
* Three providers (notifiers, Home Assistant, Alexa) serve the same shape, so the pieces are shared:
|
|
14614
14845
|
* - {@link defineNotificationTargetKind}: the record kind of one `TargetKind` descriptor;
|
|
14615
|
-
* - {@link NotificationTargetStore}: the `Target`-shaped facade the
|
|
14616
|
-
* using, over one handle per kind
|
|
14846
|
+
* - {@link NotificationTargetStore}: the `Target`-shaped facade the provider's own code keeps
|
|
14847
|
+
* using (send, the notification-center bridge), over one handle per kind;
|
|
14848
|
+
* - {@link listNotificationTargets}: the targets as an addon on `ctx.api` reads them;
|
|
14617
14849
|
* - {@link buildNotificationTargetInstances}: the `integration-instances` provider, one kind each;
|
|
14618
14850
|
* - {@link moveTargetRows}: the D987 procedure that flattens the old `config` blobs into the bodies.
|
|
14619
14851
|
*/
|
|
@@ -14870,6 +15102,10 @@ function notificationTargetParts(records, spec) {
|
|
|
14870
15102
|
initialBody: (name) => ({ name }),
|
|
14871
15103
|
mintId: () => spec.mintId(),
|
|
14872
15104
|
configured: () => true,
|
|
15105
|
+
validateWrite: ({ body }) => spec.validate?.({
|
|
15106
|
+
kind: descriptor.kind,
|
|
15107
|
+
config: body
|
|
15108
|
+
}),
|
|
14873
15109
|
test: async ({ id, values }) => {
|
|
14874
15110
|
if (spec.probe === void 0 || !canTest) return {
|
|
14875
15111
|
success: false,
|
|
@@ -23265,7 +23501,6 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
23265
23501
|
"notification-output": system("notification delivery — reached through notifier devices and rules"),
|
|
23266
23502
|
"notification-rules": system("notification rules — authored on their own page"),
|
|
23267
23503
|
"oauth-integration": system(IDENTITY),
|
|
23268
|
-
"oidc-connections": system(IDENTITY),
|
|
23269
23504
|
"osd-manager": system(PIPELINE),
|
|
23270
23505
|
"pipeline-executor": system(PIPELINE),
|
|
23271
23506
|
"pipeline-orchestrator": system(PIPELINE),
|
|
@@ -45486,6 +45721,8 @@ var SettingsSchemaWithValuesSchema = object({
|
|
|
45486
45721
|
});
|
|
45487
45722
|
/** Patch object — keys are field names, values are the new field values. */
|
|
45488
45723
|
var SettingsPatchSchema = record(string(), unknown());
|
|
45724
|
+
/** The scope of a scoped-settings call (D986): where the capability declared the values. */
|
|
45725
|
+
var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
|
|
45489
45726
|
/** Standard success response for update operations. */
|
|
45490
45727
|
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
45491
45728
|
method(object({
|
|
@@ -45501,6 +45738,25 @@ method(object({
|
|
|
45501
45738
|
kind: "mutation",
|
|
45502
45739
|
auth: "admin"
|
|
45503
45740
|
}), method(object({
|
|
45741
|
+
addonId: string(),
|
|
45742
|
+
cap: string(),
|
|
45743
|
+
scope: ScopedSettingsScopeSchema,
|
|
45744
|
+
nodeId: string().optional()
|
|
45745
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
45746
|
+
addonId: string(),
|
|
45747
|
+
cap: string(),
|
|
45748
|
+
scope: ScopedSettingsScopeSchema,
|
|
45749
|
+
nodeId: string().optional(),
|
|
45750
|
+
patch: SettingsPatchSchema
|
|
45751
|
+
}), SettingsUpdateResultSchema, {
|
|
45752
|
+
kind: "mutation",
|
|
45753
|
+
auth: "admin"
|
|
45754
|
+
}), method(object({
|
|
45755
|
+
addonId: string(),
|
|
45756
|
+
cap: string(),
|
|
45757
|
+
scope: ScopedSettingsScopeSchema,
|
|
45758
|
+
nodeId: string().optional()
|
|
45759
|
+
}), record(string(), unknown()).nullable()), method(object({
|
|
45504
45760
|
addonId: string(),
|
|
45505
45761
|
nodeId: string().optional()
|
|
45506
45762
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
@@ -57057,7 +57313,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
57057
57313
|
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
57058
57314
|
* clears it.
|
|
57059
57315
|
*/
|
|
57060
|
-
var ConfigSchemaPassthrough$
|
|
57316
|
+
var ConfigSchemaPassthrough$2 = custom();
|
|
57061
57317
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
57062
57318
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
57063
57319
|
/**
|
|
@@ -57103,7 +57359,16 @@ var IntegrationInstanceKindSchema = object({
|
|
|
57103
57359
|
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
57104
57360
|
connectPath: string().min(1).optional(),
|
|
57105
57361
|
/** Relative hub path to disconnect that account. */
|
|
57106
|
-
disconnectPath: string().min(1).optional()
|
|
57362
|
+
disconnectPath: string().min(1).optional(),
|
|
57363
|
+
/**
|
|
57364
|
+
* Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
|
|
57365
|
+
* GENERIC widget id the client maps to a component; a client that knows none of them shows none.
|
|
57366
|
+
* `connection` widgets write connection values, `model` widgets own the model choice.
|
|
57367
|
+
*/
|
|
57368
|
+
widgets: array(object({
|
|
57369
|
+
id: string().min(1),
|
|
57370
|
+
placement: _enum(["connection", "model"])
|
|
57371
|
+
})).optional()
|
|
57107
57372
|
});
|
|
57108
57373
|
/**
|
|
57109
57374
|
* How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
|
|
@@ -57208,7 +57473,7 @@ var integrationInstancesCapability = {
|
|
|
57208
57473
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
57209
57474
|
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
57210
57475
|
kind: string().min(1).optional()
|
|
57211
|
-
}), ConfigSchemaPassthrough$
|
|
57476
|
+
}), ConfigSchemaPassthrough$2, { auth: "admin" }),
|
|
57212
57477
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
57213
57478
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
57214
57479
|
/**
|
|
@@ -58310,12 +58575,12 @@ var LlmModelInfoSchema = object({
|
|
|
58310
58575
|
});
|
|
58311
58576
|
/**
|
|
58312
58577
|
* The provider-specific part of a profile, validated by the provider that owns
|
|
58313
|
-
* the kind (
|
|
58578
|
+
* the kind (saving an instance refuses what it does not accept). A field its
|
|
58314
58579
|
* `connectionSchema` declares `type: 'password'` is a secret: it is held in the
|
|
58315
58580
|
* keychain, never in the row, and reads back as the redaction marker.
|
|
58316
58581
|
*/
|
|
58317
58582
|
var LlmProviderConfigSchema = record(string(), unknown());
|
|
58318
|
-
|
|
58583
|
+
object({
|
|
58319
58584
|
id: string(),
|
|
58320
58585
|
name: string(),
|
|
58321
58586
|
/** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
|
|
@@ -58409,8 +58674,8 @@ var LlmProviderWidgetSchema = object({
|
|
|
58409
58674
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
58410
58675
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
58411
58676
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
58412
|
-
var ConfigSchemaPassthrough$
|
|
58413
|
-
|
|
58677
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
58678
|
+
object({
|
|
58414
58679
|
kind: LlmProfileKindSchema,
|
|
58415
58680
|
label: string(),
|
|
58416
58681
|
/** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
|
|
@@ -58418,9 +58683,9 @@ var LlmProfileKindDescriptorSchema = object({
|
|
|
58418
58683
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
58419
58684
|
addonId: string(),
|
|
58420
58685
|
/** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
|
|
58421
|
-
connectionSchema: ConfigSchemaPassthrough$
|
|
58686
|
+
connectionSchema: ConfigSchemaPassthrough$1,
|
|
58422
58687
|
/** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
|
|
58423
|
-
advancedSchema: ConfigSchemaPassthrough$
|
|
58688
|
+
advancedSchema: ConfigSchemaPassthrough$1.optional(),
|
|
58424
58689
|
/** The kind's defaults; a model from `listModels` may override any of them. */
|
|
58425
58690
|
features: LlmModelFeaturesSchema,
|
|
58426
58691
|
/** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
|
|
@@ -58549,20 +58814,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
|
|
|
58549
58814
|
}), method(object({
|
|
58550
58815
|
addonId: string().optional(),
|
|
58551
58816
|
requestId: string()
|
|
58552
|
-
}), _void(), { kind: "mutation" }), method(
|
|
58553
|
-
kind: "mutation",
|
|
58554
|
-
auth: "admin"
|
|
58555
|
-
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
58556
|
-
kind: "mutation",
|
|
58557
|
-
auth: "admin"
|
|
58558
|
-
}), method(ProfileRefInputSchema, _void(), {
|
|
58559
|
-
kind: "mutation",
|
|
58560
|
-
auth: "admin"
|
|
58561
|
-
}), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
|
|
58562
|
-
kind: "mutation",
|
|
58563
|
-
auth: "admin",
|
|
58564
|
-
timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
|
|
58565
|
-
}), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
58817
|
+
}), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
58566
58818
|
kind: "mutation",
|
|
58567
58819
|
auth: "admin"
|
|
58568
58820
|
}), method(object({}), array(LlmDefaultSchema)), method(object({
|
|
@@ -59625,20 +59877,7 @@ method(_void(), MeshStatusSchema), method(object({
|
|
|
59625
59877
|
/** Optional hostname override the host should advertise once joined. */
|
|
59626
59878
|
hostname: string().optional() }), object({
|
|
59627
59879
|
/** Authentication URL the operator should open in a browser. */
|
|
59628
|
-
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() }))
|
|
59629
|
-
/** Optional auth key — when provided, probes the key validity
|
|
59630
|
-
* against the provider's API. Omit when already joined to
|
|
59631
|
-
* just ping the daemon. */
|
|
59632
|
-
authKey: string().optional() }), object({
|
|
59633
|
-
ok: boolean(),
|
|
59634
|
-
/** Provider-side identifier resolved by the probe (tailnet
|
|
59635
|
-
* name for Tailscale, network id for ZeroTier, etc.). */
|
|
59636
|
-
tenant: string().optional(),
|
|
59637
|
-
/** Daemon binary version, when reachable. */
|
|
59638
|
-
daemonVersion: string().optional(),
|
|
59639
|
-
/** Human-readable error when `ok: false`. */
|
|
59640
|
-
error: string().optional()
|
|
59641
|
-
}), { kind: "mutation" });
|
|
59880
|
+
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() }));
|
|
59642
59881
|
/**
|
|
59643
59882
|
* A monotonic total between resets (D944). Energy uses `power-meter.energy`
|
|
59644
59883
|
* so dashboards keep one series; every other instantaneous reading lands here.
|
|
@@ -69845,61 +70084,6 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
69845
70084
|
});
|
|
69846
70085
|
method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
|
|
69847
70086
|
/**
|
|
69848
|
-
* `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
|
|
69849
|
-
*
|
|
69850
|
-
* One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
|
|
69851
|
-
* ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
|
|
69852
|
-
* Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
|
|
69853
|
-
* immutable after create.
|
|
69854
|
-
*/
|
|
69855
|
-
var ConfigSchemaPassthrough$1 = unknown();
|
|
69856
|
-
var OidcConnectionSchema = object({
|
|
69857
|
-
id: string().min(1),
|
|
69858
|
-
displayName: string(),
|
|
69859
|
-
icon: string(),
|
|
69860
|
-
issuerUrl: string(),
|
|
69861
|
-
clientId: string(),
|
|
69862
|
-
/** Client id + secret present — never the secret value. */
|
|
69863
|
-
configured: boolean(),
|
|
69864
|
-
/**
|
|
69865
|
-
* Values for the connection form schema. `clientSecret` is a keychain
|
|
69866
|
-
* {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
|
|
69867
|
-
*/
|
|
69868
|
-
formValues: record(string(), unknown()).optional()
|
|
69869
|
-
});
|
|
69870
|
-
/**
|
|
69871
|
-
* Patch one connection. Omitted or empty `clientSecret` keeps the stored
|
|
69872
|
-
* secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
|
|
69873
|
-
*/
|
|
69874
|
-
var OidcConnectionUpdateInputSchema = object({
|
|
69875
|
-
id: string().min(1),
|
|
69876
|
-
displayName: string().min(1),
|
|
69877
|
-
icon: string().optional(),
|
|
69878
|
-
issuerUrl: string().optional(),
|
|
69879
|
-
clientId: string().optional(),
|
|
69880
|
-
clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
|
|
69881
|
-
redirectUri: string().optional(),
|
|
69882
|
-
scopes: string().optional(),
|
|
69883
|
-
usernameClaim: _enum([
|
|
69884
|
-
"preferred_username",
|
|
69885
|
-
"email",
|
|
69886
|
-
"sub"
|
|
69887
|
-
]).optional()
|
|
69888
|
-
});
|
|
69889
|
-
method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
|
|
69890
|
-
id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
|
|
69891
|
-
displayName: string().min(1)
|
|
69892
|
-
}), object({ id: string() }), {
|
|
69893
|
-
kind: "mutation",
|
|
69894
|
-
auth: "admin"
|
|
69895
|
-
}), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
|
|
69896
|
-
kind: "mutation",
|
|
69897
|
-
auth: "admin"
|
|
69898
|
-
}), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
69899
|
-
kind: "mutation",
|
|
69900
|
-
auth: "admin"
|
|
69901
|
-
});
|
|
69902
|
-
/**
|
|
69903
70087
|
* Generic on-screen-display (video overlay) cap. Each camera exposes
|
|
69904
70088
|
* a firmware-defined list of overlays (channel name, timestamp,
|
|
69905
70089
|
* watermark, logo, …); the cap models them as a homogeneous array of
|
|
@@ -76981,21 +77165,6 @@ method(object({
|
|
|
76981
77165
|
*/
|
|
76982
77166
|
ttlSec: union([number$1().int().positive(), literal("never")]).optional()
|
|
76983
77167
|
}), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
|
|
76984
|
-
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
76985
|
-
providerId: string().min(1),
|
|
76986
|
-
displayName: string().min(1),
|
|
76987
|
-
configSchema: unknown(),
|
|
76988
|
-
nodeLocal: boolean(),
|
|
76989
|
-
shouldSaveDiskSpace: literal(true),
|
|
76990
|
-
minFreePercent: number$1().min(0).max(100)
|
|
76991
|
-
}), object({
|
|
76992
|
-
providerId: string().min(1),
|
|
76993
|
-
displayName: string().min(1),
|
|
76994
|
-
configSchema: unknown(),
|
|
76995
|
-
nodeLocal: boolean(),
|
|
76996
|
-
shouldSaveDiskSpace: literal(false),
|
|
76997
|
-
minFreePercent: literal(null)
|
|
76998
|
-
})]);
|
|
76999
77168
|
method(object({
|
|
77000
77169
|
location: StorageLocationRefSchema,
|
|
77001
77170
|
relativePath: string()
|
|
@@ -77057,42 +77226,7 @@ method(object({
|
|
|
77057
77226
|
}), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
|
|
77058
77227
|
kind: "mutation",
|
|
77059
77228
|
auth: "admin"
|
|
77060
|
-
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(
|
|
77061
|
-
createdAt: true,
|
|
77062
|
-
updatedAt: true
|
|
77063
|
-
}), StorageLocationSchema, {
|
|
77064
|
-
kind: "mutation",
|
|
77065
|
-
auth: "admin"
|
|
77066
|
-
}), method(object({
|
|
77067
|
-
id: string(),
|
|
77068
|
-
force: boolean().optional()
|
|
77069
|
-
}), _void(), {
|
|
77070
|
-
kind: "mutation",
|
|
77071
|
-
auth: "admin"
|
|
77072
|
-
}), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
|
|
77073
|
-
ok: boolean(),
|
|
77074
|
-
error: string().optional()
|
|
77075
|
-
}), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
|
|
77076
|
-
providerId: string(),
|
|
77077
|
-
/**
|
|
77078
|
-
* The location this config is an UNSAVED edit of, when there is one.
|
|
77079
|
-
*
|
|
77080
|
-
* `listLocations` replaces every declared secret with the redaction
|
|
77081
|
-
* sentinel, so the edit modal's form state holds the sentinel for any
|
|
77082
|
-
* credential the operator did not retype — and posting that here
|
|
77083
|
-
* without a way to resolve it makes the provider try to authenticate
|
|
77084
|
-
* as `__camstack_redacted__` and report the operator's own working
|
|
77085
|
-
* password as wrong. Given this id, the orchestrator restores each
|
|
77086
|
-
* sentinel from the stored config (same rule as `upsertLocation`)
|
|
77087
|
-
* before dispatching. Omitted by the "Add location" wizard, where
|
|
77088
|
-
* every value was typed just now and nothing is stored yet.
|
|
77089
|
-
*/
|
|
77090
|
-
locationId: string().optional(),
|
|
77091
|
-
config: record(string(), unknown())
|
|
77092
|
-
}), object({
|
|
77093
|
-
ok: boolean(),
|
|
77094
|
-
error: string().optional()
|
|
77095
|
-
}), { auth: "admin" });
|
|
77229
|
+
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
|
|
77096
77230
|
/** One table of the settings database. */
|
|
77097
77231
|
var StorageTableSchema = object({
|
|
77098
77232
|
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
@@ -78614,7 +78748,7 @@ var TerminalInstanceInfoSchema = object({
|
|
|
78614
78748
|
/**
|
|
78615
78749
|
* False while the node the instance is bound to is not in the cluster
|
|
78616
78750
|
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
78617
|
-
* operator re-points it
|
|
78751
|
+
* operator re-points it on the Terminal page.
|
|
78618
78752
|
*/
|
|
78619
78753
|
nodeInCluster: boolean(),
|
|
78620
78754
|
profileId: string(),
|
|
@@ -78652,41 +78786,7 @@ var TerminalOutputBatchSchema = object({
|
|
|
78652
78786
|
snapshot: string().optional(),
|
|
78653
78787
|
events: array(TerminalOutputEventSchema).readonly()
|
|
78654
78788
|
});
|
|
78655
|
-
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(
|
|
78656
|
-
targetNodeId: string().min(1),
|
|
78657
|
-
profileId: string().min(1),
|
|
78658
|
-
name: string().trim().min(1).max(160).optional(),
|
|
78659
|
-
executable: string().max(1024).optional(),
|
|
78660
|
-
args: array(string().max(2048)).max(64).optional(),
|
|
78661
|
-
cwd: string().max(1024).optional(),
|
|
78662
|
-
environment: array(string().max(4096)).max(64).optional(),
|
|
78663
|
-
profileSettings: ProfileSettingsBagSchema.optional()
|
|
78664
|
-
}), TerminalInstanceInfoSchema, {
|
|
78665
|
-
kind: "mutation",
|
|
78666
|
-
auth: "admin"
|
|
78667
|
-
}), method(object({
|
|
78668
|
-
instanceId: string().min(1),
|
|
78669
|
-
/** Re-point the instance to another node; the profile must exist there. */
|
|
78670
|
-
targetNodeId: string().min(1).max(256).optional(),
|
|
78671
|
-
name: string().trim().min(1).max(160).optional(),
|
|
78672
|
-
executable: string().max(1024).optional(),
|
|
78673
|
-
args: array(string().max(2048)).max(64).optional(),
|
|
78674
|
-
cwd: string().max(1024).optional(),
|
|
78675
|
-
environment: array(string().max(4096)).max(64).optional(),
|
|
78676
|
-
profileSettings: ProfileSettingsBagSchema.optional()
|
|
78677
|
-
}), TerminalInstanceInfoSchema, {
|
|
78678
|
-
kind: "mutation",
|
|
78679
|
-
auth: "admin"
|
|
78680
|
-
}), method(object({ instanceId: string().min(1) }), _void(), {
|
|
78681
|
-
kind: "mutation",
|
|
78682
|
-
auth: "admin"
|
|
78683
|
-
}), method(object({
|
|
78684
|
-
instanceId: string().min(1),
|
|
78685
|
-
enabled: boolean()
|
|
78686
|
-
}), TerminalInstanceInfoSchema, {
|
|
78687
|
-
kind: "mutation",
|
|
78688
|
-
auth: "admin"
|
|
78689
|
-
}), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
|
|
78789
|
+
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
|
|
78690
78790
|
stableId: string().min(1),
|
|
78691
78791
|
name: string().trim().min(1).max(160).optional()
|
|
78692
78792
|
}), TerminalInstanceInfoSchema, {
|
|
@@ -83506,6 +83606,20 @@ Object.freeze({
|
|
|
83506
83606
|
access: "view",
|
|
83507
83607
|
role: "admin"
|
|
83508
83608
|
},
|
|
83609
|
+
"addonSettings.getScopedSettings": {
|
|
83610
|
+
capName: "addon-settings",
|
|
83611
|
+
capScope: "system",
|
|
83612
|
+
addonId: null,
|
|
83613
|
+
access: "view",
|
|
83614
|
+
role: "admin"
|
|
83615
|
+
},
|
|
83616
|
+
"addonSettings.readScopedValues": {
|
|
83617
|
+
capName: "addon-settings",
|
|
83618
|
+
capScope: "system",
|
|
83619
|
+
addonId: null,
|
|
83620
|
+
access: "view",
|
|
83621
|
+
role: "admin"
|
|
83622
|
+
},
|
|
83509
83623
|
"addonSettings.unsetStoredSettingsKeys": {
|
|
83510
83624
|
capName: "addon-settings",
|
|
83511
83625
|
capScope: "system",
|
|
@@ -83527,6 +83641,13 @@ Object.freeze({
|
|
|
83527
83641
|
access: "create",
|
|
83528
83642
|
role: "admin"
|
|
83529
83643
|
},
|
|
83644
|
+
"addonSettings.updateScopedSettings": {
|
|
83645
|
+
capName: "addon-settings",
|
|
83646
|
+
capScope: "system",
|
|
83647
|
+
addonId: null,
|
|
83648
|
+
access: "create",
|
|
83649
|
+
role: "admin"
|
|
83650
|
+
},
|
|
83530
83651
|
"addonWidgets.listWidgets": {
|
|
83531
83652
|
capName: "addon-widgets",
|
|
83532
83653
|
capScope: "system",
|
|
@@ -86978,13 +87099,6 @@ Object.freeze({
|
|
|
86978
87099
|
access: "delete",
|
|
86979
87100
|
role: "admin"
|
|
86980
87101
|
},
|
|
86981
|
-
"llm.deleteProfile": {
|
|
86982
|
-
capName: "llm",
|
|
86983
|
-
capScope: "system",
|
|
86984
|
-
addonId: null,
|
|
86985
|
-
access: "delete",
|
|
86986
|
-
role: "admin"
|
|
86987
|
-
},
|
|
86988
87102
|
"llm.generate": {
|
|
86989
87103
|
capName: "llm",
|
|
86990
87104
|
capScope: "system",
|
|
@@ -87048,20 +87162,6 @@ Object.freeze({
|
|
|
87048
87162
|
access: "view",
|
|
87049
87163
|
role: "admin"
|
|
87050
87164
|
},
|
|
87051
|
-
"llm.listProfileKinds": {
|
|
87052
|
-
capName: "llm",
|
|
87053
|
-
capScope: "system",
|
|
87054
|
-
addonId: null,
|
|
87055
|
-
access: "view",
|
|
87056
|
-
role: "admin"
|
|
87057
|
-
},
|
|
87058
|
-
"llm.listProfiles": {
|
|
87059
|
-
capName: "llm",
|
|
87060
|
-
capScope: "system",
|
|
87061
|
-
addonId: null,
|
|
87062
|
-
access: "view",
|
|
87063
|
-
role: "admin"
|
|
87064
|
-
},
|
|
87065
87165
|
"llm.listRuntimeNodes": {
|
|
87066
87166
|
capName: "llm",
|
|
87067
87167
|
capScope: "system",
|
|
@@ -87069,13 +87169,6 @@ Object.freeze({
|
|
|
87069
87169
|
access: "view",
|
|
87070
87170
|
role: "admin"
|
|
87071
87171
|
},
|
|
87072
|
-
"llm.migrateRawSecrets": {
|
|
87073
|
-
capName: "llm",
|
|
87074
|
-
capScope: "system",
|
|
87075
|
-
addonId: null,
|
|
87076
|
-
access: "create",
|
|
87077
|
-
role: "admin"
|
|
87078
|
-
},
|
|
87079
87172
|
"llm.resolveModelRef": {
|
|
87080
87173
|
capName: "llm",
|
|
87081
87174
|
capScope: "system",
|
|
@@ -87104,20 +87197,6 @@ Object.freeze({
|
|
|
87104
87197
|
access: "create",
|
|
87105
87198
|
role: "admin"
|
|
87106
87199
|
},
|
|
87107
|
-
"llm.testProfile": {
|
|
87108
|
-
capName: "llm",
|
|
87109
|
-
capScope: "system",
|
|
87110
|
-
addonId: null,
|
|
87111
|
-
access: "create",
|
|
87112
|
-
role: "admin"
|
|
87113
|
-
},
|
|
87114
|
-
"llm.upsertProfile": {
|
|
87115
|
-
capName: "llm",
|
|
87116
|
-
capScope: "system",
|
|
87117
|
-
addonId: null,
|
|
87118
|
-
access: "create",
|
|
87119
|
-
role: "admin"
|
|
87120
|
-
},
|
|
87121
87200
|
"llmRuntime.appleAvailability": {
|
|
87122
87201
|
capName: "llm-runtime",
|
|
87123
87202
|
capScope: "system",
|
|
@@ -87622,13 +87701,6 @@ Object.freeze({
|
|
|
87622
87701
|
access: "create",
|
|
87623
87702
|
role: "admin"
|
|
87624
87703
|
},
|
|
87625
|
-
"meshNetwork.testConnection": {
|
|
87626
|
-
capName: "mesh-network",
|
|
87627
|
-
capScope: "system",
|
|
87628
|
-
addonId: null,
|
|
87629
|
-
access: "create",
|
|
87630
|
-
role: "admin"
|
|
87631
|
-
},
|
|
87632
87704
|
"meter.getStatus": {
|
|
87633
87705
|
capName: "meter",
|
|
87634
87706
|
capScope: "device",
|
|
@@ -88287,13 +88359,6 @@ Object.freeze({
|
|
|
88287
88359
|
access: "create",
|
|
88288
88360
|
role: "admin"
|
|
88289
88361
|
},
|
|
88290
|
-
"notificationOutput.deleteTarget": {
|
|
88291
|
-
capName: "notification-output",
|
|
88292
|
-
capScope: "system",
|
|
88293
|
-
addonId: null,
|
|
88294
|
-
access: "delete",
|
|
88295
|
-
role: "admin"
|
|
88296
|
-
},
|
|
88297
88362
|
"notificationOutput.discoverTargets": {
|
|
88298
88363
|
capName: "notification-output",
|
|
88299
88364
|
capScope: "system",
|
|
@@ -88301,13 +88366,6 @@ Object.freeze({
|
|
|
88301
88366
|
access: "view",
|
|
88302
88367
|
role: "admin"
|
|
88303
88368
|
},
|
|
88304
|
-
"notificationOutput.getTarget": {
|
|
88305
|
-
capName: "notification-output",
|
|
88306
|
-
capScope: "system",
|
|
88307
|
-
addonId: null,
|
|
88308
|
-
access: "view",
|
|
88309
|
-
role: "admin"
|
|
88310
|
-
},
|
|
88311
88369
|
"notificationOutput.listTargetKinds": {
|
|
88312
88370
|
capName: "notification-output",
|
|
88313
88371
|
capScope: "system",
|
|
@@ -88315,13 +88373,6 @@ Object.freeze({
|
|
|
88315
88373
|
access: "view",
|
|
88316
88374
|
role: "admin"
|
|
88317
88375
|
},
|
|
88318
|
-
"notificationOutput.listTargets": {
|
|
88319
|
-
capName: "notification-output",
|
|
88320
|
-
capScope: "system",
|
|
88321
|
-
addonId: null,
|
|
88322
|
-
access: "view",
|
|
88323
|
-
role: "admin"
|
|
88324
|
-
},
|
|
88325
88376
|
"notificationOutput.probeTargetConfig": {
|
|
88326
88377
|
capName: "notification-output",
|
|
88327
88378
|
capScope: "system",
|
|
@@ -88336,13 +88387,6 @@ Object.freeze({
|
|
|
88336
88387
|
access: "create",
|
|
88337
88388
|
role: "admin"
|
|
88338
88389
|
},
|
|
88339
|
-
"notificationOutput.setTargetEnabled": {
|
|
88340
|
-
capName: "notification-output",
|
|
88341
|
-
capScope: "system",
|
|
88342
|
-
addonId: null,
|
|
88343
|
-
access: "create",
|
|
88344
|
-
role: "admin"
|
|
88345
|
-
},
|
|
88346
88390
|
"notificationOutput.testTarget": {
|
|
88347
88391
|
capName: "notification-output",
|
|
88348
88392
|
capScope: "system",
|
|
@@ -88350,13 +88394,6 @@ Object.freeze({
|
|
|
88350
88394
|
access: "create",
|
|
88351
88395
|
role: "admin"
|
|
88352
88396
|
},
|
|
88353
|
-
"notificationOutput.upsertTarget": {
|
|
88354
|
-
capName: "notification-output",
|
|
88355
|
-
capScope: "system",
|
|
88356
|
-
addonId: null,
|
|
88357
|
-
access: "create",
|
|
88358
|
-
role: "admin"
|
|
88359
|
-
},
|
|
88360
88397
|
"notificationRules.activateAlarmProfile": {
|
|
88361
88398
|
capName: "notification-rules",
|
|
88362
88399
|
capScope: "system",
|
|
@@ -88616,41 +88653,6 @@ Object.freeze({
|
|
|
88616
88653
|
access: "view",
|
|
88617
88654
|
role: "admin"
|
|
88618
88655
|
},
|
|
88619
|
-
"oidcConnections.connectionFormSchema": {
|
|
88620
|
-
capName: "oidc-connections",
|
|
88621
|
-
capScope: "system",
|
|
88622
|
-
addonId: null,
|
|
88623
|
-
access: "view",
|
|
88624
|
-
role: "admin"
|
|
88625
|
-
},
|
|
88626
|
-
"oidcConnections.createConnection": {
|
|
88627
|
-
capName: "oidc-connections",
|
|
88628
|
-
capScope: "system",
|
|
88629
|
-
addonId: null,
|
|
88630
|
-
access: "create",
|
|
88631
|
-
role: "admin"
|
|
88632
|
-
},
|
|
88633
|
-
"oidcConnections.deleteConnection": {
|
|
88634
|
-
capName: "oidc-connections",
|
|
88635
|
-
capScope: "system",
|
|
88636
|
-
addonId: null,
|
|
88637
|
-
access: "delete",
|
|
88638
|
-
role: "admin"
|
|
88639
|
-
},
|
|
88640
|
-
"oidcConnections.listConnections": {
|
|
88641
|
-
capName: "oidc-connections",
|
|
88642
|
-
capScope: "system",
|
|
88643
|
-
addonId: null,
|
|
88644
|
-
access: "view",
|
|
88645
|
-
role: "admin"
|
|
88646
|
-
},
|
|
88647
|
-
"oidcConnections.updateConnection": {
|
|
88648
|
-
capName: "oidc-connections",
|
|
88649
|
-
capScope: "system",
|
|
88650
|
-
addonId: null,
|
|
88651
|
-
access: "create",
|
|
88652
|
-
role: "admin"
|
|
88653
|
-
},
|
|
88654
88656
|
"osd.getStatus": {
|
|
88655
88657
|
capName: "osd",
|
|
88656
88658
|
capScope: "device",
|
|
@@ -91682,13 +91684,6 @@ Object.freeze({
|
|
|
91682
91684
|
access: "delete",
|
|
91683
91685
|
role: "admin"
|
|
91684
91686
|
},
|
|
91685
|
-
"storage.deleteLocation": {
|
|
91686
|
-
capName: "storage",
|
|
91687
|
-
capScope: "system",
|
|
91688
|
-
addonId: null,
|
|
91689
|
-
access: "delete",
|
|
91690
|
-
role: "admin"
|
|
91691
|
-
},
|
|
91692
91687
|
"storage.endDownload": {
|
|
91693
91688
|
capName: "storage",
|
|
91694
91689
|
capScope: "system",
|
|
@@ -91745,13 +91740,6 @@ Object.freeze({
|
|
|
91745
91740
|
access: "view",
|
|
91746
91741
|
role: "admin"
|
|
91747
91742
|
},
|
|
91748
|
-
"storage.listProviders": {
|
|
91749
|
-
capName: "storage",
|
|
91750
|
-
capScope: "system",
|
|
91751
|
-
addonId: null,
|
|
91752
|
-
access: "view",
|
|
91753
|
-
role: "admin"
|
|
91754
|
-
},
|
|
91755
91743
|
"storage.read": {
|
|
91756
91744
|
capName: "storage",
|
|
91757
91745
|
capScope: "system",
|
|
@@ -91773,27 +91761,6 @@ Object.freeze({
|
|
|
91773
91761
|
access: "view",
|
|
91774
91762
|
role: "admin"
|
|
91775
91763
|
},
|
|
91776
|
-
"storage.testConfig": {
|
|
91777
|
-
capName: "storage",
|
|
91778
|
-
capScope: "system",
|
|
91779
|
-
addonId: null,
|
|
91780
|
-
access: "view",
|
|
91781
|
-
role: "admin"
|
|
91782
|
-
},
|
|
91783
|
-
"storage.testLocation": {
|
|
91784
|
-
capName: "storage",
|
|
91785
|
-
capScope: "system",
|
|
91786
|
-
addonId: null,
|
|
91787
|
-
access: "view",
|
|
91788
|
-
role: "admin"
|
|
91789
|
-
},
|
|
91790
|
-
"storage.upsertLocation": {
|
|
91791
|
-
capName: "storage",
|
|
91792
|
-
capScope: "system",
|
|
91793
|
-
addonId: null,
|
|
91794
|
-
access: "create",
|
|
91795
|
-
role: "admin"
|
|
91796
|
-
},
|
|
91797
91764
|
"storage.write": {
|
|
91798
91765
|
capName: "storage",
|
|
91799
91766
|
capScope: "system",
|
|
@@ -92571,27 +92538,6 @@ Object.freeze({
|
|
|
92571
92538
|
access: "create",
|
|
92572
92539
|
role: "admin"
|
|
92573
92540
|
},
|
|
92574
|
-
"terminalSession.createInstance": {
|
|
92575
|
-
capName: "terminal-session",
|
|
92576
|
-
capScope: "system",
|
|
92577
|
-
addonId: null,
|
|
92578
|
-
access: "create",
|
|
92579
|
-
role: "admin"
|
|
92580
|
-
},
|
|
92581
|
-
"terminalSession.deleteInstance": {
|
|
92582
|
-
capName: "terminal-session",
|
|
92583
|
-
capScope: "system",
|
|
92584
|
-
addonId: null,
|
|
92585
|
-
access: "delete",
|
|
92586
|
-
role: "admin"
|
|
92587
|
-
},
|
|
92588
|
-
"terminalSession.listInstances": {
|
|
92589
|
-
capName: "terminal-session",
|
|
92590
|
-
capScope: "system",
|
|
92591
|
-
addonId: null,
|
|
92592
|
-
access: "view",
|
|
92593
|
-
role: "admin"
|
|
92594
|
-
},
|
|
92595
92541
|
"terminalSession.listLegacyCameras": {
|
|
92596
92542
|
capName: "terminal-session",
|
|
92597
92543
|
capScope: "system",
|
|
@@ -92634,20 +92580,6 @@ Object.freeze({
|
|
|
92634
92580
|
access: "create",
|
|
92635
92581
|
role: "admin"
|
|
92636
92582
|
},
|
|
92637
|
-
"terminalSession.setInstanceEnabled": {
|
|
92638
|
-
capName: "terminal-session",
|
|
92639
|
-
capScope: "system",
|
|
92640
|
-
addonId: null,
|
|
92641
|
-
access: "create",
|
|
92642
|
-
role: "admin"
|
|
92643
|
-
},
|
|
92644
|
-
"terminalSession.updateInstance": {
|
|
92645
|
-
capName: "terminal-session",
|
|
92646
|
-
capScope: "system",
|
|
92647
|
-
addonId: null,
|
|
92648
|
-
access: "create",
|
|
92649
|
-
role: "admin"
|
|
92650
|
-
},
|
|
92651
92583
|
"terminalSession.writeInput": {
|
|
92652
92584
|
capName: "terminal-session",
|
|
92653
92585
|
capScope: "system",
|
|
@@ -144400,6 +144332,7 @@ var EchoSessionRegistry = class {
|
|
|
144400
144332
|
#resolve = null;
|
|
144401
144333
|
#dnd = /* @__PURE__ */ new Map();
|
|
144402
144334
|
#volumes = /* @__PURE__ */ new Map();
|
|
144335
|
+
#groupPlayers = /* @__PURE__ */ new Map();
|
|
144403
144336
|
setResolver(fn) {
|
|
144404
144337
|
this.#resolve = fn;
|
|
144405
144338
|
}
|
|
@@ -144438,10 +144371,35 @@ var EchoSessionRegistry = class {
|
|
|
144438
144371
|
});
|
|
144439
144372
|
return pending;
|
|
144440
144373
|
}
|
|
144374
|
+
/**
|
|
144375
|
+
* The player state of a speaker group, shared by its members within one poll cycle.
|
|
144376
|
+
* A failed read is not kept for the next member.
|
|
144377
|
+
*/
|
|
144378
|
+
groupPlayer(brokerId, group, now, deviceId) {
|
|
144379
|
+
const key = `${brokerId}\u0000${group.id}`;
|
|
144380
|
+
const shared = this.#groupPlayers.get(key);
|
|
144381
|
+
if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
|
|
144382
|
+
const session = this.get(brokerId);
|
|
144383
|
+
if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
|
|
144384
|
+
const pending = session.playerState(group.id, group.deviceType, deviceId);
|
|
144385
|
+
this.#groupPlayers.set(key, {
|
|
144386
|
+
at: now,
|
|
144387
|
+
pending
|
|
144388
|
+
});
|
|
144389
|
+
pending.catch(() => {
|
|
144390
|
+
if (this.#groupPlayers.get(key)?.pending === pending) this.#groupPlayers.delete(key);
|
|
144391
|
+
});
|
|
144392
|
+
return pending;
|
|
144393
|
+
}
|
|
144441
144394
|
/** Forget the shared volume list: a list fetched before a set must not answer a poll after it. */
|
|
144442
144395
|
invalidateVolumes(brokerId) {
|
|
144443
144396
|
this.#volumes.delete(brokerId);
|
|
144444
144397
|
}
|
|
144398
|
+
/** Forget the shared group players: a transport command or a push makes a cached group answer stale. */
|
|
144399
|
+
invalidateGroupPlayers(brokerId) {
|
|
144400
|
+
const prefix = `${brokerId}\u0000`;
|
|
144401
|
+
for (const key of [...this.#groupPlayers.keys()]) if (key.startsWith(prefix)) this.#groupPlayers.delete(key);
|
|
144402
|
+
}
|
|
144445
144403
|
/** Forget the shared Do Not Disturb list, for the same reason. */
|
|
144446
144404
|
invalidateDnd(brokerId) {
|
|
144447
144405
|
this.#dnd.delete(brokerId);
|
|
@@ -144457,6 +144415,7 @@ var EchoSessionRegistry = class {
|
|
|
144457
144415
|
}
|
|
144458
144416
|
event(brokerId, serial) {
|
|
144459
144417
|
this.invalidateVolumes(brokerId);
|
|
144418
|
+
this.invalidateGroupPlayers(brokerId);
|
|
144460
144419
|
this.invalidateDnd(brokerId);
|
|
144461
144420
|
for (const listener of [...this.#pushListeners.get(`${brokerId}\u0000${serial}`) ?? []]) listener();
|
|
144462
144421
|
}
|
|
@@ -144475,6 +144434,7 @@ var EchoSessionRegistry = class {
|
|
|
144475
144434
|
this.#resolve = null;
|
|
144476
144435
|
this.#dnd.clear();
|
|
144477
144436
|
this.#volumes.clear();
|
|
144437
|
+
this.#groupPlayers.clear();
|
|
144478
144438
|
}
|
|
144479
144439
|
};
|
|
144480
144440
|
function subscribe(map, key, listener) {
|
|
@@ -144517,6 +144477,11 @@ function mapEchoPlaybackState(value) {
|
|
|
144517
144477
|
default: return "idle";
|
|
144518
144478
|
}
|
|
144519
144479
|
}
|
|
144480
|
+
/** Whether a player answer carries any playback: an active state, a text or artwork. */
|
|
144481
|
+
function hasPlayback(info) {
|
|
144482
|
+
if (mapEchoPlaybackState(info.state) !== "idle") return true;
|
|
144483
|
+
return nonEmpty(info.infoText?.title) !== null || nonEmpty(info.mainArt?.url) !== null;
|
|
144484
|
+
}
|
|
144520
144485
|
function percent(value) {
|
|
144521
144486
|
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
144522
144487
|
return Math.round(Math.max(0, Math.min(100, value)));
|
|
@@ -144606,7 +144571,9 @@ var EchoDeviceSchema = object({
|
|
|
144606
144571
|
dnd: boolean(),
|
|
144607
144572
|
deviceType: string(),
|
|
144608
144573
|
/** The room the account files the device under, when Amazon names one. */
|
|
144609
|
-
room: string().optional()
|
|
144574
|
+
room: string().optional(),
|
|
144575
|
+
/** Serials of the speaker groups this device belongs to (Amazon's `parentClusters`); absent when none. */
|
|
144576
|
+
parentClusters: array(string()).optional()
|
|
144610
144577
|
});
|
|
144611
144578
|
var LoginStatusSchema = _enum(["ok", "failed"]);
|
|
144612
144579
|
var SessionStatusSchema = object({
|
|
@@ -144835,6 +144802,8 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144835
144802
|
unsubscribers = [];
|
|
144836
144803
|
polling = false;
|
|
144837
144804
|
failureLogged = false;
|
|
144805
|
+
/** The name of the speaker group whose playback this Echo shows; null when it plays on its own. */
|
|
144806
|
+
viaGroup = null;
|
|
144838
144807
|
volumeFailureLogged = false;
|
|
144839
144808
|
health = "ok";
|
|
144840
144809
|
/** When this device last wrote its volume / Do Not Disturb optimistically; a read that began earlier must not overwrite it. */
|
|
@@ -144962,9 +144931,11 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144962
144931
|
}
|
|
144963
144932
|
const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
|
|
144964
144933
|
try {
|
|
144965
|
-
const
|
|
144934
|
+
const own = await session.playerState(this.serial, deviceType, this.id);
|
|
144935
|
+
const now = Date.now();
|
|
144936
|
+
const fromPlayer = mapPlayerInfoToStatus(await this.withGroupPlayback(own, listed, session), {
|
|
144966
144937
|
online: true,
|
|
144967
|
-
now
|
|
144938
|
+
now
|
|
144968
144939
|
});
|
|
144969
144940
|
const read = await this.completeVolume(fromPlayer);
|
|
144970
144941
|
const kept = this.keepNewerVolume(read, readStartedAt);
|
|
@@ -144979,6 +144950,49 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144979
144950
|
await this.readDoNotDisturb(readStartedAt);
|
|
144980
144951
|
}
|
|
144981
144952
|
/**
|
|
144953
|
+
* A member of a playing speaker group is answered empty by its own player read while
|
|
144954
|
+
* the group plays. When the member's answer carries no playback, serve the first
|
|
144955
|
+
* parent group that is playing or paused (its track and artwork; the volume stays the
|
|
144956
|
+
* member's own). A failed group read leaves the member's own answer.
|
|
144957
|
+
*/
|
|
144958
|
+
async withGroupPlayback(own, listed, session) {
|
|
144959
|
+
const parents = listed?.parentClusters ?? [];
|
|
144960
|
+
if (hasPlayback(own) || parents.length === 0) {
|
|
144961
|
+
this.noteViaGroup(null);
|
|
144962
|
+
return own;
|
|
144963
|
+
}
|
|
144964
|
+
for (const parentId of parents) {
|
|
144965
|
+
const group = session.devices.find((device) => device.id === parentId);
|
|
144966
|
+
if (group === void 0 || !group.online) continue;
|
|
144967
|
+
try {
|
|
144968
|
+
const groupInfo = await alexaEchoSessions.groupPlayer(this.brokerId, group, Date.now(), this.id);
|
|
144969
|
+
if (groupInfo.state === "PLAYING" || groupInfo.state === "PAUSED" || groupInfo.state === "BUFFERING") {
|
|
144970
|
+
this.noteViaGroup(group.name);
|
|
144971
|
+
return {
|
|
144972
|
+
...groupInfo,
|
|
144973
|
+
volume: own.volume
|
|
144974
|
+
};
|
|
144975
|
+
}
|
|
144976
|
+
} catch (err) {
|
|
144977
|
+
this.ctx.logger.debug("alexa echo: the group player could not be read", {
|
|
144978
|
+
tags: { deviceId: this.id },
|
|
144979
|
+
meta: {
|
|
144980
|
+
group: group.name,
|
|
144981
|
+
error: err instanceof Error ? err.message : String(err)
|
|
144982
|
+
}
|
|
144983
|
+
});
|
|
144984
|
+
}
|
|
144985
|
+
}
|
|
144986
|
+
this.noteViaGroup(null);
|
|
144987
|
+
return own;
|
|
144988
|
+
}
|
|
144989
|
+
/** Log each transition into and out of "playing via a group" once. */
|
|
144990
|
+
noteViaGroup(name) {
|
|
144991
|
+
if (name === this.viaGroup) return;
|
|
144992
|
+
this.viaGroup = name;
|
|
144993
|
+
this.ctx.logger.info(name === null ? "alexa echo: no longer playing via a group" : `alexa echo: playing via group ${name}`, { tags: { deviceId: this.id } });
|
|
144994
|
+
}
|
|
144995
|
+
/**
|
|
144982
144996
|
* An idle Echo's player read carries no volume. Ask the account's volume list
|
|
144983
144997
|
* (shared by every Echo of the account in this cycle); a failed read leaves null.
|
|
144984
144998
|
*/
|
|
@@ -145050,6 +145064,7 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
145050
145064
|
}
|
|
145051
145065
|
async command(command) {
|
|
145052
145066
|
await this.session().npCommand(this.serial, command, this.id);
|
|
145067
|
+
alexaEchoSessions.invalidateGroupPlayers(this.brokerId);
|
|
145053
145068
|
this.rereadSoon();
|
|
145054
145069
|
}
|
|
145055
145070
|
/**
|
|
@@ -145521,6 +145536,33 @@ function quietLogger$1() {
|
|
|
145521
145536
|
};
|
|
145522
145537
|
return logger;
|
|
145523
145538
|
}
|
|
145539
|
+
/**
|
|
145540
|
+
* The account a target speaks through: the one it names; else the only one there is; else the one
|
|
145541
|
+
* whose Echoes include the target's Echo.
|
|
145542
|
+
*/
|
|
145543
|
+
function pickBroker(brokers, config) {
|
|
145544
|
+
const named = readString(config, "brokerId");
|
|
145545
|
+
if (named.length > 0) return brokers.find((broker) => broker.id === named);
|
|
145546
|
+
if (brokers.length === 1) return brokers[0];
|
|
145547
|
+
const echoId = readString(config, "echoId");
|
|
145548
|
+
const owners = brokers.filter((broker) => broker.devices.some((device) => device.id === echoId));
|
|
145549
|
+
return owners.length === 1 ? owners[0] : void 0;
|
|
145550
|
+
}
|
|
145551
|
+
/**
|
|
145552
|
+
* The save-time rule of an Echo target: an Echo and an account must be picked, and the account is
|
|
145553
|
+
* pinned the moment it is known, so a second account added later cannot silently change where an
|
|
145554
|
+
* existing target speaks. Throws the reason; returns the keys to keep with the write.
|
|
145555
|
+
*/
|
|
145556
|
+
function validateEchoTarget(brokers, config) {
|
|
145557
|
+
if (readString(config, "echoId").length === 0) throw new Error("Pick the Echo that should speak");
|
|
145558
|
+
const owner = pickBroker(brokers, config);
|
|
145559
|
+
if (owner === void 0) {
|
|
145560
|
+
const named = readString(config, "brokerId");
|
|
145561
|
+
if (named.length > 0) throw new Error(`The Amazon account ${named} is not available`);
|
|
145562
|
+
throw new Error(brokers.length === 0 ? "Add an Amazon account first: an Echo speaks through one" : "Pick the Amazon account this Echo belongs to");
|
|
145563
|
+
}
|
|
145564
|
+
return readString(config, "brokerId").length === 0 ? { brokerId: owner.id } : {};
|
|
145565
|
+
}
|
|
145524
145566
|
function readString(config, key) {
|
|
145525
145567
|
const value = config[key];
|
|
145526
145568
|
return typeof value === "string" ? value : "";
|
|
@@ -145550,7 +145592,7 @@ function echoOptionLabel(device) {
|
|
|
145550
145592
|
/**
|
|
145551
145593
|
* The two pickers are not `required`: a required select shows its first option
|
|
145552
145594
|
* while the form still holds no value, so a save would drop the choice. Left
|
|
145553
|
-
* optional they open on "Select", and `
|
|
145595
|
+
* optional they open on "Select", and `validateEchoTarget` refuses an empty pick on save.
|
|
145554
145596
|
*
|
|
145555
145597
|
* The account is a broker pick (existing, or created in place); the Echo list
|
|
145556
145598
|
* is fetched from that account, so a new Echo shows up without a deploy.
|
|
@@ -145642,18 +145684,8 @@ function createAlexaNotificationProvider(ports) {
|
|
|
145642
145684
|
function record(row) {
|
|
145643
145685
|
deliveries = [...deliveries, row];
|
|
145644
145686
|
}
|
|
145645
|
-
/**
|
|
145646
|
-
* The account a target speaks through: the one it names; else the only one
|
|
145647
|
-
* there is; else the one whose Echoes include the target's Echo.
|
|
145648
|
-
*/
|
|
145649
145687
|
function brokerFor(config) {
|
|
145650
|
-
|
|
145651
|
-
const named = readString(config, "brokerId");
|
|
145652
|
-
if (named.length > 0) return all.find((broker) => broker.id === named);
|
|
145653
|
-
if (all.length === 1) return all[0];
|
|
145654
|
-
const echoId = readString(config, "echoId");
|
|
145655
|
-
const owners = all.filter((broker) => broker.devices.some((device) => device.id === echoId));
|
|
145656
|
-
return owners.length === 1 ? owners[0] : void 0;
|
|
145688
|
+
return pickBroker(ports.brokers(), config);
|
|
145657
145689
|
}
|
|
145658
145690
|
async function deliver(target, notification) {
|
|
145659
145691
|
const speech = speechFromNotification(notification);
|
|
@@ -145855,14 +145887,6 @@ function createAlexaNotificationProvider(ports) {
|
|
|
145855
145887
|
async listTargetKinds() {
|
|
145856
145888
|
return [alexaTargetKind()];
|
|
145857
145889
|
},
|
|
145858
|
-
async listTargets() {
|
|
145859
|
-
return [...await ports.loadTargets()];
|
|
145860
|
-
},
|
|
145861
|
-
async getTarget(input) {
|
|
145862
|
-
const found = (await ports.loadTargets()).find((target) => target.id === input.targetId);
|
|
145863
|
-
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
145864
|
-
return found;
|
|
145865
|
-
},
|
|
145866
145890
|
async discoverTargets(input) {
|
|
145867
145891
|
for (const broker of ports.brokers()) if (!broker.ready) ports.logger.warn(`alexa echo discovery: no Echo listed for '${broker.name}', the Amazon account is not signed in for this broker`, { tags: { brokerId: broker.id } });
|
|
145868
145892
|
const named = readString(input.config ?? {}, "brokerId");
|
|
@@ -145923,33 +145947,6 @@ function createAlexaNotificationProvider(ports) {
|
|
|
145923
145947
|
format: "text",
|
|
145924
145948
|
priority: 3
|
|
145925
145949
|
});
|
|
145926
|
-
},
|
|
145927
|
-
async upsertTarget(input) {
|
|
145928
|
-
const owner = brokerFor(input.target.config);
|
|
145929
|
-
if (readString(input.target.config, "echoId").length === 0) throw new Error("Pick the Echo that should speak");
|
|
145930
|
-
if (owner === void 0) throw new Error("Pick the Amazon account this Echo belongs to");
|
|
145931
|
-
const pinned = readString(input.target.config, "brokerId").length === 0 ? {
|
|
145932
|
-
...input.target.config,
|
|
145933
|
-
brokerId: owner.id
|
|
145934
|
-
} : input.target.config;
|
|
145935
|
-
const target = {
|
|
145936
|
-
...input.target,
|
|
145937
|
-
config: pinned,
|
|
145938
|
-
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
145939
|
-
kind: ALEXA_SPEAK_KIND
|
|
145940
|
-
};
|
|
145941
|
-
await ports.putTarget(target);
|
|
145942
|
-
return target;
|
|
145943
|
-
},
|
|
145944
|
-
async deleteTarget(input) {
|
|
145945
|
-
await ports.removeTarget(input.targetId);
|
|
145946
|
-
},
|
|
145947
|
-
async setTargetEnabled(input) {
|
|
145948
|
-
const found = (await ports.loadTargets()).find((row) => row.id === input.targetId);
|
|
145949
|
-
if (found !== void 0) await ports.putTarget({
|
|
145950
|
-
...found,
|
|
145951
|
-
enabled: input.enabled
|
|
145952
|
-
});
|
|
145953
145950
|
}
|
|
145954
145951
|
};
|
|
145955
145952
|
}
|
|
@@ -146040,6 +146037,14 @@ function echoTargetProbe(speak) {
|
|
|
146040
146037
|
});
|
|
146041
146038
|
};
|
|
146042
146039
|
}
|
|
146040
|
+
/**
|
|
146041
|
+
* The Echo instance's save-time rule over the accounts now (D1013): the check the removed
|
|
146042
|
+
* `upsertTarget` made, on the instance door. Speech not started yet has no account to name, so the
|
|
146043
|
+
* write is refused with the reason, never accepted blind.
|
|
146044
|
+
*/
|
|
146045
|
+
function echoTargetValidator(brokers) {
|
|
146046
|
+
return ({ config }) => validateEchoTarget(brokers() ?? [], config);
|
|
146047
|
+
}
|
|
146043
146048
|
process.platform, process.env["SystemRoot"];
|
|
146044
146049
|
/**
|
|
146045
146050
|
* Files only their owner may read: the one place a secret-bearing file is
|
|
@@ -146880,8 +146885,6 @@ var AlexaSpeakController = class {
|
|
|
146880
146885
|
now: this.now,
|
|
146881
146886
|
logger: host.logger,
|
|
146882
146887
|
loadTargets: () => host.readTargets(),
|
|
146883
|
-
putTarget: (target) => host.putTarget(target),
|
|
146884
|
-
removeTarget: (targetId) => host.removeTarget(targetId),
|
|
146885
146888
|
speak: (command) => this.utter(command.brokerId, command.mode, command.targets, command.text)
|
|
146886
146889
|
});
|
|
146887
146890
|
}
|
|
@@ -146911,6 +146914,7 @@ var AlexaSpeakController = class {
|
|
|
146911
146914
|
echoFor(brokerId) {
|
|
146912
146915
|
return this.echoes.get(brokerId) ?? null;
|
|
146913
146916
|
}
|
|
146917
|
+
/** The accounts right now, as the provider and the save-time rule see them. */
|
|
146914
146918
|
brokerViews() {
|
|
146915
146919
|
return this.host.listBrokers().map((ref) => {
|
|
146916
146920
|
const echo = this.echoes.get(ref.id);
|
|
@@ -147458,8 +147462,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
147458
147462
|
const speak = new AlexaSpeakController({
|
|
147459
147463
|
logger: this.ctx.logger.child("speak"),
|
|
147460
147464
|
readTargets: () => echoStore.list(),
|
|
147461
|
-
putTarget: (target) => echoStore.upsert(target),
|
|
147462
|
-
removeTarget: (targetId) => echoStore.delete(targetId),
|
|
147463
147465
|
listBrokers: () => this.activeBrokers.map((entry) => ({
|
|
147464
147466
|
id: entry.id,
|
|
147465
147467
|
name: entry.name,
|
|
@@ -147561,6 +147563,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
147561
147563
|
kinds: alexaNotifyRecordKinds,
|
|
147562
147564
|
mintId: () => (0, node_crypto.randomUUID)(),
|
|
147563
147565
|
probe: echoTargetProbe(() => this.speak),
|
|
147566
|
+
validate: echoTargetValidator(() => this.speak?.brokerViews() ?? null),
|
|
147564
147567
|
canProbe: () => true
|
|
147565
147568
|
})
|
|
147566
147569
|
], {
|