@camstack/addon-import-alexa 0.2.169 → 0.2.171
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 +12 -0
- package/dist/addon.js +1115 -196
- package/dist/addon.mjs +1115 -196
- package/i18n/en.json +0 -1
- package/i18n/it.json +0 -1
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -5651,7 +5651,7 @@ function number(params) {
|
|
|
5651
5651
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
5652
5652
|
}
|
|
5653
5653
|
//#endregion
|
|
5654
|
-
//#region ../types/dist/sleep-
|
|
5654
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5655
5655
|
/**
|
|
5656
5656
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5657
5657
|
* window to float samples (D455).
|
|
@@ -7718,6 +7718,7 @@ var BaseAddon = class {
|
|
|
7718
7718
|
* a warning and falls back rather than crashing boot.
|
|
7719
7719
|
*/
|
|
7720
7720
|
state(key, schema, fallback) {
|
|
7721
|
+
this._declaredStateKeys.add(key);
|
|
7721
7722
|
return createDurableState({
|
|
7722
7723
|
key,
|
|
7723
7724
|
schema,
|
|
@@ -7733,6 +7734,96 @@ var BaseAddon = class {
|
|
|
7733
7734
|
} })
|
|
7734
7735
|
});
|
|
7735
7736
|
}
|
|
7737
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7738
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7739
|
+
/**
|
|
7740
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7741
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7742
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7743
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7744
|
+
*/
|
|
7745
|
+
async getDeclaredSettingsKeys() {
|
|
7746
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7747
|
+
}
|
|
7748
|
+
declaredKeySet() {
|
|
7749
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7750
|
+
const schema = this.globalSettingsSchema();
|
|
7751
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7752
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7753
|
+
for (const path of this.secretStatePaths) {
|
|
7754
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7755
|
+
if (root) keys.add(root);
|
|
7756
|
+
}
|
|
7757
|
+
return keys;
|
|
7758
|
+
}
|
|
7759
|
+
/**
|
|
7760
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7761
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7762
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7763
|
+
*/
|
|
7764
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7765
|
+
const settings = this._ctx?.settings;
|
|
7766
|
+
const raw = this.rawSettings();
|
|
7767
|
+
if (!settings || !raw) return {
|
|
7768
|
+
removed: [],
|
|
7769
|
+
refused: keys.map((key) => ({
|
|
7770
|
+
key,
|
|
7771
|
+
reason: "no settings store"
|
|
7772
|
+
})),
|
|
7773
|
+
secretsCleared: 0
|
|
7774
|
+
};
|
|
7775
|
+
let stored;
|
|
7776
|
+
try {
|
|
7777
|
+
stored = await raw.readAddonStore();
|
|
7778
|
+
} catch (error) {
|
|
7779
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7780
|
+
return {
|
|
7781
|
+
removed: [],
|
|
7782
|
+
refused: keys.map((key) => ({
|
|
7783
|
+
key,
|
|
7784
|
+
reason
|
|
7785
|
+
})),
|
|
7786
|
+
secretsCleared: 0
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
const declared = this.declaredKeySet();
|
|
7790
|
+
const removed = [];
|
|
7791
|
+
const refused = [];
|
|
7792
|
+
for (const key of keys) {
|
|
7793
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7794
|
+
if (declared.has(base)) refused.push({
|
|
7795
|
+
key,
|
|
7796
|
+
reason: "still declared by the addon"
|
|
7797
|
+
});
|
|
7798
|
+
else if (!(key in stored)) refused.push({
|
|
7799
|
+
key,
|
|
7800
|
+
reason: "not stored"
|
|
7801
|
+
});
|
|
7802
|
+
else removed.push(key);
|
|
7803
|
+
}
|
|
7804
|
+
if (removed.length === 0) return {
|
|
7805
|
+
removed,
|
|
7806
|
+
refused,
|
|
7807
|
+
secretsCleared: 0
|
|
7808
|
+
};
|
|
7809
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7810
|
+
const held = /* @__PURE__ */ new Set();
|
|
7811
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7812
|
+
const target = removed.includes(key) ? held : kept;
|
|
7813
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7814
|
+
}
|
|
7815
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7816
|
+
const patch = {};
|
|
7817
|
+
for (const key of removed) patch[key] = void 0;
|
|
7818
|
+
await settings.writeAddonStore(patch);
|
|
7819
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7820
|
+
await this.resolveConfig();
|
|
7821
|
+
return {
|
|
7822
|
+
removed,
|
|
7823
|
+
refused,
|
|
7824
|
+
secretsCleared
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7736
7827
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7737
7828
|
deviceState(deviceId, key, schema, fallback) {
|
|
7738
7829
|
return createDurableState({
|
|
@@ -8003,6 +8094,21 @@ function dropPerNodeFields(fields) {
|
|
|
8003
8094
|
}
|
|
8004
8095
|
return kept;
|
|
8005
8096
|
}
|
|
8097
|
+
function collectFieldKeys(fields) {
|
|
8098
|
+
const collected = [];
|
|
8099
|
+
for (const field of fields) {
|
|
8100
|
+
if (field.type === "group") {
|
|
8101
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
8102
|
+
continue;
|
|
8103
|
+
}
|
|
8104
|
+
if (field.type === "sub-tabs") {
|
|
8105
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
8106
|
+
continue;
|
|
8107
|
+
}
|
|
8108
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8109
|
+
}
|
|
8110
|
+
return collected;
|
|
8111
|
+
}
|
|
8006
8112
|
function collectPerNodeFieldKeys(fields) {
|
|
8007
8113
|
const collected = [];
|
|
8008
8114
|
for (const field of fields) {
|
|
@@ -16557,8 +16663,172 @@ object({
|
|
|
16557
16663
|
quantity: ReadingQuantitySchema,
|
|
16558
16664
|
unit: string().min(1)
|
|
16559
16665
|
});
|
|
16560
|
-
|
|
16561
|
-
|
|
16666
|
+
new Set([
|
|
16667
|
+
{
|
|
16668
|
+
id: "person",
|
|
16669
|
+
name: "Person"
|
|
16670
|
+
},
|
|
16671
|
+
{
|
|
16672
|
+
id: "vehicle",
|
|
16673
|
+
name: "Vehicle"
|
|
16674
|
+
},
|
|
16675
|
+
{
|
|
16676
|
+
id: "animal",
|
|
16677
|
+
name: "Animal"
|
|
16678
|
+
},
|
|
16679
|
+
{
|
|
16680
|
+
id: "package",
|
|
16681
|
+
name: "Package"
|
|
16682
|
+
}
|
|
16683
|
+
].map((l) => l.id));
|
|
16684
|
+
var COCO_TO_MACRO = {
|
|
16685
|
+
mapping: {
|
|
16686
|
+
person: "person",
|
|
16687
|
+
bicycle: "vehicle",
|
|
16688
|
+
car: "vehicle",
|
|
16689
|
+
motorcycle: "vehicle",
|
|
16690
|
+
airplane: "vehicle",
|
|
16691
|
+
bus: "vehicle",
|
|
16692
|
+
train: "vehicle",
|
|
16693
|
+
truck: "vehicle",
|
|
16694
|
+
boat: "vehicle",
|
|
16695
|
+
bird: "animal",
|
|
16696
|
+
cat: "animal",
|
|
16697
|
+
dog: "animal",
|
|
16698
|
+
horse: "animal",
|
|
16699
|
+
sheep: "animal",
|
|
16700
|
+
cow: "animal",
|
|
16701
|
+
elephant: "animal",
|
|
16702
|
+
bear: "animal",
|
|
16703
|
+
zebra: "animal",
|
|
16704
|
+
giraffe: "animal",
|
|
16705
|
+
suitcase: "package",
|
|
16706
|
+
backpack: "package",
|
|
16707
|
+
handbag: "package"
|
|
16708
|
+
},
|
|
16709
|
+
preserveOriginal: false
|
|
16710
|
+
};
|
|
16711
|
+
/**
|
|
16712
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16713
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16714
|
+
*
|
|
16715
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16716
|
+
* second list. The label that fits depends on the macro class:
|
|
16717
|
+
*
|
|
16718
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16719
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16720
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16721
|
+
* - `package` → a sub-class.
|
|
16722
|
+
*
|
|
16723
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16724
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16725
|
+
*
|
|
16726
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16727
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16728
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16729
|
+
* the subject GETS it — not at the first detection.
|
|
16730
|
+
*/
|
|
16731
|
+
var SubClassLabelSchema = object({
|
|
16732
|
+
kind: literal("sub-class"),
|
|
16733
|
+
className: string().min(1)
|
|
16734
|
+
});
|
|
16735
|
+
var IdentityLabelSchema = object({
|
|
16736
|
+
kind: literal("identity"),
|
|
16737
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16738
|
+
identityId: string().min(1),
|
|
16739
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16740
|
+
name: string().min(1)
|
|
16741
|
+
});
|
|
16742
|
+
var PlateLabelSchema = object({
|
|
16743
|
+
kind: literal("plate"),
|
|
16744
|
+
text: string().min(1)
|
|
16745
|
+
});
|
|
16746
|
+
function macroOf$1(className) {
|
|
16747
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16748
|
+
}
|
|
16749
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16750
|
+
function subClassBelongs(macro, label) {
|
|
16751
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16752
|
+
}
|
|
16753
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16754
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16755
|
+
object({
|
|
16756
|
+
macro: literal("animal"),
|
|
16757
|
+
label: subClassOf("animal").optional()
|
|
16758
|
+
}),
|
|
16759
|
+
object({
|
|
16760
|
+
macro: literal("person"),
|
|
16761
|
+
label: IdentityLabelSchema.optional()
|
|
16762
|
+
}),
|
|
16763
|
+
object({
|
|
16764
|
+
macro: literal("vehicle"),
|
|
16765
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16766
|
+
}),
|
|
16767
|
+
object({
|
|
16768
|
+
macro: literal("package"),
|
|
16769
|
+
label: subClassOf("package").optional()
|
|
16770
|
+
}),
|
|
16771
|
+
object({
|
|
16772
|
+
macro: literal("other"),
|
|
16773
|
+
className: string().min(1)
|
|
16774
|
+
})
|
|
16775
|
+
]);
|
|
16776
|
+
/**
|
|
16777
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16778
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16779
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16780
|
+
*/
|
|
16781
|
+
var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
|
|
16782
|
+
/**
|
|
16783
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16784
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16785
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16786
|
+
*/
|
|
16787
|
+
function subjectFromClassName(className) {
|
|
16788
|
+
const name = className.trim().toLowerCase();
|
|
16789
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16790
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16791
|
+
if (macro === void 0) return {
|
|
16792
|
+
macro: "other",
|
|
16793
|
+
className
|
|
16794
|
+
};
|
|
16795
|
+
const label = {
|
|
16796
|
+
kind: "sub-class",
|
|
16797
|
+
className: name
|
|
16798
|
+
};
|
|
16799
|
+
switch (macro) {
|
|
16800
|
+
case "animal": return {
|
|
16801
|
+
macro: "animal",
|
|
16802
|
+
label
|
|
16803
|
+
};
|
|
16804
|
+
case "vehicle": return {
|
|
16805
|
+
macro: "vehicle",
|
|
16806
|
+
label
|
|
16807
|
+
};
|
|
16808
|
+
case "package": return {
|
|
16809
|
+
macro: "package",
|
|
16810
|
+
label
|
|
16811
|
+
};
|
|
16812
|
+
default: return {
|
|
16813
|
+
macro: "other",
|
|
16814
|
+
className
|
|
16815
|
+
};
|
|
16816
|
+
}
|
|
16817
|
+
}
|
|
16818
|
+
function macroOnly(macro) {
|
|
16819
|
+
switch (macro) {
|
|
16820
|
+
case "animal": return { macro: "animal" };
|
|
16821
|
+
case "person": return { macro: "person" };
|
|
16822
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16823
|
+
case "package": return { macro: "package" };
|
|
16824
|
+
default: return {
|
|
16825
|
+
macro: "other",
|
|
16826
|
+
className: macro
|
|
16827
|
+
};
|
|
16828
|
+
}
|
|
16829
|
+
}
|
|
16830
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16831
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16562
16832
|
var AutomationConditionOperatorSchema = _enum([
|
|
16563
16833
|
"eq",
|
|
16564
16834
|
"ne",
|
|
@@ -16569,12 +16839,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16569
16839
|
"contains",
|
|
16570
16840
|
"in"
|
|
16571
16841
|
]);
|
|
16572
|
-
|
|
16573
|
-
|
|
16842
|
+
/**
|
|
16843
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16844
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16845
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16846
|
+
* fire; it does not lose it.
|
|
16847
|
+
*/
|
|
16848
|
+
var DeviceBecomesLeafSchema = object({
|
|
16849
|
+
kind: literal("device-becomes"),
|
|
16850
|
+
deviceId: number$1().int().nonnegative(),
|
|
16851
|
+
cap: string().min(1),
|
|
16852
|
+
fieldPath: string().min(1),
|
|
16853
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16854
|
+
becomes: union([
|
|
16855
|
+
string(),
|
|
16856
|
+
number$1(),
|
|
16857
|
+
boolean()
|
|
16858
|
+
]).optional(),
|
|
16859
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16860
|
+
edge: boolean().optional(),
|
|
16861
|
+
/** The new value must hold this long before it counts. */
|
|
16862
|
+
forMs: number$1().int().min(0).max(864e5).optional(),
|
|
16863
|
+
/** Collapse a burst into one occurrence. */
|
|
16864
|
+
debounceMs: number$1().int().min(0).max(6e5).optional()
|
|
16865
|
+
});
|
|
16866
|
+
var DeviceEventLeafSchema = object({
|
|
16867
|
+
kind: literal("device-event"),
|
|
16868
|
+
/** An `EventCategory` value. */
|
|
16869
|
+
category: string().min(1),
|
|
16870
|
+
deviceId: number$1().int().nonnegative().optional()
|
|
16871
|
+
});
|
|
16872
|
+
var ScheduleLeafSchema = object({
|
|
16873
|
+
kind: literal("schedule"),
|
|
16874
|
+
cron: string().min(1).max(120)
|
|
16875
|
+
});
|
|
16876
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16877
|
+
var IntervalLeafSchema = object({
|
|
16878
|
+
kind: literal("interval"),
|
|
16879
|
+
minutes: number$1().int().min(1).max(1440)
|
|
16880
|
+
});
|
|
16881
|
+
var DetectionLeafSchema = object({
|
|
16882
|
+
kind: literal("detection"),
|
|
16883
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16884
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16885
|
+
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16886
|
+
/** The track must have visited at least one of these zones. */
|
|
16887
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16888
|
+
/** Best-so-far confidence the track must reach. */
|
|
16889
|
+
minScore: number$1().min(0).max(1).optional()
|
|
16890
|
+
});
|
|
16891
|
+
var StationaryLeafSchema = object({
|
|
16892
|
+
kind: literal("stationary"),
|
|
16893
|
+
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16894
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16895
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16896
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16897
|
+
phase: _enum(["appeared", "departed"])
|
|
16898
|
+
});
|
|
16899
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16900
|
+
var DeviceIsLeafSchema = object({
|
|
16901
|
+
kind: literal("device-is"),
|
|
16574
16902
|
deviceId: number$1().int().nonnegative(),
|
|
16575
16903
|
cap: string().min(1),
|
|
16576
16904
|
fieldPath: string().min(1),
|
|
16577
16905
|
operator: AutomationConditionOperatorSchema,
|
|
16906
|
+
/**
|
|
16907
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16908
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16909
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16910
|
+
* one is never evaluated (it fails).
|
|
16911
|
+
*/
|
|
16578
16912
|
value: union([
|
|
16579
16913
|
string(),
|
|
16580
16914
|
number$1(),
|
|
@@ -16582,24 +16916,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16582
16916
|
array(union([string(), number$1()])),
|
|
16583
16917
|
QuantityValueSchema
|
|
16584
16918
|
]),
|
|
16919
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16585
16920
|
unit: string().min(1).optional()
|
|
16586
16921
|
});
|
|
16587
|
-
|
|
16922
|
+
/**
|
|
16923
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16924
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16925
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16926
|
+
*/
|
|
16927
|
+
var StationaryPresentLeafSchema = object({
|
|
16588
16928
|
kind: literal("stationary-present"),
|
|
16589
16929
|
deviceId: number$1().int().nonnegative(),
|
|
16590
|
-
|
|
16591
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16592
|
-
/** The parked object must be in at least one of these zones. */
|
|
16930
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16593
16931
|
zones: array(string().min(1)).min(1).optional()
|
|
16594
16932
|
});
|
|
16595
16933
|
/**
|
|
16596
|
-
* The
|
|
16597
|
-
*
|
|
16598
|
-
*
|
|
16599
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16600
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16934
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16935
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16936
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16601
16937
|
*/
|
|
16602
|
-
var
|
|
16938
|
+
var ExpressionLeafSchema = object({
|
|
16603
16939
|
kind: literal("expression"),
|
|
16604
16940
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16605
16941
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16611,55 +16947,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16611
16947
|
path: ["expr"]
|
|
16612
16948
|
});
|
|
16613
16949
|
});
|
|
16614
|
-
var
|
|
16950
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16615
16951
|
object({
|
|
16616
16952
|
kind: literal("all"),
|
|
16617
|
-
children: array(
|
|
16953
|
+
children: array(AutomationWhenSchema)
|
|
16618
16954
|
}),
|
|
16619
16955
|
object({
|
|
16620
16956
|
kind: literal("any"),
|
|
16621
|
-
children: array(
|
|
16957
|
+
children: array(AutomationWhenSchema)
|
|
16622
16958
|
}),
|
|
16623
16959
|
object({
|
|
16624
16960
|
kind: literal("not"),
|
|
16625
|
-
child:
|
|
16961
|
+
child: AutomationWhenSchema
|
|
16626
16962
|
}),
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16963
|
+
DeviceBecomesLeafSchema,
|
|
16964
|
+
DeviceEventLeafSchema,
|
|
16965
|
+
ScheduleLeafSchema,
|
|
16966
|
+
ManualLeafSchema,
|
|
16967
|
+
IntervalLeafSchema,
|
|
16968
|
+
DetectionLeafSchema,
|
|
16969
|
+
StationaryLeafSchema,
|
|
16970
|
+
DeviceIsLeafSchema,
|
|
16971
|
+
StationaryPresentLeafSchema,
|
|
16972
|
+
ExpressionLeafSchema
|
|
16630
16973
|
]));
|
|
16631
16974
|
/**
|
|
16632
|
-
*
|
|
16633
|
-
*
|
|
16634
|
-
*
|
|
16635
|
-
*
|
|
16636
|
-
*
|
|
16637
|
-
*
|
|
16638
|
-
*
|
|
16639
|
-
*
|
|
16640
|
-
|
|
16641
|
-
|
|
16975
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16976
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16977
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16978
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16979
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16980
|
+
* way. Nothing writes this shape any more.
|
|
16981
|
+
*
|
|
16982
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16983
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16984
|
+
* a single trigger is not wrapped in `any`.
|
|
16985
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16986
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16987
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16988
|
+
*/
|
|
16989
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16642
16990
|
object({
|
|
16643
16991
|
kind: literal("device-state"),
|
|
16644
16992
|
deviceId: number$1().int().nonnegative(),
|
|
16645
16993
|
cap: string().min(1),
|
|
16646
16994
|
fieldPath: string().min(1),
|
|
16647
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16648
16995
|
becomes: union([
|
|
16649
16996
|
string(),
|
|
16650
16997
|
number$1(),
|
|
16651
16998
|
boolean()
|
|
16652
16999
|
]).optional(),
|
|
16653
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16654
17000
|
edge: boolean().optional(),
|
|
16655
|
-
/** The condition must hold this long before the run starts. */
|
|
16656
17001
|
forMs: number$1().int().min(0).max(864e5).optional(),
|
|
16657
|
-
/** Collapse a burst into one run. */
|
|
16658
17002
|
debounceMs: number$1().int().min(0).max(6e5).optional()
|
|
16659
17003
|
}),
|
|
16660
17004
|
object({
|
|
16661
17005
|
kind: literal("device-event"),
|
|
16662
|
-
/** An `EventCategory` value. */
|
|
16663
17006
|
category: string().min(1),
|
|
16664
17007
|
deviceId: number$1().int().nonnegative().optional()
|
|
16665
17008
|
}),
|
|
@@ -16670,25 +17013,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16670
17013
|
object({ kind: literal("manual") }),
|
|
16671
17014
|
object({
|
|
16672
17015
|
kind: literal("detection"),
|
|
16673
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16674
|
-
* matches every raw class that maps to it; any other name matches
|
|
16675
|
-
* exactly. */
|
|
16676
17016
|
classes: array(string().min(1)).min(1),
|
|
16677
17017
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16678
|
-
/** The track must have visited at least one of these zones. */
|
|
16679
17018
|
zones: array(string().min(1)).min(1).optional(),
|
|
16680
|
-
/** Best-so-far confidence the track must reach. */
|
|
16681
17019
|
minScore: number$1().min(0).max(1).optional()
|
|
16682
17020
|
}),
|
|
16683
17021
|
object({
|
|
16684
17022
|
kind: literal("stationary"),
|
|
16685
17023
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16686
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16687
17024
|
classes: array(string().min(1)).min(1).optional(),
|
|
16688
|
-
/** The parked object must be in at least one of these zones. */
|
|
16689
17025
|
zones: array(string().min(1)).min(1).optional(),
|
|
16690
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16691
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16692
17026
|
phase: _enum(["appeared", "departed"])
|
|
16693
17027
|
}),
|
|
16694
17028
|
object({
|
|
@@ -16696,6 +17030,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16696
17030
|
minutes: number$1().int().min(1).max(1440)
|
|
16697
17031
|
})
|
|
16698
17032
|
]);
|
|
17033
|
+
var LegacyConditionLeafSchema = object({
|
|
17034
|
+
kind: literal("condition"),
|
|
17035
|
+
deviceId: number$1().int().nonnegative(),
|
|
17036
|
+
cap: string().min(1),
|
|
17037
|
+
fieldPath: string().min(1),
|
|
17038
|
+
operator: AutomationConditionOperatorSchema,
|
|
17039
|
+
value: union([
|
|
17040
|
+
string(),
|
|
17041
|
+
number$1(),
|
|
17042
|
+
boolean(),
|
|
17043
|
+
array(union([string(), number$1()])),
|
|
17044
|
+
QuantityValueSchema
|
|
17045
|
+
]),
|
|
17046
|
+
unit: string().min(1).optional()
|
|
17047
|
+
});
|
|
17048
|
+
var LegacyStationaryPresentSchema = object({
|
|
17049
|
+
kind: literal("stationary-present"),
|
|
17050
|
+
deviceId: number$1().int().nonnegative(),
|
|
17051
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
17052
|
+
zones: array(string().min(1)).min(1).optional()
|
|
17053
|
+
});
|
|
17054
|
+
var LegacyExpressionSchema = object({
|
|
17055
|
+
kind: literal("expression"),
|
|
17056
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
17057
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
17058
|
+
}).superRefine((src, ctx) => {
|
|
17059
|
+
const err = validateExpressionSource(src);
|
|
17060
|
+
if (err !== null) ctx.addIssue({
|
|
17061
|
+
code: "custom",
|
|
17062
|
+
message: err,
|
|
17063
|
+
path: ["expr"]
|
|
17064
|
+
});
|
|
17065
|
+
});
|
|
17066
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
17067
|
+
object({
|
|
17068
|
+
kind: literal("all"),
|
|
17069
|
+
children: array(LegacyConditionSchema)
|
|
17070
|
+
}),
|
|
17071
|
+
object({
|
|
17072
|
+
kind: literal("any"),
|
|
17073
|
+
children: array(LegacyConditionSchema)
|
|
17074
|
+
}),
|
|
17075
|
+
object({
|
|
17076
|
+
kind: literal("not"),
|
|
17077
|
+
child: LegacyConditionSchema
|
|
17078
|
+
}),
|
|
17079
|
+
LegacyConditionLeafSchema,
|
|
17080
|
+
LegacyExpressionSchema,
|
|
17081
|
+
LegacyStationaryPresentSchema
|
|
17082
|
+
]));
|
|
17083
|
+
function convertTrigger(trigger) {
|
|
17084
|
+
switch (trigger.kind) {
|
|
17085
|
+
case "device-state": return {
|
|
17086
|
+
...trigger,
|
|
17087
|
+
kind: "device-becomes"
|
|
17088
|
+
};
|
|
17089
|
+
case "detection": {
|
|
17090
|
+
const { classes, ...rest } = trigger;
|
|
17091
|
+
return {
|
|
17092
|
+
...rest,
|
|
17093
|
+
subjects: classes.map(subjectFromClassName)
|
|
17094
|
+
};
|
|
17095
|
+
}
|
|
17096
|
+
case "stationary": {
|
|
17097
|
+
const { classes, ...rest } = trigger;
|
|
17098
|
+
return {
|
|
17099
|
+
...rest,
|
|
17100
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17101
|
+
};
|
|
17102
|
+
}
|
|
17103
|
+
default: return trigger;
|
|
17104
|
+
}
|
|
17105
|
+
}
|
|
17106
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
17107
|
+
function convertLegacyCondition(condition) {
|
|
17108
|
+
switch (condition.kind) {
|
|
17109
|
+
case "condition": return {
|
|
17110
|
+
...condition,
|
|
17111
|
+
kind: "device-is"
|
|
17112
|
+
};
|
|
17113
|
+
case "stationary-present": {
|
|
17114
|
+
const { classes, ...rest } = condition;
|
|
17115
|
+
return {
|
|
17116
|
+
...rest,
|
|
17117
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17118
|
+
};
|
|
17119
|
+
}
|
|
17120
|
+
case "expression": return condition;
|
|
17121
|
+
case "not": return {
|
|
17122
|
+
kind: "not",
|
|
17123
|
+
child: convertLegacyCondition(condition.child)
|
|
17124
|
+
};
|
|
17125
|
+
case "all":
|
|
17126
|
+
case "any": return {
|
|
17127
|
+
kind: condition.kind,
|
|
17128
|
+
children: condition.children.map(convertLegacyCondition)
|
|
17129
|
+
};
|
|
17130
|
+
}
|
|
17131
|
+
}
|
|
17132
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
17133
|
+
function convertLegacyWhen(half) {
|
|
17134
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
17135
|
+
const first = leaves[0];
|
|
17136
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
17137
|
+
kind: "any",
|
|
17138
|
+
children: leaves
|
|
17139
|
+
};
|
|
17140
|
+
if (half.conditions === void 0) return triggers;
|
|
17141
|
+
return {
|
|
17142
|
+
kind: "all",
|
|
17143
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
17144
|
+
};
|
|
17145
|
+
}
|
|
17146
|
+
/**
|
|
17147
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
17148
|
+
* becomes code.
|
|
17149
|
+
*
|
|
17150
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
17151
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
17152
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
17153
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
17154
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
17155
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
17156
|
+
*
|
|
17157
|
+
* Two parts:
|
|
17158
|
+
*
|
|
17159
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
17160
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
17161
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
17162
|
+
* ADR "one tree".
|
|
17163
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
17164
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
17165
|
+
* this had to diverge from "verbatim", and why.
|
|
17166
|
+
*
|
|
17167
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
17168
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
17169
|
+
*/
|
|
16699
17170
|
/**
|
|
16700
17171
|
* One action step.
|
|
16701
17172
|
*
|
|
@@ -16798,69 +17269,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16798
17269
|
minutes: number$1().int().min(1).max(1440)
|
|
16799
17270
|
})
|
|
16800
17271
|
]);
|
|
16801
|
-
var
|
|
16802
|
-
|
|
16803
|
-
conditions: AutomationConditionSchema.optional(),
|
|
17272
|
+
var CurrentAutomationRecipeSchema = object({
|
|
17273
|
+
when: AutomationWhenSchema,
|
|
16804
17274
|
actions: array(AutomationActionSchema),
|
|
16805
17275
|
runMode: AutomationRunModeSchema.optional(),
|
|
16806
17276
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16807
17277
|
keepTraces: number$1().int().min(1).max(100).optional()
|
|
16808
17278
|
});
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
bus: "vehicle",
|
|
16835
|
-
train: "vehicle",
|
|
16836
|
-
truck: "vehicle",
|
|
16837
|
-
boat: "vehicle",
|
|
16838
|
-
bird: "animal",
|
|
16839
|
-
cat: "animal",
|
|
16840
|
-
dog: "animal",
|
|
16841
|
-
horse: "animal",
|
|
16842
|
-
sheep: "animal",
|
|
16843
|
-
cow: "animal",
|
|
16844
|
-
elephant: "animal",
|
|
16845
|
-
bear: "animal",
|
|
16846
|
-
zebra: "animal",
|
|
16847
|
-
giraffe: "animal",
|
|
16848
|
-
suitcase: "package",
|
|
16849
|
-
backpack: "package",
|
|
16850
|
-
handbag: "package"
|
|
16851
|
-
},
|
|
16852
|
-
preserveOriginal: false
|
|
16853
|
-
};
|
|
17279
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
17280
|
+
var LegacyAutomationRecipeSchema = object({
|
|
17281
|
+
triggers: array(LegacyTriggerSchema),
|
|
17282
|
+
conditions: LegacyConditionSchema.optional(),
|
|
17283
|
+
actions: array(AutomationActionSchema),
|
|
17284
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
17285
|
+
keepTraces: number$1().int().min(1).max(100).optional()
|
|
17286
|
+
});
|
|
17287
|
+
/** The old shape as the new one, lossless. */
|
|
17288
|
+
function convertLegacyRecipe(legacy) {
|
|
17289
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
17290
|
+
return {
|
|
17291
|
+
when: convertLegacyWhen({
|
|
17292
|
+
triggers,
|
|
17293
|
+
conditions
|
|
17294
|
+
}),
|
|
17295
|
+
...rest
|
|
17296
|
+
};
|
|
17297
|
+
}
|
|
17298
|
+
/**
|
|
17299
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
17300
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
17301
|
+
* it. The output type is always the new one.
|
|
17302
|
+
*/
|
|
17303
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16854
17304
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16855
17305
|
/**
|
|
16856
17306
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16857
17307
|
* so the editor can paint every block green or red with its reason.
|
|
16858
17308
|
*
|
|
16859
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16860
|
-
* every
|
|
16861
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16862
|
-
* each addressed by a stable path (`
|
|
16863
|
-
* for an Action run).
|
|
17309
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
17310
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
17311
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
17312
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
17313
|
+
* for an Action run). Traces written before the single tree carry
|
|
17314
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16864
17315
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16865
17316
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16866
17317
|
*
|
|
@@ -16892,6 +17343,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16892
17343
|
]);
|
|
16893
17344
|
var AutomationTriggerTraceSchema = object({
|
|
16894
17345
|
kind: string().min(1),
|
|
17346
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
17347
|
+
path: string().min(1).optional(),
|
|
16895
17348
|
outcome: _enum(["pass", "fail"]),
|
|
16896
17349
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16897
17350
|
reason: string(),
|
|
@@ -16899,15 +17352,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16899
17352
|
vars: record(string(), TraceValueSchema)
|
|
16900
17353
|
});
|
|
16901
17354
|
var AutomationConditionTraceNodeSchema = object({
|
|
16902
|
-
/** `
|
|
17355
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16903
17356
|
path: string().min(1),
|
|
16904
17357
|
kind: _enum([
|
|
16905
17358
|
"all",
|
|
16906
17359
|
"any",
|
|
16907
17360
|
"not",
|
|
16908
17361
|
"condition",
|
|
17362
|
+
"device-is",
|
|
16909
17363
|
"expression",
|
|
16910
|
-
"stationary-present"
|
|
17364
|
+
"stationary-present",
|
|
17365
|
+
"device-becomes",
|
|
17366
|
+
"device-event",
|
|
17367
|
+
"schedule",
|
|
17368
|
+
"interval",
|
|
17369
|
+
"manual",
|
|
17370
|
+
"detection",
|
|
17371
|
+
"stationary"
|
|
16911
17372
|
]),
|
|
16912
17373
|
outcome: TraceNodeOutcomeSchema,
|
|
16913
17374
|
reason: string(),
|
|
@@ -16941,6 +17402,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16941
17402
|
deviceId: number$1().int().optional(),
|
|
16942
17403
|
startedAt: number$1().optional(),
|
|
16943
17404
|
endedAt: number$1().optional(),
|
|
17405
|
+
/**
|
|
17406
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
17407
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
17408
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
17409
|
+
*/
|
|
17410
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16944
17411
|
outcome: _enum([
|
|
16945
17412
|
"ok",
|
|
16946
17413
|
"error",
|
|
@@ -17731,6 +18198,23 @@ method(object({
|
|
|
17731
18198
|
addonId: string(),
|
|
17732
18199
|
nodeId: string().optional()
|
|
17733
18200
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
18201
|
+
addonId: string(),
|
|
18202
|
+
nodeId: string().optional()
|
|
18203
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
18204
|
+
addonId: string(),
|
|
18205
|
+
nodeId: string().optional(),
|
|
18206
|
+
keys: array(string())
|
|
18207
|
+
}), object({
|
|
18208
|
+
removed: array(string()),
|
|
18209
|
+
refused: array(object({
|
|
18210
|
+
key: string(),
|
|
18211
|
+
reason: string()
|
|
18212
|
+
})),
|
|
18213
|
+
secretsCleared: number$1()
|
|
18214
|
+
}), {
|
|
18215
|
+
kind: "mutation",
|
|
18216
|
+
auth: "admin"
|
|
18217
|
+
}), method(object({
|
|
17734
18218
|
addonId: string(),
|
|
17735
18219
|
deviceId: number$1(),
|
|
17736
18220
|
nodeId: string().optional()
|
|
@@ -21809,6 +22293,8 @@ var StorageKindSchema = object({
|
|
|
21809
22293
|
"orphaned",
|
|
21810
22294
|
"dropped"
|
|
21811
22295
|
]),
|
|
22296
|
+
/** An owner declared this kind since the engine started. */
|
|
22297
|
+
declaredThisBoot: boolean(),
|
|
21812
22298
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21813
22299
|
rows: number$1().int().nonnegative().nullable(),
|
|
21814
22300
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21820,6 +22306,51 @@ var StorageKindSchema = object({
|
|
|
21820
22306
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21821
22307
|
declaration: RecordKindDeclarationSchema
|
|
21822
22308
|
});
|
|
22309
|
+
_enum([
|
|
22310
|
+
"orphaned",
|
|
22311
|
+
"stale",
|
|
22312
|
+
"unknown"
|
|
22313
|
+
]);
|
|
22314
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
22315
|
+
var StaleKindFindingSchema = object({
|
|
22316
|
+
owner: string(),
|
|
22317
|
+
kind: string(),
|
|
22318
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
22319
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
22320
|
+
reason: string(),
|
|
22321
|
+
rows: number$1().int().nonnegative().nullable(),
|
|
22322
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
22323
|
+
protected: boolean()
|
|
22324
|
+
});
|
|
22325
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
22326
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
22327
|
+
addonId: string(),
|
|
22328
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
22329
|
+
key: string(),
|
|
22330
|
+
/**
|
|
22331
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
22332
|
+
* not orphans yet. Removing the key releases them.
|
|
22333
|
+
*/
|
|
22334
|
+
secretRefs: array(string())
|
|
22335
|
+
});
|
|
22336
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
22337
|
+
var StaleUnknownSchema = object({
|
|
22338
|
+
subject: string(),
|
|
22339
|
+
reason: string()
|
|
22340
|
+
});
|
|
22341
|
+
var StaleDataReportSchema = object({
|
|
22342
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
22343
|
+
scannedAt: number$1().nullable(),
|
|
22344
|
+
scanning: boolean(),
|
|
22345
|
+
trigger: _enum([
|
|
22346
|
+
"boot",
|
|
22347
|
+
"interval",
|
|
22348
|
+
"manual"
|
|
22349
|
+
]).nullable(),
|
|
22350
|
+
kinds: array(StaleKindFindingSchema),
|
|
22351
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
22352
|
+
unknown: array(StaleUnknownSchema)
|
|
22353
|
+
});
|
|
21823
22354
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21824
22355
|
owner: string().min(1).optional(),
|
|
21825
22356
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21827,7 +22358,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21827
22358
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21828
22359
|
owner: string().min(1),
|
|
21829
22360
|
kind: RecordKindNameSchema
|
|
21830
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
22361
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
22362
|
+
kind: "mutation",
|
|
22363
|
+
auth: "admin"
|
|
22364
|
+
}), method(object({
|
|
22365
|
+
owner: string().min(1),
|
|
22366
|
+
kind: RecordKindNameSchema
|
|
22367
|
+
}), object({
|
|
22368
|
+
deleted: number$1().int().nonnegative(),
|
|
22369
|
+
secretsReleased: number$1().int().nonnegative()
|
|
22370
|
+
}), {
|
|
22371
|
+
kind: "mutation",
|
|
22372
|
+
auth: "admin",
|
|
22373
|
+
caller: "required"
|
|
22374
|
+
}), method(object({
|
|
22375
|
+
addonId: string().min(1),
|
|
22376
|
+
key: string().min(1)
|
|
22377
|
+
}), object({
|
|
22378
|
+
removed: boolean(),
|
|
22379
|
+
secretsCleared: number$1().int().nonnegative()
|
|
22380
|
+
}), {
|
|
22381
|
+
kind: "mutation",
|
|
22382
|
+
auth: "admin",
|
|
22383
|
+
caller: "required"
|
|
22384
|
+
});
|
|
21831
22385
|
/**
|
|
21832
22386
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21833
22387
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21966,6 +22520,12 @@ method(object({
|
|
|
21966
22520
|
}), method(object({ deviceId: number$1().int() }), object({ refs: array(string()).readonly() }), {
|
|
21967
22521
|
kind: "mutation",
|
|
21968
22522
|
auth: "admin"
|
|
22523
|
+
}), method(object({
|
|
22524
|
+
refs: RefListSchema,
|
|
22525
|
+
onBehalfOf: string().min(1)
|
|
22526
|
+
}), object({ released: number$1().int() }), {
|
|
22527
|
+
kind: "mutation",
|
|
22528
|
+
auth: "admin"
|
|
21969
22529
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21970
22530
|
/**
|
|
21971
22531
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22755,7 +23315,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22755
23315
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22756
23316
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22757
23317
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22758
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
23318
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
23319
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
23320
|
+
parentStableId: string().optional()
|
|
22759
23321
|
}));
|
|
22760
23322
|
/**
|
|
22761
23323
|
* Status block — populated by the integration after each
|
|
@@ -43138,6 +43700,79 @@ object({
|
|
|
43138
43700
|
metadata: record(string(), unknown()).optional()
|
|
43139
43701
|
});
|
|
43140
43702
|
/**
|
|
43703
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43704
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43705
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43706
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43707
|
+
* except through a `custom` mode.
|
|
43708
|
+
*/
|
|
43709
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43710
|
+
object({
|
|
43711
|
+
kind: literal("mdns"),
|
|
43712
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43713
|
+
serviceType: string().min(1),
|
|
43714
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43715
|
+
instancePattern: string().optional()
|
|
43716
|
+
}),
|
|
43717
|
+
object({
|
|
43718
|
+
kind: literal("ws-discovery"),
|
|
43719
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43720
|
+
types: array(string()).optional()
|
|
43721
|
+
}),
|
|
43722
|
+
object({
|
|
43723
|
+
kind: literal("ssdp"),
|
|
43724
|
+
searchTarget: string().min(1)
|
|
43725
|
+
}),
|
|
43726
|
+
object({
|
|
43727
|
+
kind: literal("udp-broadcast"),
|
|
43728
|
+
port: number$1().int().min(1).max(65535),
|
|
43729
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43730
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43731
|
+
listenPort: number$1().int().min(1).max(65535).optional()
|
|
43732
|
+
}),
|
|
43733
|
+
object({
|
|
43734
|
+
kind: literal("http-probe"),
|
|
43735
|
+
port: number$1().int().min(1).max(65535),
|
|
43736
|
+
path: string().startsWith("/"),
|
|
43737
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43738
|
+
bodyText: string().optional(),
|
|
43739
|
+
contentType: string().optional(),
|
|
43740
|
+
tls: boolean().default(false),
|
|
43741
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43742
|
+
acceptStatus: array(number$1().int()).optional()
|
|
43743
|
+
}),
|
|
43744
|
+
object({
|
|
43745
|
+
kind: literal("custom"),
|
|
43746
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43747
|
+
reason: string().min(1)
|
|
43748
|
+
})
|
|
43749
|
+
]);
|
|
43750
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43751
|
+
var DiscoveryHitSchema = object({
|
|
43752
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43753
|
+
modeIndex: number$1().int().min(0),
|
|
43754
|
+
host: string(),
|
|
43755
|
+
port: number$1().int(),
|
|
43756
|
+
http: object({
|
|
43757
|
+
status: number$1().int(),
|
|
43758
|
+
headers: record(string(), string()),
|
|
43759
|
+
/** At most 16 KiB. */
|
|
43760
|
+
bodyText: string()
|
|
43761
|
+
}).optional(),
|
|
43762
|
+
mdns: object({
|
|
43763
|
+
instance: string(),
|
|
43764
|
+
host: string(),
|
|
43765
|
+
port: number$1().int(),
|
|
43766
|
+
txt: record(string(), string())
|
|
43767
|
+
}).optional(),
|
|
43768
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43769
|
+
wsd: object({
|
|
43770
|
+
xaddrs: array(string()),
|
|
43771
|
+
types: array(string()),
|
|
43772
|
+
scopes: array(string())
|
|
43773
|
+
}).optional()
|
|
43774
|
+
});
|
|
43775
|
+
/**
|
|
43141
43776
|
* Candidate handed back from discovery and accepted by
|
|
43142
43777
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
43143
43778
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -43172,7 +43807,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
43172
43807
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
43173
43808
|
onboardedDeviceId: number$1().optional(),
|
|
43174
43809
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
43175
|
-
onboardedName: string().optional()
|
|
43810
|
+
onboardedName: string().optional(),
|
|
43811
|
+
/**
|
|
43812
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43813
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43814
|
+
* which is still read for one release.
|
|
43815
|
+
*/
|
|
43816
|
+
setupIntegration: boolean().optional(),
|
|
43817
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43818
|
+
parentStableId: string().optional(),
|
|
43819
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43820
|
+
selectable: boolean().optional()
|
|
43176
43821
|
});
|
|
43177
43822
|
/**
|
|
43178
43823
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -43269,6 +43914,24 @@ var deviceProviderCapability = {
|
|
|
43269
43914
|
auth: "admin"
|
|
43270
43915
|
}),
|
|
43271
43916
|
/**
|
|
43917
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43918
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43919
|
+
*/
|
|
43920
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43921
|
+
/**
|
|
43922
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43923
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43924
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43925
|
+
* from `getDiscoveryParamsSchema`.
|
|
43926
|
+
*/
|
|
43927
|
+
classifyDiscoveryHits: method(object({
|
|
43928
|
+
hits: array(DiscoveryHitSchema),
|
|
43929
|
+
params: record(string(), unknown()).optional()
|
|
43930
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43931
|
+
kind: "mutation",
|
|
43932
|
+
auth: "admin"
|
|
43933
|
+
}),
|
|
43934
|
+
/**
|
|
43272
43935
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
43273
43936
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
43274
43937
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -44033,17 +44696,23 @@ method(object({
|
|
|
44033
44696
|
}), method(object({}), object({ providers: array(object({
|
|
44034
44697
|
addonId: string(),
|
|
44035
44698
|
label: string()
|
|
44036
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
44037
|
-
|
|
44038
|
-
|
|
44039
|
-
|
|
44040
|
-
|
|
44041
|
-
|
|
44042
|
-
|
|
44043
|
-
|
|
44044
|
-
}),
|
|
44699
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44700
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44701
|
+
object({ subnets: array(string()).optional() }),
|
|
44702
|
+
object({ groups: array(object({
|
|
44703
|
+
addonId: string(),
|
|
44704
|
+
label: string(),
|
|
44705
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44706
|
+
error: string().nullable()
|
|
44707
|
+
})).readonly() }),
|
|
44708
|
+
{
|
|
44709
|
+
kind: "mutation",
|
|
44710
|
+
auth: "admin"
|
|
44711
|
+
}
|
|
44712
|
+
), method(object({
|
|
44045
44713
|
addonId: string(),
|
|
44046
|
-
params: record(string(), unknown()).optional()
|
|
44714
|
+
params: record(string(), unknown()).optional(),
|
|
44715
|
+
subnets: array(string()).optional()
|
|
44047
44716
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
44048
44717
|
kind: "mutation",
|
|
44049
44718
|
auth: "admin"
|
|
@@ -56710,6 +57379,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
56710
57379
|
instanceId: string(),
|
|
56711
57380
|
cameraStableId: string(),
|
|
56712
57381
|
nodeId: string(),
|
|
57382
|
+
/**
|
|
57383
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57384
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57385
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57386
|
+
*/
|
|
57387
|
+
nodeInCluster: boolean(),
|
|
56713
57388
|
profileId: string(),
|
|
56714
57389
|
profileLabel: string(),
|
|
56715
57390
|
name: string(),
|
|
@@ -56759,6 +57434,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
56759
57434
|
auth: "admin"
|
|
56760
57435
|
}), method(object({
|
|
56761
57436
|
instanceId: string().min(1),
|
|
57437
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57438
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
56762
57439
|
name: string().trim().min(1).max(160).optional(),
|
|
56763
57440
|
executable: string().max(1024).optional(),
|
|
56764
57441
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -63394,7 +64071,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63394
64071
|
id: string(),
|
|
63395
64072
|
settings: record(string(), unknown()),
|
|
63396
64073
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63397
|
-
secretKeys: array(string()).optional()
|
|
64074
|
+
secretKeys: array(string()).optional(),
|
|
64075
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
64076
|
+
skipRestart: boolean().optional()
|
|
63398
64077
|
}), object({ success: boolean() }), {
|
|
63399
64078
|
kind: "mutation",
|
|
63400
64079
|
auth: "admin"
|
|
@@ -64022,6 +64701,39 @@ var TlsStatusSchema = object({
|
|
|
64022
64701
|
hasCa: boolean()
|
|
64023
64702
|
}).nullable()
|
|
64024
64703
|
});
|
|
64704
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64705
|
+
var ScanRequestSchema = object({
|
|
64706
|
+
providerId: string().min(1),
|
|
64707
|
+
modes: array(DiscoveryModeSchema)
|
|
64708
|
+
});
|
|
64709
|
+
var ScanErrorSchema = object({
|
|
64710
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64711
|
+
providerId: string().optional(),
|
|
64712
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64713
|
+
mode: string().optional(),
|
|
64714
|
+
message: string()
|
|
64715
|
+
});
|
|
64716
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64717
|
+
var ScanStatsSchema = object({
|
|
64718
|
+
hostsProbed: number$1().int(),
|
|
64719
|
+
hits: number$1().int(),
|
|
64720
|
+
timeouts: number$1().int(),
|
|
64721
|
+
byMode: record(string(), object({
|
|
64722
|
+
probed: number$1().int(),
|
|
64723
|
+
hits: number$1().int(),
|
|
64724
|
+
timeouts: number$1().int()
|
|
64725
|
+
}))
|
|
64726
|
+
});
|
|
64727
|
+
var ScanNetworkResultSchema = object({
|
|
64728
|
+
hits: array(object({
|
|
64729
|
+
providerId: string(),
|
|
64730
|
+
hit: DiscoveryHitSchema
|
|
64731
|
+
})),
|
|
64732
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64733
|
+
subnetsScanned: array(string()),
|
|
64734
|
+
errors: array(ScanErrorSchema),
|
|
64735
|
+
stats: ScanStatsSchema
|
|
64736
|
+
});
|
|
64025
64737
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
64026
64738
|
/**
|
|
64027
64739
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -64076,6 +64788,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
64076
64788
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
64077
64789
|
kind: "mutation",
|
|
64078
64790
|
auth: "admin"
|
|
64791
|
+
}), method(object({
|
|
64792
|
+
requests: array(ScanRequestSchema),
|
|
64793
|
+
subnets: array(string()).optional()
|
|
64794
|
+
}), ScanNetworkResultSchema, {
|
|
64795
|
+
kind: "mutation",
|
|
64796
|
+
auth: "admin",
|
|
64797
|
+
timeoutMs: 9e4
|
|
64798
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64799
|
+
kind: "mutation",
|
|
64800
|
+
auth: "admin"
|
|
64079
64801
|
}), method(_void(), TlsStatusSchema, {
|
|
64080
64802
|
kind: "mutation",
|
|
64081
64803
|
auth: "admin"
|
|
@@ -73658,6 +74380,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73658
74380
|
async discoverDevices(_input) {
|
|
73659
74381
|
return [];
|
|
73660
74382
|
}
|
|
74383
|
+
/**
|
|
74384
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74385
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74386
|
+
*/
|
|
74387
|
+
async getDiscoveryModes() {
|
|
74388
|
+
return [];
|
|
74389
|
+
}
|
|
74390
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74391
|
+
async classifyDiscoveryHits(_input) {
|
|
74392
|
+
return [];
|
|
74393
|
+
}
|
|
73661
74394
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73662
74395
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73663
74396
|
async getDiscoveryParamsSchema() {
|
|
@@ -74526,6 +75259,13 @@ Object.freeze({
|
|
|
74526
75259
|
access: "create",
|
|
74527
75260
|
role: "admin"
|
|
74528
75261
|
},
|
|
75262
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
75263
|
+
capName: "addon-settings",
|
|
75264
|
+
capScope: "system",
|
|
75265
|
+
addonId: null,
|
|
75266
|
+
access: "view",
|
|
75267
|
+
role: "admin"
|
|
75268
|
+
},
|
|
74529
75269
|
"addonSettings.getDeviceSettings": {
|
|
74530
75270
|
capName: "addon-settings",
|
|
74531
75271
|
capScope: "system",
|
|
@@ -74547,6 +75287,13 @@ Object.freeze({
|
|
|
74547
75287
|
access: "view",
|
|
74548
75288
|
role: "admin"
|
|
74549
75289
|
},
|
|
75290
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
75291
|
+
capName: "addon-settings",
|
|
75292
|
+
capScope: "system",
|
|
75293
|
+
addonId: null,
|
|
75294
|
+
access: "create",
|
|
75295
|
+
role: "admin"
|
|
75296
|
+
},
|
|
74550
75297
|
"addonSettings.updateDeviceSettings": {
|
|
74551
75298
|
capName: "addon-settings",
|
|
74552
75299
|
capScope: "system",
|
|
@@ -76962,6 +77709,13 @@ Object.freeze({
|
|
|
76962
77709
|
access: "create",
|
|
76963
77710
|
role: "admin"
|
|
76964
77711
|
},
|
|
77712
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77713
|
+
capName: "device-provider",
|
|
77714
|
+
capScope: "system",
|
|
77715
|
+
addonId: null,
|
|
77716
|
+
access: "create",
|
|
77717
|
+
role: "admin"
|
|
77718
|
+
},
|
|
76965
77719
|
"deviceProvider.createDevice": {
|
|
76966
77720
|
capName: "device-provider",
|
|
76967
77721
|
capScope: "system",
|
|
@@ -76990,6 +77744,13 @@ Object.freeze({
|
|
|
76990
77744
|
access: "view",
|
|
76991
77745
|
role: "admin"
|
|
76992
77746
|
},
|
|
77747
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77748
|
+
capName: "device-provider",
|
|
77749
|
+
capScope: "system",
|
|
77750
|
+
addonId: null,
|
|
77751
|
+
access: "view",
|
|
77752
|
+
role: "admin"
|
|
77753
|
+
},
|
|
76993
77754
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76994
77755
|
capName: "device-provider",
|
|
76995
77756
|
capScope: "system",
|
|
@@ -77767,6 +78528,13 @@ Object.freeze({
|
|
|
77767
78528
|
access: "create",
|
|
77768
78529
|
role: "admin"
|
|
77769
78530
|
},
|
|
78531
|
+
"keychain.releaseRecordRefs": {
|
|
78532
|
+
capName: "keychain",
|
|
78533
|
+
capScope: "system",
|
|
78534
|
+
addonId: null,
|
|
78535
|
+
access: "create",
|
|
78536
|
+
role: "admin"
|
|
78537
|
+
},
|
|
77770
78538
|
"keychain.resolve": {
|
|
77771
78539
|
capName: "keychain",
|
|
77772
78540
|
capScope: "system",
|
|
@@ -78187,6 +78955,13 @@ Object.freeze({
|
|
|
78187
78955
|
access: "view",
|
|
78188
78956
|
role: "admin"
|
|
78189
78957
|
},
|
|
78958
|
+
"localNetwork.getScanSubnets": {
|
|
78959
|
+
capName: "local-network",
|
|
78960
|
+
capScope: "system",
|
|
78961
|
+
addonId: null,
|
|
78962
|
+
access: "view",
|
|
78963
|
+
role: "admin"
|
|
78964
|
+
},
|
|
78190
78965
|
"localNetwork.getTlsStatus": {
|
|
78191
78966
|
capName: "local-network",
|
|
78192
78967
|
capScope: "system",
|
|
@@ -78236,6 +79011,13 @@ Object.freeze({
|
|
|
78236
79011
|
access: "create",
|
|
78237
79012
|
role: "admin"
|
|
78238
79013
|
},
|
|
79014
|
+
"localNetwork.scanNetwork": {
|
|
79015
|
+
capName: "local-network",
|
|
79016
|
+
capScope: "system",
|
|
79017
|
+
addonId: null,
|
|
79018
|
+
access: "create",
|
|
79019
|
+
role: "admin"
|
|
79020
|
+
},
|
|
78239
79021
|
"localNetwork.setAddressRole": {
|
|
78240
79022
|
capName: "local-network",
|
|
78241
79023
|
capScope: "system",
|
|
@@ -78257,6 +79039,13 @@ Object.freeze({
|
|
|
78257
79039
|
access: "create",
|
|
78258
79040
|
role: "admin"
|
|
78259
79041
|
},
|
|
79042
|
+
"localNetwork.setScanSubnets": {
|
|
79043
|
+
capName: "local-network",
|
|
79044
|
+
capScope: "system",
|
|
79045
|
+
addonId: null,
|
|
79046
|
+
access: "create",
|
|
79047
|
+
role: "admin"
|
|
79048
|
+
},
|
|
78260
79049
|
"localNetwork.setViewerEndpoints": {
|
|
78261
79050
|
capName: "local-network",
|
|
78262
79051
|
capScope: "system",
|
|
@@ -82478,6 +83267,13 @@ Object.freeze({
|
|
|
82478
83267
|
access: "create",
|
|
82479
83268
|
role: "admin"
|
|
82480
83269
|
},
|
|
83270
|
+
"storageAdmin.dropOrphanedKind": {
|
|
83271
|
+
capName: "storage-admin",
|
|
83272
|
+
capScope: "system",
|
|
83273
|
+
addonId: null,
|
|
83274
|
+
access: "delete",
|
|
83275
|
+
role: "admin"
|
|
83276
|
+
},
|
|
82481
83277
|
"storageAdmin.listKinds": {
|
|
82482
83278
|
capName: "storage-admin",
|
|
82483
83279
|
capScope: "system",
|
|
@@ -82499,6 +83295,27 @@ Object.freeze({
|
|
|
82499
83295
|
access: "view",
|
|
82500
83296
|
role: "admin"
|
|
82501
83297
|
},
|
|
83298
|
+
"storageAdmin.scanStaleData": {
|
|
83299
|
+
capName: "storage-admin",
|
|
83300
|
+
capScope: "system",
|
|
83301
|
+
addonId: null,
|
|
83302
|
+
access: "create",
|
|
83303
|
+
role: "admin"
|
|
83304
|
+
},
|
|
83305
|
+
"storageAdmin.staleDataReport": {
|
|
83306
|
+
capName: "storage-admin",
|
|
83307
|
+
capScope: "system",
|
|
83308
|
+
addonId: null,
|
|
83309
|
+
access: "view",
|
|
83310
|
+
role: "admin"
|
|
83311
|
+
},
|
|
83312
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
83313
|
+
capName: "storage-admin",
|
|
83314
|
+
capScope: "system",
|
|
83315
|
+
addonId: null,
|
|
83316
|
+
access: "create",
|
|
83317
|
+
role: "admin"
|
|
83318
|
+
},
|
|
82502
83319
|
"storageEvictable.evict": {
|
|
82503
83320
|
capName: "storage-evictable",
|
|
82504
83321
|
capScope: "system",
|
|
@@ -92772,12 +93589,22 @@ function rewriteBody(body, kind, currentUpstream, route) {
|
|
|
92772
93589
|
/**
|
|
92773
93590
|
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
92774
93591
|
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
92775
|
-
|
|
92776
|
-
|
|
92777
|
-
|
|
93592
|
+
* `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
|
|
93593
|
+
* also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
|
|
93594
|
+
* `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
|
|
93595
|
+
*/
|
|
93596
|
+
function rewriteSetCookie(cookie, route, options = {}) {
|
|
93597
|
+
const parts = cookie.split(";").map((part) => part.trim());
|
|
93598
|
+
const dropped = options.browserSafe === true ? [
|
|
93599
|
+
"domain",
|
|
93600
|
+
"path",
|
|
93601
|
+
"secure",
|
|
93602
|
+
"samesite"
|
|
93603
|
+
] : ["domain", "path"];
|
|
93604
|
+
const kept = parts.filter((part, index) => {
|
|
92778
93605
|
if (index === 0) return true;
|
|
92779
93606
|
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
92780
|
-
return name
|
|
93607
|
+
return !dropped.includes(name);
|
|
92781
93608
|
});
|
|
92782
93609
|
kept.push(`Path=${route.basePath}/`);
|
|
92783
93610
|
return kept.join("; ");
|
|
@@ -92788,10 +93615,10 @@ function rewriteSetCookie(cookie, route) {
|
|
|
92788
93615
|
* one, so a past-dated set that only deleted the other domain's copy would delete the
|
|
92789
93616
|
* live one the same response just set: that deletion is dropped.
|
|
92790
93617
|
*/
|
|
92791
|
-
function rewriteSetCookies(cookies, route) {
|
|
93618
|
+
function rewriteSetCookies(cookies, route, options = {}) {
|
|
92792
93619
|
const now = Date.now();
|
|
92793
93620
|
const liveNames = new Set(cookies.filter((c) => !isExpiredCookie(c, now)).map(cookieName));
|
|
92794
|
-
return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).map((c) => rewriteSetCookie(c, route));
|
|
93621
|
+
return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).filter((c) => options.browserSafe !== true || !/^__(?:host|secure)-/i.test(cookieName(c))).map((c) => rewriteSetCookie(c, route, options));
|
|
92795
93622
|
}
|
|
92796
93623
|
function cookieName(cookie) {
|
|
92797
93624
|
return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
|
|
@@ -92851,6 +93678,99 @@ function rewriteFormBody(body, route) {
|
|
|
92851
93678
|
function filterForwardedCookies(cookieHeader) {
|
|
92852
93679
|
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
92853
93680
|
}
|
|
93681
|
+
/**
|
|
93682
|
+
* Server-side cookie jar of ONE vendor sign-in attempt (D955).
|
|
93683
|
+
*
|
|
93684
|
+
* A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
|
|
93685
|
+
* without the cookies it set a step earlier. Relying on the browser to store them under the
|
|
93686
|
+
* hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
|
|
93687
|
+
* on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
|
|
93688
|
+
* `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
|
|
93689
|
+
* whatever the browser did. Values never leave this object except as the upstream `Cookie`
|
|
93690
|
+
* header; nothing here logs.
|
|
93691
|
+
*/
|
|
93692
|
+
function attributeValue(attrs, name) {
|
|
93693
|
+
const found = attrs.find((a) => a.split("=")[0]?.trim().toLowerCase() === name);
|
|
93694
|
+
if (found === void 0) return void 0;
|
|
93695
|
+
const eq = found.indexOf("=");
|
|
93696
|
+
return eq === -1 ? "" : found.slice(eq + 1).trim();
|
|
93697
|
+
}
|
|
93698
|
+
function hasFlag(attrs, name) {
|
|
93699
|
+
return attrs.some((a) => a.toLowerCase() === name);
|
|
93700
|
+
}
|
|
93701
|
+
function expiryOf(attrs, nowMs) {
|
|
93702
|
+
const maxAge = attributeValue(attrs, "max-age");
|
|
93703
|
+
if (maxAge !== void 0 && maxAge.length > 0 && Number.isFinite(Number(maxAge))) return nowMs + Number(maxAge) * 1e3;
|
|
93704
|
+
const expires = attributeValue(attrs, "expires");
|
|
93705
|
+
if (expires === void 0) return null;
|
|
93706
|
+
const at = Date.parse(expires);
|
|
93707
|
+
return Number.isFinite(at) ? at : null;
|
|
93708
|
+
}
|
|
93709
|
+
function defaultPath(requestPath) {
|
|
93710
|
+
const last = requestPath.lastIndexOf("/");
|
|
93711
|
+
return last <= 0 ? "/" : requestPath.slice(0, last);
|
|
93712
|
+
}
|
|
93713
|
+
function pathMatches(cookiePath, requestPath) {
|
|
93714
|
+
if (requestPath === cookiePath) return true;
|
|
93715
|
+
if (!requestPath.startsWith(cookiePath)) return false;
|
|
93716
|
+
return cookiePath.endsWith("/") || requestPath.charAt(cookiePath.length) === "/";
|
|
93717
|
+
}
|
|
93718
|
+
function hostMatches(cookie, host) {
|
|
93719
|
+
if (cookie.domain === null) return cookie.originHost === host;
|
|
93720
|
+
const bare = hostWithoutPort(host);
|
|
93721
|
+
return bare === cookie.domain || bare.endsWith(`.${cookie.domain}`);
|
|
93722
|
+
}
|
|
93723
|
+
var VendorCookieJar = class {
|
|
93724
|
+
#cookies = /* @__PURE__ */ new Map();
|
|
93725
|
+
/** Store (or delete, when expired) the cookies of one upstream response. */
|
|
93726
|
+
store(setCookies, origin, nowMs) {
|
|
93727
|
+
for (const raw of setCookies) {
|
|
93728
|
+
const [first, ...rest] = raw.split(";").map((part) => part.trim());
|
|
93729
|
+
const eq = first?.indexOf("=") ?? -1;
|
|
93730
|
+
if (first === void 0 || eq <= 0) continue;
|
|
93731
|
+
const name = first.slice(0, eq);
|
|
93732
|
+
const attrs = rest;
|
|
93733
|
+
const domainAttr = attributeValue(attrs, "domain");
|
|
93734
|
+
const domain = domainAttr === void 0 || domainAttr.length === 0 ? null : domainAttr.replace(/^\./, "").toLowerCase();
|
|
93735
|
+
const pathAttr = attributeValue(attrs, "path");
|
|
93736
|
+
const path = pathAttr !== void 0 && pathAttr.startsWith("/") ? pathAttr : defaultPath(origin.pathname);
|
|
93737
|
+
const originBare = hostWithoutPort(origin.host);
|
|
93738
|
+
if (domain !== null && originBare !== domain && !originBare.endsWith(`.${domain}`)) continue;
|
|
93739
|
+
const expiresAtMs = expiryOf(attrs, nowMs);
|
|
93740
|
+
const key = `${name}|${domain ?? origin.host}|${path}`;
|
|
93741
|
+
if (expiresAtMs !== null && expiresAtMs <= nowMs) {
|
|
93742
|
+
this.#cookies.delete(key);
|
|
93743
|
+
continue;
|
|
93744
|
+
}
|
|
93745
|
+
this.#cookies.set(key, {
|
|
93746
|
+
name,
|
|
93747
|
+
value: first.slice(eq + 1),
|
|
93748
|
+
originHost: origin.host,
|
|
93749
|
+
domain,
|
|
93750
|
+
path,
|
|
93751
|
+
secure: hasFlag(attrs, "secure"),
|
|
93752
|
+
expiresAtMs
|
|
93753
|
+
});
|
|
93754
|
+
}
|
|
93755
|
+
}
|
|
93756
|
+
/** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
|
|
93757
|
+
headerFor(target, nowMs) {
|
|
93758
|
+
const secureOk = target.scheme === "https" || isLoopbackHost(target.host);
|
|
93759
|
+
const matching = [...this.#cookies.values()].filter((c) => (c.expiresAtMs === null || c.expiresAtMs > nowMs) && (!c.secure || secureOk) && hostMatches(c, target.host) && pathMatches(c.path, target.pathname)).sort((a, b) => b.path.length - a.path.length);
|
|
93760
|
+
if (matching.length === 0) return null;
|
|
93761
|
+
return matching.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
93762
|
+
}
|
|
93763
|
+
};
|
|
93764
|
+
/** Cookie names carried by a `Cookie` header value. */
|
|
93765
|
+
function cookieHeaderNames(header) {
|
|
93766
|
+
return new Set(header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0));
|
|
93767
|
+
}
|
|
93768
|
+
/** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
|
|
93769
|
+
function mergeCookieHeaders(browser, jar) {
|
|
93770
|
+
if (jar === null) return browser;
|
|
93771
|
+
const shadowed = cookieHeaderNames(jar);
|
|
93772
|
+
return [...browser.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !shadowed.has(part.split("=")[0]?.trim() ?? "")), jar].join("; ");
|
|
93773
|
+
}
|
|
92854
93774
|
/** A short stable label for the attempt: enough to group lines, useless as a capability. */
|
|
92855
93775
|
function attemptLabel(attemptId) {
|
|
92856
93776
|
let hash = 2166136261;
|
|
@@ -93046,6 +93966,7 @@ var VendorLoginProxy = class {
|
|
|
93046
93966
|
this.#attempts.set(spec.attemptId, {
|
|
93047
93967
|
expiresAtMs: spec.expiresAtMs,
|
|
93048
93968
|
upstreams: spec.upstreams,
|
|
93969
|
+
jar: spec.serverCookieJar === true ? new VendorCookieJar() : null,
|
|
93049
93970
|
evaluate: async (view) => {
|
|
93050
93971
|
const verdict = await spec.terminal(view);
|
|
93051
93972
|
if (verdict.kind === "continue") return verdict;
|
|
@@ -93142,7 +94063,7 @@ var VendorLoginProxy = class {
|
|
|
93142
94063
|
body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
|
|
93143
94064
|
} else body = read;
|
|
93144
94065
|
}
|
|
93145
|
-
const headers = this.#requestHeaders(req, route, upstream);
|
|
94066
|
+
const headers = this.#requestHeaders(req, route, upstream, attempt.jar, target);
|
|
93146
94067
|
const upstreamRes = await this.#fetch(target, {
|
|
93147
94068
|
method,
|
|
93148
94069
|
headers,
|
|
@@ -93151,6 +94072,10 @@ var VendorLoginProxy = class {
|
|
|
93151
94072
|
});
|
|
93152
94073
|
const location = upstreamRes.headers.get("location");
|
|
93153
94074
|
const setCookies = upstreamRes.headers.getSetCookie();
|
|
94075
|
+
attempt.jar?.store(setCookies, {
|
|
94076
|
+
host: upstream.host,
|
|
94077
|
+
pathname: target.pathname
|
|
94078
|
+
}, this.#now());
|
|
93154
94079
|
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
93155
94080
|
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
93156
94081
|
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
@@ -93198,7 +94123,7 @@ var VendorLoginProxy = class {
|
|
|
93198
94123
|
upstreamRes.headers.forEach((value, name) => {
|
|
93199
94124
|
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
93200
94125
|
});
|
|
93201
|
-
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
|
|
94126
|
+
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route, { browserSafe: attempt.jar !== null });
|
|
93202
94127
|
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
93203
94128
|
out["cache-control"] = "no-store";
|
|
93204
94129
|
if (text !== null && kind !== null) {
|
|
@@ -93220,15 +94145,19 @@ var VendorLoginProxy = class {
|
|
|
93220
94145
|
...flowTraceMeta(input)
|
|
93221
94146
|
} });
|
|
93222
94147
|
}
|
|
93223
|
-
#requestHeaders(req, route, upstream) {
|
|
94148
|
+
#requestHeaders(req, route, upstream, jar, target) {
|
|
93224
94149
|
const headers = {};
|
|
93225
94150
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
93226
94151
|
const lower = name.toLowerCase();
|
|
93227
94152
|
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
93228
94153
|
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
93229
94154
|
}
|
|
93230
|
-
const
|
|
93231
|
-
|
|
94155
|
+
const merged = mergeCookieHeaders(filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? ""), jar?.headerFor({
|
|
94156
|
+
host: upstream.host,
|
|
94157
|
+
pathname: target.pathname,
|
|
94158
|
+
scheme: upstream.scheme
|
|
94159
|
+
}, this.#now()) ?? null);
|
|
94160
|
+
if (merged.length > 0) headers["cookie"] = merged;
|
|
93232
94161
|
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
93233
94162
|
const referer = firstHeader(req.headers["referer"]);
|
|
93234
94163
|
if (referer !== void 0) {
|
|
@@ -136251,10 +137180,6 @@ var AlexaRemoteClient = class {
|
|
|
136251
137180
|
if (isAuthError(message)) throw new AlexaAuthError(message);
|
|
136252
137181
|
throw err;
|
|
136253
137182
|
}
|
|
136254
|
-
if (remote.getAuthenticationDetails().authenticated !== true) {
|
|
136255
|
-
this.#connected = false;
|
|
136256
|
-
throw new AlexaAuthError("account not authenticated (cookie/refresh token rejected or expired)");
|
|
136257
|
-
}
|
|
136258
137183
|
this.#connected = true;
|
|
136259
137184
|
await this.#persistSession(remote);
|
|
136260
137185
|
}
|
|
@@ -136951,8 +137876,17 @@ var AlexaBrokerRegistry = class {
|
|
|
136951
137876
|
}
|
|
136952
137877
|
};
|
|
136953
137878
|
//#endregion
|
|
137879
|
+
//#region src/speak/constants.ts
|
|
137880
|
+
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
137881
|
+
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
137882
|
+
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
137883
|
+
/** The kind of the Amazon-account broker the Echoes are read from. */
|
|
137884
|
+
var ALEXA_BROKER_KIND = "alexa";
|
|
137885
|
+
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
137886
|
+
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
137887
|
+
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
137888
|
+
//#endregion
|
|
136954
137889
|
//#region src/alexa-broker-provider.ts
|
|
136955
|
-
var ALEXA_KIND = "alexa";
|
|
136956
137890
|
/**
|
|
136957
137891
|
* Construct the `broker` cap provider for the Alexa import addon. Pure builder.
|
|
136958
137892
|
* Mirrors `buildEcowittBrokerProvider`. The cookie + refresh token are secrets:
|
|
@@ -136970,7 +137904,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
136970
137904
|
});
|
|
136971
137905
|
return {
|
|
136972
137906
|
list: async ({ kind }) => {
|
|
136973
|
-
if (kind && kind !==
|
|
137907
|
+
if (kind && kind !== "alexa") return [];
|
|
136974
137908
|
return registry.list().map((info) => ({
|
|
136975
137909
|
...info,
|
|
136976
137910
|
addonId: ownerAddonId
|
|
@@ -136986,16 +137920,16 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
136986
137920
|
listProviders: async () => [{
|
|
136987
137921
|
addonId: ownerAddonId,
|
|
136988
137922
|
kinds: [{
|
|
136989
|
-
kind:
|
|
137923
|
+
kind: ALEXA_BROKER_KIND,
|
|
136990
137924
|
label: "Alexa",
|
|
136991
137925
|
signIn: {
|
|
136992
137926
|
basePath: "/addon/import-alexa/login",
|
|
136993
|
-
methods: ["device-code"
|
|
137927
|
+
methods: ["device-code"]
|
|
136994
137928
|
}
|
|
136995
137929
|
}]
|
|
136996
137930
|
}],
|
|
136997
137931
|
add: async ({ kind, name, settings }) => {
|
|
136998
|
-
if (kind !==
|
|
137932
|
+
if (kind !== "alexa") throw new Error(`import-alexa: only kind '${ALEXA_BROKER_KIND}' is handled here (got '${kind}')`);
|
|
136999
137933
|
const entry = await registry.createEntry(name, settingsToAlexaConnection(settings));
|
|
137000
137934
|
await persistBrokers([...getBrokers(), entry]);
|
|
137001
137935
|
return { id: entry.id };
|
|
@@ -137053,11 +137987,11 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137053
137987
|
return entry ? redact(entry) : null;
|
|
137054
137988
|
},
|
|
137055
137989
|
getSettingsSchema: async ({ kind }) => {
|
|
137056
|
-
if (kind !==
|
|
137990
|
+
if (kind !== "alexa") return null;
|
|
137057
137991
|
return buildConnectionFormSchema();
|
|
137058
137992
|
},
|
|
137059
137993
|
testSettings: async ({ kind, settings }) => {
|
|
137060
|
-
if (kind !==
|
|
137994
|
+
if (kind !== "alexa") return {
|
|
137061
137995
|
ok: false,
|
|
137062
137996
|
error: `unsupported kind: ${kind}`
|
|
137063
137997
|
};
|
|
@@ -138174,6 +139108,15 @@ var BrokerEcho = class {
|
|
|
138174
139108
|
try {
|
|
138175
139109
|
const seed = await this.deps.readSeed();
|
|
138176
139110
|
const stored = await sessions.get(brokerId);
|
|
139111
|
+
logger.info("alexa speak: broker sign-in material", {
|
|
139112
|
+
...tags,
|
|
139113
|
+
meta: {
|
|
139114
|
+
seedFound: seed !== null,
|
|
139115
|
+
hasRefreshToken: seed !== null && seed.refreshToken.length > 0,
|
|
139116
|
+
hasDeviceAppName: seed !== null && seed.deviceAppName.length > 0,
|
|
139117
|
+
hasStoredSession: stored.length > 0
|
|
139118
|
+
}
|
|
139119
|
+
});
|
|
138177
139120
|
if (seed === null && stored.length === 0) {
|
|
138178
139121
|
this.currentState = "broker-not-signed-in";
|
|
138179
139122
|
this.currentError = null;
|
|
@@ -138267,14 +139210,6 @@ var BrokerEcho = class {
|
|
|
138267
139210
|
}
|
|
138268
139211
|
};
|
|
138269
139212
|
//#endregion
|
|
138270
|
-
//#region src/speak/constants.ts
|
|
138271
|
-
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
138272
|
-
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
138273
|
-
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
138274
|
-
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
138275
|
-
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
138276
|
-
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
138277
|
-
//#endregion
|
|
138278
139213
|
//#region src/speak/rate-limit.ts
|
|
138279
139214
|
/**
|
|
138280
139215
|
* Per-Echo admission. A burst is dropped here, not queued: a queue would be
|
|
@@ -138425,50 +139360,37 @@ function echoOptionLabel(device) {
|
|
|
138425
139360
|
if (kind.length > 0) parts.push(kind);
|
|
138426
139361
|
return parts.join(" · ");
|
|
138427
139362
|
}
|
|
138428
|
-
function echoField(devices, showWhen) {
|
|
138429
|
-
return {
|
|
138430
|
-
type: "select",
|
|
138431
|
-
key: "echoId",
|
|
138432
|
-
label: "Echo",
|
|
138433
|
-
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
138434
|
-
description: devices.length === 0 ? "No Echo found yet. Sign the Amazon account in on Integrations, Alexa." : void 0,
|
|
138435
|
-
descriptionKey: devices.length === 0 ? "import-alexa:echoTarget.s_echo.echoId.empty" : void 0,
|
|
138436
|
-
options: devices.map((device) => ({
|
|
138437
|
-
value: device.id,
|
|
138438
|
-
label: echoOptionLabel(device)
|
|
138439
|
-
})),
|
|
138440
|
-
...showWhen === void 0 ? {} : { showWhen }
|
|
138441
|
-
};
|
|
138442
|
-
}
|
|
138443
139363
|
/**
|
|
138444
139364
|
* The two pickers are not `required`: a required select shows its first option
|
|
138445
139365
|
* while the form still holds no value, so a save would drop the choice. Left
|
|
138446
139366
|
* optional they open on "Select", and `upsertTarget` refuses an empty pick.
|
|
138447
139367
|
*
|
|
138448
|
-
* The
|
|
138449
|
-
* so a new Echo shows up without a deploy.
|
|
138450
|
-
* field; with several, each account's Echoes appear when that account is chosen.
|
|
139368
|
+
* The account is a broker pick (existing, or created in place); the Echo list
|
|
139369
|
+
* is fetched from that account, so a new Echo shows up without a deploy.
|
|
138451
139370
|
*/
|
|
138452
|
-
function configSchema(
|
|
138453
|
-
const first = brokers[0];
|
|
139371
|
+
function configSchema() {
|
|
138454
139372
|
return { sections: [{
|
|
138455
139373
|
id: "echo",
|
|
138456
139374
|
title: "Echo",
|
|
138457
139375
|
titleKey: "import-alexa:echoTarget.s_echo.title",
|
|
138458
139376
|
fields: [
|
|
138459
|
-
...
|
|
138460
|
-
type: "select",
|
|
139377
|
+
...[{
|
|
139378
|
+
type: "broker-select",
|
|
138461
139379
|
key: "brokerId",
|
|
138462
139380
|
label: "Amazon account",
|
|
138463
139381
|
labelKey: "import-alexa:echoTarget.s_echo.brokerId.label",
|
|
138464
|
-
|
|
138465
|
-
|
|
138466
|
-
|
|
138467
|
-
|
|
138468
|
-
|
|
138469
|
-
|
|
138470
|
-
|
|
138471
|
-
|
|
139382
|
+
brokerKind: ALEXA_BROKER_KIND
|
|
139383
|
+
}, {
|
|
139384
|
+
type: "select",
|
|
139385
|
+
key: "echoId",
|
|
139386
|
+
label: "Echo",
|
|
139387
|
+
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
139388
|
+
options: [],
|
|
139389
|
+
discoverOptions: {
|
|
139390
|
+
kind: ALEXA_SPEAK_KIND,
|
|
139391
|
+
dependsOn: ["brokerId"]
|
|
139392
|
+
}
|
|
139393
|
+
}],
|
|
138472
139394
|
{
|
|
138473
139395
|
type: "select",
|
|
138474
139396
|
key: "mode",
|
|
@@ -138502,8 +139424,8 @@ function configSchema(brokers) {
|
|
|
138502
139424
|
]
|
|
138503
139425
|
}] };
|
|
138504
139426
|
}
|
|
138505
|
-
/** The Alexa target kind as the Add target form sees it
|
|
138506
|
-
function alexaTargetKind(
|
|
139427
|
+
/** The Alexa target kind as the Add target form sees it. */
|
|
139428
|
+
function alexaTargetKind() {
|
|
138507
139429
|
return {
|
|
138508
139430
|
kind: ALEXA_SPEAK_KIND,
|
|
138509
139431
|
label: "Alexa",
|
|
@@ -138513,7 +139435,7 @@ function alexaTargetKind(brokers) {
|
|
|
138513
139435
|
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
138514
139436
|
supportsDiscovery: true,
|
|
138515
139437
|
destinationKeys: ["brokerId", "echoId"],
|
|
138516
|
-
configSchema: configSchema(
|
|
139438
|
+
configSchema: configSchema(),
|
|
138517
139439
|
deliveryParams: DELIVERY_PARAMS,
|
|
138518
139440
|
caps: {
|
|
138519
139441
|
attachments: {
|
|
@@ -138775,7 +139697,7 @@ function createAlexaNotificationProvider(ports) {
|
|
|
138775
139697
|
return deliveries;
|
|
138776
139698
|
},
|
|
138777
139699
|
async listTargetKinds() {
|
|
138778
|
-
return [alexaTargetKind(
|
|
139700
|
+
return [alexaTargetKind()];
|
|
138779
139701
|
},
|
|
138780
139702
|
async listTargets() {
|
|
138781
139703
|
return [...targets];
|
|
@@ -138785,7 +139707,20 @@ function createAlexaNotificationProvider(ports) {
|
|
|
138785
139707
|
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
138786
139708
|
return found;
|
|
138787
139709
|
},
|
|
138788
|
-
async discoverTargets() {
|
|
139710
|
+
async discoverTargets(input) {
|
|
139711
|
+
const named = readString(input.config ?? {}, "brokerId");
|
|
139712
|
+
if (named.length > 0) {
|
|
139713
|
+
const owner = ports.brokers().find((broker) => broker.id === named);
|
|
139714
|
+
return (owner?.devices ?? []).map((device) => ({
|
|
139715
|
+
...discoveredFrom(owner ?? {
|
|
139716
|
+
id: named,
|
|
139717
|
+
name: named,
|
|
139718
|
+
ready: false,
|
|
139719
|
+
devices: []
|
|
139720
|
+
}, device),
|
|
139721
|
+
suggestedName: echoOptionLabel(device)
|
|
139722
|
+
}));
|
|
139723
|
+
}
|
|
138789
139724
|
const taken = new Set(targets.flatMap((row) => {
|
|
138790
139725
|
const owner = brokerFor(row.config);
|
|
138791
139726
|
return owner === void 0 ? [] : [`${owner.id}/${readString(row.config, "echoId")}`];
|
|
@@ -139776,22 +140711,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
139776
140711
|
labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
|
|
139777
140712
|
content: "This integration links to an Amazon Alexa account broker. Add, edit, or remove accounts from External systems → Brokers, then adopt the imported devices. The integration only stores a reference to its broker. NOTE: this is an unofficial integration that Amazon can change or break without notice."
|
|
139778
140713
|
}]
|
|
139779
|
-
}, {
|
|
139780
|
-
id: "alexa-proxy-login",
|
|
139781
|
-
title: "Interactive login (proxy)",
|
|
139782
|
-
titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
|
|
139783
|
-
description: "The \"Sign in\" button in onboarding opens the real Amazon login page through this hub (same address and HTTPS as this page) and captures the cookie after you complete it (incl. 2FA). The port below is only used on the hub itself and is never exposed.",
|
|
139784
|
-
descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
|
|
139785
|
-
columns: 2,
|
|
139786
|
-
fields: [{
|
|
139787
|
-
type: "number",
|
|
139788
|
-
key: "proxyLogin.proxyPort",
|
|
139789
|
-
label: "Internal login port (loopback)",
|
|
139790
|
-
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
|
|
139791
|
-
min: 1,
|
|
139792
|
-
max: 65535,
|
|
139793
|
-
default: 3456
|
|
139794
|
-
}]
|
|
139795
140714
|
}] });
|
|
139796
140715
|
}
|
|
139797
140716
|
async supportsManualCreation() {
|