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