@camstack/addon-model-studio 1.1.326 → 1.1.328

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.
Files changed (20) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{MotionZonesSettings-oMmkwRPD.mjs → MotionZonesSettings-3MG9zvaA.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-DHuD6yBb.mjs → PrivacyMaskSettings-BA58yuWC.mjs} +4 -4
  4. package/dist/{SceneMonitorEditor-CVG3p4fk.mjs → SceneMonitorEditor-BEf_PNcV.mjs} +3 -3
  5. package/dist/_stub.js +572 -533
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-C8W7iGsN.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DBwBRZfG.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bfyumm6x.mjs +26 -0
  8. package/dist/addon-model-studio.css +1 -1
  9. package/dist/{hostInit-tkqlW3Ri.mjs → hostInit-B4ki_WkK.mjs} +2 -2
  10. package/dist/model-studio.addon.js +909 -115
  11. package/dist/model-studio.addon.mjs +909 -115
  12. package/dist/{player-overlays-DLAI6Bot.mjs → player-overlays-BzTF5DiJ.mjs} +1 -1
  13. package/dist/remoteEntry.js +1 -1
  14. package/dist/{responsive-C-jr4jBc.mjs → responsive-BmfFp5be.mjs} +1 -1
  15. package/dist/{scene-monitor-copy-DQx7y4Y7.mjs → scene-monitor-copy-BQA2BQQq.mjs} +1 -1
  16. package/dist/{square-Bt9f6ijX.mjs → square-MPgKEZFC.mjs} +1 -1
  17. package/dist/{trash-2-u3B-GxOc.mjs → trash-2-C3CU6OqE.mjs} +1 -1
  18. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-T6H-vsb9.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-Ct9GIyeI.mjs} +1 -1
  19. package/package.json +1 -1
  20. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-YuArysha.mjs +0 -26
@@ -5850,7 +5850,7 @@ var ZodIssueCode = {
5850
5850
  var ZodFirstPartyTypeKind;
5851
5851
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5852
5852
  //#endregion
5853
- //#region ../types/dist/sleep-63gM_1JR.mjs
5853
+ //#region ../types/dist/sleep-CeyhrcOk.mjs
5854
5854
  /**
5855
5855
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5856
5856
  * window to float samples (D455).
@@ -7917,6 +7917,7 @@ var BaseAddon = class {
7917
7917
  * a warning and falls back rather than crashing boot.
7918
7918
  */
7919
7919
  state(key, schema, fallback) {
7920
+ this._declaredStateKeys.add(key);
7920
7921
  return createDurableState({
7921
7922
  key,
7922
7923
  schema,
@@ -7932,6 +7933,96 @@ var BaseAddon = class {
7932
7933
  } })
7933
7934
  });
7934
7935
  }
