@camstack/addon-import-alexa 0.2.169 → 0.2.171

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