@camstack/addon-import-alexa 0.2.182 → 0.2.183
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 +1105 -343
- package/dist/addon.mjs +1105 -343
- package/package.json +6 -2
package/dist/addon.js
CHANGED
|
@@ -6044,7 +6044,7 @@ function number(params) {
|
|
|
6044
6044
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6045
6045
|
}
|
|
6046
6046
|
//#endregion
|
|
6047
|
-
//#region ../types/dist/sleep-
|
|
6047
|
+
//#region ../types/dist/sleep-BkzQdpzF.mjs
|
|
6048
6048
|
/**
|
|
6049
6049
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6050
6050
|
* window to float samples (D455).
|
|
@@ -6192,9 +6192,16 @@ function isSecretMarker(value) {
|
|
|
6192
6192
|
const ref = value["$secret"];
|
|
6193
6193
|
return typeof ref === "string" && ref.length > 0;
|
|
6194
6194
|
}
|
|
6195
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6196
|
+
function isSecretMarkerLike(value) {
|
|
6197
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6198
|
+
}
|
|
6195
6199
|
function isSecretClear(value) {
|
|
6196
6200
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6197
6201
|
}
|
|
6202
|
+
function isSecretFieldView(value) {
|
|
6203
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6204
|
+
}
|
|
6198
6205
|
/**
|
|
6199
6206
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6200
6207
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6844,11 +6851,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6844
6851
|
}
|
|
6845
6852
|
return out;
|
|
6846
6853
|
}
|
|
6854
|
+
/**
|
|
6855
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6856
|
+
*
|
|
6857
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6858
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6859
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6860
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6861
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6862
|
+
* copied onto another ref does not decrypt.
|
|
6863
|
+
*
|
|
6864
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6865
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6866
|
+
*
|
|
6867
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6868
|
+
*/
|
|
6869
|
+
/** The URI scheme of every keychain ref. */
|
|
6870
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6847
6871
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6848
6872
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6849
6873
|
function globalSecretScope() {
|
|
6850
6874
|
return GLOBAL_SCOPE;
|
|
6851
6875
|
}
|
|
6876
|
+
/**
|
|
6877
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6878
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6879
|
+
* addon running forked.
|
|
6880
|
+
*/
|
|
6881
|
+
function normalizeSecretOwner(addonId) {
|
|
6882
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6883
|
+
}
|
|
6852
6884
|
function encodeSegment(value) {
|
|
6853
6885
|
return encodeURIComponent(value);
|
|
6854
6886
|
}
|
|
@@ -6859,6 +6891,16 @@ function decodeSegment(value) {
|
|
|
6859
6891
|
return null;
|
|
6860
6892
|
}
|
|
6861
6893
|
}
|
|
6894
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6895
|
+
function formatSecretScope(scope) {
|
|
6896
|
+
switch (scope.kind) {
|
|
6897
|
+
case "global": return "global";
|
|
6898
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6899
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6900
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6901
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6862
6904
|
/**
|
|
6863
6905
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6864
6906
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6867,6 +6909,13 @@ function decodeSegment(value) {
|
|
|
6867
6909
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6868
6910
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6869
6911
|
}
|
|
6912
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6913
|
+
function buildSecretRef(parts) {
|
|
6914
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6915
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6916
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6917
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6918
|
+
}
|
|
6870
6919
|
function parseScope(segments) {
|
|
6871
6920
|
switch (segments[0]) {
|
|
6872
6921
|
case "global": return {
|
|
@@ -8824,6 +8873,8 @@ var RECORD_BATCH_MAX = 1e3;
|
|
|
8824
8873
|
var RECORD_QUERY_MAX = 1e3;
|
|
8825
8874
|
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8826
8875
|
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8876
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8877
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8827
8878
|
/**
|
|
8828
8879
|
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8829
8880
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
@@ -8860,6 +8911,112 @@ function secretArrayPrefixes(path) {
|
|
|
8860
8911
|
}
|
|
8861
8912
|
return prefixes;
|
|
8862
8913
|
}
|
|
8914
|
+
function isRecord$4$1(value) {
|
|
8915
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8916
|
+
}
|
|
8917
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8918
|
+
const id = isRecord$4$1(element) ? element[idKey] : void 0;
|
|
8919
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
8920
|
+
return id;
|
|
8921
|
+
}
|
|
8922
|
+
/**
|
|
8923
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8924
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8925
|
+
* result is a new body, `data` is untouched.
|
|
8926
|
+
*/
|
|
8927
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8928
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8929
|
+
if (!isRecord$4$1(node)) return node;
|
|
8930
|
+
const segment = segments[index];
|
|
8931
|
+
if (segment === void 0) return node;
|
|
8932
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8933
|
+
const child = node[segment.key];
|
|
8934
|
+
const keys = [...walkedKeys, segment.key];
|
|
8935
|
+
const last = index === segments.length - 1;
|
|
8936
|
+
if (segment.each) {
|
|
8937
|
+
if (!Array.isArray(child)) return node;
|
|
8938
|
+
const prefix = keys.join(".");
|
|
8939
|
+
const idKey = arrayKeys[prefix];
|
|
8940
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8941
|
+
const mapped = child.map((element) => {
|
|
8942
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8943
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8944
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8945
|
+
});
|
|
8946
|
+
return {
|
|
8947
|
+
...node,
|
|
8948
|
+
[segment.key]: mapped
|
|
8949
|
+
};
|
|
8950
|
+
}
|
|
8951
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8952
|
+
if (last) {
|
|
8953
|
+
const replacement = visit({
|
|
8954
|
+
declared,
|
|
8955
|
+
concrete,
|
|
8956
|
+
value: child
|
|
8957
|
+
});
|
|
8958
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8959
|
+
return {
|
|
8960
|
+
...node,
|
|
8961
|
+
[segment.key]: replacement
|
|
8962
|
+
};
|
|
8963
|
+
}
|
|
8964
|
+
return {
|
|
8965
|
+
...node,
|
|
8966
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8967
|
+
};
|
|
8968
|
+
}
|
|
8969
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8970
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8971
|
+
let current = data;
|
|
8972
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8973
|
+
return current;
|
|
8974
|
+
}
|
|
8975
|
+
/**
|
|
8976
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8977
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8978
|
+
* already holds during an adoption.
|
|
8979
|
+
*/
|
|
8980
|
+
function recordSecretCollection(kind) {
|
|
8981
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8982
|
+
}
|
|
8983
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
8984
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
8985
|
+
return buildSecretRef({
|
|
8986
|
+
owner,
|
|
8987
|
+
scope: {
|
|
8988
|
+
kind: "row",
|
|
8989
|
+
collection: recordSecretCollection(kind),
|
|
8990
|
+
rowId
|
|
8991
|
+
},
|
|
8992
|
+
fieldPath: concretePath
|
|
8993
|
+
});
|
|
8994
|
+
}
|
|
8995
|
+
/**
|
|
8996
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
8997
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
8998
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
8999
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
9000
|
+
* stored secret (the records door reads a view as KEEP).
|
|
9001
|
+
*
|
|
9002
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
9003
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
9004
|
+
*/
|
|
9005
|
+
function isRecord$3$1(value) {
|
|
9006
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9007
|
+
}
|
|
9008
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
9009
|
+
if (decl.secretPaths.length === 0) return data;
|
|
9010
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
9011
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
9012
|
+
return {
|
|
9013
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
9014
|
+
v: 1,
|
|
9015
|
+
set: true
|
|
9016
|
+
};
|
|
9017
|
+
});
|
|
9018
|
+
return isRecord$3$1(shown) ? shown : data;
|
|
9019
|
+
}
|
|
8863
9020
|
/**
|
|
8864
9021
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8865
9022
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -9082,7 +9239,7 @@ function defineRecordKinds(specs) {
|
|
|
9082
9239
|
retention: spec.retention,
|
|
9083
9240
|
deviceRemoval: spec.deviceRemoval,
|
|
9084
9241
|
maxDataBytes: spec.maxDataBytes,
|
|
9085
|
-
secretPaths: [...secretPaths],
|
|
9242
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9086
9243
|
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9087
9244
|
transactFamily: spec.transactFamily,
|
|
9088
9245
|
entry: spec.entry === void 0 ? void 0 : {
|
|
@@ -9127,6 +9284,7 @@ function defineEntryKind(name, spec) {
|
|
|
9127
9284
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9128
9285
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9129
9286
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9287
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9130
9288
|
entry: {
|
|
9131
9289
|
title: spec.title,
|
|
9132
9290
|
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
@@ -11792,7 +11950,7 @@ object({
|
|
|
11792
11950
|
params: record(string(), union([string(), number$1()])).optional()
|
|
11793
11951
|
});
|
|
11794
11952
|
//#endregion
|
|
11795
|
-
//#region ../types/dist/composition-
|
|
11953
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11796
11954
|
/**
|
|
11797
11955
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11798
11956
|
*
|
|
@@ -12555,38 +12713,6 @@ function validateExpressionSource(src) {
|
|
|
12555
12713
|
return null;
|
|
12556
12714
|
}
|
|
12557
12715
|
/**
|
|
12558
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12559
|
-
*
|
|
12560
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12561
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12562
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12563
|
-
*
|
|
12564
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12565
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12566
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12567
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12568
|
-
* can move later without moving the others.
|
|
12569
|
-
*/
|
|
12570
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12571
|
-
var APPLIANCE_KINDS = [
|
|
12572
|
-
"washer",
|
|
12573
|
-
"dryer",
|
|
12574
|
-
"dishwasher"
|
|
12575
|
-
];
|
|
12576
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12577
|
-
/**
|
|
12578
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12579
|
-
*
|
|
12580
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12581
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12582
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12583
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12584
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12585
|
-
* the smallest radiator.
|
|
12586
|
-
*/
|
|
12587
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12588
|
-
DeviceType.Heater;
|
|
12589
|
-
/**
|
|
12590
12716
|
* Statistics and integrals over a history read (D944).
|
|
12591
12717
|
*
|
|
12592
12718
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12730,34 +12856,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12730
12856
|
/** Actuator groups only. Readings leave this unset. */
|
|
12731
12857
|
active: ActiveCriterionSchema.optional()
|
|
12732
12858
|
}),
|
|
12733
|
-
object({
|
|
12734
|
-
kind: literal("appliance"),
|
|
12735
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12736
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12737
|
-
thresholdWatts: number$1().positive(),
|
|
12738
|
-
startDwellMs: number$1().int().nonnegative(),
|
|
12739
|
-
stopDwellMs: number$1().int().nonnegative(),
|
|
12740
|
-
member: HelperMemberSchema,
|
|
12741
|
-
/**
|
|
12742
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12743
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12744
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12745
|
-
*/
|
|
12746
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12747
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12748
|
-
offSeconds: number$1().optional()
|
|
12749
|
-
}),
|
|
12750
|
-
object({
|
|
12751
|
-
kind: literal("heater"),
|
|
12752
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12753
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12754
|
-
plug: HelperMemberSchema,
|
|
12755
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12756
|
-
meter: HelperMemberSchema.optional(),
|
|
12757
|
-
thresholdWatts: number$1().positive(),
|
|
12758
|
-
startDwellMs: number$1().int().nonnegative(),
|
|
12759
|
-
stopDwellMs: number$1().int().nonnegative()
|
|
12760
|
-
}),
|
|
12761
12859
|
object({
|
|
12762
12860
|
kind: literal("statistic"),
|
|
12763
12861
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -13316,6 +13414,157 @@ function buildCapturingReply(envelope) {
|
|
|
13316
13414
|
};
|
|
13317
13415
|
return wrapper;
|
|
13318
13416
|
}
|
|
13417
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13418
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13419
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13420
|
+
function topLevelSecretKeys(paths) {
|
|
13421
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13422
|
+
}
|
|
13423
|
+
var RecordIntegrationInstances = class {
|
|
13424
|
+
records;
|
|
13425
|
+
kind;
|
|
13426
|
+
hooks;
|
|
13427
|
+
handle;
|
|
13428
|
+
secretKeys;
|
|
13429
|
+
nameField;
|
|
13430
|
+
constructor(records, kind, hooks) {
|
|
13431
|
+
this.records = records;
|
|
13432
|
+
this.kind = kind;
|
|
13433
|
+
this.hooks = hooks;
|
|
13434
|
+
this.handle = records.kind(kind);
|
|
13435
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13436
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13437
|
+
}
|
|
13438
|
+
/**
|
|
13439
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13440
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13441
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13442
|
+
*/
|
|
13443
|
+
async readResolved(id) {
|
|
13444
|
+
try {
|
|
13445
|
+
const record = await this.handle.getResolved(id);
|
|
13446
|
+
return record === null ? null : {
|
|
13447
|
+
id,
|
|
13448
|
+
body: record.data,
|
|
13449
|
+
enabled: record.data.enabled !== false,
|
|
13450
|
+
secrets: "resolved"
|
|
13451
|
+
};
|
|
13452
|
+
} catch (err) {
|
|
13453
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13454
|
+
const stored = await this.handle.get(id);
|
|
13455
|
+
if (stored === null) return null;
|
|
13456
|
+
return {
|
|
13457
|
+
id,
|
|
13458
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13459
|
+
enabled: stored.data.enabled !== false,
|
|
13460
|
+
secrets: "unavailable"
|
|
13461
|
+
};
|
|
13462
|
+
}
|
|
13463
|
+
}
|
|
13464
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13465
|
+
async getResolved(id) {
|
|
13466
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13467
|
+
}
|
|
13468
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13469
|
+
async listResolved() {
|
|
13470
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13471
|
+
const out = [];
|
|
13472
|
+
for (const record of page.records) {
|
|
13473
|
+
const read = await this.readResolved(record.id);
|
|
13474
|
+
if (read !== null) out.push(read);
|
|
13475
|
+
}
|
|
13476
|
+
return out;
|
|
13477
|
+
}
|
|
13478
|
+
viewOf(record) {
|
|
13479
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13480
|
+
const rawName = record.data[this.nameField];
|
|
13481
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13482
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13483
|
+
return {
|
|
13484
|
+
id: record.id,
|
|
13485
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13486
|
+
configured: this.hooks.configured(record.data),
|
|
13487
|
+
enabled: record.data.enabled !== false,
|
|
13488
|
+
formValues: shown,
|
|
13489
|
+
...kind !== void 0 ? { kind } : {},
|
|
13490
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13491
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13492
|
+
};
|
|
13493
|
+
}
|
|
13494
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13495
|
+
...this.hooks.description,
|
|
13496
|
+
mintsId: true
|
|
13497
|
+
};
|
|
13498
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13499
|
+
listInstances = async () => {
|
|
13500
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13501
|
+
};
|
|
13502
|
+
getInstance = async ({ id }) => {
|
|
13503
|
+
const record = await this.handle.get(id);
|
|
13504
|
+
return record === null ? null : this.viewOf(record);
|
|
13505
|
+
};
|
|
13506
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13507
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13508
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13509
|
+
name,
|
|
13510
|
+
...kind !== void 0 ? { kind } : {}
|
|
13511
|
+
});
|
|
13512
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13513
|
+
await this.handle.put({
|
|
13514
|
+
id: minted,
|
|
13515
|
+
data: this.hooks.initialBody(name, kind)
|
|
13516
|
+
}, { ifRev: 0 });
|
|
13517
|
+
await this.hooks.onChanged?.(minted);
|
|
13518
|
+
return { id: minted };
|
|
13519
|
+
};
|
|
13520
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13521
|
+
const patch = {};
|
|
13522
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13523
|
+
if (value === void 0) continue;
|
|
13524
|
+
if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13525
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13526
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13527
|
+
}
|
|
13528
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13529
|
+
await this.handle.patch(id, patch);
|
|
13530
|
+
await this.hooks.onChanged?.(id);
|
|
13531
|
+
return { ok: true };
|
|
13532
|
+
};
|
|
13533
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13534
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13535
|
+
await this.handle.patch(id, { enabled });
|
|
13536
|
+
await this.hooks.onChanged?.(id);
|
|
13537
|
+
return { ok: true };
|
|
13538
|
+
};
|
|
13539
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13540
|
+
deleteInstance = async ({ id }) => {
|
|
13541
|
+
await this.handle.delete(id);
|
|
13542
|
+
await this.hooks.onChanged?.(id);
|
|
13543
|
+
return { ok: true };
|
|
13544
|
+
};
|
|
13545
|
+
testInstance = async (input) => {
|
|
13546
|
+
if (this.hooks.test === void 0) return {
|
|
13547
|
+
success: false,
|
|
13548
|
+
status: "unsupported",
|
|
13549
|
+
error: "This integration declares no connection test"
|
|
13550
|
+
};
|
|
13551
|
+
return this.hooks.test(input);
|
|
13552
|
+
};
|
|
13553
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13554
|
+
toProvider() {
|
|
13555
|
+
return {
|
|
13556
|
+
describe: this.describe,
|
|
13557
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13558
|
+
listInstances: this.listInstances,
|
|
13559
|
+
getInstance: this.getInstance,
|
|
13560
|
+
createInstance: this.createInstance,
|
|
13561
|
+
updateInstance: this.updateInstance,
|
|
13562
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13563
|
+
deleteInstance: this.deleteInstance,
|
|
13564
|
+
testInstance: this.testInstance
|
|
13565
|
+
};
|
|
13566
|
+
}
|
|
13567
|
+
};
|
|
13319
13568
|
var EncodeProfileSchema = object({
|
|
13320
13569
|
video: object({
|
|
13321
13570
|
codec: _enum([
|
|
@@ -14384,6 +14633,58 @@ var MigrationOwnerStepSchema = object({
|
|
|
14384
14633
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
14385
14634
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
14386
14635
|
});
|
|
14636
|
+
function isPlain(value) {
|
|
14637
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14638
|
+
}
|
|
14639
|
+
async function flattenNestedRecords(ports, spec) {
|
|
14640
|
+
const ids = await ports.listNested(spec.legacyKey);
|
|
14641
|
+
if (ids.length === 0) return {
|
|
14642
|
+
outcome: "already-done",
|
|
14643
|
+
summary: { converted: 0 }
|
|
14644
|
+
};
|
|
14645
|
+
const rows = /* @__PURE__ */ new Map();
|
|
14646
|
+
for (const id of ids) try {
|
|
14647
|
+
rows.set(id, await ports.readResolved(id));
|
|
14648
|
+
} catch (err) {
|
|
14649
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
14650
|
+
throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
|
|
14651
|
+
}
|
|
14652
|
+
for (const [id, row] of rows) {
|
|
14653
|
+
const legacy = row[spec.legacyKey];
|
|
14654
|
+
if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
|
|
14655
|
+
const flat = spec.toFlat(legacy, row);
|
|
14656
|
+
await ports.patch(id, flat);
|
|
14657
|
+
const back = await ports.readResolved(id);
|
|
14658
|
+
const wrong = spec.flatSecretKeys.filter((key) => {
|
|
14659
|
+
const sent = flat[key];
|
|
14660
|
+
const got = back[key];
|
|
14661
|
+
return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
|
|
14662
|
+
});
|
|
14663
|
+
if (wrong.length > 0) {
|
|
14664
|
+
await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
|
|
14665
|
+
throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
|
|
14666
|
+
}
|
|
14667
|
+
await ports.patch(id, { [spec.legacyKey]: null });
|
|
14668
|
+
}
|
|
14669
|
+
return {
|
|
14670
|
+
outcome: "done",
|
|
14671
|
+
summary: { converted: rows.size }
|
|
14672
|
+
};
|
|
14673
|
+
}
|
|
14674
|
+
/** The ports over one kind's records handle. */
|
|
14675
|
+
function flattenPortsOver(handle) {
|
|
14676
|
+
return {
|
|
14677
|
+
listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
|
|
14678
|
+
readResolved: async (id) => {
|
|
14679
|
+
const row = await handle.getResolved(id);
|
|
14680
|
+
if (row === null) throw new Error("the record is gone");
|
|
14681
|
+
return row.data;
|
|
14682
|
+
},
|
|
14683
|
+
patch: async (id, patch) => {
|
|
14684
|
+
await handle.patch(id, patch);
|
|
14685
|
+
}
|
|
14686
|
+
};
|
|
14687
|
+
}
|
|
14387
14688
|
/**
|
|
14388
14689
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
14389
14690
|
* gets back (cloud training, D805+).
|
|
@@ -49914,117 +50215,6 @@ function actuator(input) {
|
|
|
49914
50215
|
};
|
|
49915
50216
|
}
|
|
49916
50217
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
49917
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
49918
|
-
"no-power-source",
|
|
49919
|
-
"switch-missing",
|
|
49920
|
-
"switch-offline",
|
|
49921
|
-
"switch-unknown",
|
|
49922
|
-
"restart-in-progress",
|
|
49923
|
-
"switch-call-failed"
|
|
49924
|
-
]);
|
|
49925
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
49926
|
-
accepted: literal(false),
|
|
49927
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
49928
|
-
/** One line for the operator, naming the switch. */
|
|
49929
|
-
message: string()
|
|
49930
|
-
})]);
|
|
49931
|
-
var PowerSourceInfoSchema = object({
|
|
49932
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
49933
|
-
linked: boolean(),
|
|
49934
|
-
switchDeviceId: number$1().nullable(),
|
|
49935
|
-
switchName: string().nullable(),
|
|
49936
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
49937
|
-
switchOn: boolean().nullable(),
|
|
49938
|
-
switchOnline: boolean().nullable(),
|
|
49939
|
-
restartInProgress: boolean(),
|
|
49940
|
-
offSeconds: number$1().int()
|
|
49941
|
-
});
|
|
49942
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
49943
|
-
var PowerSourceStateSchema = object({
|
|
49944
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
49945
|
-
switchOn: boolean().nullable(),
|
|
49946
|
-
offSeconds: number$1().int(),
|
|
49947
|
-
lastFetchedAt: number$1()
|
|
49948
|
-
});
|
|
49949
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
49950
|
-
DeviceType.Gateway,
|
|
49951
|
-
DeviceType.Hub,
|
|
49952
|
-
DeviceType.Camera,
|
|
49953
|
-
DeviceType.Sensor,
|
|
49954
|
-
DeviceType.MediaPlayer,
|
|
49955
|
-
DeviceType.Vacuum,
|
|
49956
|
-
DeviceType.Generic,
|
|
49957
|
-
DeviceType.Washer,
|
|
49958
|
-
DeviceType.Dryer,
|
|
49959
|
-
DeviceType.Dishwasher,
|
|
49960
|
-
DeviceType.PetFountain,
|
|
49961
|
-
DeviceType.Fan,
|
|
49962
|
-
DeviceType.Humidifier,
|
|
49963
|
-
DeviceType.Climate,
|
|
49964
|
-
DeviceType.WaterHeater
|
|
49965
|
-
];
|
|
49966
|
-
var powerSourceCustomize = {
|
|
49967
|
-
version: 1,
|
|
49968
|
-
targets: [{
|
|
49969
|
-
id: "switch",
|
|
49970
|
-
cardinality: "scalar",
|
|
49971
|
-
vocabulary: {
|
|
49972
|
-
cap: "power-source",
|
|
49973
|
-
stateField: "switchOn"
|
|
49974
|
-
},
|
|
49975
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49976
|
-
apply: "add",
|
|
49977
|
-
path: "switchOn",
|
|
49978
|
-
sources: [{
|
|
49979
|
-
kind: "switch",
|
|
49980
|
-
transforms: [{ kind: "identity" }]
|
|
49981
|
-
}],
|
|
49982
|
-
companions: [{
|
|
49983
|
-
kind: "constant",
|
|
49984
|
-
path: "offSeconds",
|
|
49985
|
-
value: 10,
|
|
49986
|
-
when: "add"
|
|
49987
|
-
}, {
|
|
49988
|
-
kind: "clock",
|
|
49989
|
-
path: "lastFetchedAt",
|
|
49990
|
-
when: "always"
|
|
49991
|
-
}],
|
|
49992
|
-
sourceReport: { status: "assignment-status" }
|
|
49993
|
-
}]
|
|
49994
|
-
};
|
|
49995
|
-
var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
|
|
49996
|
-
var powerSourceCapability = {
|
|
49997
|
-
name: "power-source",
|
|
49998
|
-
scope: "device",
|
|
49999
|
-
mode: "singleton",
|
|
50000
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
50001
|
-
methods: {
|
|
50002
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
50003
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50004
|
-
kind: "mutation",
|
|
50005
|
-
auth: "admin"
|
|
50006
|
-
}),
|
|
50007
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50008
|
-
kind: "mutation",
|
|
50009
|
-
auth: "admin"
|
|
50010
|
-
}),
|
|
50011
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50012
|
-
kind: "mutation",
|
|
50013
|
-
auth: "admin"
|
|
50014
|
-
})
|
|
50015
|
-
},
|
|
50016
|
-
runtimeState: PowerSourceStateSchema,
|
|
50017
|
-
customize: powerSourceCustomize,
|
|
50018
|
-
/**
|
|
50019
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
50020
|
-
* its next report; a restored value would outlive the reading it names.
|
|
50021
|
-
*
|
|
50022
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
50023
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
50024
|
-
*/
|
|
50025
|
-
durability: "session",
|
|
50026
|
-
history: "none"
|
|
50027
|
-
};
|
|
50028
50218
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
50029
50219
|
DeviceType.Camera;
|
|
50030
50220
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -50052,6 +50242,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
50052
50242
|
}).optional()
|
|
50053
50243
|
})) });
|
|
50054
50244
|
/**
|
|
50245
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
50246
|
+
* `z.infer`, they do not restate the shape.
|
|
50247
|
+
*/
|
|
50248
|
+
var OverrideSourceSchema = object({
|
|
50249
|
+
addonId: string().min(1),
|
|
50250
|
+
stableId: string().min(1),
|
|
50251
|
+
cap: string().min(1),
|
|
50252
|
+
fieldPath: string().min(1)
|
|
50253
|
+
});
|
|
50254
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
50255
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
50256
|
+
var ThresholdTransformDraftSchema = object({
|
|
50257
|
+
kind: literal("threshold"),
|
|
50258
|
+
comparison: _enum(["gte", "lte"]),
|
|
50259
|
+
threshold: number$1(),
|
|
50260
|
+
startDwellMs: number$1().nonnegative(),
|
|
50261
|
+
stopDwellMs: number$1().nonnegative()
|
|
50262
|
+
});
|
|
50263
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50264
|
+
IdentityTransformDraftSchema,
|
|
50265
|
+
InvertTransformDraftSchema,
|
|
50266
|
+
ThresholdTransformDraftSchema,
|
|
50267
|
+
object({
|
|
50268
|
+
kind: literal("linear"),
|
|
50269
|
+
scale: number$1(),
|
|
50270
|
+
offset: number$1(),
|
|
50271
|
+
clampMin: number$1().optional(),
|
|
50272
|
+
clampMax: number$1().optional()
|
|
50273
|
+
}),
|
|
50274
|
+
object({
|
|
50275
|
+
kind: literal("enum-map"),
|
|
50276
|
+
mapping: record(string(), string().nullable())
|
|
50277
|
+
}),
|
|
50278
|
+
object({ kind: literal("number") })
|
|
50279
|
+
]);
|
|
50280
|
+
var ScalarOverrideDraftSchema = object({
|
|
50281
|
+
kind: literal("scalar"),
|
|
50282
|
+
cap: string().min(1),
|
|
50283
|
+
targetId: string().min(1),
|
|
50284
|
+
source: OverrideSourceSchema,
|
|
50285
|
+
transform: CustomizeTransformDraftSchema
|
|
50286
|
+
});
|
|
50287
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
50288
|
+
kind: literal("keyedArray"),
|
|
50289
|
+
cap: string().min(1),
|
|
50290
|
+
targetId: string().min(1),
|
|
50291
|
+
key: string().min(1),
|
|
50292
|
+
label: string().min(1),
|
|
50293
|
+
fieldPath: string().min(1),
|
|
50294
|
+
source: OverrideSourceSchema,
|
|
50295
|
+
transform: CustomizeTransformDraftSchema
|
|
50296
|
+
});
|
|
50297
|
+
var PulseOverrideDraftSchema = object({
|
|
50298
|
+
kind: literal("pulse"),
|
|
50299
|
+
cap: string().min(1),
|
|
50300
|
+
targetId: string().min(1),
|
|
50301
|
+
enabled: boolean(),
|
|
50302
|
+
debounceMs: number$1().nonnegative(),
|
|
50303
|
+
edges: array(_enum([
|
|
50304
|
+
"rising",
|
|
50305
|
+
"button",
|
|
50306
|
+
"native-event"
|
|
50307
|
+
])).min(1).optional(),
|
|
50308
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
50309
|
+
});
|
|
50310
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
50311
|
+
"in-addition",
|
|
50312
|
+
"confirm",
|
|
50313
|
+
"replace"
|
|
50314
|
+
]).optional() });
|
|
50315
|
+
discriminatedUnion("kind", [
|
|
50316
|
+
ScalarOverrideDraftSchema,
|
|
50317
|
+
KeyedArrayOverrideDraftSchema,
|
|
50318
|
+
PulseOverrideDraftSchema,
|
|
50319
|
+
object({
|
|
50320
|
+
kind: literal("span"),
|
|
50321
|
+
cap: string().min(1),
|
|
50322
|
+
targetId: string().min(1),
|
|
50323
|
+
timeoutMs: number$1().nonnegative(),
|
|
50324
|
+
cooldownMs: number$1().nonnegative().optional(),
|
|
50325
|
+
sources: array(SpanSourceDraftSchema)
|
|
50326
|
+
})
|
|
50327
|
+
]);
|
|
50328
|
+
object({
|
|
50329
|
+
targetId: string().min(1),
|
|
50330
|
+
configured: boolean(),
|
|
50331
|
+
source: _enum(["native", "assigned"]),
|
|
50332
|
+
state: _enum([
|
|
50333
|
+
"active",
|
|
50334
|
+
"stale",
|
|
50335
|
+
"released",
|
|
50336
|
+
"unavailable"
|
|
50337
|
+
]),
|
|
50338
|
+
reason: string().nullable()
|
|
50339
|
+
});
|
|
50340
|
+
/**
|
|
50341
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
50342
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
50343
|
+
*
|
|
50344
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
50345
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
50346
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
50347
|
+
*/
|
|
50348
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
50349
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
50350
|
+
"high",
|
|
50351
|
+
"medium",
|
|
50352
|
+
"low"
|
|
50353
|
+
]);
|
|
50354
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
50355
|
+
"no-history",
|
|
50356
|
+
"span-too-short",
|
|
50357
|
+
"truncated",
|
|
50358
|
+
"no-idle-floor",
|
|
50359
|
+
"no-runs",
|
|
50360
|
+
"no-separation"
|
|
50361
|
+
]);
|
|
50362
|
+
var RunningProposalReplaySchema = object({
|
|
50363
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
50364
|
+
intervals: number$1().int().nonnegative(),
|
|
50365
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
50366
|
+
extra: number$1().int().nonnegative(),
|
|
50367
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
50368
|
+
missed: number$1().int().nonnegative()
|
|
50369
|
+
});
|
|
50370
|
+
var RunningProposalEvidenceSchema = object({
|
|
50371
|
+
tier: RunningProposalTierSchema,
|
|
50372
|
+
spanMs: number$1().nonnegative(),
|
|
50373
|
+
from: number$1(),
|
|
50374
|
+
to: number$1(),
|
|
50375
|
+
points: number$1().int().nonnegative(),
|
|
50376
|
+
runsFound: number$1().int().nonnegative(),
|
|
50377
|
+
pausesFound: number$1().int().nonnegative(),
|
|
50378
|
+
longestPauseMs: number$1().nonnegative(),
|
|
50379
|
+
idleFloorW: number$1(),
|
|
50380
|
+
noiseCeilingW: number$1(),
|
|
50381
|
+
plateauW: number$1(),
|
|
50382
|
+
replay: RunningProposalReplaySchema
|
|
50383
|
+
});
|
|
50384
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
50385
|
+
spanMs: number$1().nonnegative(),
|
|
50386
|
+
points: number$1().int().nonnegative(),
|
|
50387
|
+
runsFound: number$1().int().nonnegative()
|
|
50388
|
+
});
|
|
50389
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
50390
|
+
state: literal("proposal"),
|
|
50391
|
+
transform: ThresholdTransformDraftSchema,
|
|
50392
|
+
evidence: RunningProposalEvidenceSchema,
|
|
50393
|
+
confidence: RunningProposalConfidenceSchema,
|
|
50394
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
50395
|
+
notes: array(string())
|
|
50396
|
+
}), object({
|
|
50397
|
+
state: literal("insufficient"),
|
|
50398
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
50399
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
50400
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
50401
|
+
fallback: ThresholdTransformDraftSchema
|
|
50402
|
+
})]);
|
|
50403
|
+
/**
|
|
50055
50404
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
50056
50405
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
50057
50406
|
*
|
|
@@ -50341,6 +50690,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
50341
50690
|
kind: "mutation",
|
|
50342
50691
|
auth: "admin",
|
|
50343
50692
|
access: "view"
|
|
50693
|
+
}), method(object({
|
|
50694
|
+
deviceId: number$1().int(),
|
|
50695
|
+
cap: string().min(1),
|
|
50696
|
+
fieldPath: string().min(1),
|
|
50697
|
+
deviceType: _enum(DeviceType).optional()
|
|
50698
|
+
}), RunningThresholdProposalSchema, {
|
|
50699
|
+
auth: "admin",
|
|
50700
|
+
access: "view"
|
|
50344
50701
|
}), method(object({
|
|
50345
50702
|
blockId: string().min(1),
|
|
50346
50703
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -51296,7 +51653,27 @@ method(object({
|
|
|
51296
51653
|
* exposes only the COMMON surface: link state, supported device
|
|
51297
51654
|
* kinds (so the UI can filter the device picker), and per-device
|
|
51298
51655
|
* expose/unexpose.
|
|
51299
|
-
|
|
51656
|
+
*
|
|
51657
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
51658
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
51659
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
51660
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
51661
|
+
* default instance's); present = that instance; an id the provider does not
|
|
51662
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
51663
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
51664
|
+
* change always names the target it changes. A single-instance provider
|
|
51665
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
51666
|
+
*/
|
|
51667
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
51668
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
51669
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
51670
|
+
/** One independent export target of an addon. */
|
|
51671
|
+
var ExportInstanceSchema = object({
|
|
51672
|
+
instanceId: InstanceIdSchema$1,
|
|
51673
|
+
name: string(),
|
|
51674
|
+
enabled: boolean(),
|
|
51675
|
+
kind: string().optional()
|
|
51676
|
+
});
|
|
51300
51677
|
var LinkStateSchema = _enum([
|
|
51301
51678
|
"unlinked",
|
|
51302
51679
|
"linked",
|
|
@@ -51347,10 +51724,14 @@ var ExposedDeviceSchema = object({
|
|
|
51347
51724
|
capabilities: array(string()).optional()
|
|
51348
51725
|
});
|
|
51349
51726
|
var ExposeInputSchema = object({
|
|
51727
|
+
instanceId: InstanceIdSchema$1,
|
|
51350
51728
|
deviceId: string(),
|
|
51351
51729
|
capabilities: array(string()).optional()
|
|
51352
51730
|
});
|
|
51353
|
-
var UnexposeInputSchema = object({
|
|
51731
|
+
var UnexposeInputSchema = object({
|
|
51732
|
+
instanceId: InstanceIdSchema$1,
|
|
51733
|
+
deviceId: string()
|
|
51734
|
+
});
|
|
51354
51735
|
/**
|
|
51355
51736
|
* What an exporter may expose besides devices (D968). An action and a
|
|
51356
51737
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -51379,15 +51760,21 @@ var ExposedEntitySchema = object({
|
|
|
51379
51760
|
entity: ExportEntityRefSchema,
|
|
51380
51761
|
exposedAs: string().optional()
|
|
51381
51762
|
});
|
|
51382
|
-
var ExposeEntityInputSchema = object({
|
|
51383
|
-
|
|
51384
|
-
|
|
51763
|
+
var ExposeEntityInputSchema = object({
|
|
51764
|
+
instanceId: InstanceIdSchema$1,
|
|
51765
|
+
entity: ExportEntityRefSchema
|
|
51766
|
+
});
|
|
51767
|
+
var UnexposeEntityInputSchema = object({
|
|
51768
|
+
instanceId: InstanceIdSchema$1,
|
|
51769
|
+
entity: ExportEntityRefSchema
|
|
51770
|
+
});
|
|
51771
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
51385
51772
|
kind: "mutation",
|
|
51386
51773
|
auth: "admin"
|
|
51387
51774
|
}), method(UnexposeInputSchema, _void(), {
|
|
51388
51775
|
kind: "mutation",
|
|
51389
51776
|
auth: "admin"
|
|
51390
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
51777
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
51391
51778
|
kind: "mutation",
|
|
51392
51779
|
auth: "admin"
|
|
51393
51780
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -54978,15 +55365,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
54978
55365
|
items: array(FanCaptureItemSchema),
|
|
54979
55366
|
omitted: array(FanCaptureOmissionSchema)
|
|
54980
55367
|
});
|
|
54981
|
-
var deviceInput$
|
|
55368
|
+
var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
|
|
54982
55369
|
DeviceType.Fan;
|
|
54983
|
-
method(deviceInput$
|
|
55370
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
54984
55371
|
kind: "mutation",
|
|
54985
55372
|
auth: "admin"
|
|
54986
|
-
}), method(deviceInput$
|
|
55373
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
54987
55374
|
kind: "mutation",
|
|
54988
55375
|
auth: "protected"
|
|
54989
|
-
}), method(deviceInput$
|
|
55376
|
+
}), method(deviceInput$2.extend({
|
|
54990
55377
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
54991
55378
|
steps: array(ActionStepSchema).max(20).default([])
|
|
54992
55379
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -55897,13 +56284,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
55897
56284
|
*/
|
|
55898
56285
|
var ConfigSchemaPassthrough$3 = custom();
|
|
55899
56286
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
56287
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56288
|
+
/**
|
|
56289
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
56290
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
56291
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
56292
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
56293
|
+
* instance id.
|
|
56294
|
+
*/
|
|
56295
|
+
var IntegrationInstanceKindSchema = object({
|
|
56296
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
56297
|
+
id: string().min(1),
|
|
56298
|
+
label: string().min(1),
|
|
56299
|
+
/** The kind signs in through a server-held interactive session. */
|
|
56300
|
+
signIn: object({
|
|
56301
|
+
basePath: string().min(1),
|
|
56302
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
56303
|
+
}).optional(),
|
|
56304
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
56305
|
+
connectPath: string().min(1).optional(),
|
|
56306
|
+
/** Relative hub path to disconnect that account. */
|
|
56307
|
+
disconnectPath: string().min(1).optional()
|
|
56308
|
+
});
|
|
55900
56309
|
var IntegrationInstanceDescriptionSchema = object({
|
|
55901
56310
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
55902
|
-
instanceMode:
|
|
56311
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
55903
56312
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
55904
56313
|
kind: string(),
|
|
55905
56314
|
/** True when `testInstance` really probes the remote. */
|
|
55906
|
-
canTest: boolean()
|
|
56315
|
+
canTest: boolean(),
|
|
56316
|
+
/**
|
|
56317
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
56318
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
56319
|
+
*/
|
|
56320
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
56321
|
+
/**
|
|
56322
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
56323
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
56324
|
+
*/
|
|
56325
|
+
allowRestoreId: boolean().optional(),
|
|
56326
|
+
/**
|
|
56327
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
56328
|
+
* send an id (the UI then mints an opaque one).
|
|
56329
|
+
*/
|
|
56330
|
+
mintsId: boolean().optional()
|
|
55907
56331
|
});
|
|
55908
56332
|
var IntegrationInstanceStatusSchema = object({
|
|
55909
56333
|
state: _enum([
|
|
@@ -55912,7 +56336,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
55912
56336
|
"error",
|
|
55913
56337
|
"unknown"
|
|
55914
56338
|
]),
|
|
55915
|
-
detail: string().optional()
|
|
56339
|
+
detail: string().optional(),
|
|
56340
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
56341
|
+
label: string().optional()
|
|
55916
56342
|
});
|
|
55917
56343
|
var IntegrationInstanceSchema = object({
|
|
55918
56344
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55924,9 +56350,13 @@ var IntegrationInstanceSchema = object({
|
|
|
55924
56350
|
* (`enabled !== false`, so a row written before the field existed is on).
|
|
55925
56351
|
*/
|
|
55926
56352
|
enabled: boolean(),
|
|
56353
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56354
|
+
kind: string().optional(),
|
|
55927
56355
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55928
56356
|
formValues: record(string(), unknown()),
|
|
55929
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
56357
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
56358
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
56359
|
+
lastCheckedAt: number$1().optional()
|
|
55930
56360
|
});
|
|
55931
56361
|
var IntegrationInstancePatchSchema = object({
|
|
55932
56362
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55934,31 +56364,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
55934
56364
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
55935
56365
|
values: record(string(), unknown())
|
|
55936
56366
|
});
|
|
55937
|
-
|
|
55938
|
-
|
|
55939
|
-
|
|
55940
|
-
|
|
55941
|
-
|
|
55942
|
-
|
|
55943
|
-
|
|
55944
|
-
|
|
55945
|
-
|
|
55946
|
-
|
|
55947
|
-
|
|
55948
|
-
|
|
55949
|
-
|
|
55950
|
-
|
|
55951
|
-
|
|
55952
|
-
|
|
55953
|
-
|
|
55954
|
-
|
|
55955
|
-
}),
|
|
55956
|
-
|
|
55957
|
-
|
|
55958
|
-
}),
|
|
55959
|
-
|
|
55960
|
-
|
|
55961
|
-
|
|
56367
|
+
var integrationInstancesCapability = {
|
|
56368
|
+
name: "integration-instances",
|
|
56369
|
+
scope: "system",
|
|
56370
|
+
mode: "collection",
|
|
56371
|
+
methods: {
|
|
56372
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
56373
|
+
/** The form; `id: null` is the create form. */
|
|
56374
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
56375
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
56376
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
56377
|
+
/**
|
|
56378
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
56379
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
56380
|
+
*/
|
|
56381
|
+
createInstance: method(object({
|
|
56382
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
56383
|
+
name: string().min(1),
|
|
56384
|
+
kind: string().min(1).optional()
|
|
56385
|
+
}), object({ id: string() }), {
|
|
56386
|
+
kind: "mutation",
|
|
56387
|
+
auth: "admin"
|
|
56388
|
+
}),
|
|
56389
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
56390
|
+
kind: "mutation",
|
|
56391
|
+
auth: "admin"
|
|
56392
|
+
}),
|
|
56393
|
+
/**
|
|
56394
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
56395
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56396
|
+
*/
|
|
56397
|
+
setInstanceEnabled: method(object({
|
|
56398
|
+
id: IntegrationInstanceIdSchema,
|
|
56399
|
+
enabled: boolean()
|
|
56400
|
+
}), object({ ok: literal(true) }), {
|
|
56401
|
+
kind: "mutation",
|
|
56402
|
+
auth: "admin"
|
|
56403
|
+
}),
|
|
56404
|
+
/** Delete the record and its keychain refs together. */
|
|
56405
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
56406
|
+
kind: "mutation",
|
|
56407
|
+
auth: "admin"
|
|
56408
|
+
}),
|
|
56409
|
+
testInstance: optionalMethod(object({
|
|
56410
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
56411
|
+
values: record(string(), unknown())
|
|
56412
|
+
}), TestConnectionResultSchema$1, {
|
|
56413
|
+
kind: "mutation",
|
|
56414
|
+
auth: "admin"
|
|
56415
|
+
})
|
|
56416
|
+
}
|
|
56417
|
+
};
|
|
55962
56418
|
/**
|
|
55963
56419
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
55964
56420
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -57976,6 +58432,11 @@ var LogEntrySchema = object({
|
|
|
57976
58432
|
* flight, and not asking for the next slice ends the scan.
|
|
57977
58433
|
*/
|
|
57978
58434
|
var LogReadQuerySchema = object({
|
|
58435
|
+
/**
|
|
58436
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
58437
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
58438
|
+
*/
|
|
58439
|
+
instanceId: string().min(1).max(200).optional(),
|
|
57979
58440
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
57980
58441
|
requestId: string().min(1).max(64).optional(),
|
|
57981
58442
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -58038,6 +58499,8 @@ var LogReadResultSchema = object({
|
|
|
58038
58499
|
/** True when `cancelRead` ended this slice early. */
|
|
58039
58500
|
cancelled: boolean().optional()
|
|
58040
58501
|
});
|
|
58502
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
58503
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
58041
58504
|
var LogReadCapabilitiesSchema = object({
|
|
58042
58505
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
58043
58506
|
readable: boolean(),
|
|
@@ -58056,7 +58519,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
58056
58519
|
/** How far back the store reaches, when the provider knows (days). */
|
|
58057
58520
|
retentionDays: number$1().nullable()
|
|
58058
58521
|
});
|
|
58059
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
58522
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
58060
58523
|
kind: "mutation",
|
|
58061
58524
|
auth: "admin"
|
|
58062
58525
|
});
|
|
@@ -70450,6 +70913,117 @@ var powerMeterCapability = {
|
|
|
70450
70913
|
volatileStateFields: ["lastFetchedAt"],
|
|
70451
70914
|
customize: powerMeterCustomize
|
|
70452
70915
|
};
|
|
70916
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
70917
|
+
"no-power-source",
|
|
70918
|
+
"switch-missing",
|
|
70919
|
+
"switch-offline",
|
|
70920
|
+
"switch-unknown",
|
|
70921
|
+
"restart-in-progress",
|
|
70922
|
+
"switch-call-failed"
|
|
70923
|
+
]);
|
|
70924
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
70925
|
+
accepted: literal(false),
|
|
70926
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
70927
|
+
/** One line for the operator, naming the switch. */
|
|
70928
|
+
message: string()
|
|
70929
|
+
})]);
|
|
70930
|
+
var PowerSourceInfoSchema = object({
|
|
70931
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
70932
|
+
linked: boolean(),
|
|
70933
|
+
switchDeviceId: number$1().nullable(),
|
|
70934
|
+
switchName: string().nullable(),
|
|
70935
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
70936
|
+
switchOn: boolean().nullable(),
|
|
70937
|
+
switchOnline: boolean().nullable(),
|
|
70938
|
+
restartInProgress: boolean(),
|
|
70939
|
+
offSeconds: number$1().int()
|
|
70940
|
+
});
|
|
70941
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
70942
|
+
var PowerSourceStateSchema = object({
|
|
70943
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
70944
|
+
switchOn: boolean().nullable(),
|
|
70945
|
+
offSeconds: number$1().int(),
|
|
70946
|
+
lastFetchedAt: number$1()
|
|
70947
|
+
});
|
|
70948
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
70949
|
+
DeviceType.Gateway,
|
|
70950
|
+
DeviceType.Hub,
|
|
70951
|
+
DeviceType.Camera,
|
|
70952
|
+
DeviceType.Sensor,
|
|
70953
|
+
DeviceType.MediaPlayer,
|
|
70954
|
+
DeviceType.Vacuum,
|
|
70955
|
+
DeviceType.Generic,
|
|
70956
|
+
DeviceType.Washer,
|
|
70957
|
+
DeviceType.Dryer,
|
|
70958
|
+
DeviceType.Dishwasher,
|
|
70959
|
+
DeviceType.PetFountain,
|
|
70960
|
+
DeviceType.Fan,
|
|
70961
|
+
DeviceType.Humidifier,
|
|
70962
|
+
DeviceType.Climate,
|
|
70963
|
+
DeviceType.WaterHeater
|
|
70964
|
+
];
|
|
70965
|
+
var powerSourceCustomize = {
|
|
70966
|
+
version: 1,
|
|
70967
|
+
targets: [{
|
|
70968
|
+
id: "switch",
|
|
70969
|
+
cardinality: "scalar",
|
|
70970
|
+
vocabulary: {
|
|
70971
|
+
cap: "power-source",
|
|
70972
|
+
stateField: "switchOn"
|
|
70973
|
+
},
|
|
70974
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
70975
|
+
apply: "add",
|
|
70976
|
+
path: "switchOn",
|
|
70977
|
+
sources: [{
|
|
70978
|
+
kind: "switch",
|
|
70979
|
+
transforms: [{ kind: "identity" }]
|
|
70980
|
+
}],
|
|
70981
|
+
companions: [{
|
|
70982
|
+
kind: "constant",
|
|
70983
|
+
path: "offSeconds",
|
|
70984
|
+
value: 10,
|
|
70985
|
+
when: "add"
|
|
70986
|
+
}, {
|
|
70987
|
+
kind: "clock",
|
|
70988
|
+
path: "lastFetchedAt",
|
|
70989
|
+
when: "always"
|
|
70990
|
+
}],
|
|
70991
|
+
sourceReport: { status: "assignment-status" }
|
|
70992
|
+
}]
|
|
70993
|
+
};
|
|
70994
|
+
var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
|
|
70995
|
+
var powerSourceCapability = {
|
|
70996
|
+
name: "power-source",
|
|
70997
|
+
scope: "device",
|
|
70998
|
+
mode: "singleton",
|
|
70999
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71000
|
+
methods: {
|
|
71001
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
71002
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71003
|
+
kind: "mutation",
|
|
71004
|
+
auth: "admin"
|
|
71005
|
+
}),
|
|
71006
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71007
|
+
kind: "mutation",
|
|
71008
|
+
auth: "admin"
|
|
71009
|
+
}),
|
|
71010
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71011
|
+
kind: "mutation",
|
|
71012
|
+
auth: "admin"
|
|
71013
|
+
})
|
|
71014
|
+
},
|
|
71015
|
+
runtimeState: PowerSourceStateSchema,
|
|
71016
|
+
customize: powerSourceCustomize,
|
|
71017
|
+
/**
|
|
71018
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
71019
|
+
* its next report; a restored value would outlive the reading it names.
|
|
71020
|
+
*
|
|
71021
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71022
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71023
|
+
*/
|
|
71024
|
+
durability: "session",
|
|
71025
|
+
history: "none"
|
|
71026
|
+
};
|
|
70453
71027
|
/**
|
|
70454
71028
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
70455
71029
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -79811,6 +80385,25 @@ function buildDescriptor(capName, kind) {
|
|
|
79811
80385
|
* contributes one entry per bound cap present in this map.
|
|
79812
80386
|
*/
|
|
79813
80387
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
80388
|
+
/**
|
|
80389
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
80390
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
80391
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
80392
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
80393
|
+
* server sends an id, a client maps it to its own icon set.
|
|
80394
|
+
*
|
|
80395
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
80396
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
80397
|
+
* to `icon`.
|
|
80398
|
+
*/
|
|
80399
|
+
function digit(n) {
|
|
80400
|
+
return {
|
|
80401
|
+
label: t(n, n),
|
|
80402
|
+
icon: "hash",
|
|
80403
|
+
legend: n
|
|
80404
|
+
};
|
|
80405
|
+
}
|
|
80406
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
79814
80407
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
79815
80408
|
var btn = (idSuffix, row, col, init) => ({
|
|
79816
80409
|
idSuffix,
|
|
@@ -79938,6 +80531,34 @@ discriminatedUnion("kind", [object({
|
|
|
79938
80531
|
kind: literal("pronto"),
|
|
79939
80532
|
code: string()
|
|
79940
80533
|
})]);
|
|
80534
|
+
/**
|
|
80535
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
80536
|
+
*
|
|
80537
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
80538
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
80539
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
80540
|
+
* count of samples.
|
|
80541
|
+
*
|
|
80542
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
80543
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
80544
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
80545
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
80546
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
80547
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
80548
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
80549
|
+
* covers the longest standby blip above T.
|
|
80550
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
80551
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
80552
|
+
*/
|
|
80553
|
+
var SECOND_MS = 1e3;
|
|
80554
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
80555
|
+
24 * (60 * MINUTE_MS$1);
|
|
80556
|
+
10 * MINUTE_MS$1;
|
|
80557
|
+
30 * SECOND_MS;
|
|
80558
|
+
60 * SECOND_MS;
|
|
80559
|
+
20 * MINUTE_MS$1;
|
|
80560
|
+
10 * SECOND_MS;
|
|
80561
|
+
10 * SECOND_MS;
|
|
79941
80562
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79942
80563
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79943
80564
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -83043,6 +83664,13 @@ Object.freeze({
|
|
|
83043
83664
|
access: "create",
|
|
83044
83665
|
role: "admin"
|
|
83045
83666
|
},
|
|
83667
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
83668
|
+
capName: "core-blocks",
|
|
83669
|
+
capScope: "system",
|
|
83670
|
+
addonId: null,
|
|
83671
|
+
access: "view",
|
|
83672
|
+
role: "admin"
|
|
83673
|
+
},
|
|
83046
83674
|
"coreBlocks.reportAutomationRun": {
|
|
83047
83675
|
capName: "core-blocks",
|
|
83048
83676
|
capScope: "system",
|
|
@@ -83554,6 +84182,13 @@ Object.freeze({
|
|
|
83554
84182
|
access: "view",
|
|
83555
84183
|
role: "admin"
|
|
83556
84184
|
},
|
|
84185
|
+
"deviceExport.listInstances": {
|
|
84186
|
+
capName: "device-export",
|
|
84187
|
+
capScope: "system",
|
|
84188
|
+
addonId: null,
|
|
84189
|
+
access: "view",
|
|
84190
|
+
role: "admin"
|
|
84191
|
+
},
|
|
83557
84192
|
"deviceExport.listSupportedDeviceKinds": {
|
|
83558
84193
|
capName: "device-export",
|
|
83559
84194
|
capScope: "system",
|
|
@@ -92501,6 +93136,11 @@ Object.freeze({
|
|
|
92501
93136
|
form: "single",
|
|
92502
93137
|
optional: false
|
|
92503
93138
|
}],
|
|
93139
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
93140
|
+
name: "deviceId",
|
|
93141
|
+
form: "single",
|
|
93142
|
+
optional: false
|
|
93143
|
+
}],
|
|
92504
93144
|
"cover.close": [{
|
|
92505
93145
|
name: "deviceId",
|
|
92506
93146
|
form: "single",
|
|
@@ -95164,99 +95804,6 @@ Object.freeze({
|
|
|
95164
95804
|
"network-access": "ingress",
|
|
95165
95805
|
"smtp-provider": "email"
|
|
95166
95806
|
});
|
|
95167
|
-
/**
|
|
95168
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
95169
|
-
* `z.infer`, they do not restate the shape.
|
|
95170
|
-
*/
|
|
95171
|
-
var OverrideSourceSchema = object({
|
|
95172
|
-
addonId: string().min(1),
|
|
95173
|
-
stableId: string().min(1),
|
|
95174
|
-
cap: string().min(1),
|
|
95175
|
-
fieldPath: string().min(1)
|
|
95176
|
-
});
|
|
95177
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
95178
|
-
object({ kind: literal("identity") }),
|
|
95179
|
-
object({ kind: literal("invert") }),
|
|
95180
|
-
object({
|
|
95181
|
-
kind: literal("threshold"),
|
|
95182
|
-
comparison: _enum(["gte", "lte"]),
|
|
95183
|
-
threshold: number$1(),
|
|
95184
|
-
startDwellMs: number$1().nonnegative(),
|
|
95185
|
-
stopDwellMs: number$1().nonnegative()
|
|
95186
|
-
}),
|
|
95187
|
-
object({
|
|
95188
|
-
kind: literal("linear"),
|
|
95189
|
-
scale: number$1(),
|
|
95190
|
-
offset: number$1(),
|
|
95191
|
-
clampMin: number$1().optional(),
|
|
95192
|
-
clampMax: number$1().optional()
|
|
95193
|
-
}),
|
|
95194
|
-
object({
|
|
95195
|
-
kind: literal("enum-map"),
|
|
95196
|
-
mapping: record(string(), string().nullable())
|
|
95197
|
-
}),
|
|
95198
|
-
object({ kind: literal("number") })
|
|
95199
|
-
]);
|
|
95200
|
-
var ScalarOverrideDraftSchema = object({
|
|
95201
|
-
kind: literal("scalar"),
|
|
95202
|
-
cap: string().min(1),
|
|
95203
|
-
targetId: string().min(1),
|
|
95204
|
-
source: OverrideSourceSchema,
|
|
95205
|
-
transform: CustomizeTransformDraftSchema
|
|
95206
|
-
});
|
|
95207
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
95208
|
-
kind: literal("keyedArray"),
|
|
95209
|
-
cap: string().min(1),
|
|
95210
|
-
targetId: string().min(1),
|
|
95211
|
-
key: string().min(1),
|
|
95212
|
-
label: string().min(1),
|
|
95213
|
-
fieldPath: string().min(1),
|
|
95214
|
-
source: OverrideSourceSchema,
|
|
95215
|
-
transform: CustomizeTransformDraftSchema
|
|
95216
|
-
});
|
|
95217
|
-
var PulseOverrideDraftSchema = object({
|
|
95218
|
-
kind: literal("pulse"),
|
|
95219
|
-
cap: string().min(1),
|
|
95220
|
-
targetId: string().min(1),
|
|
95221
|
-
enabled: boolean(),
|
|
95222
|
-
debounceMs: number$1().nonnegative(),
|
|
95223
|
-
edges: array(_enum([
|
|
95224
|
-
"rising",
|
|
95225
|
-
"button",
|
|
95226
|
-
"native-event"
|
|
95227
|
-
])).min(1).optional(),
|
|
95228
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
95229
|
-
});
|
|
95230
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
95231
|
-
"in-addition",
|
|
95232
|
-
"confirm",
|
|
95233
|
-
"replace"
|
|
95234
|
-
]).optional() });
|
|
95235
|
-
discriminatedUnion("kind", [
|
|
95236
|
-
ScalarOverrideDraftSchema,
|
|
95237
|
-
KeyedArrayOverrideDraftSchema,
|
|
95238
|
-
PulseOverrideDraftSchema,
|
|
95239
|
-
object({
|
|
95240
|
-
kind: literal("span"),
|
|
95241
|
-
cap: string().min(1),
|
|
95242
|
-
targetId: string().min(1),
|
|
95243
|
-
timeoutMs: number$1().nonnegative(),
|
|
95244
|
-
cooldownMs: number$1().nonnegative().optional(),
|
|
95245
|
-
sources: array(SpanSourceDraftSchema)
|
|
95246
|
-
})
|
|
95247
|
-
]);
|
|
95248
|
-
object({
|
|
95249
|
-
targetId: string().min(1),
|
|
95250
|
-
configured: boolean(),
|
|
95251
|
-
source: _enum(["native", "assigned"]),
|
|
95252
|
-
state: _enum([
|
|
95253
|
-
"active",
|
|
95254
|
-
"stale",
|
|
95255
|
-
"released",
|
|
95256
|
-
"unavailable"
|
|
95257
|
-
]),
|
|
95258
|
-
reason: string().nullable()
|
|
95259
|
-
});
|
|
95260
95807
|
var G711_SCALE_CORRECTION_DB = {
|
|
95261
95808
|
PCMU: 20 * Math.log10(4),
|
|
95262
95809
|
PCMA: 20 * Math.log10(8)
|
|
@@ -97562,7 +98109,7 @@ var alexaConnectionSchema = object({
|
|
|
97562
98109
|
*/
|
|
97563
98110
|
refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)").meta({ secret: true }),
|
|
97564
98111
|
/** Device-registration secret some flows need. */
|
|
97565
|
-
macDms: string().default("").describe("macDms device-registration secret (optional)"),
|
|
98112
|
+
macDms: string().default("").describe("macDms device-registration secret (optional)").meta({ secret: true }),
|
|
97566
98113
|
/** Poll interval for state sync (Alexa has no clean 3rd-party push). */
|
|
97567
98114
|
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number$1().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
|
|
97568
98115
|
});
|
|
@@ -97580,7 +98127,9 @@ var AlexaBrokerEntrySchema = object({
|
|
|
97580
98127
|
id: string().min(1),
|
|
97581
98128
|
name: string().min(1),
|
|
97582
98129
|
connection: alexaConnectionSchema,
|
|
97583
|
-
integrationId: string().optional()
|
|
98130
|
+
integrationId: string().optional(),
|
|
98131
|
+
/** Absent means on; the instances page switches a connection off without deleting it. */
|
|
98132
|
+
enabled: boolean().optional()
|
|
97584
98133
|
});
|
|
97585
98134
|
/**
|
|
97586
98135
|
* Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
|
|
@@ -97617,7 +98166,6 @@ function readEchoTargets(config) {
|
|
|
97617
98166
|
}
|
|
97618
98167
|
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
97619
98168
|
var alexaAddonConfigSchema = object({
|
|
97620
|
-
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
97621
98169
|
proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
|
|
97622
98170
|
echoTargets: array(alexaEchoTargetSchema).default([])
|
|
97623
98171
|
});
|
|
@@ -141162,13 +141710,16 @@ var AlexaBrokerRegistry = class {
|
|
|
141162
141710
|
}
|
|
141163
141711
|
async restore(entries) {
|
|
141164
141712
|
for (const entry of entries) this.#seedCounter(entry.id);
|
|
141165
|
-
for (const entry of entries)
|
|
141166
|
-
|
|
141167
|
-
|
|
141168
|
-
|
|
141169
|
-
|
|
141170
|
-
|
|
141171
|
-
|
|
141713
|
+
for (const entry of entries) {
|
|
141714
|
+
if (entry.enabled === false) continue;
|
|
141715
|
+
try {
|
|
141716
|
+
await this.#startManager(entry);
|
|
141717
|
+
} catch (err) {
|
|
141718
|
+
this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
|
|
141719
|
+
tags: { brokerId: entry.id },
|
|
141720
|
+
meta: { error: errMsg(err) }
|
|
141721
|
+
});
|
|
141722
|
+
}
|
|
141172
141723
|
}
|
|
141173
141724
|
}
|
|
141174
141725
|
async shutdown() {
|
|
@@ -141202,7 +141753,8 @@ var AlexaBrokerRegistry = class {
|
|
|
141202
141753
|
this.#onStatesPolled = cb;
|
|
141203
141754
|
}
|
|
141204
141755
|
async createEntry(name, connection, opts = {}) {
|
|
141205
|
-
const id = this.#allocateId();
|
|
141756
|
+
const id = opts.id ?? this.#allocateId();
|
|
141757
|
+
if (opts.id !== void 0) this.#seedCounter(opts.id);
|
|
141206
141758
|
const entry = {
|
|
141207
141759
|
id,
|
|
141208
141760
|
name,
|
|
@@ -141215,7 +141767,7 @@ var AlexaBrokerRegistry = class {
|
|
|
141215
141767
|
}
|
|
141216
141768
|
async removeEntry(id) {
|
|
141217
141769
|
const mgr = this.#managers.get(id);
|
|
141218
|
-
if (!mgr)
|
|
141770
|
+
if (!mgr) return;
|
|
141219
141771
|
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
141220
141772
|
this.#managers.delete(id);
|
|
141221
141773
|
try {
|
|
@@ -141282,6 +141834,10 @@ var AlexaBrokerRegistry = class {
|
|
|
141282
141834
|
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
141283
141835
|
await mgr.start();
|
|
141284
141836
|
}
|
|
141837
|
+
/** The next free broker id, for a record minted before its manager exists. */
|
|
141838
|
+
allocateId() {
|
|
141839
|
+
return this.#allocateId();
|
|
141840
|
+
}
|
|
141285
141841
|
#allocateId() {
|
|
141286
141842
|
const id = `alexa_${String(this.#nextId).padStart(3, "0")}`;
|
|
141287
141843
|
this.#nextId++;
|
|
@@ -142703,10 +143259,12 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
142703
143259
|
var alexaBrokersEntryKind = defineEntryKind("brokers", {
|
|
142704
143260
|
title: "Alexa import brokers",
|
|
142705
143261
|
schema: entryBody({
|
|
142706
|
-
|
|
142707
|
-
integrationId: string().optional()
|
|
143262
|
+
...alexaConnectionSchema.shape,
|
|
143263
|
+
integrationId: string().optional(),
|
|
143264
|
+
enabled: boolean().optional()
|
|
142708
143265
|
}),
|
|
142709
|
-
writeTrigger: "an operator links or removes an Alexa account broker"
|
|
143266
|
+
writeTrigger: "an operator links or removes an Alexa account broker",
|
|
143267
|
+
retiredSecretPaths: ["connection.cookie", "connection.refreshToken"]
|
|
142710
143268
|
});
|
|
142711
143269
|
var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
|
|
142712
143270
|
title: "Alexa Echo speech sessions",
|
|
@@ -142719,13 +143277,118 @@ var alexaRecordKinds = {
|
|
|
142719
143277
|
"speak-sessions": alexaSpeakSessionsEntryKind
|
|
142720
143278
|
};
|
|
142721
143279
|
//#endregion
|
|
143280
|
+
//#region src/alexa-instances.ts
|
|
143281
|
+
/**
|
|
143282
|
+
* The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
|
|
143283
|
+
* record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
|
|
143284
|
+
* the form the instances page edits is the stored shape. Signing in (device code or the Amazon
|
|
143285
|
+
* login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
|
|
143286
|
+
*/
|
|
143287
|
+
var ALEXA_INSTANCE_KIND = "alexa";
|
|
143288
|
+
var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
|
|
143289
|
+
function isAlexaBodyConfigured(body) {
|
|
143290
|
+
return body.cookie.length > 0 || body.refreshToken.length > 0;
|
|
143291
|
+
}
|
|
143292
|
+
function statusOf(info, configured) {
|
|
143293
|
+
if (!configured) return {
|
|
143294
|
+
state: "unknown",
|
|
143295
|
+
label: "Not signed in"
|
|
143296
|
+
};
|
|
143297
|
+
if (info === null) return {
|
|
143298
|
+
state: "unknown",
|
|
143299
|
+
label: "Not connected"
|
|
143300
|
+
};
|
|
143301
|
+
if (info.status === "connected") return {
|
|
143302
|
+
state: "ok",
|
|
143303
|
+
label: "Connected"
|
|
143304
|
+
};
|
|
143305
|
+
if (info.status === "connecting") return {
|
|
143306
|
+
state: "unknown",
|
|
143307
|
+
label: "Connecting"
|
|
143308
|
+
};
|
|
143309
|
+
return {
|
|
143310
|
+
state: "error",
|
|
143311
|
+
label: info.status,
|
|
143312
|
+
...info.error !== null ? { detail: info.error } : {}
|
|
143313
|
+
};
|
|
143314
|
+
}
|
|
143315
|
+
function buildAlexaInstances(records, deps) {
|
|
143316
|
+
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143317
|
+
description: {
|
|
143318
|
+
instanceMode: "multiple",
|
|
143319
|
+
kind: "account",
|
|
143320
|
+
canTest: true,
|
|
143321
|
+
kinds: [{
|
|
143322
|
+
id: ALEXA_INSTANCE_KIND,
|
|
143323
|
+
label: "Alexa",
|
|
143324
|
+
signIn: {
|
|
143325
|
+
basePath: ALEXA_SIGN_IN_PATH,
|
|
143326
|
+
methods: ["device-code", "proxy"]
|
|
143327
|
+
}
|
|
143328
|
+
}],
|
|
143329
|
+
allowRestoreId: true
|
|
143330
|
+
},
|
|
143331
|
+
formSchema: () => buildConnectionFormSchema(),
|
|
143332
|
+
initialBody: (name) => ({
|
|
143333
|
+
name,
|
|
143334
|
+
amazonDomain: "amazon.com",
|
|
143335
|
+
cookie: "",
|
|
143336
|
+
refreshToken: "",
|
|
143337
|
+
macDms: "",
|
|
143338
|
+
pollIntervalMs: 6e4
|
|
143339
|
+
}),
|
|
143340
|
+
mintId: () => deps.mintId(),
|
|
143341
|
+
kindOf: () => ALEXA_INSTANCE_KIND,
|
|
143342
|
+
health: (body, id) => {
|
|
143343
|
+
const info = deps.infoOf(id);
|
|
143344
|
+
return {
|
|
143345
|
+
status: statusOf(info, isAlexaBodyConfigured(body)),
|
|
143346
|
+
...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
|
|
143347
|
+
};
|
|
143348
|
+
},
|
|
143349
|
+
configured: isAlexaBodyConfigured,
|
|
143350
|
+
onChanged: deps.onChanged,
|
|
143351
|
+
test: async ({ id, values }) => {
|
|
143352
|
+
if (["cookie", "refreshToken"].some((key) => {
|
|
143353
|
+
const v = values[key];
|
|
143354
|
+
return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
|
|
143355
|
+
})) return {
|
|
143356
|
+
success: true,
|
|
143357
|
+
status: "inconclusive",
|
|
143358
|
+
error: "Save the account to check the sign-in"
|
|
143359
|
+
};
|
|
143360
|
+
const info = id === null ? null : deps.infoOf(id);
|
|
143361
|
+
if (info === null) return {
|
|
143362
|
+
success: false,
|
|
143363
|
+
status: "inconclusive",
|
|
143364
|
+
error: "Not connected: sign in first"
|
|
143365
|
+
};
|
|
143366
|
+
return info.status === "connected" ? {
|
|
143367
|
+
success: true,
|
|
143368
|
+
status: "validated"
|
|
143369
|
+
} : {
|
|
143370
|
+
success: false,
|
|
143371
|
+
status: "rejected",
|
|
143372
|
+
error: info.error ?? `status: ${info.status}`
|
|
143373
|
+
};
|
|
143374
|
+
}
|
|
143375
|
+
});
|
|
143376
|
+
}
|
|
143377
|
+
//#endregion
|
|
142722
143378
|
//#region src/alexa-broker-store.ts
|
|
142723
143379
|
function rowToEntry(id, data) {
|
|
142724
143380
|
const parsed = AlexaBrokerEntrySchema.safeParse({
|
|
142725
143381
|
id,
|
|
142726
143382
|
name: data.name,
|
|
142727
|
-
connection:
|
|
142728
|
-
|
|
143383
|
+
connection: {
|
|
143384
|
+
amazonDomain: data.amazonDomain,
|
|
143385
|
+
cookie: data.cookie,
|
|
143386
|
+
refreshToken: data.refreshToken,
|
|
143387
|
+
macDms: data.macDms,
|
|
143388
|
+
pollIntervalMs: data.pollIntervalMs
|
|
143389
|
+
},
|
|
143390
|
+
integrationId: data.integrationId,
|
|
143391
|
+
enabled: data.enabled
|
|
142729
143392
|
});
|
|
142730
143393
|
return parsed.success ? parsed.data : null;
|
|
142731
143394
|
}
|
|
@@ -142753,16 +143416,20 @@ var AlexaBrokerStore = class AlexaBrokerStore {
|
|
|
142753
143416
|
}
|
|
142754
143417
|
return out;
|
|
142755
143418
|
}
|
|
143419
|
+
async getById(id) {
|
|
143420
|
+
return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
|
|
143421
|
+
}
|
|
142756
143422
|
async replaceAll(entries) {
|
|
142757
143423
|
const page = await this.kind.query({ limit: 1e3 });
|
|
142758
143424
|
const nextIds = new Set(entries.map((e) => e.id));
|
|
142759
143425
|
for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
|
|
142760
143426
|
for (const entry of entries) {
|
|
142761
|
-
const { id, name, connection, integrationId } = entry;
|
|
143427
|
+
const { id, name, connection, integrationId, enabled } = entry;
|
|
142762
143428
|
const body = {
|
|
142763
143429
|
name,
|
|
142764
|
-
connection,
|
|
142765
|
-
...integrationId === void 0 ? {} : { integrationId }
|
|
143430
|
+
...connection,
|
|
143431
|
+
...integrationId === void 0 ? {} : { integrationId },
|
|
143432
|
+
...enabled === void 0 ? {} : { enabled }
|
|
142766
143433
|
};
|
|
142767
143434
|
const parsed = alexaBrokersEntryKind.schema.safeParse(body);
|
|
142768
143435
|
if (!parsed.success) throw new Error(`invalid Alexa broker '${id}': ${parsed.error.message}`);
|
|
@@ -144150,6 +144817,34 @@ var AlexaSpeakController = class {
|
|
|
144150
144817
|
}
|
|
144151
144818
|
};
|
|
144152
144819
|
//#endregion
|
|
144820
|
+
//#region src/migrations/flatten-broker-records.ts
|
|
144821
|
+
/**
|
|
144822
|
+
* The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
|
|
144823
|
+
* object (`amazonDomain`, `cookie`, `refreshToken`, `macDms`, `pollIntervalMs`) becomes top-level
|
|
144824
|
+
* fields under the SAME record id (`alexa_NNN`). `macDms`, which used to be stored in plain text,
|
|
144825
|
+
* becomes a keychain secret. The work is `flattenNestedRecords`: every secret is read before
|
|
144826
|
+
* anything is written, read back from its new ref before the nested object goes, and the old refs
|
|
144827
|
+
* are deleted last.
|
|
144828
|
+
*/
|
|
144829
|
+
var FLATTEN_BROKER_RECORDS_ID = "import-alexa/2026-11-04-flatten-broker-records";
|
|
144830
|
+
async function flattenBrokerRecords(ports) {
|
|
144831
|
+
return flattenNestedRecords(ports, {
|
|
144832
|
+
legacyKey: "connection",
|
|
144833
|
+
flatSecretKeys: [
|
|
144834
|
+
"cookie",
|
|
144835
|
+
"refreshToken",
|
|
144836
|
+
"macDms"
|
|
144837
|
+
],
|
|
144838
|
+
toFlat: (legacy) => Object.fromEntries([
|
|
144839
|
+
"amazonDomain",
|
|
144840
|
+
"cookie",
|
|
144841
|
+
"refreshToken",
|
|
144842
|
+
"macDms",
|
|
144843
|
+
"pollIntervalMs"
|
|
144844
|
+
].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
|
|
144845
|
+
});
|
|
144846
|
+
}
|
|
144847
|
+
//#endregion
|
|
144153
144848
|
//#region src/migrations/strip-echo-target-volume-and-mode.ts
|
|
144154
144849
|
var RETIRED_KEYS = ["volume", "mode"];
|
|
144155
144850
|
function isRecord(value) {
|
|
@@ -144199,6 +144894,21 @@ var ADDON_MIGRATIONS = [{
|
|
|
144199
144894
|
fixtureMark: "the Echo id and the name the migration does not know"
|
|
144200
144895
|
},
|
|
144201
144896
|
procedure: stripEchoTargetVolumeAndMode
|
|
144897
|
+
}, {
|
|
144898
|
+
meta: {
|
|
144899
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
144900
|
+
owner: "import-alexa",
|
|
144901
|
+
level: "addon-state",
|
|
144902
|
+
seq: 2,
|
|
144903
|
+
dependsOn: [],
|
|
144904
|
+
store: "settings",
|
|
144905
|
+
scope: "cluster",
|
|
144906
|
+
onFailure: "block-owner",
|
|
144907
|
+
bodyKind: "procedure",
|
|
144908
|
+
description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
|
|
144909
|
+
fixtureMark: "the integration link and the amazon domain of each account"
|
|
144910
|
+
},
|
|
144911
|
+
procedure: flattenBrokerRecords
|
|
144202
144912
|
}];
|
|
144203
144913
|
//#endregion
|
|
144204
144914
|
//#region src/speak/icon-routes.ts
|
|
@@ -144253,7 +144963,6 @@ var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
|
|
|
144253
144963
|
//#region src/addon.ts
|
|
144254
144964
|
/** Default config — a fresh install starts with no accounts. */
|
|
144255
144965
|
var DEFAULTS = {
|
|
144256
|
-
brokers: [],
|
|
144257
144966
|
proxyLogin: { proxyPort: 3456 },
|
|
144258
144967
|
echoTargets: []
|
|
144259
144968
|
};
|
|
@@ -144301,6 +145010,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144301
145010
|
loginPlane = null;
|
|
144302
145011
|
loginProxy = null;
|
|
144303
145012
|
brokerStore = null;
|
|
145013
|
+
instances = null;
|
|
144304
145014
|
activeBrokers = [];
|
|
144305
145015
|
speak = null;
|
|
144306
145016
|
constructor() {
|
|
@@ -144312,6 +145022,12 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144312
145022
|
if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
|
|
144313
145023
|
await records.declare(alexaRecordKinds);
|
|
144314
145024
|
this.brokerStore = AlexaBrokerStore.of(records);
|
|
145025
|
+
this.instances = buildAlexaInstances(records, {
|
|
145026
|
+
mintId: () => this.requireRegistry().allocateId(),
|
|
145027
|
+
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
145028
|
+
onChanged: (id) => this.syncBrokerRuntime(id)
|
|
145029
|
+
});
|
|
145030
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
|
|
144315
145031
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
144316
145032
|
this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
|
|
144317
145033
|
dataDir: this.ctx.dataDir,
|
|
@@ -144356,12 +145072,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144356
145072
|
deviceCode: this.deviceCodeLogin,
|
|
144357
145073
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
144358
145074
|
}), ...speakIconRoutes()];
|
|
144359
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS,
|
|
144360
|
-
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
144361
|
-
writeRows: async (rows) => {
|
|
144362
|
-
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
144363
|
-
}
|
|
144364
|
-
}));
|
|
145075
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
|
|
144365
145076
|
const speak = new AlexaSpeakController({
|
|
144366
145077
|
logger: this.ctx.logger.child("speak"),
|
|
144367
145078
|
readConfig: () => this.config,
|
|
@@ -144391,6 +145102,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144391
145102
|
capability: brokerCapability,
|
|
144392
145103
|
provider: this.buildBrokerProvider()
|
|
144393
145104
|
},
|
|
145105
|
+
{
|
|
145106
|
+
capability: integrationInstancesCapability,
|
|
145107
|
+
provider: this.requireInstances().toProvider()
|
|
145108
|
+
},
|
|
144394
145109
|
{
|
|
144395
145110
|
capability: deviceAdoptionCapability,
|
|
144396
145111
|
provider: this.buildAdoptionProvider()
|
|
@@ -144405,19 +145120,65 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144405
145120
|
}
|
|
144406
145121
|
] };
|
|
144407
145122
|
}
|
|
144408
|
-
|
|
144409
|
-
|
|
144410
|
-
|
|
144411
|
-
|
|
144412
|
-
|
|
145123
|
+
migrationPorts(records) {
|
|
145124
|
+
if (this.ctx.secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
|
|
145125
|
+
return {
|
|
145126
|
+
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
145127
|
+
writeRows: async (rows) => {
|
|
145128
|
+
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
145129
|
+
},
|
|
145130
|
+
...flattenPortsOver(records.kind(alexaBrokersEntryKind))
|
|
145131
|
+
};
|
|
145132
|
+
}
|
|
145133
|
+
/**
|
|
145134
|
+
* Bring the runtime in line with one record after the instances page changed it: a removed
|
|
145135
|
+
* record stops its manager and cascades its devices, an off record only stops the manager, a
|
|
145136
|
+
* live one re-applies its connection or starts under the record's own id.
|
|
145137
|
+
*/
|
|
145138
|
+
async syncBrokerRuntime(id) {
|
|
145139
|
+
const reg = this.requireRegistry();
|
|
145140
|
+
const entry = await this.requireBrokerStore().getById(id);
|
|
145141
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
145142
|
+
if (entry === null) {
|
|
145143
|
+
await reg.removeEntry(id);
|
|
145144
|
+
await this.cascadeRemoveDevicesForBroker(id);
|
|
145145
|
+
this.speak?.brokersChanged();
|
|
145146
|
+
return;
|
|
144413
145147
|
}
|
|
144414
|
-
|
|
145148
|
+
const configured = entry.connection.cookie.length > 0 || entry.connection.refreshToken.length > 0;
|
|
145149
|
+
if (entry.enabled === false || !configured) {
|
|
145150
|
+
await reg.removeEntry(id);
|
|
145151
|
+
this.speak?.brokersChanged();
|
|
145152
|
+
return;
|
|
145153
|
+
}
|
|
145154
|
+
try {
|
|
145155
|
+
if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
|
|
145156
|
+
else await reg.createEntry(entry.name, entry.connection, {
|
|
145157
|
+
id,
|
|
145158
|
+
...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
|
|
145159
|
+
});
|
|
145160
|
+
} catch (err) {
|
|
145161
|
+
this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
|
|
145162
|
+
brokerId: id,
|
|
145163
|
+
error: errMsg(err)
|
|
145164
|
+
} });
|
|
145165
|
+
}
|
|
145166
|
+
this.speak?.brokersChanged();
|
|
145167
|
+
}
|
|
145168
|
+
requireBrokerStore() {
|
|
145169
|
+
if (this.brokerStore === null) throw new Error("import-alexa: broker store not ready");
|
|
145170
|
+
return this.brokerStore;
|
|
145171
|
+
}
|
|
145172
|
+
requireInstances() {
|
|
145173
|
+
if (this.instances === null) throw new Error("import-alexa: instances not ready");
|
|
145174
|
+
return this.instances;
|
|
145175
|
+
}
|
|
145176
|
+
async loadPersistedBrokers() {
|
|
145177
|
+
return this.requireBrokerStore().listResolved();
|
|
144415
145178
|
}
|
|
144416
145179
|
async persistBrokerEntries(entries) {
|
|
144417
145180
|
this.activeBrokers = [...entries];
|
|
144418
|
-
|
|
144419
|
-
if (store !== null) await store.replaceAll(entries);
|
|
144420
|
-
else await this.updateGlobalSettings({ brokers: [...entries] });
|
|
145181
|
+
await this.requireBrokerStore().replaceAll(entries);
|
|
144421
145182
|
this.speak?.brokersChanged();
|
|
144422
145183
|
}
|
|
144423
145184
|
async onConfigChanged() {
|
|
@@ -144436,6 +145197,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144436
145197
|
} });
|
|
144437
145198
|
}
|
|
144438
145199
|
for (const entry of persisted) {
|
|
145200
|
+
if (entry.enabled === false) continue;
|
|
144439
145201
|
if (entry.id && liveIds.has(entry.id)) {
|
|
144440
145202
|
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
144441
145203
|
try {
|