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