@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.js
CHANGED
|
@@ -5651,7 +5651,7 @@ function number(params) {
|
|
|
5651
5651
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
5652
5652
|
}
|
|
5653
5653
|
//#endregion
|
|
5654
|
-
//#region ../types/dist/sleep-
|
|
5654
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5655
5655
|
/**
|
|
5656
5656
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5657
5657
|
* window to float samples (D455).
|
|
@@ -7718,6 +7718,7 @@ var BaseAddon = class {
|
|
|
7718
7718
|
* a warning and falls back rather than crashing boot.
|
|
7719
7719
|
*/
|
|
7720
7720
|
state(key, schema, fallback) {
|
|
7721
|
+
this._declaredStateKeys.add(key);
|
|
7721
7722
|
return createDurableState({
|
|
7722
7723
|
key,
|
|
7723
7724
|
schema,
|
|
@@ -7733,6 +7734,96 @@ var BaseAddon = class {
|
|
|
7733
7734
|
} })
|
|
7734
7735
|
});
|
|
7735
7736
|
}
|
|
7737
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7738
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7739
|
+
/**
|
|
7740
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7741
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7742
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7743
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7744
|
+
*/
|
|
7745
|
+
async getDeclaredSettingsKeys() {
|
|
7746
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7747
|
+
}
|
|
7748
|
+
declaredKeySet() {
|
|
7749
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7750
|
+
const schema = this.globalSettingsSchema();
|
|
7751
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7752
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7753
|
+
for (const path of this.secretStatePaths) {
|
|
7754
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7755
|
+
if (root) keys.add(root);
|
|
7756
|
+
}
|
|
7757
|
+
return keys;
|
|
7758
|
+
}
|
|
7759
|
+
/**
|
|
7760
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7761
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7762
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7763
|
+
*/
|
|
7764
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7765
|
+
const settings = this._ctx?.settings;
|
|
7766
|
+
const raw = this.rawSettings();
|
|
7767
|
+
if (!settings || !raw) return {
|
|
7768
|
+
removed: [],
|
|
7769
|
+
refused: keys.map((key) => ({
|
|
7770
|
+
key,
|
|
7771
|
+
reason: "no settings store"
|
|
7772
|
+
})),
|
|
7773
|
+
secretsCleared: 0
|
|
7774
|
+
};
|
|
7775
|
+
let stored;
|
|
7776
|
+
try {
|
|
7777
|
+
stored = await raw.readAddonStore();
|
|
7778
|
+
} catch (error) {
|
|
7779
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7780
|
+
return {
|
|
7781
|
+
removed: [],
|
|
7782
|
+
refused: keys.map((key) => ({
|
|
7783
|
+
key,
|
|
7784
|
+
reason
|
|
7785
|
+
})),
|
|
7786
|
+
secretsCleared: 0
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
const declared = this.declaredKeySet();
|
|
7790
|
+
const removed = [];
|
|
7791
|
+
const refused = [];
|
|
7792
|
+
for (const key of keys) {
|
|
7793
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7794
|
+
if (declared.has(base)) refused.push({
|
|
7795
|
+
key,
|
|
7796
|
+
reason: "still declared by the addon"
|
|
7797
|
+
});
|
|
7798
|
+
else if (!(key in stored)) refused.push({
|
|
7799
|
+
key,
|
|
7800
|
+
reason: "not stored"
|
|
7801
|
+
});
|
|
7802
|
+
else removed.push(key);
|
|
7803
|
+
}
|
|
7804
|
+
if (removed.length === 0) return {
|
|
7805
|
+
removed,
|
|
7806
|
+
refused,
|
|
7807
|
+
secretsCleared: 0
|
|
7808
|
+
};
|
|
7809
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7810
|
+
const held = /* @__PURE__ */ new Set();
|
|
7811
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7812
|
+
const target = removed.includes(key) ? held : kept;
|
|
7813
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7814
|
+
}
|
|
7815
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7816
|
+
const patch = {};
|
|
7817
|
+
for (const key of removed) patch[key] = void 0;
|
|
7818
|
+
await settings.writeAddonStore(patch);
|
|
7819
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7820
|
+
await this.resolveConfig();
|
|
7821
|
+
return {
|
|
7822
|
+
removed,
|
|
7823
|
+
refused,
|
|
7824
|
+
secretsCleared
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7736
7827
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7737
7828
|
deviceState(deviceId, key, schema, fallback) {
|
|
7738
7829
|
return createDurableState({
|
|
@@ -8003,6 +8094,21 @@ function dropPerNodeFields(fields) {
|
|
|
8003
8094
|
}
|
|
8004
8095
|
return kept;
|
|
8005
8096
|
}
|
|
8097
|
+
function collectFieldKeys(fields) {
|
|
8098
|
+
const collected = [];
|
|
8099
|
+
for (const field of fields) {
|
|
8100
|
+
if (field.type === "group") {
|
|
8101
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
8102
|
+
continue;
|
|
8103
|
+
}
|
|
8104
|
+
if (field.type === "sub-tabs") {
|
|
8105
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
8106
|
+
continue;
|
|
8107
|
+
}
|
|
8108
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8109
|
+
}
|
|
8110
|
+
return collected;
|
|
8111
|
+
}
|
|
8006
8112
|
function collectPerNodeFieldKeys(fields) {
|
|
8007
8113
|
const collected = [];
|
|
8008
8114
|
for (const field of fields) {
|
|
@@ -16557,8 +16663,172 @@ object({
|
|
|
16557
16663
|
quantity: ReadingQuantitySchema,
|
|
16558
16664
|
unit: string().min(1)
|
|
16559
16665
|
});
|
|
16560
|
-
|
|
16561
|
-
|
|
16666
|
+
new Set([
|
|
16667
|
+
{
|
|
16668
|
+
id: "person",
|
|
16669
|
+
name: "Person"
|
|
16670
|
+
},
|
|
16671
|
+
{
|
|
16672
|
+
id: "vehicle",
|
|
16673
|
+
name: "Vehicle"
|
|
16674
|
+
},
|
|
16675
|
+
{
|
|
16676
|
+
id: "animal",
|
|
16677
|
+
name: "Animal"
|
|
16678
|
+
},
|
|
16679
|
+
{
|
|
16680
|
+
id: "package",
|
|
16681
|
+
name: "Package"
|
|
16682
|
+
}
|
|
16683
|
+
].map((l) => l.id));
|
|
16684
|
+
var COCO_TO_MACRO = {
|
|
16685
|
+
mapping: {
|
|
16686
|
+
person: "person",
|
|
16687
|
+
bicycle: "vehicle",
|
|
16688
|
+
car: "vehicle",
|
|
16689
|
+
motorcycle: "vehicle",
|
|
16690
|
+
airplane: "vehicle",
|
|
16691
|
+
bus: "vehicle",
|
|
16692
|
+
train: "vehicle",
|
|
16693
|
+
truck: "vehicle",
|
|
16694
|
+
boat: "vehicle",
|
|
16695
|
+
bird: "animal",
|
|
16696
|
+
cat: "animal",
|
|
16697
|
+
dog: "animal",
|
|
16698
|
+
horse: "animal",
|
|
16699
|
+
sheep: "animal",
|
|
16700
|
+
cow: "animal",
|
|
16701
|
+
elephant: "animal",
|
|
16702
|
+
bear: "animal",
|
|
16703
|
+
zebra: "animal",
|
|
16704
|
+
giraffe: "animal",
|
|
16705
|
+
suitcase: "package",
|
|
16706
|
+
backpack: "package",
|
|
16707
|
+
handbag: "package"
|
|
16708
|
+
},
|
|
16709
|
+
preserveOriginal: false
|
|
16710
|
+
};
|
|
16711
|
+
/**
|
|
16712
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16713
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16714
|
+
*
|
|
16715
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16716
|
+
* second list. The label that fits depends on the macro class:
|
|
16717
|
+
*
|
|
16718
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16719
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16720
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16721
|
+
* - `package` → a sub-class.
|
|
16722
|
+
*
|
|
16723
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16724
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16725
|
+
*
|
|
16726
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16727
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16728
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16729
|
+
* the subject GETS it — not at the first detection.
|
|
16730
|
+
*/
|
|
16731
|
+
var SubClassLabelSchema = object({
|
|
16732
|
+
kind: literal("sub-class"),
|
|
16733
|
+
className: string().min(1)
|
|
16734
|
+
});
|
|
16735
|
+
var IdentityLabelSchema = object({
|
|
16736
|
+
kind: literal("identity"),
|
|
16737
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16738
|
+
identityId: string().min(1),
|
|
16739
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16740
|
+
name: string().min(1)
|
|
16741
|
+
});
|
|
16742
|
+
var PlateLabelSchema = object({
|
|
16743
|
+
kind: literal("plate"),
|
|
16744
|
+
text: string().min(1)
|
|
16745
|
+
});
|
|
16746
|
+
function macroOf$1(className) {
|
|
16747
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16748
|
+
}
|
|
16749
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16750
|
+
function subClassBelongs(macro, label) {
|
|
16751
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16752
|
+
}
|
|
16753
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16754
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16755
|
+
object({
|
|
16756
|
+
macro: literal("animal"),
|
|
16757
|
+
label: subClassOf("animal").optional()
|
|
16758
|
+
}),
|
|
16759
|
+
object({
|
|
16760
|
+
macro: literal("person"),
|
|
16761
|
+
label: IdentityLabelSchema.optional()
|
|
16762
|
+
}),
|
|
16763
|
+
object({
|
|
16764
|
+
macro: literal("vehicle"),
|
|
16765
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16766
|
+
}),
|
|
16767
|
+
object({
|
|
16768
|
+
macro: literal("package"),
|
|
16769
|
+
label: subClassOf("package").optional()
|
|
16770
|
+
}),
|
|
16771
|
+
object({
|
|
16772
|
+
macro: literal("other"),
|
|
16773
|
+
className: string().min(1)
|
|
16774
|
+
})
|
|
16775
|
+
]);
|
|
16776
|
+
/**
|
|
16777
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16778
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16779
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16780
|
+
*/
|
|
16781
|
+
var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
|
|
16782
|
+
/**
|
|
16783
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16784
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16785
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16786
|
+
*/
|
|
16787
|
+
function subjectFromClassName(className) {
|
|
16788
|
+
const name = className.trim().toLowerCase();
|
|
16789
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16790
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16791
|
+
if (macro === void 0) return {
|
|
16792
|
+
macro: "other",
|
|
16793
|
+
className
|
|
16794
|
+
};
|
|
16795
|
+
const label = {
|
|
16796
|
+
kind: "sub-class",
|
|
16797
|
+
className: name
|
|
16798
|
+
};
|
|
16799
|
+
switch (macro) {
|
|
16800
|
+
case "animal": return {
|
|
16801
|
+
macro: "animal",
|
|
16802
|
+
label
|
|
16803
|
+
};
|
|
16804
|
+
case "vehicle": return {
|
|
16805
|
+
macro: "vehicle",
|
|
16806
|
+
label
|
|
16807
|
+
};
|
|
16808
|
+
case "package": return {
|
|
16809
|
+
macro: "package",
|
|
16810
|
+
label
|
|
16811
|
+
};
|
|
16812
|
+
default: return {
|
|
16813
|
+
macro: "other",
|
|
16814
|
+
className
|
|
16815
|
+
};
|
|
16816
|
+
}
|
|
16817
|
+
}
|
|
16818
|
+
function macroOnly(macro) {
|
|
16819
|
+
switch (macro) {
|
|
16820
|
+
case "animal": return { macro: "animal" };
|
|
16821
|
+
case "person": return { macro: "person" };
|
|
16822
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16823
|
+
case "package": return { macro: "package" };
|
|
16824
|
+
default: return {
|
|
16825
|
+
macro: "other",
|
|
16826
|
+
className: macro
|
|
16827
|
+
};
|
|
16828
|
+
}
|
|
16829
|
+
}
|
|
16830
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16831
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16562
16832
|
var AutomationConditionOperatorSchema = _enum([
|
|
16563
16833
|
"eq",
|
|
16564
16834
|
"ne",
|
|
@@ -16569,12 +16839,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16569
16839
|
"contains",
|
|
16570
16840
|
"in"
|
|
16571
16841
|
]);
|
|
16572
|
-
|
|
16573
|
-
|
|
16842
|
+
/**
|
|
16843
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16844
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16845
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16846
|
+
* fire; it does not lose it.
|
|
16847
|
+
*/
|
|
16848
|
+
var DeviceBecomesLeafSchema = object({
|
|
16849
|
+
kind: literal("device-becomes"),
|
|
16850
|
+
deviceId: number$1().int().nonnegative(),
|
|
16851
|
+
cap: string().min(1),
|
|
16852
|
+
fieldPath: string().min(1),
|
|
16853
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16854
|
+
becomes: union([
|
|
16855
|
+
string(),
|
|
16856
|
+
number$1(),
|
|
16857
|
+
boolean()
|
|
16858
|
+
]).optional(),
|
|
16859
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16860
|
+
edge: boolean().optional(),
|
|
16861
|
+
/** The new value must hold this long before it counts. */
|
|
16862
|
+
forMs: number$1().int().min(0).max(864e5).optional(),
|
|
16863
|
+
/** Collapse a burst into one occurrence. */
|
|
16864
|
+
debounceMs: number$1().int().min(0).max(6e5).optional()
|
|
16865
|
+
});
|
|
16866
|
+
var DeviceEventLeafSchema = object({
|
|
16867
|
+
kind: literal("device-event"),
|
|
16868
|
+
/** An `EventCategory` value. */
|
|
16869
|
+
category: string().min(1),
|
|
16870
|
+
deviceId: number$1().int().nonnegative().optional()
|
|
16871
|
+
});
|
|
16872
|
+
var ScheduleLeafSchema = object({
|
|
16873
|
+
kind: literal("schedule"),
|
|
16874
|
+
cron: string().min(1).max(120)
|
|
16875
|
+
});
|
|
16876
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16877
|
+
var IntervalLeafSchema = object({
|
|
16878
|
+
kind: literal("interval"),
|
|
16879
|
+
minutes: number$1().int().min(1).max(1440)
|
|
16880
|
+
});
|
|
16881
|
+
var DetectionLeafSchema = object({
|
|
16882
|
+
kind: literal("detection"),
|
|
16883
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16884
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16885
|
+
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16886
|
+
/** The track must have visited at least one of these zones. */
|
|
16887
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16888
|
+
/** Best-so-far confidence the track must reach. */
|
|
16889
|
+
minScore: number$1().min(0).max(1).optional()
|
|
16890
|
+
});
|
|
16891
|
+
var StationaryLeafSchema = object({
|
|
16892
|
+
kind: literal("stationary"),
|
|
16893
|
+
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16894
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16895
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16896
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16897
|
+
phase: _enum(["appeared", "departed"])
|
|
16898
|
+
});
|
|
16899
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16900
|
+
var DeviceIsLeafSchema = object({
|
|
16901
|
+
kind: literal("device-is"),
|
|
16574
16902
|
deviceId: number$1().int().nonnegative(),
|
|
16575
16903
|
cap: string().min(1),
|
|
16576
16904
|
fieldPath: string().min(1),
|
|
16577
16905
|
operator: AutomationConditionOperatorSchema,
|
|
16906
|
+
/**
|
|
16907
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16908
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16909
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16910
|
+
* one is never evaluated (it fails).
|
|
16911
|
+
*/
|
|
16578
16912
|
value: union([
|
|
16579
16913
|
string(),
|
|
16580
16914
|
number$1(),
|
|
@@ -16582,24 +16916,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16582
16916
|
array(union([string(), number$1()])),
|
|
16583
16917
|
QuantityValueSchema
|
|
16584
16918
|
]),
|
|
16919
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16585
16920
|
unit: string().min(1).optional()
|
|
16586
16921
|
});
|
|
16587
|
-
|
|
16922
|
+
/**
|
|
16923
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16924
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16925
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16926
|
+
*/
|
|
16927
|
+
var StationaryPresentLeafSchema = object({
|
|
16588
16928
|
kind: literal("stationary-present"),
|
|
16589
16929
|
deviceId: number$1().int().nonnegative(),
|
|
16590
|
-
|
|
16591
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16592
|
-
/** The parked object must be in at least one of these zones. */
|
|
16930
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16593
16931
|
zones: array(string().min(1)).min(1).optional()
|
|
16594
16932
|
});
|
|
16595
16933
|
/**
|
|
16596
|
-
* The
|
|
16597
|
-
*
|
|
16598
|
-
*
|
|
16599
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16600
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16934
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16935
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16936
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16601
16937
|
*/
|
|
16602
|
-
var
|
|
16938
|
+
var ExpressionLeafSchema = object({
|
|
16603
16939
|
kind: literal("expression"),
|
|
16604
16940
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16605
16941
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16611,55 +16947,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16611
16947
|
path: ["expr"]
|
|
16612
16948
|
});
|
|
16613
16949
|
});
|
|
16614
|
-
var
|
|
16950
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16615
16951
|
object({
|
|
16616
16952
|
kind: literal("all"),
|
|
16617
|
-
children: array(
|
|
16953
|
+
children: array(AutomationWhenSchema)
|
|
16618
16954
|
}),
|
|
16619
16955
|
object({
|
|
16620
16956
|
kind: literal("any"),
|
|
16621
|
-
children: array(
|
|
16957
|
+
children: array(AutomationWhenSchema)
|
|
16622
16958
|
}),
|
|
16623
16959
|
object({
|
|
16624
16960
|
kind: literal("not"),
|
|
16625
|
-
child:
|
|
16961
|
+
child: AutomationWhenSchema
|
|
16626
16962
|
}),
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16963
|
+
DeviceBecomesLeafSchema,
|
|
16964
|
+
DeviceEventLeafSchema,
|
|
16965
|
+
ScheduleLeafSchema,
|
|
16966
|
+
ManualLeafSchema,
|
|
16967
|
+
IntervalLeafSchema,
|
|
16968
|
+
DetectionLeafSchema,
|
|
16969
|
+
StationaryLeafSchema,
|
|
16970
|
+
DeviceIsLeafSchema,
|
|
16971
|
+
StationaryPresentLeafSchema,
|
|
16972
|
+
ExpressionLeafSchema
|
|
16630
16973
|
]));
|
|
16631
16974
|
/**
|
|
16632
|
-
*
|
|
16633
|
-
*
|
|
16634
|
-
*
|
|
16635
|
-
*
|
|
16636
|
-
*
|
|
16637
|
-
*
|
|
16638
|
-
*
|
|
16639
|
-
*
|
|
16640
|
-
|
|
16641
|
-
|
|
16975
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16976
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16977
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16978
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16979
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16980
|
+
* way. Nothing writes this shape any more.
|
|
16981
|
+
*
|
|
16982
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16983
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16984
|
+
* a single trigger is not wrapped in `any`.
|
|
16985
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16986
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16987
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16988
|
+
*/
|
|
16989
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16642
16990
|
object({
|
|
16643
16991
|
kind: literal("device-state"),
|
|
16644
16992
|
deviceId: number$1().int().nonnegative(),
|
|
16645
16993
|
cap: string().min(1),
|
|
16646
16994
|
fieldPath: string().min(1),
|
|
16647
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16648
16995
|
becomes: union([
|
|
16649
16996
|
string(),
|
|
16650
16997
|
number$1(),
|
|
16651
16998
|
boolean()
|
|
16652
16999
|
]).optional(),
|
|
16653
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16654
17000
|
edge: boolean().optional(),
|
|
16655
|
-
/** The condition must hold this long before the run starts. */
|
|
16656
17001
|
forMs: number$1().int().min(0).max(864e5).optional(),
|
|
16657
|
-
/** Collapse a burst into one run. */
|
|
16658
17002
|
debounceMs: number$1().int().min(0).max(6e5).optional()
|
|
16659
17003
|
}),
|
|
16660
17004
|
object({
|
|
16661
17005
|
kind: literal("device-event"),
|
|
16662
|
-
/** An `EventCategory` value. */
|
|
16663
17006
|
category: string().min(1),
|
|
16664
17007
|
deviceId: number$1().int().nonnegative().optional()
|
|
16665
17008
|
}),
|
|
@@ -16670,25 +17013,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16670
17013
|
object({ kind: literal("manual") }),
|
|
16671
17014
|
object({
|
|
16672
17015
|
kind: literal("detection"),
|
|
16673
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16674
|
-
* matches every raw class that maps to it; any other name matches
|
|
16675
|
-
* exactly. */
|
|
16676
17016
|
classes: array(string().min(1)).min(1),
|
|
16677
17017
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16678
|
-
/** The track must have visited at least one of these zones. */
|
|
16679
17018
|
zones: array(string().min(1)).min(1).optional(),
|
|
16680
|
-
/** Best-so-far confidence the track must reach. */
|
|
16681
17019
|
minScore: number$1().min(0).max(1).optional()
|
|
16682
17020
|
}),
|
|
16683
17021
|
object({
|
|
16684
17022
|
kind: literal("stationary"),
|
|
16685
17023
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
16686
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16687
17024
|
classes: array(string().min(1)).min(1).optional(),
|
|
16688
|
-
/** The parked object must be in at least one of these zones. */
|
|
16689
17025
|
zones: array(string().min(1)).min(1).optional(),
|
|
16690
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16691
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16692
17026
|
phase: _enum(["appeared", "departed"])
|
|
16693
17027
|
}),
|
|
16694
17028
|
object({
|
|
@@ -16696,6 +17030,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16696
17030
|
minutes: number$1().int().min(1).max(1440)
|
|
16697
17031
|
})
|
|
16698
17032
|
]);
|
|
17033
|
+
var LegacyConditionLeafSchema = object({
|
|
17034
|
+
kind: literal("condition"),
|
|
17035
|
+
deviceId: number$1().int().nonnegative(),
|
|
17036
|
+
cap: string().min(1),
|
|
17037
|
+
fieldPath: string().min(1),
|
|
17038
|
+
operator: AutomationConditionOperatorSchema,
|
|
17039
|
+
value: union([
|
|
17040
|
+
string(),
|
|
17041
|
+
number$1(),
|
|
17042
|
+
boolean(),
|
|
17043
|
+
array(union([string(), number$1()])),
|
|
17044
|
+
QuantityValueSchema
|
|
17045
|
+
]),
|
|
17046
|
+
unit: string().min(1).optional()
|
|
17047
|
+
});
|
|
17048
|
+
var LegacyStationaryPresentSchema = object({
|
|
17049
|
+
kind: literal("stationary-present"),
|
|
17050
|
+
deviceId: number$1().int().nonnegative(),
|
|
17051
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
17052
|
+
zones: array(string().min(1)).min(1).optional()
|
|
17053
|
+
});
|
|
17054
|
+
var LegacyExpressionSchema = object({
|
|
17055
|
+
kind: literal("expression"),
|
|
17056
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
17057
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
17058
|
+
}).superRefine((src, ctx) => {
|
|
17059
|
+
const err = validateExpressionSource(src);
|
|
17060
|
+
if (err !== null) ctx.addIssue({
|
|
17061
|
+
code: "custom",
|
|
17062
|
+
message: err,
|
|
17063
|
+
path: ["expr"]
|
|
17064
|
+
});
|
|
17065
|
+
});
|
|
17066
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
17067
|
+
object({
|
|
17068
|
+
kind: literal("all"),
|
|
17069
|
+
children: array(LegacyConditionSchema)
|
|
17070
|
+
}),
|
|
17071
|
+
object({
|
|
17072
|
+
kind: literal("any"),
|
|
17073
|
+
children: array(LegacyConditionSchema)
|
|
17074
|
+
}),
|
|
17075
|
+
object({
|
|
17076
|
+
kind: literal("not"),
|
|
17077
|
+
child: LegacyConditionSchema
|
|
17078
|
+
}),
|
|
17079
|
+
LegacyConditionLeafSchema,
|
|
17080
|
+
LegacyExpressionSchema,
|
|
17081
|
+
LegacyStationaryPresentSchema
|
|
17082
|
+
]));
|
|
17083
|
+
function convertTrigger(trigger) {
|
|
17084
|
+
switch (trigger.kind) {
|
|
17085
|
+
case "device-state": return {
|
|
17086
|
+
...trigger,
|
|
17087
|
+
kind: "device-becomes"
|
|
17088
|
+
};
|
|
17089
|
+
case "detection": {
|
|
17090
|
+
const { classes, ...rest } = trigger;
|
|
17091
|
+
return {
|
|
17092
|
+
...rest,
|
|
17093
|
+
subjects: classes.map(subjectFromClassName)
|
|
17094
|
+
};
|
|
17095
|
+
}
|
|
17096
|
+
case "stationary": {
|
|
17097
|
+
const { classes, ...rest } = trigger;
|
|
17098
|
+
return {
|
|
17099
|
+
...rest,
|
|
17100
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17101
|
+
};
|
|
17102
|
+
}
|
|
17103
|
+
default: return trigger;
|
|
17104
|
+
}
|
|
17105
|
+
}
|
|
17106
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
17107
|
+
function convertLegacyCondition(condition) {
|
|
17108
|
+
switch (condition.kind) {
|
|
17109
|
+
case "condition": return {
|
|
17110
|
+
...condition,
|
|
17111
|
+
kind: "device-is"
|
|
17112
|
+
};
|
|
17113
|
+
case "stationary-present": {
|
|
17114
|
+
const { classes, ...rest } = condition;
|
|
17115
|
+
return {
|
|
17116
|
+
...rest,
|
|
17117
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17118
|
+
};
|
|
17119
|
+
}
|
|
17120
|
+
case "expression": return condition;
|
|
17121
|
+
case "not": return {
|
|
17122
|
+
kind: "not",
|
|
17123
|
+
child: convertLegacyCondition(condition.child)
|
|
17124
|
+
};
|
|
17125
|
+
case "all":
|
|
17126
|
+
case "any": return {
|
|
17127
|
+
kind: condition.kind,
|
|
17128
|
+
children: condition.children.map(convertLegacyCondition)
|
|
17129
|
+
};
|
|
17130
|
+
}
|
|
17131
|
+
}
|
|
17132
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
17133
|
+
function convertLegacyWhen(half) {
|
|
17134
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
17135
|
+
const first = leaves[0];
|
|
17136
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
17137
|
+
kind: "any",
|
|
17138
|
+
children: leaves
|
|
17139
|
+
};
|
|
17140
|
+
if (half.conditions === void 0) return triggers;
|
|
17141
|
+
return {
|
|
17142
|
+
kind: "all",
|
|
17143
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
17144
|
+
};
|
|
17145
|
+
}
|
|
17146
|
+
/**
|
|
17147
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
17148
|
+
* becomes code.
|
|
17149
|
+
*
|
|
17150
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
17151
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
17152
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
17153
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
17154
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
17155
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
17156
|
+
*
|
|
17157
|
+
* Two parts:
|
|
17158
|
+
*
|
|
17159
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
17160
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
17161
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
17162
|
+
* ADR "one tree".
|
|
17163
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
17164
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
17165
|
+
* this had to diverge from "verbatim", and why.
|
|
17166
|
+
*
|
|
17167
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
17168
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
17169
|
+
*/
|
|
16699
17170
|
/**
|
|
16700
17171
|
* One action step.
|
|
16701
17172
|
*
|
|
@@ -16798,69 +17269,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16798
17269
|
minutes: number$1().int().min(1).max(1440)
|
|
16799
17270
|
})
|
|
16800
17271
|
]);
|
|
16801
|
-
var
|
|
16802
|
-
|
|
16803
|
-
conditions: AutomationConditionSchema.optional(),
|
|
17272
|
+
var CurrentAutomationRecipeSchema = object({
|
|
17273
|
+
when: AutomationWhenSchema,
|
|
16804
17274
|
actions: array(AutomationActionSchema),
|
|
16805
17275
|
runMode: AutomationRunModeSchema.optional(),
|
|
16806
17276
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16807
17277
|
keepTraces: number$1().int().min(1).max(100).optional()
|
|
16808
17278
|
});
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
bus: "vehicle",
|
|
16835
|
-
train: "vehicle",
|
|
16836
|
-
truck: "vehicle",
|
|
16837
|
-
boat: "vehicle",
|
|
16838
|
-
bird: "animal",
|
|
16839
|
-
cat: "animal",
|
|
16840
|
-
dog: "animal",
|
|
16841
|
-
horse: "animal",
|
|
16842
|
-
sheep: "animal",
|
|
16843
|
-
cow: "animal",
|
|
16844
|
-
elephant: "animal",
|
|
16845
|
-
bear: "animal",
|
|
16846
|
-
zebra: "animal",
|
|
16847
|
-
giraffe: "animal",
|
|
16848
|
-
suitcase: "package",
|
|
16849
|
-
backpack: "package",
|
|
16850
|
-
handbag: "package"
|
|
16851
|
-
},
|
|
16852
|
-
preserveOriginal: false
|
|
16853
|
-
};
|
|
17279
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
17280
|
+
var LegacyAutomationRecipeSchema = object({
|
|
17281
|
+
triggers: array(LegacyTriggerSchema),
|
|
17282
|
+
conditions: LegacyConditionSchema.optional(),
|
|
17283
|
+
actions: array(AutomationActionSchema),
|
|
17284
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
17285
|
+
keepTraces: number$1().int().min(1).max(100).optional()
|
|
17286
|
+
});
|
|
17287
|
+
/** The old shape as the new one, lossless. */
|
|
17288
|
+
function convertLegacyRecipe(legacy) {
|
|
17289
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
17290
|
+
return {
|
|
17291
|
+
when: convertLegacyWhen({
|
|
17292
|
+
triggers,
|
|
17293
|
+
conditions
|
|
17294
|
+
}),
|
|
17295
|
+
...rest
|
|
17296
|
+
};
|
|
17297
|
+
}
|
|
17298
|
+
/**
|
|
17299
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
17300
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
17301
|
+
* it. The output type is always the new one.
|
|
17302
|
+
*/
|
|
17303
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16854
17304
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16855
17305
|
/**
|
|
16856
17306
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16857
17307
|
* so the editor can paint every block green or red with its reason.
|
|
16858
17308
|
*
|
|
16859
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16860
|
-
* every
|
|
16861
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16862
|
-
* each addressed by a stable path (`
|
|
16863
|
-
* for an Action run).
|
|
17309
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
17310
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
17311
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
17312
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
17313
|
+
* for an Action run). Traces written before the single tree carry
|
|
17314
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16864
17315
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16865
17316
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16866
17317
|
*
|
|
@@ -16892,6 +17343,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16892
17343
|
]);
|
|
16893
17344
|
var AutomationTriggerTraceSchema = object({
|
|
16894
17345
|
kind: string().min(1),
|
|
17346
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
17347
|
+
path: string().min(1).optional(),
|
|
16895
17348
|
outcome: _enum(["pass", "fail"]),
|
|
16896
17349
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16897
17350
|
reason: string(),
|
|
@@ -16899,15 +17352,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16899
17352
|
vars: record(string(), TraceValueSchema)
|
|
16900
17353
|
});
|
|
16901
17354
|
var AutomationConditionTraceNodeSchema = object({
|
|
16902
|
-
/** `
|
|
17355
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16903
17356
|
path: string().min(1),
|
|
16904
17357
|
kind: _enum([
|
|
16905
17358
|
"all",
|
|
16906
17359
|
"any",
|
|
16907
17360
|
"not",
|
|
16908
17361
|
"condition",
|
|
17362
|
+
"device-is",
|
|
16909
17363
|
"expression",
|
|
16910
|
-
"stationary-present"
|
|
17364
|
+
"stationary-present",
|
|
17365
|
+
"device-becomes",
|
|
17366
|
+
"device-event",
|
|
17367
|
+
"schedule",
|
|
17368
|
+
"interval",
|
|
17369
|
+
"manual",
|
|
17370
|
+
"detection",
|
|
17371
|
+
"stationary"
|
|
16911
17372
|
]),
|
|
16912
17373
|
outcome: TraceNodeOutcomeSchema,
|
|
16913
17374
|
reason: string(),
|
|
@@ -16941,6 +17402,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16941
17402
|
deviceId: number$1().int().optional(),
|
|
16942
17403
|
startedAt: number$1().optional(),
|
|
16943
17404
|
endedAt: number$1().optional(),
|
|
17405
|
+
/**
|
|
17406
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
17407
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
17408
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
17409
|
+
*/
|
|
17410
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16944
17411
|
outcome: _enum([
|
|
16945
17412
|
"ok",
|
|
16946
17413
|
"error",
|
|
@@ -17731,6 +18198,23 @@ method(object({
|
|
|
17731
18198
|
addonId: string(),
|
|
17732
18199
|
nodeId: string().optional()
|
|
17733
18200
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
18201
|
+
addonId: string(),
|
|
18202
|
+
nodeId: string().optional()
|
|
18203
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
18204
|
+
addonId: string(),
|
|
18205
|
+
nodeId: string().optional(),
|
|
18206
|
+
keys: array(string())
|
|
18207
|
+
}), object({
|
|
18208
|
+
removed: array(string()),
|
|
18209
|
+
refused: array(object({
|
|
18210
|
+
key: string(),
|
|
18211
|
+
reason: string()
|
|
18212
|
+
})),
|
|
18213
|
+
secretsCleared: number$1()
|
|
18214
|
+
}), {
|
|
18215
|
+
kind: "mutation",
|
|
18216
|
+
auth: "admin"
|
|
18217
|
+
}), method(object({
|
|
17734
18218
|
addonId: string(),
|
|
17735
18219
|
deviceId: number$1(),
|
|
17736
18220
|
nodeId: string().optional()
|
|
@@ -21809,6 +22293,8 @@ var StorageKindSchema = object({
|
|
|
21809
22293
|
"orphaned",
|
|
21810
22294
|
"dropped"
|
|
21811
22295
|
]),
|
|
22296
|
+
/** An owner declared this kind since the engine started. */
|
|
22297
|
+
declaredThisBoot: boolean(),
|
|
21812
22298
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21813
22299
|
rows: number$1().int().nonnegative().nullable(),
|
|
21814
22300
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21820,6 +22306,51 @@ var StorageKindSchema = object({
|
|
|
21820
22306
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21821
22307
|
declaration: RecordKindDeclarationSchema
|
|
21822
22308
|
});
|
|
22309
|
+
_enum([
|
|
22310
|
+
"orphaned",
|
|
22311
|
+
"stale",
|
|
22312
|
+
"unknown"
|
|
22313
|
+
]);
|
|
22314
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
22315
|
+
var StaleKindFindingSchema = object({
|
|
22316
|
+
owner: string(),
|
|
22317
|
+
kind: string(),
|
|
22318
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
22319
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
22320
|
+
reason: string(),
|
|
22321
|
+
rows: number$1().int().nonnegative().nullable(),
|
|
22322
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
22323
|
+
protected: boolean()
|
|
22324
|
+
});
|
|
22325
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
22326
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
22327
|
+
addonId: string(),
|
|
22328
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
22329
|
+
key: string(),
|
|
22330
|
+
/**
|
|
22331
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
22332
|
+
* not orphans yet. Removing the key releases them.
|
|
22333
|
+
*/
|
|
22334
|
+
secretRefs: array(string())
|
|
22335
|
+
});
|
|
22336
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
22337
|
+
var StaleUnknownSchema = object({
|
|
22338
|
+
subject: string(),
|
|
22339
|
+
reason: string()
|
|
22340
|
+
});
|
|
22341
|
+
var StaleDataReportSchema = object({
|
|
22342
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
22343
|
+
scannedAt: number$1().nullable(),
|
|
22344
|
+
scanning: boolean(),
|
|
22345
|
+
trigger: _enum([
|
|
22346
|
+
"boot",
|
|
22347
|
+
"interval",
|
|
22348
|
+
"manual"
|
|
22349
|
+
]).nullable(),
|
|
22350
|
+
kinds: array(StaleKindFindingSchema),
|
|
22351
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
22352
|
+
unknown: array(StaleUnknownSchema)
|
|
22353
|
+
});
|
|
21823
22354
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21824
22355
|
owner: string().min(1).optional(),
|
|
21825
22356
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21827,7 +22358,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21827
22358
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21828
22359
|
owner: string().min(1),
|
|
21829
22360
|
kind: RecordKindNameSchema
|
|
21830
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
22361
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
22362
|
+
kind: "mutation",
|
|
22363
|
+
auth: "admin"
|
|
22364
|
+
}), method(object({
|
|
22365
|
+
owner: string().min(1),
|
|
22366
|
+
kind: RecordKindNameSchema
|
|
22367
|
+
}), object({
|
|
22368
|
+
deleted: number$1().int().nonnegative(),
|
|
22369
|
+
secretsReleased: number$1().int().nonnegative()
|
|
22370
|
+
}), {
|
|
22371
|
+
kind: "mutation",
|
|
22372
|
+
auth: "admin",
|
|
22373
|
+
caller: "required"
|
|
22374
|
+
}), method(object({
|
|
22375
|
+
addonId: string().min(1),
|
|
22376
|
+
key: string().min(1)
|
|
22377
|
+
}), object({
|
|
22378
|
+
removed: boolean(),
|
|
22379
|
+
secretsCleared: number$1().int().nonnegative()
|
|
22380
|
+
}), {
|
|
22381
|
+
kind: "mutation",
|
|
22382
|
+
auth: "admin",
|
|
22383
|
+
caller: "required"
|
|
22384
|
+
});
|
|
21831
22385
|
/**
|
|
21832
22386
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21833
22387
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21966,6 +22520,12 @@ method(object({
|
|
|
21966
22520
|
}), method(object({ deviceId: number$1().int() }), object({ refs: array(string()).readonly() }), {
|
|
21967
22521
|
kind: "mutation",
|
|
21968
22522
|
auth: "admin"
|
|
22523
|
+
}), method(object({
|
|
22524
|
+
refs: RefListSchema,
|
|
22525
|
+
onBehalfOf: string().min(1)
|
|
22526
|
+
}), object({ released: number$1().int() }), {
|
|
22527
|
+
kind: "mutation",
|
|
22528
|
+
auth: "admin"
|
|
21969
22529
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21970
22530
|
/**
|
|
21971
22531
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22755,7 +23315,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22755
23315
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22756
23316
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22757
23317
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22758
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
23318
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
23319
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
23320
|
+
parentStableId: string().optional()
|
|
22759
23321
|
}));
|
|
22760
23322
|
/**
|
|
22761
23323
|
* Status block — populated by the integration after each
|
|
@@ -43138,6 +43700,79 @@ object({
|
|
|
43138
43700
|
metadata: record(string(), unknown()).optional()
|
|
43139
43701
|
});
|
|
43140
43702
|
/**
|
|
43703
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43704
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43705
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43706
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43707
|
+
* except through a `custom` mode.
|
|
43708
|
+
*/
|
|
43709
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43710
|
+
object({
|
|
43711
|
+
kind: literal("mdns"),
|
|
43712
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43713
|
+
serviceType: string().min(1),
|
|
43714
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43715
|
+
instancePattern: string().optional()
|
|
43716
|
+
}),
|
|
43717
|
+
object({
|
|
43718
|
+
kind: literal("ws-discovery"),
|
|
43719
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43720
|
+
types: array(string()).optional()
|
|
43721
|
+
}),
|
|
43722
|
+
object({
|
|
43723
|
+
kind: literal("ssdp"),
|
|
43724
|
+
searchTarget: string().min(1)
|
|
43725
|
+
}),
|
|
43726
|
+
object({
|
|
43727
|
+
kind: literal("udp-broadcast"),
|
|
43728
|
+
port: number$1().int().min(1).max(65535),
|
|
43729
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43730
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43731
|
+
listenPort: number$1().int().min(1).max(65535).optional()
|
|
43732
|
+
}),
|
|
43733
|
+
object({
|
|
43734
|
+
kind: literal("http-probe"),
|
|
43735
|
+
port: number$1().int().min(1).max(65535),
|
|
43736
|
+
path: string().startsWith("/"),
|
|
43737
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43738
|
+
bodyText: string().optional(),
|
|
43739
|
+
contentType: string().optional(),
|
|
43740
|
+
tls: boolean().default(false),
|
|
43741
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43742
|
+
acceptStatus: array(number$1().int()).optional()
|
|
43743
|
+
}),
|
|
43744
|
+
object({
|
|
43745
|
+
kind: literal("custom"),
|
|
43746
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43747
|
+
reason: string().min(1)
|
|
43748
|
+
})
|
|
43749
|
+
]);
|
|
43750
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43751
|
+
var DiscoveryHitSchema = object({
|
|
43752
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43753
|
+
modeIndex: number$1().int().min(0),
|
|
43754
|
+
host: string(),
|
|
43755
|
+
port: number$1().int(),
|
|
43756
|
+
http: object({
|
|
43757
|
+
status: number$1().int(),
|
|
43758
|
+
headers: record(string(), string()),
|
|
43759
|
+
/** At most 16 KiB. */
|
|
43760
|
+
bodyText: string()
|
|
43761
|
+
}).optional(),
|
|
43762
|
+
mdns: object({
|
|
43763
|
+
instance: string(),
|
|
43764
|
+
host: string(),
|
|
43765
|
+
port: number$1().int(),
|
|
43766
|
+
txt: record(string(), string())
|
|
43767
|
+
}).optional(),
|
|
43768
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43769
|
+
wsd: object({
|
|
43770
|
+
xaddrs: array(string()),
|
|
43771
|
+
types: array(string()),
|
|
43772
|
+
scopes: array(string())
|
|
43773
|
+
}).optional()
|
|
43774
|
+
});
|
|
43775
|
+
/**
|
|
43141
43776
|
* Candidate handed back from discovery and accepted by
|
|
43142
43777
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
43143
43778
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -43172,7 +43807,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
43172
43807
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
43173
43808
|
onboardedDeviceId: number$1().optional(),
|
|
43174
43809
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
43175
|
-
onboardedName: string().optional()
|
|
43810
|
+
onboardedName: string().optional(),
|
|
43811
|
+
/**
|
|
43812
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43813
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43814
|
+
* which is still read for one release.
|
|
43815
|
+
*/
|
|
43816
|
+
setupIntegration: boolean().optional(),
|
|
43817
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43818
|
+
parentStableId: string().optional(),
|
|
43819
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43820
|
+
selectable: boolean().optional()
|
|
43176
43821
|
});
|
|
43177
43822
|
/**
|
|
43178
43823
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -43269,6 +43914,24 @@ var deviceProviderCapability = {
|
|
|
43269
43914
|
auth: "admin"
|
|
43270
43915
|
}),
|
|
43271
43916
|
/**
|
|
43917
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43918
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43919
|
+
*/
|
|
43920
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43921
|
+
/**
|
|
43922
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43923
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43924
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43925
|
+
* from `getDiscoveryParamsSchema`.
|
|
43926
|
+
*/
|
|
43927
|
+
classifyDiscoveryHits: method(object({
|
|
43928
|
+
hits: array(DiscoveryHitSchema),
|
|
43929
|
+
params: record(string(), unknown()).optional()
|
|
43930
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43931
|
+
kind: "mutation",
|
|
43932
|
+
auth: "admin"
|
|
43933
|
+
}),
|
|
43934
|
+
/**
|
|
43272
43935
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
43273
43936
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
43274
43937
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -44033,17 +44696,23 @@ method(object({
|
|
|
44033
44696
|
}), method(object({}), object({ providers: array(object({
|
|
44034
44697
|
addonId: string(),
|
|
44035
44698
|
label: string()
|
|
44036
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
44037
|
-
|
|
44038
|
-
|
|
44039
|
-
|
|
44040
|
-
|
|
44041
|
-
|
|
44042
|
-
|
|
44043
|
-
|
|
44044
|
-
}),
|
|
44699
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44700
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44701
|
+
object({ subnets: array(string()).optional() }),
|
|
44702
|
+
object({ groups: array(object({
|
|
44703
|
+
addonId: string(),
|
|
44704
|
+
label: string(),
|
|
44705
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44706
|
+
error: string().nullable()
|
|
44707
|
+
})).readonly() }),
|
|
44708
|
+
{
|
|
44709
|
+
kind: "mutation",
|
|
44710
|
+
auth: "admin"
|
|
44711
|
+
}
|
|
44712
|
+
), method(object({
|
|
44045
44713
|
addonId: string(),
|
|
44046
|
-
params: record(string(), unknown()).optional()
|
|
44714
|
+
params: record(string(), unknown()).optional(),
|
|
44715
|
+
subnets: array(string()).optional()
|
|
44047
44716
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
44048
44717
|
kind: "mutation",
|
|
44049
44718
|
auth: "admin"
|
|
@@ -63394,7 +64063,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63394
64063
|
id: string(),
|
|
63395
64064
|
settings: record(string(), unknown()),
|
|
63396
64065
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63397
|
-
secretKeys: array(string()).optional()
|
|
64066
|
+
secretKeys: array(string()).optional(),
|
|
64067
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
64068
|
+
skipRestart: boolean().optional()
|
|
63398
64069
|
}), object({ success: boolean() }), {
|
|
63399
64070
|
kind: "mutation",
|
|
63400
64071
|
auth: "admin"
|
|
@@ -64022,6 +64693,39 @@ var TlsStatusSchema = object({
|
|
|
64022
64693
|
hasCa: boolean()
|
|
64023
64694
|
}).nullable()
|
|
64024
64695
|
});
|
|
64696
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64697
|
+
var ScanRequestSchema = object({
|
|
64698
|
+
providerId: string().min(1),
|
|
64699
|
+
modes: array(DiscoveryModeSchema)
|
|
64700
|
+
});
|
|
64701
|
+
var ScanErrorSchema = object({
|
|
64702
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64703
|
+
providerId: string().optional(),
|
|
64704
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64705
|
+
mode: string().optional(),
|
|
64706
|
+
message: string()
|
|
64707
|
+
});
|
|
64708
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64709
|
+
var ScanStatsSchema = object({
|
|
64710
|
+
hostsProbed: number$1().int(),
|
|
64711
|
+
hits: number$1().int(),
|
|
64712
|
+
timeouts: number$1().int(),
|
|
64713
|
+
byMode: record(string(), object({
|
|
64714
|
+
probed: number$1().int(),
|
|
64715
|
+
hits: number$1().int(),
|
|
64716
|
+
timeouts: number$1().int()
|
|
64717
|
+
}))
|
|
64718
|
+
});
|
|
64719
|
+
var ScanNetworkResultSchema = object({
|
|
64720
|
+
hits: array(object({
|
|
64721
|
+
providerId: string(),
|
|
64722
|
+
hit: DiscoveryHitSchema
|
|
64723
|
+
})),
|
|
64724
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64725
|
+
subnetsScanned: array(string()),
|
|
64726
|
+
errors: array(ScanErrorSchema),
|
|
64727
|
+
stats: ScanStatsSchema
|
|
64728
|
+
});
|
|
64025
64729
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
64026
64730
|
/**
|
|
64027
64731
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -64076,6 +64780,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
64076
64780
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
64077
64781
|
kind: "mutation",
|
|
64078
64782
|
auth: "admin"
|
|
64783
|
+
}), method(object({
|
|
64784
|
+
requests: array(ScanRequestSchema),
|
|
64785
|
+
subnets: array(string()).optional()
|
|
64786
|
+
}), ScanNetworkResultSchema, {
|
|
64787
|
+
kind: "mutation",
|
|
64788
|
+
auth: "admin",
|
|
64789
|
+
timeoutMs: 9e4
|
|
64790
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64791
|
+
kind: "mutation",
|
|
64792
|
+
auth: "admin"
|
|
64079
64793
|
}), method(_void(), TlsStatusSchema, {
|
|
64080
64794
|
kind: "mutation",
|
|
64081
64795
|
auth: "admin"
|
|
@@ -73658,6 +74372,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73658
74372
|
async discoverDevices(_input) {
|
|
73659
74373
|
return [];
|
|
73660
74374
|
}
|
|
74375
|
+
/**
|
|
74376
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74377
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74378
|
+
*/
|
|
74379
|
+
async getDiscoveryModes() {
|
|
74380
|
+
return [];
|
|
74381
|
+
}
|
|
74382
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74383
|
+
async classifyDiscoveryHits(_input) {
|
|
74384
|
+
return [];
|
|
74385
|
+
}
|
|
73661
74386
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73662
74387
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73663
74388
|
async getDiscoveryParamsSchema() {
|
|
@@ -74526,6 +75251,13 @@ Object.freeze({
|
|
|
74526
75251
|
access: "create",
|
|
74527
75252
|
role: "admin"
|
|
74528
75253
|
},
|
|
75254
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
75255
|
+
capName: "addon-settings",
|
|
75256
|
+
capScope: "system",
|
|
75257
|
+
addonId: null,
|
|
75258
|
+
access: "view",
|
|
75259
|
+
role: "admin"
|
|
75260
|
+
},
|
|
74529
75261
|
"addonSettings.getDeviceSettings": {
|
|
74530
75262
|
capName: "addon-settings",
|
|
74531
75263
|
capScope: "system",
|
|
@@ -74547,6 +75279,13 @@ Object.freeze({
|
|
|
74547
75279
|
access: "view",
|
|
74548
75280
|
role: "admin"
|
|
74549
75281
|
},
|
|
75282
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
75283
|
+
capName: "addon-settings",
|
|
75284
|
+
capScope: "system",
|
|
75285
|
+
addonId: null,
|
|
75286
|
+
access: "create",
|
|
75287
|
+
role: "admin"
|
|
75288
|
+
},
|
|
74550
75289
|
"addonSettings.updateDeviceSettings": {
|
|
74551
75290
|
capName: "addon-settings",
|
|
74552
75291
|
capScope: "system",
|
|
@@ -76962,6 +77701,13 @@ Object.freeze({
|
|
|
76962
77701
|
access: "create",
|
|
76963
77702
|
role: "admin"
|
|
76964
77703
|
},
|
|
77704
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77705
|
+
capName: "device-provider",
|
|
77706
|
+
capScope: "system",
|
|
77707
|
+
addonId: null,
|
|
77708
|
+
access: "create",
|
|
77709
|
+
role: "admin"
|
|
77710
|
+
},
|
|
76965
77711
|
"deviceProvider.createDevice": {
|
|
76966
77712
|
capName: "device-provider",
|
|
76967
77713
|
capScope: "system",
|
|
@@ -76990,6 +77736,13 @@ Object.freeze({
|
|
|
76990
77736
|
access: "view",
|
|
76991
77737
|
role: "admin"
|
|
76992
77738
|
},
|
|
77739
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77740
|
+
capName: "device-provider",
|
|
77741
|
+
capScope: "system",
|
|
77742
|
+
addonId: null,
|
|
77743
|
+
access: "view",
|
|
77744
|
+
role: "admin"
|
|
77745
|
+
},
|
|
76993
77746
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76994
77747
|
capName: "device-provider",
|
|
76995
77748
|
capScope: "system",
|
|
@@ -77767,6 +78520,13 @@ Object.freeze({
|
|
|
77767
78520
|
access: "create",
|
|
77768
78521
|
role: "admin"
|
|
77769
78522
|
},
|
|
78523
|
+
"keychain.releaseRecordRefs": {
|
|
78524
|
+
capName: "keychain",
|
|
78525
|
+
capScope: "system",
|
|
78526
|
+
addonId: null,
|
|
78527
|
+
access: "create",
|
|
78528
|
+
role: "admin"
|
|
78529
|
+
},
|
|
77770
78530
|
"keychain.resolve": {
|
|
77771
78531
|
capName: "keychain",
|
|
77772
78532
|
capScope: "system",
|
|
@@ -78187,6 +78947,13 @@ Object.freeze({
|
|
|
78187
78947
|
access: "view",
|
|
78188
78948
|
role: "admin"
|
|
78189
78949
|
},
|
|
78950
|
+
"localNetwork.getScanSubnets": {
|
|
78951
|
+
capName: "local-network",
|
|
78952
|
+
capScope: "system",
|
|
78953
|
+
addonId: null,
|
|
78954
|
+
access: "view",
|
|
78955
|
+
role: "admin"
|
|
78956
|
+
},
|
|
78190
78957
|
"localNetwork.getTlsStatus": {
|
|
78191
78958
|
capName: "local-network",
|
|
78192
78959
|
capScope: "system",
|
|
@@ -78236,6 +79003,13 @@ Object.freeze({
|
|
|
78236
79003
|
access: "create",
|
|
78237
79004
|
role: "admin"
|
|
78238
79005
|
},
|
|
79006
|
+
"localNetwork.scanNetwork": {
|
|
79007
|
+
capName: "local-network",
|
|
79008
|
+
capScope: "system",
|
|
79009
|
+
addonId: null,
|
|
79010
|
+
access: "create",
|
|
79011
|
+
role: "admin"
|
|
79012
|
+
},
|
|
78239
79013
|
"localNetwork.setAddressRole": {
|
|
78240
79014
|
capName: "local-network",
|
|
78241
79015
|
capScope: "system",
|
|
@@ -78257,6 +79031,13 @@ Object.freeze({
|
|
|
78257
79031
|
access: "create",
|
|
78258
79032
|
role: "admin"
|
|
78259
79033
|
},
|
|
79034
|
+
"localNetwork.setScanSubnets": {
|
|
79035
|
+
capName: "local-network",
|
|
79036
|
+
capScope: "system",
|
|
79037
|
+
addonId: null,
|
|
79038
|
+
access: "create",
|
|
79039
|
+
role: "admin"
|
|
79040
|
+
},
|
|
78260
79041
|
"localNetwork.setViewerEndpoints": {
|
|
78261
79042
|
capName: "local-network",
|
|
78262
79043
|
capScope: "system",
|
|
@@ -82478,6 +83259,13 @@ Object.freeze({
|
|
|
82478
83259
|
access: "create",
|
|
82479
83260
|
role: "admin"
|
|
82480
83261
|
},
|
|
83262
|
+
"storageAdmin.dropOrphanedKind": {
|
|
83263
|
+
capName: "storage-admin",
|
|
83264
|
+
capScope: "system",
|
|
83265
|
+
addonId: null,
|
|
83266
|
+
access: "delete",
|
|
83267
|
+
role: "admin"
|
|
83268
|
+
},
|
|
82481
83269
|
"storageAdmin.listKinds": {
|
|
82482
83270
|
capName: "storage-admin",
|
|
82483
83271
|
capScope: "system",
|
|
@@ -82499,6 +83287,27 @@ Object.freeze({
|
|
|
82499
83287
|
access: "view",
|
|
82500
83288
|
role: "admin"
|
|
82501
83289
|
},
|
|
83290
|
+
"storageAdmin.scanStaleData": {
|
|
83291
|
+
capName: "storage-admin",
|
|
83292
|
+
capScope: "system",
|
|
83293
|
+
addonId: null,
|
|
83294
|
+
access: "create",
|
|
83295
|
+
role: "admin"
|
|
83296
|
+
},
|
|
83297
|
+
"storageAdmin.staleDataReport": {
|
|
83298
|
+
capName: "storage-admin",
|
|
83299
|
+
capScope: "system",
|
|
83300
|
+
addonId: null,
|
|
83301
|
+
access: "view",
|
|
83302
|
+
role: "admin"
|
|
83303
|
+
},
|
|
83304
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
83305
|
+
capName: "storage-admin",
|
|
83306
|
+
capScope: "system",
|
|
83307
|
+
addonId: null,
|
|
83308
|
+
access: "create",
|
|
83309
|
+
role: "admin"
|
|
83310
|
+
},
|
|
82502
83311
|
"storageEvictable.evict": {
|
|
82503
83312
|
capName: "storage-evictable",
|
|
82504
83313
|
capScope: "system",
|
|
@@ -92772,12 +93581,22 @@ function rewriteBody(body, kind, currentUpstream, route) {
|
|
|
92772
93581
|
/**
|
|
92773
93582
|
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
92774
93583
|
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
92775
|
-
|
|
92776
|
-
|
|
92777
|
-
|
|
93584
|
+
* `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
|
|
93585
|
+
* also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
|
|
93586
|
+
* `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
|
|
93587
|
+
*/
|
|
93588
|
+
function rewriteSetCookie(cookie, route, options = {}) {
|
|
93589
|
+
const parts = cookie.split(";").map((part) => part.trim());
|
|
93590
|
+
const dropped = options.browserSafe === true ? [
|
|
93591
|
+
"domain",
|
|
93592
|
+
"path",
|
|
93593
|
+
"secure",
|
|
93594
|
+
"samesite"
|
|
93595
|
+
] : ["domain", "path"];
|
|
93596
|
+
const kept = parts.filter((part, index) => {
|
|
92778
93597
|
if (index === 0) return true;
|
|
92779
93598
|
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
92780
|
-
return name
|
|
93599
|
+
return !dropped.includes(name);
|
|
92781
93600
|
});
|
|
92782
93601
|
kept.push(`Path=${route.basePath}/`);
|
|
92783
93602
|
return kept.join("; ");
|
|
@@ -92788,10 +93607,10 @@ function rewriteSetCookie(cookie, route) {
|
|
|
92788
93607
|
* one, so a past-dated set that only deleted the other domain's copy would delete the
|
|
92789
93608
|
* live one the same response just set: that deletion is dropped.
|
|
92790
93609
|
*/
|
|
92791
|
-
function rewriteSetCookies(cookies, route) {
|
|
93610
|
+
function rewriteSetCookies(cookies, route, options = {}) {
|
|
92792
93611
|
const now = Date.now();
|
|
92793
93612
|
const liveNames = new Set(cookies.filter((c) => !isExpiredCookie(c, now)).map(cookieName));
|
|
92794
|
-
return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).map((c) => rewriteSetCookie(c, route));
|
|
93613
|
+
return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).filter((c) => options.browserSafe !== true || !/^__(?:host|secure)-/i.test(cookieName(c))).map((c) => rewriteSetCookie(c, route, options));
|
|
92795
93614
|
}
|
|
92796
93615
|
function cookieName(cookie) {
|
|
92797
93616
|
return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
|
|
@@ -92851,6 +93670,99 @@ function rewriteFormBody(body, route) {
|
|
|
92851
93670
|
function filterForwardedCookies(cookieHeader) {
|
|
92852
93671
|
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
92853
93672
|
}
|
|
93673
|
+
/**
|
|
93674
|
+
* Server-side cookie jar of ONE vendor sign-in attempt (D955).
|
|
93675
|
+
*
|
|
93676
|
+
* A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
|
|
93677
|
+
* without the cookies it set a step earlier. Relying on the browser to store them under the
|
|
93678
|
+
* hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
|
|
93679
|
+
* on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
|
|
93680
|
+
* `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
|
|
93681
|
+
* whatever the browser did. Values never leave this object except as the upstream `Cookie`
|
|
93682
|
+
* header; nothing here logs.
|
|
93683
|
+
*/
|
|
93684
|
+
function attributeValue(attrs, name) {
|
|
93685
|
+
const found = attrs.find((a) => a.split("=")[0]?.trim().toLowerCase() === name);
|
|
93686
|
+
if (found === void 0) return void 0;
|
|
93687
|
+
const eq = found.indexOf("=");
|
|
93688
|
+
return eq === -1 ? "" : found.slice(eq + 1).trim();
|
|
93689
|
+
}
|
|
93690
|
+
function hasFlag(attrs, name) {
|
|
93691
|
+
return attrs.some((a) => a.toLowerCase() === name);
|
|
93692
|
+
}
|
|
93693
|
+
function expiryOf(attrs, nowMs) {
|
|
93694
|
+
const maxAge = attributeValue(attrs, "max-age");
|
|
93695
|
+
if (maxAge !== void 0 && maxAge.length > 0 && Number.isFinite(Number(maxAge))) return nowMs + Number(maxAge) * 1e3;
|
|
93696
|
+
const expires = attributeValue(attrs, "expires");
|
|
93697
|
+
if (expires === void 0) return null;
|
|
93698
|
+
const at = Date.parse(expires);
|
|
93699
|
+
return Number.isFinite(at) ? at : null;
|
|
93700
|
+
}
|
|
93701
|
+
function defaultPath(requestPath) {
|
|
93702
|
+
const last = requestPath.lastIndexOf("/");
|
|
93703
|
+
return last <= 0 ? "/" : requestPath.slice(0, last);
|
|
93704
|
+
}
|
|
93705
|
+
function pathMatches(cookiePath, requestPath) {
|
|
93706
|
+
if (requestPath === cookiePath) return true;
|
|
93707
|
+
if (!requestPath.startsWith(cookiePath)) return false;
|
|
93708
|
+
return cookiePath.endsWith("/") || requestPath.charAt(cookiePath.length) === "/";
|
|
93709
|
+
}
|
|
93710
|
+
function hostMatches(cookie, host) {
|
|
93711
|
+
if (cookie.domain === null) return cookie.originHost === host;
|
|
93712
|
+
const bare = hostWithoutPort(host);
|
|
93713
|
+
return bare === cookie.domain || bare.endsWith(`.${cookie.domain}`);
|
|
93714
|
+
}
|
|
93715
|
+
var VendorCookieJar = class {
|
|
93716
|
+
#cookies = /* @__PURE__ */ new Map();
|
|
93717
|
+
/** Store (or delete, when expired) the cookies of one upstream response. */
|
|
93718
|
+
store(setCookies, origin, nowMs) {
|
|
93719
|
+
for (const raw of setCookies) {
|
|
93720
|
+
const [first, ...rest] = raw.split(";").map((part) => part.trim());
|
|
93721
|
+
const eq = first?.indexOf("=") ?? -1;
|
|
93722
|
+
if (first === void 0 || eq <= 0) continue;
|
|
93723
|
+
const name = first.slice(0, eq);
|
|
93724
|
+
const attrs = rest;
|
|
93725
|
+
const domainAttr = attributeValue(attrs, "domain");
|
|
93726
|
+
const domain = domainAttr === void 0 || domainAttr.length === 0 ? null : domainAttr.replace(/^\./, "").toLowerCase();
|
|
93727
|
+
const pathAttr = attributeValue(attrs, "path");
|
|
93728
|
+
const path = pathAttr !== void 0 && pathAttr.startsWith("/") ? pathAttr : defaultPath(origin.pathname);
|
|
93729
|
+
const originBare = hostWithoutPort(origin.host);
|
|
93730
|
+
if (domain !== null && originBare !== domain && !originBare.endsWith(`.${domain}`)) continue;
|
|
93731
|
+
const expiresAtMs = expiryOf(attrs, nowMs);
|
|
93732
|
+
const key = `${name}|${domain ?? origin.host}|${path}`;
|
|
93733
|
+
if (expiresAtMs !== null && expiresAtMs <= nowMs) {
|
|
93734
|
+
this.#cookies.delete(key);
|
|
93735
|
+
continue;
|
|
93736
|
+
}
|
|
93737
|
+
this.#cookies.set(key, {
|
|
93738
|
+
name,
|
|
93739
|
+
value: first.slice(eq + 1),
|
|
93740
|
+
originHost: origin.host,
|
|
93741
|
+
domain,
|
|
93742
|
+
path,
|
|
93743
|
+
secure: hasFlag(attrs, "secure"),
|
|
93744
|
+
expiresAtMs
|
|
93745
|
+
});
|
|
93746
|
+
}
|
|
93747
|
+
}
|
|
93748
|
+
/** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
|
|
93749
|
+
headerFor(target, nowMs) {
|
|
93750
|
+
const secureOk = target.scheme === "https" || isLoopbackHost(target.host);
|
|
93751
|
+
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);
|
|
93752
|
+
if (matching.length === 0) return null;
|
|
93753
|
+
return matching.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
93754
|
+
}
|
|
93755
|
+
};
|
|
93756
|
+
/** Cookie names carried by a `Cookie` header value. */
|
|
93757
|
+
function cookieHeaderNames(header) {
|
|
93758
|
+
return new Set(header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0));
|
|
93759
|
+
}
|
|
93760
|
+
/** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
|
|
93761
|
+
function mergeCookieHeaders(browser, jar) {
|
|
93762
|
+
if (jar === null) return browser;
|
|
93763
|
+
const shadowed = cookieHeaderNames(jar);
|
|
93764
|
+
return [...browser.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !shadowed.has(part.split("=")[0]?.trim() ?? "")), jar].join("; ");
|
|
93765
|
+
}
|
|
92854
93766
|
/** A short stable label for the attempt: enough to group lines, useless as a capability. */
|
|
92855
93767
|
function attemptLabel(attemptId) {
|
|
92856
93768
|
let hash = 2166136261;
|
|
@@ -93046,6 +93958,7 @@ var VendorLoginProxy = class {
|
|
|
93046
93958
|
this.#attempts.set(spec.attemptId, {
|
|
93047
93959
|
expiresAtMs: spec.expiresAtMs,
|
|
93048
93960
|
upstreams: spec.upstreams,
|
|
93961
|
+
jar: spec.serverCookieJar === true ? new VendorCookieJar() : null,
|
|
93049
93962
|
evaluate: async (view) => {
|
|
93050
93963
|
const verdict = await spec.terminal(view);
|
|
93051
93964
|
if (verdict.kind === "continue") return verdict;
|
|
@@ -93142,7 +94055,7 @@ var VendorLoginProxy = class {
|
|
|
93142
94055
|
body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
|
|
93143
94056
|
} else body = read;
|
|
93144
94057
|
}
|
|
93145
|
-
const headers = this.#requestHeaders(req, route, upstream);
|
|
94058
|
+
const headers = this.#requestHeaders(req, route, upstream, attempt.jar, target);
|
|
93146
94059
|
const upstreamRes = await this.#fetch(target, {
|
|
93147
94060
|
method,
|
|
93148
94061
|
headers,
|
|
@@ -93151,6 +94064,10 @@ var VendorLoginProxy = class {
|
|
|
93151
94064
|
});
|
|
93152
94065
|
const location = upstreamRes.headers.get("location");
|
|
93153
94066
|
const setCookies = upstreamRes.headers.getSetCookie();
|
|
94067
|
+
attempt.jar?.store(setCookies, {
|
|
94068
|
+
host: upstream.host,
|
|
94069
|
+
pathname: target.pathname
|
|
94070
|
+
}, this.#now());
|
|
93154
94071
|
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
93155
94072
|
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
93156
94073
|
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
@@ -93198,7 +94115,7 @@ var VendorLoginProxy = class {
|
|
|
93198
94115
|
upstreamRes.headers.forEach((value, name) => {
|
|
93199
94116
|
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
93200
94117
|
});
|
|
93201
|
-
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
|
|
94118
|
+
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route, { browserSafe: attempt.jar !== null });
|
|
93202
94119
|
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
93203
94120
|
out["cache-control"] = "no-store";
|
|
93204
94121
|
if (text !== null && kind !== null) {
|
|
@@ -93220,15 +94137,19 @@ var VendorLoginProxy = class {
|
|
|
93220
94137
|
...flowTraceMeta(input)
|
|
93221
94138
|
} });
|
|
93222
94139
|
}
|
|
93223
|
-
#requestHeaders(req, route, upstream) {
|
|
94140
|
+
#requestHeaders(req, route, upstream, jar, target) {
|
|
93224
94141
|
const headers = {};
|
|
93225
94142
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
93226
94143
|
const lower = name.toLowerCase();
|
|
93227
94144
|
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
93228
94145
|
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
93229
94146
|
}
|
|
93230
|
-
const
|
|
93231
|
-
|
|
94147
|
+
const merged = mergeCookieHeaders(filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? ""), jar?.headerFor({
|
|
94148
|
+
host: upstream.host,
|
|
94149
|
+
pathname: target.pathname,
|
|
94150
|
+
scheme: upstream.scheme
|
|
94151
|
+
}, this.#now()) ?? null);
|
|
94152
|
+
if (merged.length > 0) headers["cookie"] = merged;
|
|
93232
94153
|
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
93233
94154
|
const referer = firstHeader(req.headers["referer"]);
|
|
93234
94155
|
if (referer !== void 0) {
|
|
@@ -136251,10 +137172,6 @@ var AlexaRemoteClient = class {
|
|
|
136251
137172
|
if (isAuthError(message)) throw new AlexaAuthError(message);
|
|
136252
137173
|
throw err;
|
|
136253
137174
|
}
|
|
136254
|
-
if (remote.getAuthenticationDetails().authenticated !== true) {
|
|
136255
|
-
this.#connected = false;
|
|
136256
|
-
throw new AlexaAuthError("account not authenticated (cookie/refresh token rejected or expired)");
|
|
136257
|
-
}
|
|
136258
137175
|
this.#connected = true;
|
|
136259
137176
|
await this.#persistSession(remote);
|
|
136260
137177
|
}
|
|
@@ -136990,7 +137907,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
136990
137907
|
label: "Alexa",
|
|
136991
137908
|
signIn: {
|
|
136992
137909
|
basePath: "/addon/import-alexa/login",
|
|
136993
|
-
methods: ["device-code"
|
|
137910
|
+
methods: ["device-code"]
|
|
136994
137911
|
}
|
|
136995
137912
|
}]
|
|
136996
137913
|
}],
|
|
@@ -138174,6 +139091,15 @@ var BrokerEcho = class {
|
|
|
138174
139091
|
try {
|
|
138175
139092
|
const seed = await this.deps.readSeed();
|
|
138176
139093
|
const stored = await sessions.get(brokerId);
|
|
139094
|
+
logger.info("alexa speak: broker sign-in material", {
|
|
139095
|
+
...tags,
|
|
139096
|
+
meta: {
|
|
139097
|
+
seedFound: seed !== null,
|
|
139098
|
+
hasRefreshToken: seed !== null && seed.refreshToken.length > 0,
|
|
139099
|
+
hasDeviceAppName: seed !== null && seed.deviceAppName.length > 0,
|
|
139100
|
+
hasStoredSession: stored.length > 0
|
|
139101
|
+
}
|
|
139102
|
+
});
|
|
138177
139103
|
if (seed === null && stored.length === 0) {
|
|
138178
139104
|
this.currentState = "broker-not-signed-in";
|
|
138179
139105
|
this.currentError = null;
|
|
@@ -139776,22 +140702,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
139776
140702
|
labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
|
|
139777
140703
|
content: "This integration links to an Amazon Alexa account broker. Add, edit, or remove accounts from External systems → Brokers, then adopt the imported devices. The integration only stores a reference to its broker. NOTE: this is an unofficial integration that Amazon can change or break without notice."
|
|
139778
140704
|
}]
|
|
139779
|
-
}, {
|
|
139780
|
-
id: "alexa-proxy-login",
|
|
139781
|
-
title: "Interactive login (proxy)",
|
|
139782
|
-
titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
|
|
139783
|
-
description: "The \"Sign in\" button in onboarding opens the real Amazon login page through this hub (same address and HTTPS as this page) and captures the cookie after you complete it (incl. 2FA). The port below is only used on the hub itself and is never exposed.",
|
|
139784
|
-
descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
|
|
139785
|
-
columns: 2,
|
|
139786
|
-
fields: [{
|
|
139787
|
-
type: "number",
|
|
139788
|
-
key: "proxyLogin.proxyPort",
|
|
139789
|
-
label: "Internal login port (loopback)",
|
|
139790
|
-
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
|
|
139791
|
-
min: 1,
|
|
139792
|
-
max: 65535,
|
|
139793
|
-
default: 3456
|
|
139794
|
-
}]
|
|
139795
140705
|
}] });
|
|
139796
140706
|
}
|
|
139797
140707
|
async supportsManualCreation() {
|