7936
+ /** Durable-state keys this process registered through {@link state}. */
7937
+ _declaredStateKeys = /* @__PURE__ */ new Set();
7938
+ /**
7939
+ * Stale-data audit: every global-settings key this addon still declares —
7940
+ * constructor defaults, settings-schema fields (layout containers included),
7941
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
7942
+ * here, in the addon's own process, so the hub keeps no copy of it.
7943
+ */
7944
+ async getDeclaredSettingsKeys() {
7945
+ return { keys: [...this.declaredKeySet()].toSorted() };
7946
+ }
7947
+ declaredKeySet() {
7948
+ const keys = new Set(Object.keys(this.defaults));
7949
+ const schema = this.globalSettingsSchema();
7950
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
7951
+ for (const key of this._declaredStateKeys) keys.add(key);
7952
+ for (const path of this.secretStatePaths) {
7953
+ const root = path.split(/[.[]/, 1)[0];
7954
+ if (root) keys.add(root);
7955
+ }
7956
+ return keys;
7957
+ }
7958
+ /**
7959
+ * Stale-data audit: remove stored global-settings keys the addon no longer
7960
+ * declares, and clear the keychain values their markers held — except a value
7961
+ * a surviving key still points at. A declared or absent key is refused.
7962
+ */
7963
+ async unsetStoredSettingsKeys(keys) {
7964
+ const settings = this._ctx?.settings;
7965
+ const raw = this.rawSettings();
7966
+ if (!settings || !raw) return {
7967
+ removed: [],
7968
+ refused: keys.map((key) => ({
7969
+ key,
7970
+ reason: "no settings store"
7971
+ })),
7972
+ secretsCleared: 0
7973
+ };
7974
+ let stored;
7975
+ try {
7976
+ stored = await raw.readAddonStore();
7977
+ } catch (error) {
7978
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
7979
+ return {
7980
+ removed: [],
7981
+ refused: keys.map((key) => ({
7982
+ key,
7983
+ reason
7984
+ })),
7985
+ secretsCleared: 0
7986
+ };
7987
+ }
7988
+ const declared = this.declaredKeySet();
7989
+ const removed = [];
7990
+ const refused = [];
7991
+ for (const key of keys) {
7992
+ const base = key.split("@", 1)[0] ?? key;
7993
+ if (declared.has(base)) refused.push({
7994
+ key,
7995
+ reason: "still declared by the addon"
7996
+ });
7997
+ else if (!(key in stored)) refused.push({
7998
+ key,
7999
+ reason: "not stored"
8000
+ });
8001
+ else removed.push(key);
8002
+ }
8003
+ if (removed.length === 0) return {
8004
+ removed,
8005
+ refused,
8006
+ secretsCleared: 0
8007
+ };
8008
+ const kept = /* @__PURE__ */ new Set();
8009
+ const held = /* @__PURE__ */ new Set();
8010
+ for (const [key, value] of Object.entries(stored)) {
8011
+ const target = removed.includes(key) ? held : kept;
8012
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
8013
+ }
8014
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
8015
+ const patch = {};
8016
+ for (const key of removed) patch[key] = void 0;
8017
+ await settings.writeAddonStore(patch);
8018
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
8019
+ await this.resolveConfig();
8020
+ return {
8021
+ removed,
8022
+ refused,
8023
+ secretsCleared
8024
+ };
8025
+ }
7935
8026
  /** Per-device variant of {@link state}, backed by the per-device store. */
7936
8027
  deviceState(deviceId, key, schema, fallback) {
7937
8028
  return createDurableState({
@@ -8202,6 +8293,21 @@ function dropPerNodeFields(fields) {
8202
8293
  }
8203
8294
  return kept;
8204
8295
  }
8296
+ function collectFieldKeys(fields) {
8297
+ const collected = [];
8298
+ for (const field of fields) {
8299
+ if (field.type === "group") {
8300
+ collected.push(...collectFieldKeys(field.fields));
8301
+ continue;
8302
+ }
8303
+ if (field.type === "sub-tabs") {
8304
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
8305
+ continue;
8306
+ }
8307
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8308
+ }
8309
+ return collected;
8310
+ }
8205
8311
  function collectPerNodeFieldKeys(fields) {
8206
8312
  const collected = [];
8207
8313
  for (const field of fields) {
@@ -16547,8 +16653,172 @@ object({
16547
16653
  quantity: ReadingQuantitySchema,
16548
16654
  unit: string().min(1)
16549
16655
  });
16550
- /** How a leaf compares a device field to a value. Derived from the field's
16551
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16656
+ new Set([
16657
+ {
16658
+ id: "person",
16659
+ name: "Person"
16660
+ },
16661
+ {
16662
+ id: "vehicle",
16663
+ name: "Vehicle"
16664
+ },
16665
+ {
16666
+ id: "animal",
16667
+ name: "Animal"
16668
+ },
16669
+ {
16670
+ id: "package",
16671
+ name: "Package"
16672
+ }
16673
+ ].map((l) => l.id));
16674
+ var COCO_TO_MACRO = {
16675
+ mapping: {
16676
+ person: "person",
16677
+ bicycle: "vehicle",
16678
+ car: "vehicle",
16679
+ motorcycle: "vehicle",
16680
+ airplane: "vehicle",
16681
+ bus: "vehicle",
16682
+ train: "vehicle",
16683
+ truck: "vehicle",
16684
+ boat: "vehicle",
16685
+ bird: "animal",
16686
+ cat: "animal",
16687
+ dog: "animal",
16688
+ horse: "animal",
16689
+ sheep: "animal",
16690
+ cow: "animal",
16691
+ elephant: "animal",
16692
+ bear: "animal",
16693
+ zebra: "animal",
16694
+ giraffe: "animal",
16695
+ suitcase: "package",
16696
+ backpack: "package",
16697
+ handbag: "package"
16698
+ },
16699
+ preserveOriginal: false
16700
+ };
16701
+ /**
16702
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
16703
+ * animal, package) plus an OPTIONAL label that narrows it.
16704
+ *
16705
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
16706
+ * second list. The label that fits depends on the macro class:
16707
+ *
16708
+ * - `animal` → a sub-class (`dog`, `cat`, …);
16709
+ * - `person` → a known face (an identity of the `face-gallery`);
16710
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
16711
+ * - `package` → a sub-class.
16712
+ *
16713
+ * `other` exists only so a class the taxonomy does not know (a custom model's
16714
+ * `forklift`) survives the conversion of a stored recipe without widening.
16715
+ *
16716
+ * A label is decided LATE (D683): face recognition and LPR land after the
16717
+ * detection starts. The track-lifecycle `update` event carries the newly
16718
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
16719
+ * the subject GETS it — not at the first detection.
16720
+ */
16721
+ var SubClassLabelSchema = object({
16722
+ kind: literal("sub-class"),
16723
+ className: string().min(1)
16724
+ });
16725
+ var IdentityLabelSchema = object({
16726
+ kind: literal("identity"),
16727
+ /** The `face-gallery` identity id (stable across a rename). */
16728
+ identityId: string().min(1),
16729
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
16730
+ name: string().min(1)
16731
+ });
16732
+ var PlateLabelSchema = object({
16733
+ kind: literal("plate"),
16734
+ text: string().min(1)
16735
+ });
16736
+ function macroOf$1(className) {
16737
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
16738
+ }
16739
+ /** A sub-class label must belong to the macro it is under. */
16740
+ function subClassBelongs(macro, label) {
16741
+ return label === void 0 || macroOf$1(label.className) === macro;
16742
+ }
16743
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
16744
+ var DetectionSubjectSchema = discriminatedUnion("macro", [
16745
+ object({
16746
+ macro: literal("animal"),
16747
+ label: subClassOf("animal").optional()
16748
+ }),
16749
+ object({
16750
+ macro: literal("person"),
16751
+ label: IdentityLabelSchema.optional()
16752
+ }),
16753
+ object({
16754
+ macro: literal("vehicle"),
16755
+ label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
16756
+ }),
16757
+ object({
16758
+ macro: literal("package"),
16759
+ label: subClassOf("package").optional()
16760
+ }),
16761
+ object({
16762
+ macro: literal("other"),
16763
+ className: string().min(1)
16764
+ })
16765
+ ]);
16766
+ /**
16767
+ * The subject of a PARKED object. The stationary registry's event carries a
16768
+ * class and a zone, no face and no plate — an identity or plate label there
16769
+ * could never match, so the schema refuses it instead of letting it sit dead.
16770
+ */
16771
+ 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" });
16772
+ /**
16773
+ * A stored class string as a subject: a macro name stays a macro; a raw class
16774
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
16775
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
16776
+ */
16777
+ function subjectFromClassName(className) {
16778
+ const name = className.trim().toLowerCase();
16779
+ const macro = COCO_TO_MACRO.mapping[name];
16780
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
16781
+ if (macro === void 0) return {
16782
+ macro: "other",
16783
+ className
16784
+ };
16785
+ const label = {
16786
+ kind: "sub-class",
16787
+ className: name
16788
+ };
16789
+ switch (macro) {
16790
+ case "animal": return {
16791
+ macro: "animal",
16792
+ label
16793
+ };
16794
+ case "vehicle": return {
16795
+ macro: "vehicle",
16796
+ label
16797
+ };
16798
+ case "package": return {
16799
+ macro: "package",
16800
+ label
16801
+ };
16802
+ default: return {
16803
+ macro: "other",
16804
+ className
16805
+ };
16806
+ }
16807
+ }
16808
+ function macroOnly(macro) {
16809
+ switch (macro) {
16810
+ case "animal": return { macro: "animal" };
16811
+ case "person": return { macro: "person" };
16812
+ case "vehicle": return { macro: "vehicle" };
16813
+ case "package": return { macro: "package" };
16814
+ default: return {
16815
+ macro: "other",
16816
+ className: macro
16817
+ };
16818
+ }
16819
+ }
16820
+ /** How a state leaf compares a device field to a value. Derived from the
16821
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16552
16822
  var AutomationConditionOperatorSchema = _enum([
16553
16823
  "eq",
16554
16824
  "ne",
@@ -16559,12 +16829,76 @@ var AutomationConditionOperatorSchema = _enum([
16559
16829
  "contains",
16560
16830
  "in"
16561
16831
  ]);
16562
- var AutomationConditionLeafSchema = object({
16563
- kind: literal("condition"),
16832
+ /**
16833
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
16834
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
16835
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
16836
+ * fire; it does not lose it.
16837
+ */
16838
+ var DeviceBecomesLeafSchema = object({
16839
+ kind: literal("device-becomes"),
16840
+ deviceId: number().int().nonnegative(),
16841
+ cap: string().min(1),
16842
+ fieldPath: string().min(1),
16843
+ /** Fire when the field takes this value. Omit to fire on any change. */
16844
+ becomes: union([
16845
+ string(),
16846
+ number(),
16847
+ boolean()
16848
+ ]).optional(),
16849
+ /** Only on a CHANGE of value, not on every re-report. */
16850
+ edge: boolean().optional(),
16851
+ /** The new value must hold this long before it counts. */
16852
+ forMs: number().int().min(0).max(864e5).optional(),
16853
+ /** Collapse a burst into one occurrence. */
16854
+ debounceMs: number().int().min(0).max(6e5).optional()
16855
+ });
16856
+ var DeviceEventLeafSchema = object({
16857
+ kind: literal("device-event"),
16858
+ /** An `EventCategory` value. */
16859
+ category: string().min(1),
16860
+ deviceId: number().int().nonnegative().optional()
16861
+ });
16862
+ var ScheduleLeafSchema = object({
16863
+ kind: literal("schedule"),
16864
+ cron: string().min(1).max(120)
16865
+ });
16866
+ var ManualLeafSchema = object({ kind: literal("manual") });
16867
+ var IntervalLeafSchema = object({
16868
+ kind: literal("interval"),
16869
+ minutes: number().int().min(1).max(1440)
16870
+ });
16871
+ var DetectionLeafSchema = object({
16872
+ kind: literal("detection"),
16873
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
16874
+ subjects: array(DetectionSubjectSchema).min(1),
16875
+ deviceIds: array(number().int().nonnegative()).min(1),
16876
+ /** The track must have visited at least one of these zones. */
16877
+ zones: array(string().min(1)).min(1).optional(),
16878
+ /** Best-so-far confidence the track must reach. */
16879
+ minScore: number().min(0).max(1).optional()
16880
+ });
16881
+ var StationaryLeafSchema = object({
16882
+ kind: literal("stationary"),
16883
+ deviceIds: array(number().int().nonnegative()).min(1),
16884
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16885
+ zones: array(string().min(1)).min(1).optional(),
16886
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
16887
+ phase: _enum(["appeared", "departed"])
16888
+ });
16889
+ /** "is": one device field compared to one value, NOW. */
16890
+ var DeviceIsLeafSchema = object({
16891
+ kind: literal("device-is"),
16564
16892
  deviceId: number().int().nonnegative(),
16565
16893
  cap: string().min(1),
16566
16894
  fieldPath: string().min(1),
16567
16895
  operator: AutomationConditionOperatorSchema,
16896
+ /**
16897
+ * The value. A number on a quantity field is stored in the field's base unit
16898
+ * (D944); a quantity value is the WRITE form, which the save boundary
16899
+ * replaces with the converted number and `unit`. A leaf that still carries
16900
+ * one is never evaluated (it fails).
16901
+ */
16568
16902
  value: union([
16569
16903
  string(),
16570
16904
  number(),
@@ -16572,24 +16906,26 @@ var AutomationConditionLeafSchema = object({
16572
16906
  array(union([string(), number()])),
16573
16907
  QuantityValueSchema
16574
16908
  ]),
16909
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
16575
16910
  unit: string().min(1).optional()
16576
16911
  });
16577
- var AutomationConditionStationaryPresentSchema = object({
16912
+ /**
16913
+ * True when the camera currently has a PARKED object matching `subjects` in at
16914
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
16915
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
16916
+ */
16917
+ var StationaryPresentLeafSchema = object({
16578
16918
  kind: literal("stationary-present"),
16579
16919
  deviceId: number().int().nonnegative(),
16580
- /** Class names; a macro class matches every raw class that maps to it. */
16581
- classes: array(string().min(1)).min(1).optional(),
16582
- /** The parked object must be in at least one of these zones. */
16920
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16583
16921
  zones: array(string().min(1)).min(1).optional()
16584
16922
  });
16585
16923
  /**
16586
- * The expression leaf, declared as a plain object rather than an intersection
16587
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
16588
- * read `kind` off each option, and an intersection hides it. The author-time
16589
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
16590
- * cannot drift — an expression that one accepts, the other accepts.
16924
+ * The escape hatch: the bounded expression engine over named device bindings.
16925
+ * Declared as a plain object so a discriminated union can read `kind`; the
16926
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
16591
16927
  */
16592
- var AutomationConditionExpressionSchema = object({
16928
+ var ExpressionLeafSchema = object({
16593
16929
  kind: literal("expression"),
16594
16930
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16595
16931
  bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -16601,55 +16937,62 @@ var AutomationConditionExpressionSchema = object({
16601
16937
  path: ["expr"]
16602
16938
  });
16603
16939
  });
16604
- var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
16940
+ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
16605
16941
  object({
16606
16942
  kind: literal("all"),
16607
- children: array(AutomationConditionSchema)
16943
+ children: array(AutomationWhenSchema)
16608
16944
  }),
16609
16945
  object({
16610
16946
  kind: literal("any"),
16611
- children: array(AutomationConditionSchema)
16947
+ children: array(AutomationWhenSchema)
16612
16948
  }),
16613
16949
  object({
16614
16950
  kind: literal("not"),
16615
- child: AutomationConditionSchema
16951
+ child: AutomationWhenSchema
16616
16952
  }),
16617
- AutomationConditionLeafSchema,
16618
- AutomationConditionExpressionSchema,
16619
- AutomationConditionStationaryPresentSchema
16953
+ DeviceBecomesLeafSchema,
16954
+ DeviceEventLeafSchema,
16955
+ ScheduleLeafSchema,
16956
+ ManualLeafSchema,
16957
+ IntervalLeafSchema,
16958
+ DetectionLeafSchema,
16959
+ StationaryLeafSchema,
16960
+ DeviceIsLeafSchema,
16961
+ StationaryPresentLeafSchema,
16962
+ ExpressionLeafSchema
16620
16963
  ]));
16621
16964
  /**
16622
- * What starts a run.
16623
- *
16624
- * D8 compliance, and it is the reason `device-state` is not merely an event
16625
- * subscription: the trigger evaluates against the **state mirror**, which is
16626
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
16627
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
16628
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
16629
- * not.
16630
- */
16631
- var AutomationTriggerSchema = discriminatedUnion("kind", [
16965
+ * The recipe shape before the single `when` tree: a flat list of triggers
16966
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
16967
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
16968
+ * older client still posts — are in this shape, so it stays READABLE here, and
16969
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
16970
+ * way. Nothing writes this shape any more.
16971
+ *
16972
+ * `{ triggers: [t1…tn], conditions?: C }`
16973
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
16974
+ * a single trigger is not wrapped in `any`.
16975
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
16976
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
16977
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
16978
+ */
16979
+ var LegacyTriggerSchema = discriminatedUnion("kind", [
16632
16980
  object({
16633
16981
  kind: literal("device-state"),
16634
16982
  deviceId: number().int().nonnegative(),
16635
16983
  cap: string().min(1),
16636
16984
  fieldPath: string().min(1),
16637
- /** Fire when the field takes this value. Omit to fire on any change. */
16638
16985
  becomes: union([
16639
16986
  string(),
16640
16987
  number(),
16641
16988
  boolean()
16642
16989
  ]).optional(),
16643
- /** Only on a CHANGE of value, not on every re-report. */
16644
16990
  edge: boolean().optional(),
16645
- /** The condition must hold this long before the run starts. */
16646
16991
  forMs: number().int().min(0).max(864e5).optional(),
16647
- /** Collapse a burst into one run. */
16648
16992
  debounceMs: number().int().min(0).max(6e5).optional()
16649
16993
  }),
16650
16994
  object({
16651
16995
  kind: literal("device-event"),
16652
- /** An `EventCategory` value. */
16653
16996
  category: string().min(1),
16654
16997
  deviceId: number().int().nonnegative().optional()
16655
16998
  }),
@@ -16660,25 +17003,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16660
17003
  object({ kind: literal("manual") }),
16661
17004
  object({
16662
17005
  kind: literal("detection"),
16663
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
16664
- * matches every raw class that maps to it; any other name matches
16665
- * exactly. */
16666
17006
  classes: array(string().min(1)).min(1),
16667
17007
  deviceIds: array(number().int().nonnegative()).min(1),
16668
- /** The track must have visited at least one of these zones. */
16669
17008
  zones: array(string().min(1)).min(1).optional(),
16670
- /** Best-so-far confidence the track must reach. */
16671
17009
  minScore: number().min(0).max(1).optional()
16672
17010
  }),
16673
17011
  object({
16674
17012
  kind: literal("stationary"),
16675
17013
  deviceIds: array(number().int().nonnegative()).min(1),
16676
- /** Class names; a macro class matches like the detection trigger. */
16677
17014
  classes: array(string().min(1)).min(1).optional(),
16678
- /** The parked object must be in at least one of these zones. */
16679
17015
  zones: array(string().min(1)).min(1).optional(),
16680
- /** `appeared`: it became parked. `departed`: the registry retired it.
16681
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
16682
17016
  phase: _enum(["appeared", "departed"])
16683
17017
  }),
16684
17018
  object({
@@ -16686,6 +17020,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16686
17020
  minutes: number().int().min(1).max(1440)
16687
17021
  })
16688
17022
  ]);
17023
+ var LegacyConditionLeafSchema = object({
17024
+ kind: literal("condition"),
17025
+ deviceId: number().int().nonnegative(),
17026
+ cap: string().min(1),
17027
+ fieldPath: string().min(1),
17028
+ operator: AutomationConditionOperatorSchema,
17029
+ value: union([
17030
+ string(),
17031
+ number(),
17032
+ boolean(),
17033
+ array(union([string(), number()])),
17034
+ QuantityValueSchema
17035
+ ]),
17036
+ unit: string().min(1).optional()
17037
+ });
17038
+ var LegacyStationaryPresentSchema = object({
17039
+ kind: literal("stationary-present"),
17040
+ deviceId: number().int().nonnegative(),
17041
+ classes: array(string().min(1)).min(1).optional(),
17042
+ zones: array(string().min(1)).min(1).optional()
17043
+ });
17044
+ var LegacyExpressionSchema = object({
17045
+ kind: literal("expression"),
17046
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17047
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
17048
+ }).superRefine((src, ctx) => {
17049
+ const err = validateExpressionSource(src);
17050
+ if (err !== null) ctx.addIssue({
17051
+ code: "custom",
17052
+ message: err,
17053
+ path: ["expr"]
17054
+ });
17055
+ });
17056
+ var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
17057
+ object({
17058
+ kind: literal("all"),
17059
+ children: array(LegacyConditionSchema)
17060
+ }),
17061
+ object({
17062
+ kind: literal("any"),
17063
+ children: array(LegacyConditionSchema)
17064
+ }),
17065
+ object({
17066
+ kind: literal("not"),
17067
+ child: LegacyConditionSchema
17068
+ }),
17069
+ LegacyConditionLeafSchema,
17070
+ LegacyExpressionSchema,
17071
+ LegacyStationaryPresentSchema
17072
+ ]));
17073
+ function convertTrigger(trigger) {
17074
+ switch (trigger.kind) {
17075
+ case "device-state": return {
17076
+ ...trigger,
17077
+ kind: "device-becomes"
17078
+ };
17079
+ case "detection": {
17080
+ const { classes, ...rest } = trigger;
17081
+ return {
17082
+ ...rest,
17083
+ subjects: classes.map(subjectFromClassName)
17084
+ };
17085
+ }
17086
+ case "stationary": {
17087
+ const { classes, ...rest } = trigger;
17088
+ return {
17089
+ ...rest,
17090
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17091
+ };
17092
+ }
17093
+ default: return trigger;
17094
+ }
17095
+ }
17096
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
17097
+ function convertLegacyCondition(condition) {
17098
+ switch (condition.kind) {
17099
+ case "condition": return {
17100
+ ...condition,
17101
+ kind: "device-is"
17102
+ };
17103
+ case "stationary-present": {
17104
+ const { classes, ...rest } = condition;
17105
+ return {
17106
+ ...rest,
17107
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17108
+ };
17109
+ }
17110
+ case "expression": return condition;
17111
+ case "not": return {
17112
+ kind: "not",
17113
+ child: convertLegacyCondition(condition.child)
17114
+ };
17115
+ case "all":
17116
+ case "any": return {
17117
+ kind: condition.kind,
17118
+ children: condition.children.map(convertLegacyCondition)
17119
+ };
17120
+ }
17121
+ }
17122
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
17123
+ function convertLegacyWhen(half) {
17124
+ const leaves = half.triggers.map(convertTrigger);
17125
+ const first = leaves[0];
17126
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
17127
+ kind: "any",
17128
+ children: leaves
17129
+ };
17130
+ if (half.conditions === void 0) return triggers;
17131
+ return {
17132
+ kind: "all",
17133
+ children: [triggers, convertLegacyCondition(half.conditions)]
17134
+ };
17135
+ }
17136
+ /**
17137
+ * The automation RECIPE — what a visually-authored automation is, before it
17138
+ * becomes code.
17139
+ *
17140
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
17141
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
17142
+ * declarative description that {@link generateAutomationBlock} compiles to the
17143
+ * TypeScript of an ordinary core block. There is therefore no second rule
17144
+ * engine, no second store and no second runtime — the block runner is the
17145
+ * runtime, `core_blocks` is the store (§2.1).
17146
+ *
17147
+ * Two parts:
17148
+ *
17149
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
17150
+ * (instants) and state leaves (durations). It replaces the flat trigger list
17151
+ * plus the separate condition tree; the firing rule is in that file and in
17152
+ * ADR "one tree".
17153
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
17154
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
17155
+ * this had to diverge from "verbatim", and why.
17156
+ *
17157
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
17158
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
17159
+ */
16689
17160
  /**
16690
17161
  * One action step.
16691
17162
  *
@@ -16788,69 +17259,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
16788
17259
  minutes: number().int().min(1).max(1440)
16789
17260
  })
16790
17261
  ]);
16791
- var AutomationRecipeSchema = object({
16792
- triggers: array(AutomationTriggerSchema),
16793
- conditions: AutomationConditionSchema.optional(),
17262
+ var CurrentAutomationRecipeSchema = object({
17263
+ when: AutomationWhenSchema,
16794
17264
  actions: array(AutomationActionSchema),
16795
17265
  runMode: AutomationRunModeSchema.optional(),
16796
17266
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
16797
17267
  keepTraces: number().int().min(1).max(100).optional()
16798
17268
  });
16799
- new Set([
16800
- {
16801
- id: "person",
16802
- name: "Person"
16803
- },
16804
- {
16805
- id: "vehicle",
16806
- name: "Vehicle"
16807
- },
16808
- {
16809
- id: "animal",
16810
- name: "Animal"
16811
- },
16812
- {
16813
- id: "package",
16814
- name: "Package"
16815
- }
16816
- ].map((l) => l.id));
16817
- var COCO_TO_MACRO = {
16818
- mapping: {
16819
- person: "person",
16820
- bicycle: "vehicle",
16821
- car: "vehicle",
16822
- motorcycle: "vehicle",
16823
- airplane: "vehicle",
16824
- bus: "vehicle",
16825
- train: "vehicle",
16826
- truck: "vehicle",
16827
- boat: "vehicle",
16828
- bird: "animal",
16829
- cat: "animal",
16830
- dog: "animal",
16831
- horse: "animal",
16832
- sheep: "animal",
16833
- cow: "animal",
16834
- elephant: "animal",
16835
- bear: "animal",
16836
- zebra: "animal",
16837
- giraffe: "animal",
16838
- suitcase: "package",
16839
- backpack: "package",
16840
- handbag: "package"
16841
- },
16842
- preserveOriginal: false
16843
- };
17269
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
17270
+ var LegacyAutomationRecipeSchema = object({
17271
+ triggers: array(LegacyTriggerSchema),
17272
+ conditions: LegacyConditionSchema.optional(),
17273
+ actions: array(AutomationActionSchema),
17274
+ runMode: AutomationRunModeSchema.optional(),
17275
+ keepTraces: number().int().min(1).max(100).optional()
17276
+ });
17277
+ /** The old shape as the new one, lossless. */
17278
+ function convertLegacyRecipe(legacy) {
17279
+ const { triggers, conditions, ...rest } = legacy;
17280
+ return {
17281
+ when: convertLegacyWhen({
17282
+ triggers,
17283
+ conditions
17284
+ }),
17285
+ ...rest
17286
+ };
17287
+ }
17288
+ /**
17289
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
17290
+ * saved block carries): the new shape as it is, or the old shape converted to
17291
+ * it. The output type is always the new one.
17292
+ */
17293
+ var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
16844
17294
  new Set(Object.values(COCO_TO_MACRO.mapping));
16845
17295
  /**
16846
17296
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
16847
17297
  * so the editor can paint every block green or red with its reason.
16848
17298
  *
16849
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
16850
- * every condition node (containers and leaves, including the ones a
16851
- * short-circuit never reached), the run-mode verdict and every action step —
16852
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
16853
- * for an Action run). Skipped
17299
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
17300
+ * fired, every node of the `when` tree (containers and leaves, including the
17301
+ * ones a short-circuit never reached), the run-mode verdict and every action
17302
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
17303
+ * for an Action run). Traces written before the single tree carry
17304
+ * `conditions.*` paths and still parse. Skipped
16854
17305
  * evaluations are traced too, and identical consecutive skips collapse into a
16855
17306
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
16856
17307
  *
@@ -16882,6 +17333,8 @@ var TraceNodeOutcomeSchema = _enum([
16882
17333
  ]);
16883
17334
  var AutomationTriggerTraceSchema = object({
16884
17335
  kind: string().min(1),
17336
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
17337
+ path: string().min(1).optional(),
16885
17338
  outcome: _enum(["pass", "fail"]),
16886
17339
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
16887
17340
  reason: string(),
@@ -16889,15 +17342,23 @@ var AutomationTriggerTraceSchema = object({
16889
17342
  vars: record(string(), TraceValueSchema)
16890
17343
  });
16891
17344
  var AutomationConditionTraceNodeSchema = object({
16892
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
17345
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
16893
17346
  path: string().min(1),
16894
17347
  kind: _enum([
16895
17348
  "all",
16896
17349
  "any",
16897
17350
  "not",
16898
17351
  "condition",
17352
+ "device-is",
16899
17353
  "expression",
16900
- "stationary-present"
17354
+ "stationary-present",
17355
+ "device-becomes",
17356
+ "device-event",
17357
+ "schedule",
17358
+ "interval",
17359
+ "manual",
17360
+ "detection",
17361
+ "stationary"
16901
17362
  ]),
16902
17363
  outcome: TraceNodeOutcomeSchema,
16903
17364
  reason: string(),
@@ -16931,6 +17392,12 @@ var AutomationStepTraceSchema = object({
16931
17392
  deviceId: number().int().optional(),
16932
17393
  startedAt: number().optional(),
16933
17394
  endedAt: number().optional(),
17395
+ /**
17396
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
17397
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
17398
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
17399
+ */
17400
+ args: record(string(), TraceValueSchema).optional(),
16934
17401
  outcome: _enum([
16935
17402
  "ok",
16936
17403
  "error",
@@ -17680,6 +18147,23 @@ method(object({
17680
18147
  addonId: string(),
17681
18148
  nodeId: string().optional()
17682
18149
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
18150
+ addonId: string(),
18151
+ nodeId: string().optional()
18152
+ }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
18153
+ addonId: string(),
18154
+ nodeId: string().optional(),
18155
+ keys: array(string())
18156
+ }), object({
18157
+ removed: array(string()),
18158
+ refused: array(object({
18159
+ key: string(),
18160
+ reason: string()
18161
+ })),
18162
+ secretsCleared: number()
18163
+ }), {
18164
+ kind: "mutation",
18165
+ auth: "admin"
18166
+ }), method(object({
17683
18167
  addonId: string(),
17684
18168
  deviceId: number(),
17685
18169
  nodeId: string().optional()
@@ -21631,6 +22115,8 @@ var StorageKindSchema = object({
21631
22115
  "orphaned",
21632
22116
  "dropped"
21633
22117
  ]),
22118
+ /** An owner declared this kind since the engine started. */
22119
+ declaredThisBoot: boolean(),
21634
22120
  /** `null` when the caller asked for no counts (`counts: false`). */
21635
22121
  rows: number().int().nonnegative().nullable(),
21636
22122
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -21642,6 +22128,51 @@ var StorageKindSchema = object({
21642
22128
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
21643
22129
  declaration: RecordKindDeclarationSchema
21644
22130
  });
22131
+ _enum([
22132
+ "orphaned",
22133
+ "stale",
22134
+ "unknown"
22135
+ ]);
22136
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
22137
+ var StaleKindFindingSchema = object({
22138
+ owner: string(),
22139
+ kind: string(),
22140
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
22141
+ verdict: _enum(["orphaned", "unknown"]),
22142
+ reason: string(),
22143
+ rows: number().int().nonnegative().nullable(),
22144
+ /** local-auth's tables are never dropped by this audit. */
22145
+ protected: boolean()
22146
+ });
22147
+ /** A stored global-settings key the running addon no longer declares. */
22148
+ var StaleSettingsKeyFindingSchema = object({
22149
+ addonId: string(),
22150
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
22151
+ key: string(),
22152
+ /**
22153
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
22154
+ * not orphans yet. Removing the key releases them.
22155
+ */
22156
+ secretRefs: array(string())
22157
+ });
22158
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
22159
+ var StaleUnknownSchema = object({
22160
+ subject: string(),
22161
+ reason: string()
22162
+ });
22163
+ var StaleDataReportSchema = object({
22164
+ /** Epoch ms of the last finished scan; `null` before the first. */
22165
+ scannedAt: number().nullable(),
22166
+ scanning: boolean(),
22167
+ trigger: _enum([
22168
+ "boot",
22169
+ "interval",
22170
+ "manual"
22171
+ ]).nullable(),
22172
+ kinds: array(StaleKindFindingSchema),
22173
+ settingsKeys: array(StaleSettingsKeyFindingSchema),
22174
+ unknown: array(StaleUnknownSchema)
22175
+ });
21645
22176
  method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
21646
22177
  owner: string().min(1).optional(),
21647
22178
  /** `false` skips the per-kind row count (a catalog read only). Default true. */
@@ -21649,7 +22180,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
21649
22180
  }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
21650
22181
  owner: string().min(1),
21651
22182
  kind: RecordKindNameSchema
21652
- }), RecordQueryResultSchema, { auth: "admin" });
22183
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
22184
+ kind: "mutation",
22185
+ auth: "admin"
22186
+ }), method(object({
22187
+ owner: string().min(1),
22188
+ kind: RecordKindNameSchema
22189
+ }), object({
22190
+ deleted: number().int().nonnegative(),
22191
+ secretsReleased: number().int().nonnegative()
22192
+ }), {
22193
+ kind: "mutation",
22194
+ auth: "admin",
22195
+ caller: "required"
22196
+ }), method(object({
22197
+ addonId: string().min(1),
22198
+ key: string().min(1)
22199
+ }), object({
22200
+ removed: boolean(),
22201
+ secretsCleared: number().int().nonnegative()
22202
+ }), {
22203
+ kind: "mutation",
22204
+ auth: "admin",
22205
+ caller: "required"
22206
+ });
21653
22207
  /**
21654
22208
  * record-entries - the ONE admin surface for every list of user-created entries
21655
22209
  * (D881): notification targets, brokers, LLM profiles, anything an addon
@@ -21788,6 +22342,12 @@ method(object({
21788
22342
  }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
21789
22343
  kind: "mutation",
21790
22344
  auth: "admin"
22345
+ }), method(object({
22346
+ refs: RefListSchema,
22347
+ onBehalfOf: string().min(1)
22348
+ }), object({ released: number().int() }), {
22349
+ kind: "mutation",
22350
+ auth: "admin"
21791
22351
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
21792
22352
  /**
21793
22353
  * device-history — the ONE mechanism for numeric history (D850).
@@ -22577,7 +23137,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
22577
23137
  /** Upstream-system identity + rendering hints for the candidate. */
22578
23138
  sourceInfo: SourceInfoSchema.optional(),
22579
23139
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
22580
- children: array(DiscoveredChildDeviceSchema).readonly().optional()
23140
+ children: array(DiscoveredChildDeviceSchema).readonly().optional(),
23141
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
23142
+ parentStableId: string().optional()
22581
23143
  }));
22582
23144
  /**
22583
23145
  * Status block — populated by the integration after each
@@ -42892,6 +43454,79 @@ object({
42892
43454
  metadata: record(string(), unknown()).optional()
42893
43455
  });
42894
43456
  /**
43457
+ * HOW a provider can be found on the network. The provider only DECLARES the
43458
+ * mode; the `local-network` scan engine runs it once for every provider that
43459
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
43460
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
43461
+ * except through a `custom` mode.
43462
+ */
43463
+ var DiscoveryModeSchema = discriminatedUnion("kind", [
43464
+ object({
43465
+ kind: literal("mdns"),
43466
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
43467
+ serviceType: string().min(1),
43468
+ /** Regular expression an instance name must match (case-insensitive). */
43469
+ instancePattern: string().optional()
43470
+ }),
43471
+ object({
43472
+ kind: literal("ws-discovery"),
43473
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
43474
+ types: array(string()).optional()
43475
+ }),
43476
+ object({
43477
+ kind: literal("ssdp"),
43478
+ searchTarget: string().min(1)
43479
+ }),
43480
+ object({
43481
+ kind: literal("udp-broadcast"),
43482
+ port: number().int().min(1).max(65535),
43483
+ payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
43484
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
43485
+ listenPort: number().int().min(1).max(65535).optional()
43486
+ }),
43487
+ object({
43488
+ kind: literal("http-probe"),
43489
+ port: number().int().min(1).max(65535),
43490
+ path: string().startsWith("/"),
43491
+ method: _enum(["GET", "POST"]).default("GET"),
43492
+ bodyText: string().optional(),
43493
+ contentType: string().optional(),
43494
+ tls: boolean().default(false),
43495
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
43496
+ acceptStatus: array(number().int()).optional()
43497
+ }),
43498
+ object({
43499
+ kind: literal("custom"),
43500
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
43501
+ reason: string().min(1)
43502
+ })
43503
+ ]);
43504
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
43505
+ var DiscoveryHitSchema = object({
43506
+ /** Index into the provider's own `getDiscoveryModes()` array. */
43507
+ modeIndex: number().int().min(0),
43508
+ host: string(),
43509
+ port: number().int(),
43510
+ http: object({
43511
+ status: number().int(),
43512
+ headers: record(string(), string()),
43513
+ /** At most 16 KiB. */
43514
+ bodyText: string()
43515
+ }).optional(),
43516
+ mdns: object({
43517
+ instance: string(),
43518
+ host: string(),
43519
+ port: number().int(),
43520
+ txt: record(string(), string())
43521
+ }).optional(),
43522
+ udp: object({ payloadHex: string() }).optional(),
43523
+ wsd: object({
43524
+ xaddrs: array(string()),
43525
+ types: array(string()),
43526
+ scopes: array(string())
43527
+ }).optional()
43528
+ });
43529
+ /**
42895
43530
  * Candidate handed back from discovery and accepted by
42896
43531
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
42897
43532
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -42926,7 +43561,17 @@ var DiscoveryCandidateSchema = object({
42926
43561
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
42927
43562
  onboardedDeviceId: number().optional(),
42928
43563
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
42929
- onboardedName: string().optional()
43564
+ onboardedName: string().optional(),
43565
+ /**
43566
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
43567
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
43568
+ * which is still read for one release.
43569
+ */
43570
+ setupIntegration: boolean().optional(),
43571
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
43572
+ parentStableId: string().optional(),
43573
+ /** `false` = shown for context but not pickable. Default true. */
43574
+ selectable: boolean().optional()
42930
43575
  });
42931
43576
  /**
42932
43577
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -42979,6 +43624,12 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
42979
43624
  }), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
42980
43625
  kind: "mutation",
42981
43626
  auth: "admin"
43627
+ }), method(object({}), array(DiscoveryModeSchema)), method(object({
43628
+ hits: array(DiscoveryHitSchema),
43629
+ params: record(string(), unknown()).optional()
43630
+ }), array(DiscoveryCandidateSchema), {
43631
+ kind: "mutation",
43632
+ auth: "admin"
42982
43633
  }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
42983
43634
  kind: "mutation",
42984
43635
  auth: "admin"
@@ -43706,17 +44357,23 @@ method(object({
43706
44357
  }), method(object({}), object({ providers: array(object({
43707
44358
  addonId: string(),
43708
44359
  label: string()
43709
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
43710
- addonId: string(),
43711
- label: string(),
43712
- candidates: array(DiscoveryCandidateSchema).readonly(),
43713
- error: string().nullable()
43714
- })).readonly() }), {
43715
- kind: "mutation",
43716
- auth: "admin"
43717
- }), method(object({
44360
+ })).readonly() }), { auth: "admin" }), method(
44361
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
44362
+ object({ subnets: array(string()).optional() }),
44363
+ object({ groups: array(object({
44364
+ addonId: string(),
44365
+ label: string(),
44366
+ candidates: array(DiscoveryCandidateSchema).readonly(),
44367
+ error: string().nullable()
44368
+ })).readonly() }),
44369
+ {
44370
+ kind: "mutation",
44371
+ auth: "admin"
44372
+ }
44373
+ ), method(object({
43718
44374
  addonId: string(),
43719
- params: record(string(), unknown()).optional()
44375
+ params: record(string(), unknown()).optional(),
44376
+ subnets: array(string()).optional()
43720
44377
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
43721
44378
  kind: "mutation",
43722
44379
  auth: "admin"
@@ -56268,6 +56925,12 @@ var TerminalInstanceInfoSchema = object({
56268
56925
  instanceId: string(),
56269
56926
  cameraStableId: string(),
56270
56927
  nodeId: string(),
56928
+ /**
56929
+ * False while the node the instance is bound to is not in the cluster
56930
+ * topology (forgotten, renamed, re-paired). The instance is kept; the
56931
+ * operator re-points it with `updateInstance({ targetNodeId })`.
56932
+ */
56933
+ nodeInCluster: boolean(),
56271
56934
  profileId: string(),
56272
56935
  profileLabel: string(),
56273
56936
  name: string(),
@@ -56317,6 +56980,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
56317
56980
  auth: "admin"
56318
56981
  }), method(object({
56319
56982
  instanceId: string().min(1),
56983
+ /** Re-point the instance to another node; the profile must exist there. */
56984
+ targetNodeId: string().min(1).max(256).optional(),
56320
56985
  name: string().trim().min(1).max(160).optional(),
56321
56986
  executable: string().max(1024).optional(),
56322
56987
  args: array(string().max(2048)).max(64).optional(),
@@ -61342,7 +62007,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
61342
62007
  id: string(),
61343
62008
  settings: record(string(), unknown()),
61344
62009
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
61345
- secretKeys: array(string()).optional()
62010
+ secretKeys: array(string()).optional(),
62011
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
62012
+ skipRestart: boolean().optional()
61346
62013
  }), object({ success: boolean() }), {
61347
62014
  kind: "mutation",
61348
62015
  auth: "admin"
@@ -61819,6 +62486,39 @@ var TlsStatusSchema = object({
61819
62486
  hasCa: boolean()
61820
62487
  }).nullable()
61821
62488
  });
62489
+ /** One provider's ask of the scan engine: the modes it declared. */
62490
+ var ScanRequestSchema = object({
62491
+ providerId: string().min(1),
62492
+ modes: array(DiscoveryModeSchema)
62493
+ });
62494
+ var ScanErrorSchema = object({
62495
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
62496
+ providerId: string().optional(),
62497
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
62498
+ mode: string().optional(),
62499
+ message: string()
62500
+ });
62501
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
62502
+ var ScanStatsSchema = object({
62503
+ hostsProbed: number().int(),
62504
+ hits: number().int(),
62505
+ timeouts: number().int(),
62506
+ byMode: record(string(), object({
62507
+ probed: number().int(),
62508
+ hits: number().int(),
62509
+ timeouts: number().int()
62510
+ }))
62511
+ });
62512
+ var ScanNetworkResultSchema = object({
62513
+ hits: array(object({
62514
+ providerId: string(),
62515
+ hit: DiscoveryHitSchema
62516
+ })),
62517
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
62518
+ subnetsScanned: array(string()),
62519
+ errors: array(ScanErrorSchema),
62520
+ stats: ScanStatsSchema
62521
+ });
61822
62522
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
61823
62523
  /**
61824
62524
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -61873,6 +62573,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
61873
62573
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
61874
62574
  kind: "mutation",
61875
62575
  auth: "admin"
62576
+ }), method(object({
62577
+ requests: array(ScanRequestSchema),
62578
+ subnets: array(string()).optional()
62579
+ }), ScanNetworkResultSchema, {
62580
+ kind: "mutation",
62581
+ auth: "admin",
62582
+ timeoutMs: 9e4
62583
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
62584
+ kind: "mutation",
62585
+ auth: "admin"
61876
62586
  }), method(_void(), TlsStatusSchema, {
61877
62587
  kind: "mutation",
61878
62588
  auth: "admin"
@@ -68812,6 +69522,13 @@ Object.freeze({
68812
69522
  access: "create",
68813
69523
  role: "admin"
68814
69524
  },
69525
+ "addonSettings.getDeclaredSettingsKeys": {
69526
+ capName: "addon-settings",
69527
+ capScope: "system",
69528
+ addonId: null,
69529
+ access: "view",
69530
+ role: "admin"
69531
+ },
68815
69532
  "addonSettings.getDeviceSettings": {
68816
69533
  capName: "addon-settings",
68817
69534
  capScope: "system",
@@ -68833,6 +69550,13 @@ Object.freeze({
68833
69550
  access: "view",
68834
69551
  role: "admin"
68835
69552
  },
69553
+ "addonSettings.unsetStoredSettingsKeys": {
69554
+ capName: "addon-settings",
69555
+ capScope: "system",
69556
+ addonId: null,
69557
+ access: "create",
69558
+ role: "admin"
69559
+ },
68836
69560
  "addonSettings.updateDeviceSettings": {
68837
69561
  capName: "addon-settings",
68838
69562
  capScope: "system",
@@ -71248,6 +71972,13 @@ Object.freeze({
71248
71972
  access: "create",
71249
71973
  role: "admin"
71250
71974
  },
71975
+ "deviceProvider.classifyDiscoveryHits": {
71976
+ capName: "device-provider",
71977
+ capScope: "system",
71978
+ addonId: null,
71979
+ access: "create",
71980
+ role: "admin"
71981
+ },
71251
71982
  "deviceProvider.createDevice": {
71252
71983
  capName: "device-provider",
71253
71984
  capScope: "system",
@@ -71276,6 +72007,13 @@ Object.freeze({
71276
72007
  access: "view",
71277
72008
  role: "admin"
71278
72009
  },
72010
+ "deviceProvider.getDiscoveryModes": {
72011
+ capName: "device-provider",
72012
+ capScope: "system",
72013
+ addonId: null,
72014
+ access: "view",
72015
+ role: "admin"
72016
+ },
71279
72017
  "deviceProvider.getDiscoveryParamsSchema": {
71280
72018
  capName: "device-provider",
71281
72019
  capScope: "system",
@@ -72053,6 +72791,13 @@ Object.freeze({
72053
72791
  access: "create",
72054
72792
  role: "admin"
72055
72793
  },
72794
+ "keychain.releaseRecordRefs": {
72795
+ capName: "keychain",
72796
+ capScope: "system",
72797
+ addonId: null,
72798
+ access: "create",
72799
+ role: "admin"
72800
+ },
72056
72801
  "keychain.resolve": {
72057
72802
  capName: "keychain",
72058
72803
  capScope: "system",
@@ -72473,6 +73218,13 @@ Object.freeze({
72473
73218
  access: "view",
72474
73219
  role: "admin"
72475
73220
  },
73221
+ "localNetwork.getScanSubnets": {
73222
+ capName: "local-network",
73223
+ capScope: "system",
73224
+ addonId: null,
73225
+ access: "view",
73226
+ role: "admin"
73227
+ },
72476
73228
  "localNetwork.getTlsStatus": {
72477
73229
  capName: "local-network",
72478
73230
  capScope: "system",
@@ -72522,6 +73274,13 @@ Object.freeze({
72522
73274
  access: "create",
72523
73275
  role: "admin"
72524
73276
  },
73277
+ "localNetwork.scanNetwork": {
73278
+ capName: "local-network",
73279
+ capScope: "system",
73280
+ addonId: null,
73281
+ access: "create",
73282
+ role: "admin"
73283
+ },
72525
73284
  "localNetwork.setAddressRole": {
72526
73285
  capName: "local-network",
72527
73286
  capScope: "system",
@@ -72543,6 +73302,13 @@ Object.freeze({
72543
73302
  access: "create",
72544
73303
  role: "admin"
72545
73304
  },
73305
+ "localNetwork.setScanSubnets": {
73306
+ capName: "local-network",
73307
+ capScope: "system",
73308
+ addonId: null,
73309
+ access: "create",
73310
+ role: "admin"
73311
+ },
72546
73312
  "localNetwork.setViewerEndpoints": {
72547
73313
  capName: "local-network",
72548
73314
  capScope: "system",
@@ -76764,6 +77530,13 @@ Object.freeze({
76764
77530
  access: "create",
76765
77531
  role: "admin"
76766
77532
  },
77533
+ "storageAdmin.dropOrphanedKind": {
77534
+ capName: "storage-admin",
77535
+ capScope: "system",
77536
+ addonId: null,
77537
+ access: "delete",
77538
+ role: "admin"
77539
+ },
76767
77540
  "storageAdmin.listKinds": {
76768
77541
  capName: "storage-admin",
76769
77542
  capScope: "system",
@@ -76785,6 +77558,27 @@ Object.freeze({
76785
77558
  access: "view",
76786
77559
  role: "admin"
76787
77560
  },
77561
+ "storageAdmin.scanStaleData": {
77562
+ capName: "storage-admin",
77563
+ capScope: "system",
77564
+ addonId: null,
77565
+ access: "create",
77566
+ role: "admin"
77567
+ },
77568
+ "storageAdmin.staleDataReport": {
77569
+ capName: "storage-admin",
77570
+ capScope: "system",
77571
+ addonId: null,
77572
+ access: "view",
77573
+ role: "admin"
77574
+ },
77575
+ "storageAdmin.unsetStaleSettingsKey": {
77576
+ capName: "storage-admin",
77577
+ capScope: "system",
77578
+ addonId: null,
77579
+ access: "create",
77580
+ role: "admin"
77581
+ },
76788
77582
  "storageEvictable.evict": {
76789
77583
  capName: "storage-evictable",
76790
77584
  capScope: "system",