@camstack/addon-model-studio 1.1.326 → 1.1.327

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 +6 -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 +238 -199
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-C8W7iGsN.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-BP5KTsDn.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-DZPtjh_2.mjs} +2 -2
  10. package/dist/model-studio.addon.js +901 -115
  11. package/dist/model-studio.addon.mjs +901 -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-DXGb6Oaj.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
@@ -5824,7 +5824,7 @@ var ZodIssueCode = {
5824
5824
  var ZodFirstPartyTypeKind;
5825
5825
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5826
5826
  //#endregion
5827
- //#region ../types/dist/sleep-63gM_1JR.mjs
5827
+ //#region ../types/dist/sleep-CeyhrcOk.mjs
5828
5828
  /**
5829
5829
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5830
5830
  * window to float samples (D455).
@@ -7891,6 +7891,7 @@ var BaseAddon = class {
7891
7891
  * a warning and falls back rather than crashing boot.
7892
7892
  */
7893
7893
  state(key, schema, fallback) {
7894
+ this._declaredStateKeys.add(key);
7894
7895
  return createDurableState({
7895
7896
  key,
7896
7897
  schema,
@@ -7906,6 +7907,96 @@ var BaseAddon = class {
7906
7907
  } })
7907
7908
  });
7908
7909
  }
