@camstack/addon-import-alexa 0.2.169 → 0.2.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +1057 -147
- package/dist/addon.mjs +1057 -147
- 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"
|
|
@@ -63392,7 +64061,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63392
64061
|
id: string(),
|
|
63393
64062
|
settings: record(string(), unknown()),
|
|
63394
64063
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63395
|
-
secretKeys: array(string()).optional()
|
|
64064
|
+
secretKeys: array(string()).optional(),
|
|
64065
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
64066
|
+
skipRestart: boolean().optional()
|
|
63396
64067
|
}), object({ success: boolean() }), {
|
|
63397
64068
|
kind: "mutation",
|
|
63398
64069
|
auth: "admin"
|
|
@@ -64020,6 +64691,39 @@ var TlsStatusSchema = object({
|
|
|
64020
64691
|
hasCa: boolean()
|
|
64021
64692
|
}).nullable()
|
|
64022
64693
|
});
|
|
64694
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64695
|
+
var ScanRequestSchema = object({
|
|
64696
|
+
providerId: string().min(1),
|
|
64697
|
+
modes: array(DiscoveryModeSchema)
|
|
64698
|
+
});
|
|
64699
|
+
var ScanErrorSchema = object({
|
|
64700
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64701
|
+
providerId: string().optional(),
|
|
64702
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64703
|
+
mode: string().optional(),
|
|
64704
|
+
message: string()
|
|
64705
|
+
});
|
|
64706
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64707
|
+
var ScanStatsSchema = object({
|
|
64708
|
+
hostsProbed: number$1().int(),
|
|
64709
|
+
hits: number$1().int(),
|
|
64710
|
+
timeouts: number$1().int(),
|
|
64711
|
+
byMode: record(string(), object({
|
|
64712
|
+
probed: number$1().int(),
|
|
64713
|
+
hits: number$1().int(),
|
|
64714
|
+
timeouts: number$1().int()
|
|
64715
|
+
}))
|
|
64716
|
+
});
|
|
64717
|
+
var ScanNetworkResultSchema = object({
|
|
64718
|
+
hits: array(object({
|
|
64719
|
+
providerId: string(),
|
|
64720
|
+
hit: DiscoveryHitSchema
|
|
64721
|
+
})),
|
|
64722
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64723
|
+
subnetsScanned: array(string()),
|
|
64724
|
+
errors: array(ScanErrorSchema),
|
|
64725
|
+
stats: ScanStatsSchema
|
|
64726
|
+
});
|
|
64023
64727
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
64024
64728
|
/**
|
|
64025
64729
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -64074,6 +64778,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
64074
64778
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
64075
64779
|
kind: "mutation",
|
|
64076
64780
|
auth: "admin"
|
|
64781
|
+
}), method(object({
|
|
64782
|
+
requests: array(ScanRequestSchema),
|
|
64783
|
+
subnets: array(string()).optional()
|
|
64784
|
+
}), ScanNetworkResultSchema, {
|
|
64785
|
+
kind: "mutation",
|
|
64786
|
+
auth: "admin",
|
|
64787
|
+
timeoutMs: 9e4
|
|
64788
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64789
|
+
kind: "mutation",
|
|
64790
|
+
auth: "admin"
|
|
64077
64791
|
}), method(_void(), TlsStatusSchema, {
|
|
64078
64792
|
kind: "mutation",
|
|
64079
64793
|
auth: "admin"
|
|
@@ -73656,6 +74370,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73656
74370
|
async discoverDevices(_input) {
|
|
73657
74371
|
return [];
|
|
73658
74372
|
}
|
|
74373
|
+
/**
|
|
74374
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74375
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74376
|
+
*/
|
|
74377
|
+
async getDiscoveryModes() {
|
|
74378
|
+
return [];
|
|
74379
|
+
}
|
|
74380
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74381
|
+
async classifyDiscoveryHits(_input) {
|
|
74382
|
+
return [];
|
|
74383
|
+
}
|
|
73659
74384
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73660
74385
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73661
74386
|
async getDiscoveryParamsSchema() {
|
|
@@ -74524,6 +75249,13 @@ Object.freeze({
|
|
|
74524
75249
|
access: "create",
|
|
74525
75250
|
role: "admin"
|
|
74526
75251
|
},
|
|
75252
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
75253
|
+
capName: "addon-settings",
|
|
75254
|
+
capScope: "system",
|
|
75255
|
+
addonId: null,
|
|
75256
|
+
access: "view",
|
|
75257
|
+
role: "admin"
|
|
75258
|
+
},
|
|
74527
75259
|
"addonSettings.getDeviceSettings": {
|
|
74528
75260
|
capName: "addon-settings",
|
|
74529
75261
|
capScope: "system",
|
|
@@ -74545,6 +75277,13 @@ Object.freeze({
|
|
|
74545
75277
|
access: "view",
|
|
74546
75278
|
role: "admin"
|
|
74547
75279
|
},
|
|
75280
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
75281
|
+
capName: "addon-settings",
|
|
75282
|
+
capScope: "system",
|
|
75283
|
+
addonId: null,
|
|
75284
|
+
access: "create",
|
|
75285
|
+
role: "admin"
|
|
75286
|
+
},
|
|
74548
75287
|
"addonSettings.updateDeviceSettings": {
|
|
74549
75288
|
capName: "addon-settings",
|
|
74550
75289
|
capScope: "system",
|
|
@@ -76960,6 +77699,13 @@ Object.freeze({
|
|
|
76960
77699
|
access: "create",
|
|
76961
77700
|
role: "admin"
|
|
76962
77701
|
},
|
|
77702
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77703
|
+
capName: "device-provider",
|
|
77704
|
+
capScope: "system",
|
|
77705
|
+
addonId: null,
|
|
77706
|
+
access: "create",
|
|
77707
|
+
role: "admin"
|
|
77708
|
+
},
|
|
76963
77709
|
"deviceProvider.createDevice": {
|
|
76964
77710
|
capName: "device-provider",
|
|
76965
77711
|
capScope: "system",
|
|
@@ -76988,6 +77734,13 @@ Object.freeze({
|
|
|
76988
77734
|
access: "view",
|
|
76989
77735
|
role: "admin"
|
|
76990
77736
|
},
|
|
77737
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77738
|
+
capName: "device-provider",
|
|
77739
|
+
capScope: "system",
|
|
77740
|
+
addonId: null,
|
|
77741
|
+
access: "view",
|
|
77742
|
+
role: "admin"
|
|
77743
|
+
},
|
|
76991
77744
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76992
77745
|
capName: "device-provider",
|
|
76993
77746
|
capScope: "system",
|
|
@@ -77765,6 +78518,13 @@ Object.freeze({
|
|
|
77765
78518
|
access: "create",
|
|
77766
78519
|
role: "admin"
|
|
77767
78520
|
},
|
|
78521
|
+
"keychain.releaseRecordRefs": {
|
|
78522
|
+
capName: "keychain",
|
|
78523
|
+
capScope: "system",
|
|
78524
|
+
addonId: null,
|
|
78525
|
+
access: "create",
|
|
78526
|
+
role: "admin"
|
|
78527
|
+
},
|
|
77768
78528
|
"keychain.resolve": {
|
|
77769
78529
|
capName: "keychain",
|
|
77770
78530
|
capScope: "system",
|
|
@@ -78185,6 +78945,13 @@ Object.freeze({
|
|
|
78185
78945
|
access: "view",
|
|
78186
78946
|
role: "admin"
|
|
78187
78947
|
},
|
|
78948
|
+
"localNetwork.getScanSubnets": {
|
|
78949
|
+
capName: "local-network",
|
|
78950
|
+
capScope: "system",
|
|
78951
|
+
addonId: null,
|
|
78952
|
+
access: "view",
|
|
78953
|
+
role: "admin"
|
|
78954
|
+
},
|
|
78188
78955
|
"localNetwork.getTlsStatus": {
|
|
78189
78956
|
capName: "local-network",
|
|
78190
78957
|
capScope: "system",
|
|
@@ -78234,6 +79001,13 @@ Object.freeze({
|
|
|
78234
79001
|
access: "create",
|
|
78235
79002
|
role: "admin"
|
|
78236
79003
|
},
|
|
79004
|
+
"localNetwork.scanNetwork": {
|
|
79005
|
+
capName: "local-network",
|
|
79006
|
+
capScope: "system",
|
|
79007
|
+
addonId: null,
|
|
79008
|
+
access: "create",
|
|
79009
|
+
role: "admin"
|
|
79010
|
+
},
|
|
78237
79011
|
"localNetwork.setAddressRole": {
|
|
78238
79012
|
capName: "local-network",
|
|
78239
79013
|
capScope: "system",
|
|
@@ -78255,6 +79029,13 @@ Object.freeze({
|
|
|
78255
79029
|
access: "create",
|
|
78256
79030
|
role: "admin"
|
|
78257
79031
|
},
|
|
79032
|
+
"localNetwork.setScanSubnets": {
|
|
79033
|
+
capName: "local-network",
|
|
79034
|
+
capScope: "system",
|
|
79035
|
+
addonId: null,
|
|
79036
|
+
access: "create",
|
|
79037
|
+
role: "admin"
|
|
79038
|
+
},
|
|
78258
79039
|
"localNetwork.setViewerEndpoints": {
|
|
78259
79040
|
capName: "local-network",
|
|
78260
79041
|
capScope: "system",
|
|
@@ -82476,6 +83257,13 @@ Object.freeze({
|
|
|
82476
83257
|
access: "create",
|
|
82477
83258
|
role: "admin"
|
|
82478
83259
|
},
|
|
83260
|
+
"storageAdmin.dropOrphanedKind": {
|
|
83261
|
+
capName: "storage-admin",
|
|
83262
|
+
capScope: "system",
|
|
83263
|
+
addonId: null,
|
|
83264
|
+
access: "delete",
|
|
83265
|
+
role: "admin"
|
|
83266
|
+
},
|
|
82479
83267
|
"storageAdmin.listKinds": {
|
|
82480
83268
|
capName: "storage-admin",
|
|
82481
83269
|
capScope: "system",
|
|
@@ -82497,6 +83285,27 @@ Object.freeze({
|
|
|
82497
83285
|
access: "view",
|
|
82498
83286
|
role: "admin"
|
|
82499
83287
|
},
|
|
83288
|
+
"storageAdmin.scanStaleData": {
|
|
83289
|
+
capName: "storage-admin",
|
|
83290
|
+
capScope: "system",
|
|
83291
|
+
addonId: null,
|
|
83292
|
+
access: "create",
|
|
83293
|
+
role: "admin"
|
|
83294
|
+
},
|
|
83295
|
+
"storageAdmin.staleDataReport": {
|
|
83296
|
+
capName: "storage-admin",
|
|
83297
|
+
capScope: "system",
|
|
83298
|
+
addonId: null,
|
|
83299
|
+
access: "view",
|
|
83300
|
+
role: "admin"
|
|
83301
|
+
},
|
|
83302
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
83303
|
+
capName: "storage-admin",
|
|
83304
|
+
capScope: "system",
|
|
83305
|
+
addonId: null,
|
|
83306
|
+
access: "create",
|
|
83307
|
+
role: "admin"
|
|
83308
|
+
},
|
|
82500
83309
|
"storageEvictable.evict": {
|
|
82501
83310
|
capName: "storage-evictable",
|
|
82502
83311
|
capScope: "system",
|
|
@@ -92770,12 +93579,22 @@ function rewriteBody(body, kind, currentUpstream, route) {
|
|
|
92770
93579
|
/**
|
|
92771
93580
|
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
92772
93581
|
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
92773
|
-
|
|
92774
|
-
|
|
92775
|
-
|
|
93582
|
+
* `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
|
|
93583
|
+
* also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
|
|
93584
|
+
* `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
|
|
93585
|
+
*/
|
|
93586
|
+
function rewriteSetCookie(cookie, route, options = {}) {
|
|
93587
|
+
const parts = cookie.split(";").map((part) => part.trim());
|
|
93588
|
+
const dropped = options.browserSafe === true ? [
|
|
93589
|
+
"domain",
|
|
93590
|
+
"path",
|
|
93591
|
+
"secure",
|
|
93592
|
+
"samesite"
|
|
93593
|
+
] : ["domain", "path"];
|
|
93594
|
+
const kept = parts.filter((part, index) => {
|
|
92776
93595
|
if (index === 0) return true;
|
|
92777
93596
|
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
92778
|
-
return name
|
|
93597
|
+
return !dropped.includes(name);
|
|
92779
93598
|
});
|
|
92780
93599
|
kept.push(`Path=${route.basePath}/`);
|
|
92781
93600
|
return kept.join("; ");
|
|
@@ -92786,10 +93605,10 @@ function rewriteSetCookie(cookie, route) {
|
|
|
92786
93605
|
* one, so a past-dated set that only deleted the other domain's copy would delete the
|
|
92787
93606
|
* live one the same response just set: that deletion is dropped.
|
|
92788
93607
|
*/
|
|
92789
|
-
function rewriteSetCookies(cookies, route) {
|
|
93608
|
+
function rewriteSetCookies(cookies, route, options = {}) {
|
|
92790
93609
|
const now = Date.now();
|
|
92791
93610
|
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));
|
|
93611
|
+
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
93612
|
}
|
|
92794
93613
|
function cookieName(cookie) {
|
|
92795
93614
|
return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
|
|
@@ -92849,6 +93668,99 @@ function rewriteFormBody(body, route) {
|
|
|
92849
93668
|
function filterForwardedCookies(cookieHeader) {
|
|
92850
93669
|
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
92851
93670
|
}
|
|
93671
|
+
/**
|
|
93672
|
+
* Server-side cookie jar of ONE vendor sign-in attempt (D955).
|
|
93673
|
+
*
|
|
93674
|
+
* A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
|
|
93675
|
+
* without the cookies it set a step earlier. Relying on the browser to store them under the
|
|
93676
|
+
* hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
|
|
93677
|
+
* on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
|
|
93678
|
+
* `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
|
|
93679
|
+
* whatever the browser did. Values never leave this object except as the upstream `Cookie`
|
|
93680
|
+
* header; nothing here logs.
|
|
93681
|
+
*/
|
|
93682
|
+
function attributeValue(attrs, name) {
|
|
93683
|
+
const found = attrs.find((a) => a.split("=")[0]?.trim().toLowerCase() === name);
|
|
93684
|
+
if (found === void 0) return void 0;
|
|
93685
|
+
const eq = found.indexOf("=");
|
|
93686
|
+
return eq === -1 ? "" : found.slice(eq + 1).trim();
|
|
93687
|
+
}
|
|
93688
|
+
function hasFlag(attrs, name) {
|
|
93689
|
+
return attrs.some((a) => a.toLowerCase() === name);
|
|
93690
|
+
}
|
|
93691
|
+
function expiryOf(attrs, nowMs) {
|
|
93692
|
+
const maxAge = attributeValue(attrs, "max-age");
|
|
93693
|
+
if (maxAge !== void 0 && maxAge.length > 0 && Number.isFinite(Number(maxAge))) return nowMs + Number(maxAge) * 1e3;
|
|
93694
|
+
const expires = attributeValue(attrs, "expires");
|
|
93695
|
+
if (expires === void 0) return null;
|
|
93696
|
+
const at = Date.parse(expires);
|
|
93697
|
+
return Number.isFinite(at) ? at : null;
|
|
93698
|
+
}
|
|
93699
|
+
function defaultPath(requestPath) {
|
|
93700
|
+
const last = requestPath.lastIndexOf("/");
|
|
93701
|
+
return last <= 0 ? "/" : requestPath.slice(0, last);
|
|
93702
|
+
}
|
|
93703
|
+
function pathMatches(cookiePath, requestPath) {
|
|
93704
|
+
if (requestPath === cookiePath) return true;
|
|
93705
|
+
if (!requestPath.startsWith(cookiePath)) return false;
|
|
93706
|
+
return cookiePath.endsWith("/") || requestPath.charAt(cookiePath.length) === "/";
|
|
93707
|
+
}
|
|
93708
|
+
function hostMatches(cookie, host) {
|
|
93709
|
+
if (cookie.domain === null) return cookie.originHost === host;
|
|
93710
|
+
const bare = hostWithoutPort(host);
|
|
93711
|
+
return bare === cookie.domain || bare.endsWith(`.${cookie.domain}`);
|
|
93712
|
+
}
|
|
93713
|
+
var VendorCookieJar = class {
|
|
93714
|
+
#cookies = /* @__PURE__ */ new Map();
|
|
93715
|
+
/** Store (or delete, when expired) the cookies of one upstream response. */
|
|
93716
|
+
store(setCookies, origin, nowMs) {
|
|
93717
|
+
for (const raw of setCookies) {
|
|
93718
|
+
const [first, ...rest] = raw.split(";").map((part) => part.trim());
|
|
93719
|
+
const eq = first?.indexOf("=") ?? -1;
|
|
93720
|
+
if (first === void 0 || eq <= 0) continue;
|
|
93721
|
+
const name = first.slice(0, eq);
|
|
93722
|
+
const attrs = rest;
|
|
93723
|
+
const domainAttr = attributeValue(attrs, "domain");
|
|
93724
|
+
const domain = domainAttr === void 0 || domainAttr.length === 0 ? null : domainAttr.replace(/^\./, "").toLowerCase();
|
|
93725
|
+
const pathAttr = attributeValue(attrs, "path");
|
|
93726
|
+
const path = pathAttr !== void 0 && pathAttr.startsWith("/") ? pathAttr : defaultPath(origin.pathname);
|
|
93727
|
+
const originBare = hostWithoutPort(origin.host);
|
|
93728
|
+
if (domain !== null && originBare !== domain && !originBare.endsWith(`.${domain}`)) continue;
|
|
93729
|
+
const expiresAtMs = expiryOf(attrs, nowMs);
|
|
93730
|
+
const key = `${name}|${domain ?? origin.host}|${path}`;
|
|
93731
|
+
if (expiresAtMs !== null && expiresAtMs <= nowMs) {
|
|
93732
|
+
this.#cookies.delete(key);
|
|
93733
|
+
continue;
|
|
93734
|
+
}
|
|
93735
|
+
this.#cookies.set(key, {
|
|
93736
|
+
name,
|
|
93737
|
+
value: first.slice(eq + 1),
|
|
93738
|
+
originHost: origin.host,
|
|
93739
|
+
domain,
|
|
93740
|
+
path,
|
|
93741
|
+
secure: hasFlag(attrs, "secure"),
|
|
93742
|
+
expiresAtMs
|
|
93743
|
+
});
|
|
93744
|
+
}
|
|
93745
|
+
}
|
|
93746
|
+
/** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
|
|
93747
|
+
headerFor(target, nowMs) {
|
|
93748
|
+
const secureOk = target.scheme === "https" || isLoopbackHost(target.host);
|
|
93749
|
+
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);
|
|
93750
|
+
if (matching.length === 0) return null;
|
|
93751
|
+
return matching.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
93752
|
+
}
|
|
93753
|
+
};
|
|
93754
|
+
/** Cookie names carried by a `Cookie` header value. */
|
|
93755
|
+
function cookieHeaderNames(header) {
|
|
93756
|
+
return new Set(header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0));
|
|
93757
|
+
}
|
|
93758
|
+
/** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
|
|
93759
|
+
function mergeCookieHeaders(browser, jar) {
|
|
93760
|
+
if (jar === null) return browser;
|
|
93761
|
+
const shadowed = cookieHeaderNames(jar);
|
|
93762
|
+
return [...browser.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !shadowed.has(part.split("=")[0]?.trim() ?? "")), jar].join("; ");
|
|
93763
|
+
}
|
|
92852
93764
|
/** A short stable label for the attempt: enough to group lines, useless as a capability. */
|
|
92853
93765
|
function attemptLabel(attemptId) {
|
|
92854
93766
|
let hash = 2166136261;
|
|
@@ -93044,6 +93956,7 @@ var VendorLoginProxy = class {
|
|
|
93044
93956
|
this.#attempts.set(spec.attemptId, {
|
|
93045
93957
|
expiresAtMs: spec.expiresAtMs,
|
|
93046
93958
|
upstreams: spec.upstreams,
|
|
93959
|
+
jar: spec.serverCookieJar === true ? new VendorCookieJar() : null,
|
|
93047
93960
|
evaluate: async (view) => {
|
|
93048
93961
|
const verdict = await spec.terminal(view);
|
|
93049
93962
|
if (verdict.kind === "continue") return verdict;
|
|
@@ -93140,7 +94053,7 @@ var VendorLoginProxy = class {
|
|
|
93140
94053
|
body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
|
|
93141
94054
|
} else body = read;
|
|
93142
94055
|
}
|
|
93143
|
-
const headers = this.#requestHeaders(req, route, upstream);
|
|
94056
|
+
const headers = this.#requestHeaders(req, route, upstream, attempt.jar, target);
|
|
93144
94057
|
const upstreamRes = await this.#fetch(target, {
|
|
93145
94058
|
method,
|
|
93146
94059
|
headers,
|
|
@@ -93149,6 +94062,10 @@ var VendorLoginProxy = class {
|
|
|
93149
94062
|
});
|
|
93150
94063
|
const location = upstreamRes.headers.get("location");
|
|
93151
94064
|
const setCookies = upstreamRes.headers.getSetCookie();
|
|
94065
|
+
attempt.jar?.store(setCookies, {
|
|
94066
|
+
host: upstream.host,
|
|
94067
|
+
pathname: target.pathname
|
|
94068
|
+
}, this.#now());
|
|
93152
94069
|
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
93153
94070
|
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
93154
94071
|
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
@@ -93196,7 +94113,7 @@ var VendorLoginProxy = class {
|
|
|
93196
94113
|
upstreamRes.headers.forEach((value, name) => {
|
|
93197
94114
|
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
93198
94115
|
});
|
|
93199
|
-
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
|
|
94116
|
+
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route, { browserSafe: attempt.jar !== null });
|
|
93200
94117
|
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
93201
94118
|
out["cache-control"] = "no-store";
|
|
93202
94119
|
if (text !== null && kind !== null) {
|
|
@@ -93218,15 +94135,19 @@ var VendorLoginProxy = class {
|
|
|
93218
94135
|
...flowTraceMeta(input)
|
|
93219
94136
|
} });
|
|
93220
94137
|
}
|
|
93221
|
-
#requestHeaders(req, route, upstream) {
|
|
94138
|
+
#requestHeaders(req, route, upstream, jar, target) {
|
|
93222
94139
|
const headers = {};
|
|
93223
94140
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
93224
94141
|
const lower = name.toLowerCase();
|
|
93225
94142
|
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
93226
94143
|
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
93227
94144
|
}
|
|
93228
|
-
const
|
|
93229
|
-
|
|
94145
|
+
const merged = mergeCookieHeaders(filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? ""), jar?.headerFor({
|
|
94146
|
+
host: upstream.host,
|
|
94147
|
+
pathname: target.pathname,
|
|
94148
|
+
scheme: upstream.scheme
|
|
94149
|
+
}, this.#now()) ?? null);
|
|
94150
|
+
if (merged.length > 0) headers["cookie"] = merged;
|
|
93230
94151
|
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
93231
94152
|
const referer = firstHeader(req.headers["referer"]);
|
|
93232
94153
|
if (referer !== void 0) {
|
|
@@ -136249,10 +137170,6 @@ var AlexaRemoteClient = class {
|
|
|
136249
137170
|
if (isAuthError(message)) throw new AlexaAuthError(message);
|
|
136250
137171
|
throw err;
|
|
136251
137172
|
}
|
|
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
137173
|
this.#connected = true;
|
|
136257
137174
|
await this.#persistSession(remote);
|
|
136258
137175
|
}
|
|
@@ -136988,7 +137905,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
136988
137905
|
label: "Alexa",
|
|
136989
137906
|
signIn: {
|
|
136990
137907
|
basePath: "/addon/import-alexa/login",
|
|
136991
|
-
methods: ["device-code"
|
|
137908
|
+
methods: ["device-code"]
|
|
136992
137909
|
}
|
|
136993
137910
|
}]
|
|
136994
137911
|
}],
|
|
@@ -138172,6 +139089,15 @@ var BrokerEcho = class {
|
|
|
138172
139089
|
try {
|
|
138173
139090
|
const seed = await this.deps.readSeed();
|
|
138174
139091
|
const stored = await sessions.get(brokerId);
|
|
139092
|
+
logger.info("alexa speak: broker sign-in material", {
|
|
139093
|
+
...tags,
|
|
139094
|
+
meta: {
|
|
139095
|
+
seedFound: seed !== null,
|
|
139096
|
+
hasRefreshToken: seed !== null && seed.refreshToken.length > 0,
|
|
139097
|
+
hasDeviceAppName: seed !== null && seed.deviceAppName.length > 0,
|
|
139098
|
+
hasStoredSession: stored.length > 0
|
|
139099
|
+
}
|
|
139100
|
+
});
|
|
138175
139101
|
if (seed === null && stored.length === 0) {
|
|
138176
139102
|
this.currentState = "broker-not-signed-in";
|
|
138177
139103
|
this.currentError = null;
|
|
@@ -139774,22 +140700,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
139774
140700
|
labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
|
|
139775
140701
|
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
140702
|
}]
|
|
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
140703
|
}] });
|
|
139794
140704
|
}
|
|
139795
140705
|
async supportsManualCreation() {
|