7910
+ /** Durable-state keys this process registered through {@link state}. */
7911
+ _declaredStateKeys = /* @__PURE__ */ new Set();
7912
+ /**
7913
+ * Stale-data audit: every global-settings key this addon still declares —
7914
+ * constructor defaults, settings-schema fields (layout containers included),
7915
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
7916
+ * here, in the addon's own process, so the hub keeps no copy of it.
7917
+ */
7918
+ async getDeclaredSettingsKeys() {
7919
+ return { keys: [...this.declaredKeySet()].toSorted() };
7920
+ }
7921
+ declaredKeySet() {
7922
+ const keys = new Set(Object.keys(this.defaults));
7923
+ const schema = this.globalSettingsSchema();
7924
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
7925
+ for (const key of this._declaredStateKeys) keys.add(key);
7926
+ for (const path of this.secretStatePaths) {
7927
+ const root = path.split(/[.[]/, 1)[0];
7928
+ if (root) keys.add(root);
7929
+ }
7930
+ return keys;
7931
+ }
7932
+ /**
7933
+ * Stale-data audit: remove stored global-settings keys the addon no longer
7934
+ * declares, and clear the keychain values their markers held — except a value
7935
+ * a surviving key still points at. A declared or absent key is refused.
7936
+ */
7937
+ async unsetStoredSettingsKeys(keys) {
7938
+ const settings = this._ctx?.settings;
7939
+ const raw = this.rawSettings();
7940
+ if (!settings || !raw) return {
7941
+ removed: [],
7942
+ refused: keys.map((key) => ({
7943
+ key,
7944
+ reason: "no settings store"
7945
+ })),
7946
+ secretsCleared: 0
7947
+ };
7948
+ let stored;
7949
+ try {
7950
+ stored = await raw.readAddonStore();
7951
+ } catch (error) {
7952
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
7953
+ return {
7954
+ removed: [],
7955
+ refused: keys.map((key) => ({
7956
+ key,
7957
+ reason
7958
+ })),
7959
+ secretsCleared: 0
7960
+ };
7961
+ }
7962
+ const declared = this.declaredKeySet();
7963
+ const removed = [];
7964
+ const refused = [];
7965
+ for (const key of keys) {
7966
+ const base = key.split("@", 1)[0] ?? key;
7967
+ if (declared.has(base)) refused.push({
7968
+ key,
7969
+ reason: "still declared by the addon"
7970
+ });
7971
+ else if (!(key in stored)) refused.push({
7972
+ key,
7973
+ reason: "not stored"
7974
+ });
7975
+ else removed.push(key);
7976
+ }
7977
+ if (removed.length === 0) return {
7978
+ removed,
7979
+ refused,
7980
+ secretsCleared: 0
7981
+ };
7982
+ const kept = /* @__PURE__ */ new Set();
7983
+ const held = /* @__PURE__ */ new Set();
7984
+ for (const [key, value] of Object.entries(stored)) {
7985
+ const target = removed.includes(key) ? held : kept;
7986
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
7987
+ }
7988
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
7989
+ const patch = {};
7990
+ for (const key of removed) patch[key] = void 0;
7991
+ await settings.writeAddonStore(patch);
7992
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
7993
+ await this.resolveConfig();
7994
+ return {
7995
+ removed,
7996
+ refused,
7997
+ secretsCleared
7998
+ };
7999
+ }
7909
8000
  /** Per-device variant of {@link state}, backed by the per-device store. */
7910
8001
  deviceState(deviceId, key, schema, fallback) {
7911
8002
  return createDurableState({
@@ -8176,6 +8267,21 @@ function dropPerNodeFields(fields) {
8176
8267
  }
8177
8268
  return kept;
8178
8269
  }
8270
+ function collectFieldKeys(fields) {
8271
+ const collected = [];
8272
+ for (const field of fields) {
8273
+ if (field.type === "group") {
8274
+ collected.push(...collectFieldKeys(field.fields));
8275
+ continue;
8276
+ }
8277
+ if (field.type === "sub-tabs") {
8278
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
8279
+ continue;
8280
+ }
8281
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8282
+ }
8283
+ return collected;
8284
+ }
8179
8285
  function collectPerNodeFieldKeys(fields) {
8180
8286
  const collected = [];
8181
8287
  for (const field of fields) {
@@ -16521,8 +16627,172 @@ object({
16521
16627
  quantity: ReadingQuantitySchema,
16522
16628
  unit: string().min(1)
16523
16629
  });
16524
- /** How a leaf compares a device field to a value. Derived from the field's
16525
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16630
+ new Set([
16631
+ {
16632
+ id: "person",
16633
+ name: "Person"
16634
+ },
16635
+ {
16636
+ id: "vehicle",
16637
+ name: "Vehicle"
16638
+ },
16639
+ {
16640
+ id: "animal",
16641
+ name: "Animal"
16642
+ },
16643
+ {
16644
+ id: "package",
16645
+ name: "Package"
16646
+ }
16647
+ ].map((l) => l.id));
16648
+ var COCO_TO_MACRO = {
16649
+ mapping: {
16650
+ person: "person",
16651
+ bicycle: "vehicle",
16652
+ car: "vehicle",
16653
+ motorcycle: "vehicle",
16654
+ airplane: "vehicle",
16655
+ bus: "vehicle",
16656
+ train: "vehicle",
16657
+ truck: "vehicle",
16658
+ boat: "vehicle",
16659
+ bird: "animal",
16660
+ cat: "animal",
16661
+ dog: "animal",
16662
+ horse: "animal",
16663
+ sheep: "animal",
16664
+ cow: "animal",
16665
+ elephant: "animal",
16666
+ bear: "animal",
16667
+ zebra: "animal",
16668
+ giraffe: "animal",
16669
+ suitcase: "package",
16670
+ backpack: "package",
16671
+ handbag: "package"
16672
+ },
16673
+ preserveOriginal: false
16674
+ };
16675
+ /**
16676
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
16677
+ * animal, package) plus an OPTIONAL label that narrows it.
16678
+ *
16679
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
16680
+ * second list. The label that fits depends on the macro class:
16681
+ *
16682
+ * - `animal` → a sub-class (`dog`, `cat`, …);
16683
+ * - `person` → a known face (an identity of the `face-gallery`);
16684
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
16685
+ * - `package` → a sub-class.
16686
+ *
16687
+ * `other` exists only so a class the taxonomy does not know (a custom model's
16688
+ * `forklift`) survives the conversion of a stored recipe without widening.
16689
+ *
16690
+ * A label is decided LATE (D683): face recognition and LPR land after the
16691
+ * detection starts. The track-lifecycle `update` event carries the newly
16692
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
16693
+ * the subject GETS it — not at the first detection.
16694
+ */
16695
+ var SubClassLabelSchema = object({
16696
+ kind: literal("sub-class"),
16697
+ className: string().min(1)
16698
+ });
16699
+ var IdentityLabelSchema = object({
16700
+ kind: literal("identity"),
16701
+ /** The `face-gallery` identity id (stable across a rename). */
16702
+ identityId: string().min(1),
16703
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
16704
+ name: string().min(1)
16705
+ });
16706
+ var PlateLabelSchema = object({
16707
+ kind: literal("plate"),
16708
+ text: string().min(1)
16709
+ });
16710
+ function macroOf$1(className) {
16711
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
16712
+ }
16713
+ /** A sub-class label must belong to the macro it is under. */
16714
+ function subClassBelongs(macro, label) {
16715
+ return label === void 0 || macroOf$1(label.className) === macro;
16716
+ }
16717
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
16718
+ var DetectionSubjectSchema = discriminatedUnion("macro", [
16719
+ object({
16720
+ macro: literal("animal"),
16721
+ label: subClassOf("animal").optional()
16722
+ }),
16723
+ object({
16724
+ macro: literal("person"),
16725
+ label: IdentityLabelSchema.optional()
16726
+ }),
16727
+ object({
16728
+ macro: literal("vehicle"),
16729
+ label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
16730
+ }),
16731
+ object({
16732
+ macro: literal("package"),
16733
+ label: subClassOf("package").optional()
16734
+ }),
16735
+ object({
16736
+ macro: literal("other"),
16737
+ className: string().min(1)
16738
+ })
16739
+ ]);
16740
+ /**
16741
+ * The subject of a PARKED object. The stationary registry's event carries a
16742
+ * class and a zone, no face and no plate — an identity or plate label there
16743
+ * could never match, so the schema refuses it instead of letting it sit dead.
16744
+ */
16745
+ 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" });
16746
+ /**
16747
+ * A stored class string as a subject: a macro name stays a macro; a raw class
16748
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
16749
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
16750
+ */
16751
+ function subjectFromClassName(className) {
16752
+ const name = className.trim().toLowerCase();
16753
+ const macro = COCO_TO_MACRO.mapping[name];
16754
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
16755
+ if (macro === void 0) return {
16756
+ macro: "other",
16757
+ className
16758
+ };
16759
+ const label = {
16760
+ kind: "sub-class",
16761
+ className: name
16762
+ };
16763
+ switch (macro) {
16764
+ case "animal": return {
16765
+ macro: "animal",
16766
+ label
16767
+ };
16768
+ case "vehicle": return {
16769
+ macro: "vehicle",
16770
+ label
16771
+ };
16772
+ case "package": return {
16773
+ macro: "package",
16774
+ label
16775
+ };
16776
+ default: return {
16777
+ macro: "other",
16778
+ className
16779
+ };
16780
+ }
16781
+ }
16782
+ function macroOnly(macro) {
16783
+ switch (macro) {
16784
+ case "animal": return { macro: "animal" };
16785
+ case "person": return { macro: "person" };
16786
+ case "vehicle": return { macro: "vehicle" };
16787
+ case "package": return { macro: "package" };
16788
+ default: return {
16789
+ macro: "other",
16790
+ className: macro
16791
+ };
16792
+ }
16793
+ }
16794
+ /** How a state leaf compares a device field to a value. Derived from the
16795
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16526
16796
  var AutomationConditionOperatorSchema = _enum([
16527
16797
  "eq",
16528
16798
  "ne",
@@ -16533,12 +16803,76 @@ var AutomationConditionOperatorSchema = _enum([
16533
16803
  "contains",
16534
16804
  "in"
16535
16805
  ]);
16536
- var AutomationConditionLeafSchema = object({
16537
- kind: literal("condition"),
16806
+ /**
16807
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
16808
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
16809
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
16810
+ * fire; it does not lose it.
16811
+ */
16812
+ var DeviceBecomesLeafSchema = object({
16813
+ kind: literal("device-becomes"),
16814
+ deviceId: number().int().nonnegative(),
16815
+ cap: string().min(1),
16816
+ fieldPath: string().min(1),
16817
+ /** Fire when the field takes this value. Omit to fire on any change. */
16818
+ becomes: union([
16819
+ string(),
16820
+ number(),
16821
+ boolean()
16822
+ ]).optional(),
16823
+ /** Only on a CHANGE of value, not on every re-report. */
16824
+ edge: boolean().optional(),
16825
+ /** The new value must hold this long before it counts. */
16826
+ forMs: number().int().min(0).max(864e5).optional(),
16827
+ /** Collapse a burst into one occurrence. */
16828
+ debounceMs: number().int().min(0).max(6e5).optional()
16829
+ });
16830
+ var DeviceEventLeafSchema = object({
16831
+ kind: literal("device-event"),
16832
+ /** An `EventCategory` value. */
16833
+ category: string().min(1),
16834
+ deviceId: number().int().nonnegative().optional()
16835
+ });
16836
+ var ScheduleLeafSchema = object({
16837
+ kind: literal("schedule"),
16838
+ cron: string().min(1).max(120)
16839
+ });
16840
+ var ManualLeafSchema = object({ kind: literal("manual") });
16841
+ var IntervalLeafSchema = object({
16842
+ kind: literal("interval"),
16843
+ minutes: number().int().min(1).max(1440)
16844
+ });
16845
+ var DetectionLeafSchema = object({
16846
+ kind: literal("detection"),
16847
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
16848
+ subjects: array(DetectionSubjectSchema).min(1),
16849
+ deviceIds: array(number().int().nonnegative()).min(1),
16850
+ /** The track must have visited at least one of these zones. */
16851
+ zones: array(string().min(1)).min(1).optional(),
16852
+ /** Best-so-far confidence the track must reach. */
16853
+ minScore: number().min(0).max(1).optional()
16854
+ });
16855
+ var StationaryLeafSchema = object({
16856
+ kind: literal("stationary"),
16857
+ deviceIds: array(number().int().nonnegative()).min(1),
16858
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16859
+ zones: array(string().min(1)).min(1).optional(),
16860
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
16861
+ phase: _enum(["appeared", "departed"])
16862
+ });
16863
+ /** "is": one device field compared to one value, NOW. */
16864
+ var DeviceIsLeafSchema = object({
16865
+ kind: literal("device-is"),
16538
16866
  deviceId: number().int().nonnegative(),
16539
16867
  cap: string().min(1),
16540
16868
  fieldPath: string().min(1),
16541
16869
  operator: AutomationConditionOperatorSchema,
16870
+ /**
16871
+ * The value. A number on a quantity field is stored in the field's base unit
16872
+ * (D944); a quantity value is the WRITE form, which the save boundary
16873
+ * replaces with the converted number and `unit`. A leaf that still carries
16874
+ * one is never evaluated (it fails).
16875
+ */
16542
16876
  value: union([
16543
16877
  string(),
16544
16878
  number(),
@@ -16546,24 +16880,26 @@ var AutomationConditionLeafSchema = object({
16546
16880
  array(union([string(), number()])),
16547
16881
  QuantityValueSchema
16548
16882
  ]),
16883
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
16549
16884
  unit: string().min(1).optional()
16550
16885
  });
16551
- var AutomationConditionStationaryPresentSchema = object({
16886
+ /**
16887
+ * True when the camera currently has a PARKED object matching `subjects` in at
16888
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
16889
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
16890
+ */
16891
+ var StationaryPresentLeafSchema = object({
16552
16892
  kind: literal("stationary-present"),
16553
16893
  deviceId: number().int().nonnegative(),
16554
- /** Class names; a macro class matches every raw class that maps to it. */
16555
- classes: array(string().min(1)).min(1).optional(),
16556
- /** The parked object must be in at least one of these zones. */
16894
+ subjects: array(StationarySubjectSchema).min(1).optional(),
16557
16895
  zones: array(string().min(1)).min(1).optional()
16558
16896
  });
16559
16897
  /**
16560
- * The expression leaf, declared as a plain object rather than an intersection
16561
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
16562
- * read `kind` off each option, and an intersection hides it. The author-time
16563
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
16564
- * cannot drift — an expression that one accepts, the other accepts.
16898
+ * The escape hatch: the bounded expression engine over named device bindings.
16899
+ * Declared as a plain object so a discriminated union can read `kind`; the
16900
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
16565
16901
  */
16566
- var AutomationConditionExpressionSchema = object({
16902
+ var ExpressionLeafSchema = object({
16567
16903
  kind: literal("expression"),
16568
16904
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16569
16905
  bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -16575,55 +16911,62 @@ var AutomationConditionExpressionSchema = object({
16575
16911
  path: ["expr"]
16576
16912
  });
16577
16913
  });
16578
- var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
16914
+ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
16579
16915
  object({
16580
16916
  kind: literal("all"),
16581
- children: array(AutomationConditionSchema)
16917
+ children: array(AutomationWhenSchema)
16582
16918
  }),
16583
16919
  object({
16584
16920
  kind: literal("any"),
16585
- children: array(AutomationConditionSchema)
16921
+ children: array(AutomationWhenSchema)
16586
16922
  }),
16587
16923
  object({
16588
16924
  kind: literal("not"),
16589
- child: AutomationConditionSchema
16925
+ child: AutomationWhenSchema
16590
16926
  }),
16591
- AutomationConditionLeafSchema,
16592
- AutomationConditionExpressionSchema,
16593
- AutomationConditionStationaryPresentSchema
16927
+ DeviceBecomesLeafSchema,
16928
+ DeviceEventLeafSchema,
16929
+ ScheduleLeafSchema,
16930
+ ManualLeafSchema,
16931
+ IntervalLeafSchema,
16932
+ DetectionLeafSchema,
16933
+ StationaryLeafSchema,
16934
+ DeviceIsLeafSchema,
16935
+ StationaryPresentLeafSchema,
16936
+ ExpressionLeafSchema
16594
16937
  ]));
16595
16938
  /**
16596
- * What starts a run.
16597
- *
16598
- * D8 compliance, and it is the reason `device-state` is not merely an event
16599
- * subscription: the trigger evaluates against the **state mirror**, which is
16600
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
16601
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
16602
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
16603
- * not.
16604
- */
16605
- var AutomationTriggerSchema = discriminatedUnion("kind", [
16939
+ * The recipe shape before the single `when` tree: a flat list of triggers
16940
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
16941
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
16942
+ * older client still posts — are in this shape, so it stays READABLE here, and
16943
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
16944
+ * way. Nothing writes this shape any more.
16945
+ *
16946
+ * `{ triggers: [t1…tn], conditions?: C }`
16947
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
16948
+ * a single trigger is not wrapped in `any`.
16949
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
16950
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
16951
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
16952
+ */
16953
+ var LegacyTriggerSchema = discriminatedUnion("kind", [
16606
16954
  object({
16607
16955
  kind: literal("device-state"),
16608
16956
  deviceId: number().int().nonnegative(),
16609
16957
  cap: string().min(1),
16610
16958
  fieldPath: string().min(1),
16611
- /** Fire when the field takes this value. Omit to fire on any change. */
16612
16959
  becomes: union([
16613
16960
  string(),
16614
16961
  number(),
16615
16962
  boolean()
16616
16963
  ]).optional(),
16617
- /** Only on a CHANGE of value, not on every re-report. */
16618
16964
  edge: boolean().optional(),
16619
- /** The condition must hold this long before the run starts. */
16620
16965
  forMs: number().int().min(0).max(864e5).optional(),
16621
- /** Collapse a burst into one run. */
16622
16966
  debounceMs: number().int().min(0).max(6e5).optional()
16623
16967
  }),
16624
16968
  object({
16625
16969
  kind: literal("device-event"),
16626
- /** An `EventCategory` value. */
16627
16970
  category: string().min(1),
16628
16971
  deviceId: number().int().nonnegative().optional()
16629
16972
  }),
@@ -16634,25 +16977,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16634
16977
  object({ kind: literal("manual") }),
16635
16978
  object({
16636
16979
  kind: literal("detection"),
16637
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
16638
- * matches every raw class that maps to it; any other name matches
16639
- * exactly. */
16640
16980
  classes: array(string().min(1)).min(1),
16641
16981
  deviceIds: array(number().int().nonnegative()).min(1),
16642
- /** The track must have visited at least one of these zones. */
16643
16982
  zones: array(string().min(1)).min(1).optional(),
16644
- /** Best-so-far confidence the track must reach. */
16645
16983
  minScore: number().min(0).max(1).optional()
16646
16984
  }),
16647
16985
  object({
16648
16986
  kind: literal("stationary"),
16649
16987
  deviceIds: array(number().int().nonnegative()).min(1),
16650
- /** Class names; a macro class matches like the detection trigger. */
16651
16988
  classes: array(string().min(1)).min(1).optional(),
16652
- /** The parked object must be in at least one of these zones. */
16653
16989
  zones: array(string().min(1)).min(1).optional(),
16654
- /** `appeared`: it became parked. `departed`: the registry retired it.
16655
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
16656
16990
  phase: _enum(["appeared", "departed"])
16657
16991
  }),
16658
16992
  object({
@@ -16660,6 +16994,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
16660
16994
  minutes: number().int().min(1).max(1440)
16661
16995
  })
16662
16996
  ]);
16997
+ var LegacyConditionLeafSchema = object({
16998
+ kind: literal("condition"),
16999
+ deviceId: number().int().nonnegative(),
17000
+ cap: string().min(1),
17001
+ fieldPath: string().min(1),
17002
+ operator: AutomationConditionOperatorSchema,
17003
+ value: union([
17004
+ string(),
17005
+ number(),
17006
+ boolean(),
17007
+ array(union([string(), number()])),
17008
+ QuantityValueSchema
17009
+ ]),
17010
+ unit: string().min(1).optional()
17011
+ });
17012
+ var LegacyStationaryPresentSchema = object({
17013
+ kind: literal("stationary-present"),
17014
+ deviceId: number().int().nonnegative(),
17015
+ classes: array(string().min(1)).min(1).optional(),
17016
+ zones: array(string().min(1)).min(1).optional()
17017
+ });
17018
+ var LegacyExpressionSchema = object({
17019
+ kind: literal("expression"),
17020
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17021
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
17022
+ }).superRefine((src, ctx) => {
17023
+ const err = validateExpressionSource(src);
17024
+ if (err !== null) ctx.addIssue({
17025
+ code: "custom",
17026
+ message: err,
17027
+ path: ["expr"]
17028
+ });
17029
+ });
17030
+ var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
17031
+ object({
17032
+ kind: literal("all"),
17033
+ children: array(LegacyConditionSchema)
17034
+ }),
17035
+ object({
17036
+ kind: literal("any"),
17037
+ children: array(LegacyConditionSchema)
17038
+ }),
17039
+ object({
17040
+ kind: literal("not"),
17041
+ child: LegacyConditionSchema
17042
+ }),
17043
+ LegacyConditionLeafSchema,
17044
+ LegacyExpressionSchema,
17045
+ LegacyStationaryPresentSchema
17046
+ ]));
17047
+ function convertTrigger(trigger) {
17048
+ switch (trigger.kind) {
17049
+ case "device-state": return {
17050
+ ...trigger,
17051
+ kind: "device-becomes"
17052
+ };
17053
+ case "detection": {
17054
+ const { classes, ...rest } = trigger;
17055
+ return {
17056
+ ...rest,
17057
+ subjects: classes.map(subjectFromClassName)
17058
+ };
17059
+ }
17060
+ case "stationary": {
17061
+ const { classes, ...rest } = trigger;
17062
+ return {
17063
+ ...rest,
17064
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17065
+ };
17066
+ }
17067
+ default: return trigger;
17068
+ }
17069
+ }
17070
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
17071
+ function convertLegacyCondition(condition) {
17072
+ switch (condition.kind) {
17073
+ case "condition": return {
17074
+ ...condition,
17075
+ kind: "device-is"
17076
+ };
17077
+ case "stationary-present": {
17078
+ const { classes, ...rest } = condition;
17079
+ return {
17080
+ ...rest,
17081
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17082
+ };
17083
+ }
17084
+ case "expression": return condition;
17085
+ case "not": return {
17086
+ kind: "not",
17087
+ child: convertLegacyCondition(condition.child)
17088
+ };
17089
+ case "all":
17090
+ case "any": return {
17091
+ kind: condition.kind,
17092
+ children: condition.children.map(convertLegacyCondition)
17093
+ };
17094
+ }
17095
+ }
17096
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
17097
+ function convertLegacyWhen(half) {
17098
+ const leaves = half.triggers.map(convertTrigger);
17099
+ const first = leaves[0];
17100
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
17101
+ kind: "any",
17102
+ children: leaves
17103
+ };
17104
+ if (half.conditions === void 0) return triggers;
17105
+ return {
17106
+ kind: "all",
17107
+ children: [triggers, convertLegacyCondition(half.conditions)]
17108
+ };
17109
+ }
17110
+ /**
17111
+ * The automation RECIPE — what a visually-authored automation is, before it
17112
+ * becomes code.
17113
+ *
17114
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
17115
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
17116
+ * declarative description that {@link generateAutomationBlock} compiles to the
17117
+ * TypeScript of an ordinary core block. There is therefore no second rule
17118
+ * engine, no second store and no second runtime — the block runner is the
17119
+ * runtime, `core_blocks` is the store (§2.1).
17120
+ *
17121
+ * Two parts:
17122
+ *
17123
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
17124
+ * (instants) and state leaves (durations). It replaces the flat trigger list
17125
+ * plus the separate condition tree; the firing rule is in that file and in
17126
+ * ADR "one tree".
17127
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
17128
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
17129
+ * this had to diverge from "verbatim", and why.
17130
+ *
17131
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
17132
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
17133
+ */
16663
17134
  /**
16664
17135
  * One action step.
16665
17136
  *
@@ -16762,69 +17233,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
16762
17233
  minutes: number().int().min(1).max(1440)
16763
17234
  })
16764
17235
  ]);
16765
- var AutomationRecipeSchema = object({
16766
- triggers: array(AutomationTriggerSchema),
16767
- conditions: AutomationConditionSchema.optional(),
17236
+ var CurrentAutomationRecipeSchema = object({
17237
+ when: AutomationWhenSchema,
16768
17238
  actions: array(AutomationActionSchema),
16769
17239
  runMode: AutomationRunModeSchema.optional(),
16770
17240
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
16771
17241
  keepTraces: number().int().min(1).max(100).optional()
16772
17242
  });
16773
- new Set([
16774
- {
16775
- id: "person",
16776
- name: "Person"
16777
- },
16778
- {
16779
- id: "vehicle",
16780
- name: "Vehicle"
16781
- },
16782
- {
16783
- id: "animal",
16784
- name: "Animal"
16785
- },
16786
- {
16787
- id: "package",
16788
- name: "Package"
16789
- }
16790
- ].map((l) => l.id));
16791
- var COCO_TO_MACRO = {
16792
- mapping: {
16793
- person: "person",
16794
- bicycle: "vehicle",
16795
- car: "vehicle",
16796
- motorcycle: "vehicle",
16797
- airplane: "vehicle",
16798
- bus: "vehicle",
16799
- train: "vehicle",
16800
- truck: "vehicle",
16801
- boat: "vehicle",
16802
- bird: "animal",
16803
- cat: "animal",
16804
- dog: "animal",
16805
- horse: "animal",
16806
- sheep: "animal",
16807
- cow: "animal",
16808
- elephant: "animal",
16809
- bear: "animal",
16810
- zebra: "animal",
16811
- giraffe: "animal",
16812
- suitcase: "package",
16813
- backpack: "package",
16814
- handbag: "package"
16815
- },
16816
- preserveOriginal: false
16817
- };
17243
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
17244
+ var LegacyAutomationRecipeSchema = object({
17245
+ triggers: array(LegacyTriggerSchema),
17246
+ conditions: LegacyConditionSchema.optional(),
17247
+ actions: array(AutomationActionSchema),
17248
+ runMode: AutomationRunModeSchema.optional(),
17249
+ keepTraces: number().int().min(1).max(100).optional()
17250
+ });
17251
+ /** The old shape as the new one, lossless. */
17252
+ function convertLegacyRecipe(legacy) {
17253
+ const { triggers, conditions, ...rest } = legacy;
17254
+ return {
17255
+ when: convertLegacyWhen({
17256
+ triggers,
17257
+ conditions
17258
+ }),
17259
+ ...rest
17260
+ };
17261
+ }
17262
+ /**
17263
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
17264
+ * saved block carries): the new shape as it is, or the old shape converted to
17265
+ * it. The output type is always the new one.
17266
+ */
17267
+ var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
16818
17268
  new Set(Object.values(COCO_TO_MACRO.mapping));
16819
17269
  /**
16820
17270
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
16821
17271
  * so the editor can paint every block green or red with its reason.
16822
17272
  *
16823
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
16824
- * every condition node (containers and leaves, including the ones a
16825
- * short-circuit never reached), the run-mode verdict and every action step —
16826
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
16827
- * for an Action run). Skipped
17273
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
17274
+ * fired, every node of the `when` tree (containers and leaves, including the
17275
+ * ones a short-circuit never reached), the run-mode verdict and every action
17276
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
17277
+ * for an Action run). Traces written before the single tree carry
17278
+ * `conditions.*` paths and still parse. Skipped
16828
17279
  * evaluations are traced too, and identical consecutive skips collapse into a
16829
17280
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
16830
17281
  *
@@ -16856,6 +17307,8 @@ var TraceNodeOutcomeSchema = _enum([
16856
17307
  ]);
16857
17308
  var AutomationTriggerTraceSchema = object({
16858
17309
  kind: string().min(1),
17310
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
17311
+ path: string().min(1).optional(),
16859
17312
  outcome: _enum(["pass", "fail"]),
16860
17313
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
16861
17314
  reason: string(),
@@ -16863,15 +17316,23 @@ var AutomationTriggerTraceSchema = object({
16863
17316
  vars: record(string(), TraceValueSchema)
16864
17317
  });
16865
17318
  var AutomationConditionTraceNodeSchema = object({
16866
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
17319
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
16867
17320
  path: string().min(1),
16868
17321
  kind: _enum([
16869
17322
  "all",
16870
17323
  "any",
16871
17324
  "not",
16872
17325
  "condition",
17326
+ "device-is",
16873
17327
  "expression",
16874
- "stationary-present"
17328
+ "stationary-present",
17329
+ "device-becomes",
17330
+ "device-event",
17331
+ "schedule",
17332
+ "interval",
17333
+ "manual",
17334
+ "detection",
17335
+ "stationary"
16875
17336
  ]),
16876
17337
  outcome: TraceNodeOutcomeSchema,
16877
17338
  reason: string(),
@@ -16905,6 +17366,12 @@ var AutomationStepTraceSchema = object({
16905
17366
  deviceId: number().int().optional(),
16906
17367
  startedAt: number().optional(),
16907
17368
  endedAt: number().optional(),
17369
+ /**
17370
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
17371
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
17372
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
17373
+ */
17374
+ args: record(string(), TraceValueSchema).optional(),
16908
17375
  outcome: _enum([
16909
17376
  "ok",
16910
17377
  "error",
@@ -17654,6 +18121,23 @@ method(object({
17654
18121
  addonId: string(),
17655
18122
  nodeId: string().optional()
17656
18123
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
18124
+ addonId: string(),
18125
+ nodeId: string().optional()
18126
+ }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
18127
+ addonId: string(),
18128
+ nodeId: string().optional(),
18129
+ keys: array(string())
18130
+ }), object({
18131
+ removed: array(string()),
18132
+ refused: array(object({
18133
+ key: string(),
18134
+ reason: string()
18135
+ })),
18136
+ secretsCleared: number()
18137
+ }), {
18138
+ kind: "mutation",
18139
+ auth: "admin"
18140
+ }), method(object({
17657
18141
  addonId: string(),
17658
18142
  deviceId: number(),
17659
18143
  nodeId: string().optional()
@@ -21605,6 +22089,8 @@ var StorageKindSchema = object({
21605
22089
  "orphaned",
21606
22090
  "dropped"
21607
22091
  ]),
22092
+ /** An owner declared this kind since the engine started. */
22093
+ declaredThisBoot: boolean(),
21608
22094
  /** `null` when the caller asked for no counts (`counts: false`). */
21609
22095
  rows: number().int().nonnegative().nullable(),
21610
22096
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -21616,6 +22102,51 @@ var StorageKindSchema = object({
21616
22102
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
21617
22103
  declaration: RecordKindDeclarationSchema
21618
22104
  });
22105
+ _enum([
22106
+ "orphaned",
22107
+ "stale",
22108
+ "unknown"
22109
+ ]);
22110
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
22111
+ var StaleKindFindingSchema = object({
22112
+ owner: string(),
22113
+ kind: string(),
22114
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
22115
+ verdict: _enum(["orphaned", "unknown"]),
22116
+ reason: string(),
22117
+ rows: number().int().nonnegative().nullable(),
22118
+ /** local-auth's tables are never dropped by this audit. */
22119
+ protected: boolean()
22120
+ });
22121
+ /** A stored global-settings key the running addon no longer declares. */
22122
+ var StaleSettingsKeyFindingSchema = object({
22123
+ addonId: string(),
22124
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
22125
+ key: string(),
22126
+ /**
22127
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
22128
+ * not orphans yet. Removing the key releases them.
22129
+ */
22130
+ secretRefs: array(string())
22131
+ });
22132
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
22133
+ var StaleUnknownSchema = object({
22134
+ subject: string(),
22135
+ reason: string()
22136
+ });
22137
+ var StaleDataReportSchema = object({
22138
+ /** Epoch ms of the last finished scan; `null` before the first. */
22139
+ scannedAt: number().nullable(),
22140
+ scanning: boolean(),
22141
+ trigger: _enum([
22142
+ "boot",
22143
+ "interval",
22144
+ "manual"
22145
+ ]).nullable(),
22146
+ kinds: array(StaleKindFindingSchema),
22147
+ settingsKeys: array(StaleSettingsKeyFindingSchema),
22148
+ unknown: array(StaleUnknownSchema)
22149
+ });
21619
22150
  method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
21620
22151
  owner: string().min(1).optional(),
21621
22152
  /** `false` skips the per-kind row count (a catalog read only). Default true. */
@@ -21623,7 +22154,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
21623
22154
  }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
21624
22155
  owner: string().min(1),
21625
22156
  kind: RecordKindNameSchema
21626
- }), RecordQueryResultSchema, { auth: "admin" });
22157
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
22158
+ kind: "mutation",
22159
+ auth: "admin"
22160
+ }), method(object({
22161
+ owner: string().min(1),
22162
+ kind: RecordKindNameSchema
22163
+ }), object({
22164
+ deleted: number().int().nonnegative(),
22165
+ secretsReleased: number().int().nonnegative()
22166
+ }), {
22167
+ kind: "mutation",
22168
+ auth: "admin",
22169
+ caller: "required"
22170
+ }), method(object({
22171
+ addonId: string().min(1),
22172
+ key: string().min(1)
22173
+ }), object({
22174
+ removed: boolean(),
22175
+ secretsCleared: number().int().nonnegative()
22176
+ }), {
22177
+ kind: "mutation",
22178
+ auth: "admin",
22179
+ caller: "required"
22180
+ });
21627
22181
  /**
21628
22182
  * record-entries - the ONE admin surface for every list of user-created entries
21629
22183
  * (D881): notification targets, brokers, LLM profiles, anything an addon
@@ -21762,6 +22316,12 @@ method(object({
21762
22316
  }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
21763
22317
  kind: "mutation",
21764
22318
  auth: "admin"
22319
+ }), method(object({
22320
+ refs: RefListSchema,
22321
+ onBehalfOf: string().min(1)
22322
+ }), object({ released: number().int() }), {
22323
+ kind: "mutation",
22324
+ auth: "admin"
21765
22325
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
21766
22326
  /**
21767
22327
  * device-history — the ONE mechanism for numeric history (D850).
@@ -22551,7 +23111,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
22551
23111
  /** Upstream-system identity + rendering hints for the candidate. */
22552
23112
  sourceInfo: SourceInfoSchema.optional(),
22553
23113
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
22554
- children: array(DiscoveredChildDeviceSchema).readonly().optional()
23114
+ children: array(DiscoveredChildDeviceSchema).readonly().optional(),
23115
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
23116
+ parentStableId: string().optional()
22555
23117
  }));
22556
23118
  /**
22557
23119
  * Status block — populated by the integration after each
@@ -42866,6 +43428,79 @@ object({
42866
43428
  metadata: record(string(), unknown()).optional()
42867
43429
  });
42868
43430
  /**
43431
+ * HOW a provider can be found on the network. The provider only DECLARES the
43432
+ * mode; the `local-network` scan engine runs it once for every provider that
43433
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
43434
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
43435
+ * except through a `custom` mode.
43436
+ */
43437
+ var DiscoveryModeSchema = discriminatedUnion("kind", [
43438
+ object({
43439
+ kind: literal("mdns"),
43440
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
43441
+ serviceType: string().min(1),
43442
+ /** Regular expression an instance name must match (case-insensitive). */
43443
+ instancePattern: string().optional()
43444
+ }),
43445
+ object({
43446
+ kind: literal("ws-discovery"),
43447
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
43448
+ types: array(string()).optional()
43449
+ }),
43450
+ object({
43451
+ kind: literal("ssdp"),
43452
+ searchTarget: string().min(1)
43453
+ }),
43454
+ object({
43455
+ kind: literal("udp-broadcast"),
43456
+ port: number().int().min(1).max(65535),
43457
+ payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
43458
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
43459
+ listenPort: number().int().min(1).max(65535).optional()
43460
+ }),
43461
+ object({
43462
+ kind: literal("http-probe"),
43463
+ port: number().int().min(1).max(65535),
43464
+ path: string().startsWith("/"),
43465
+ method: _enum(["GET", "POST"]).default("GET"),
43466
+ bodyText: string().optional(),
43467
+ contentType: string().optional(),
43468
+ tls: boolean().default(false),
43469
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
43470
+ acceptStatus: array(number().int()).optional()
43471
+ }),
43472
+ object({
43473
+ kind: literal("custom"),
43474
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
43475
+ reason: string().min(1)
43476
+ })
43477
+ ]);
43478
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
43479
+ var DiscoveryHitSchema = object({
43480
+ /** Index into the provider's own `getDiscoveryModes()` array. */
43481
+ modeIndex: number().int().min(0),
43482
+ host: string(),
43483
+ port: number().int(),
43484
+ http: object({
43485
+ status: number().int(),
43486
+ headers: record(string(), string()),
43487
+ /** At most 16 KiB. */
43488
+ bodyText: string()
43489
+ }).optional(),
43490
+ mdns: object({
43491
+ instance: string(),
43492
+ host: string(),
43493
+ port: number().int(),
43494
+ txt: record(string(), string())
43495
+ }).optional(),
43496
+ udp: object({ payloadHex: string() }).optional(),
43497
+ wsd: object({
43498
+ xaddrs: array(string()),
43499
+ types: array(string()),
43500
+ scopes: array(string())
43501
+ }).optional()
43502
+ });
43503
+ /**
42869
43504
  * Candidate handed back from discovery and accepted by
42870
43505
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
42871
43506
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -42900,7 +43535,17 @@ var DiscoveryCandidateSchema = object({
42900
43535
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
42901
43536
  onboardedDeviceId: number().optional(),
42902
43537
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
42903
- onboardedName: string().optional()
43538
+ onboardedName: string().optional(),
43539
+ /**
43540
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
43541
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
43542
+ * which is still read for one release.
43543
+ */
43544
+ setupIntegration: boolean().optional(),
43545
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
43546
+ parentStableId: string().optional(),
43547
+ /** `false` = shown for context but not pickable. Default true. */
43548
+ selectable: boolean().optional()
42904
43549
  });
42905
43550
  /**
42906
43551
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -42953,6 +43598,12 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
42953
43598
  }), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
42954
43599
  kind: "mutation",
42955
43600
  auth: "admin"
43601
+ }), method(object({}), array(DiscoveryModeSchema)), method(object({
43602
+ hits: array(DiscoveryHitSchema),
43603
+ params: record(string(), unknown()).optional()
43604
+ }), array(DiscoveryCandidateSchema), {
43605
+ kind: "mutation",
43606
+ auth: "admin"
42956
43607
  }), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
42957
43608
  kind: "mutation",
42958
43609
  auth: "admin"
@@ -43680,17 +44331,23 @@ method(object({
43680
44331
  }), method(object({}), object({ providers: array(object({
43681
44332
  addonId: string(),
43682
44333
  label: string()
43683
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
43684
- addonId: string(),
43685
- label: string(),
43686
- candidates: array(DiscoveryCandidateSchema).readonly(),
43687
- error: string().nullable()
43688
- })).readonly() }), {
43689
- kind: "mutation",
43690
- auth: "admin"
43691
- }), method(object({
44334
+ })).readonly() }), { auth: "admin" }), method(
44335
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
44336
+ object({ subnets: array(string()).optional() }),
44337
+ object({ groups: array(object({
44338
+ addonId: string(),
44339
+ label: string(),
44340
+ candidates: array(DiscoveryCandidateSchema).readonly(),
44341
+ error: string().nullable()
44342
+ })).readonly() }),
44343
+ {
44344
+ kind: "mutation",
44345
+ auth: "admin"
44346
+ }
44347
+ ), method(object({
43692
44348
  addonId: string(),
43693
- params: record(string(), unknown()).optional()
44349
+ params: record(string(), unknown()).optional(),
44350
+ subnets: array(string()).optional()
43694
44351
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
43695
44352
  kind: "mutation",
43696
44353
  auth: "admin"
@@ -61316,7 +61973,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
61316
61973
  id: string(),
61317
61974
  settings: record(string(), unknown()),
61318
61975
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
61319
- secretKeys: array(string()).optional()
61976
+ secretKeys: array(string()).optional(),
61977
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
61978
+ skipRestart: boolean().optional()
61320
61979
  }), object({ success: boolean() }), {
61321
61980
  kind: "mutation",
61322
61981
  auth: "admin"
@@ -61793,6 +62452,39 @@ var TlsStatusSchema = object({
61793
62452
  hasCa: boolean()
61794
62453
  }).nullable()
61795
62454
  });
62455
+ /** One provider's ask of the scan engine: the modes it declared. */
62456
+ var ScanRequestSchema = object({
62457
+ providerId: string().min(1),
62458
+ modes: array(DiscoveryModeSchema)
62459
+ });
62460
+ var ScanErrorSchema = object({
62461
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
62462
+ providerId: string().optional(),
62463
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
62464
+ mode: string().optional(),
62465
+ message: string()
62466
+ });
62467
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
62468
+ var ScanStatsSchema = object({
62469
+ hostsProbed: number().int(),
62470
+ hits: number().int(),
62471
+ timeouts: number().int(),
62472
+ byMode: record(string(), object({
62473
+ probed: number().int(),
62474
+ hits: number().int(),
62475
+ timeouts: number().int()
62476
+ }))
62477
+ });
62478
+ var ScanNetworkResultSchema = object({
62479
+ hits: array(object({
62480
+ providerId: string(),
62481
+ hit: DiscoveryHitSchema
62482
+ })),
62483
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
62484
+ subnetsScanned: array(string()),
62485
+ errors: array(ScanErrorSchema),
62486
+ stats: ScanStatsSchema
62487
+ });
61796
62488
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
61797
62489
  /**
61798
62490
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -61847,6 +62539,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
61847
62539
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
61848
62540
  kind: "mutation",
61849
62541
  auth: "admin"
62542
+ }), method(object({
62543
+ requests: array(ScanRequestSchema),
62544
+ subnets: array(string()).optional()
62545
+ }), ScanNetworkResultSchema, {
62546
+ kind: "mutation",
62547
+ auth: "admin",
62548
+ timeoutMs: 9e4
62549
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
62550
+ kind: "mutation",
62551
+ auth: "admin"
61850
62552
  }), method(_void(), TlsStatusSchema, {
61851
62553
  kind: "mutation",
61852
62554
  auth: "admin"
@@ -68786,6 +69488,13 @@ Object.freeze({
68786
69488
  access: "create",
68787
69489
  role: "admin"
68788
69490
  },
69491
+ "addonSettings.getDeclaredSettingsKeys": {
69492
+ capName: "addon-settings",
69493
+ capScope: "system",
69494
+ addonId: null,
69495
+ access: "view",
69496
+ role: "admin"
69497
+ },
68789
69498
  "addonSettings.getDeviceSettings": {
68790
69499
  capName: "addon-settings",
68791
69500
  capScope: "system",
@@ -68807,6 +69516,13 @@ Object.freeze({
68807
69516
  access: "view",
68808
69517
  role: "admin"
68809
69518
  },
69519
+ "addonSettings.unsetStoredSettingsKeys": {
69520
+ capName: "addon-settings",
69521
+ capScope: "system",
69522
+ addonId: null,
69523
+ access: "create",
69524
+ role: "admin"
69525
+ },
68810
69526
  "addonSettings.updateDeviceSettings": {
68811
69527
  capName: "addon-settings",
68812
69528
  capScope: "system",
@@ -71222,6 +71938,13 @@ Object.freeze({
71222
71938
  access: "create",
71223
71939
  role: "admin"
71224
71940
  },
71941
+ "deviceProvider.classifyDiscoveryHits": {
71942
+ capName: "device-provider",
71943
+ capScope: "system",
71944
+ addonId: null,
71945
+ access: "create",
71946
+ role: "admin"
71947
+ },
71225
71948
  "deviceProvider.createDevice": {
71226
71949
  capName: "device-provider",
71227
71950
  capScope: "system",
@@ -71250,6 +71973,13 @@ Object.freeze({
71250
71973
  access: "view",
71251
71974
  role: "admin"
71252
71975
  },
71976
+ "deviceProvider.getDiscoveryModes": {
71977
+ capName: "device-provider",
71978
+ capScope: "system",
71979
+ addonId: null,
71980
+ access: "view",
71981
+ role: "admin"
71982
+ },
71253
71983
  "deviceProvider.getDiscoveryParamsSchema": {
71254
71984
  capName: "device-provider",
71255
71985
  capScope: "system",
@@ -72027,6 +72757,13 @@ Object.freeze({
72027
72757
  access: "create",
72028
72758
  role: "admin"
72029
72759
  },
72760
+ "keychain.releaseRecordRefs": {
72761
+ capName: "keychain",
72762
+ capScope: "system",
72763
+ addonId: null,
72764
+ access: "create",
72765
+ role: "admin"
72766
+ },
72030
72767
  "keychain.resolve": {
72031
72768
  capName: "keychain",
72032
72769
  capScope: "system",
@@ -72447,6 +73184,13 @@ Object.freeze({
72447
73184
  access: "view",
72448
73185
  role: "admin"
72449
73186
  },
73187
+ "localNetwork.getScanSubnets": {
73188
+ capName: "local-network",
73189
+ capScope: "system",
73190
+ addonId: null,
73191
+ access: "view",
73192
+ role: "admin"
73193
+ },
72450
73194
  "localNetwork.getTlsStatus": {
72451
73195
  capName: "local-network",
72452
73196
  capScope: "system",
@@ -72496,6 +73240,13 @@ Object.freeze({
72496
73240
  access: "create",
72497
73241
  role: "admin"
72498
73242
  },
73243
+ "localNetwork.scanNetwork": {
73244
+ capName: "local-network",
73245
+ capScope: "system",
73246
+ addonId: null,
73247
+ access: "create",
73248
+ role: "admin"
73249
+ },
72499
73250
  "localNetwork.setAddressRole": {
72500
73251
  capName: "local-network",
72501
73252
  capScope: "system",
@@ -72517,6 +73268,13 @@ Object.freeze({
72517
73268
  access: "create",
72518
73269
  role: "admin"
72519
73270
  },
73271
+ "localNetwork.setScanSubnets": {
73272
+ capName: "local-network",
73273
+ capScope: "system",
73274
+ addonId: null,
73275
+ access: "create",
73276
+ role: "admin"
73277
+ },
72520
73278
  "localNetwork.setViewerEndpoints": {
72521
73279
  capName: "local-network",
72522
73280
  capScope: "system",
@@ -76738,6 +77496,13 @@ Object.freeze({
76738
77496
  access: "create",
76739
77497
  role: "admin"
76740
77498
  },
77499
+ "storageAdmin.dropOrphanedKind": {
77500
+ capName: "storage-admin",
77501
+ capScope: "system",
77502
+ addonId: null,
77503
+ access: "delete",
77504
+ role: "admin"
77505
+ },
76741
77506
  "storageAdmin.listKinds": {
76742
77507
  capName: "storage-admin",
76743
77508
  capScope: "system",
@@ -76759,6 +77524,27 @@ Object.freeze({
76759
77524
  access: "view",
76760
77525
  role: "admin"
76761
77526
  },
77527
+ "storageAdmin.scanStaleData": {
77528
+ capName: "storage-admin",
77529
+ capScope: "system",
77530
+ addonId: null,
77531
+ access: "create",
77532
+ role: "admin"
77533
+ },
77534
+ "storageAdmin.staleDataReport": {
77535
+ capName: "storage-admin",
77536
+ capScope: "system",
77537
+ addonId: null,
77538
+ access: "view",
77539
+ role: "admin"
77540
+ },
77541
+ "storageAdmin.unsetStaleSettingsKey": {
77542
+ capName: "storage-admin",
77543
+ capScope: "system",
77544
+ addonId: null,
77545
+ access: "create",
77546
+ role: "admin"
77547
+ },
76762
77548
  "storageEvictable.evict": {
76763
77549
  capName: "storage-evictable",
76764
77550
  capScope: "system",