@camstack/addon-import-alexa 0.2.169 → 0.2.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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"
@@ -63392,7 +64061,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
63392
64061
  id: string(),
63393
64062
  settings: record(string(), unknown()),
63394
64063
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
63395
- secretKeys: array(string()).optional()
64064
+ secretKeys: array(string()).optional(),
64065
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
64066
+ skipRestart: boolean().optional()
63396
64067
  }), object({ success: boolean() }), {
63397
64068
  kind: "mutation",
63398
64069
  auth: "admin"
@@ -64020,6 +64691,39 @@ var TlsStatusSchema = object({
64020
64691
  hasCa: boolean()
64021
64692
  }).nullable()
64022
64693
  });
64694
+ /** One provider's ask of the scan engine: the modes it declared. */
64695
+ var ScanRequestSchema = object({
64696
+ providerId: string().min(1),
64697
+ modes: array(DiscoveryModeSchema)
64698
+ });
64699
+ var ScanErrorSchema = object({
64700
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
64701
+ providerId: string().optional(),
64702
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
64703
+ mode: string().optional(),
64704
+ message: string()
64705
+ });
64706
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
64707
+ var ScanStatsSchema = object({
64708
+ hostsProbed: number$1().int(),
64709
+ hits: number$1().int(),
64710
+ timeouts: number$1().int(),
64711
+ byMode: record(string(), object({
64712
+ probed: number$1().int(),
64713
+ hits: number$1().int(),
64714
+ timeouts: number$1().int()
64715
+ }))
64716
+ });
64717
+ var ScanNetworkResultSchema = object({
64718
+ hits: array(object({
64719
+ providerId: string(),
64720
+ hit: DiscoveryHitSchema
64721
+ })),
64722
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
64723
+ subnetsScanned: array(string()),
64724
+ errors: array(ScanErrorSchema),
64725
+ stats: ScanStatsSchema
64726
+ });
64023
64727
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
64024
64728
  /**
64025
64729
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -64074,6 +64778,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
64074
64778
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
64075
64779
  kind: "mutation",
64076
64780
  auth: "admin"
64781
+ }), method(object({
64782
+ requests: array(ScanRequestSchema),
64783
+ subnets: array(string()).optional()
64784
+ }), ScanNetworkResultSchema, {
64785
+ kind: "mutation",
64786
+ auth: "admin",
64787
+ timeoutMs: 9e4
64788
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
64789
+ kind: "mutation",
64790
+ auth: "admin"
64077
64791
  }), method(_void(), TlsStatusSchema, {
64078
64792
  kind: "mutation",
64079
64793
  auth: "admin"
@@ -73656,6 +74370,17 @@ var BaseDeviceProvider = class extends BaseAddon {
73656
74370
  async discoverDevices(_input) {
73657
74371
  return [];
73658
74372
  }
74373
+ /**
74374
+ * How the scan engine finds this provider. Default `[]`: the provider is
74375
+ * not migrated and keeps answering `discoverDevices` itself.
74376
+ */
74377
+ async getDiscoveryModes() {
74378
+ return [];
74379
+ }
74380
+ /** Turn the scan engine's raw hits into candidates. Default: none. */
74381
+ async classifyDiscoveryHits(_input) {
74382
+ return [];
74383
+ }
73659
74384
  /** Extra per-scan input form (e.g. a broadcast address for another subnet).
73660
74385
  * Null = no extra params. Override in providers that support scoped scans. */
73661
74386
  async getDiscoveryParamsSchema() {
@@ -74524,6 +75249,13 @@ Object.freeze({
74524
75249
  access: "create",
74525
75250
  role: "admin"
74526
75251
  },
75252
+ "addonSettings.getDeclaredSettingsKeys": {
75253
+ capName: "addon-settings",
75254
+ capScope: "system",
75255
+ addonId: null,
75256
+ access: "view",
75257
+ role: "admin"
75258
+ },
74527
75259
  "addonSettings.getDeviceSettings": {
74528
75260
  capName: "addon-settings",
74529
75261
  capScope: "system",
@@ -74545,6 +75277,13 @@ Object.freeze({
74545
75277
  access: "view",
74546
75278
  role: "admin"
74547
75279
  },
75280
+ "addonSettings.unsetStoredSettingsKeys": {
75281
+ capName: "addon-settings",
75282
+ capScope: "system",
75283
+ addonId: null,
75284
+ access: "create",
75285
+ role: "admin"
75286
+ },
74548
75287
  "addonSettings.updateDeviceSettings": {
74549
75288
  capName: "addon-settings",
74550
75289
  capScope: "system",
@@ -76960,6 +77699,13 @@ Object.freeze({
76960
77699
  access: "create",
76961
77700
  role: "admin"
76962
77701
  },
77702
+ "deviceProvider.classifyDiscoveryHits": {
77703
+ capName: "device-provider",
77704
+ capScope: "system",
77705
+ addonId: null,
77706
+ access: "create",
77707
+ role: "admin"
77708
+ },
76963
77709
  "deviceProvider.createDevice": {
76964
77710
  capName: "device-provider",
76965
77711
  capScope: "system",
@@ -76988,6 +77734,13 @@ Object.freeze({
76988
77734
  access: "view",
76989
77735
  role: "admin"
76990
77736
  },
77737
+ "deviceProvider.getDiscoveryModes": {
77738
+ capName: "device-provider",
77739
+ capScope: "system",
77740
+ addonId: null,
77741
+ access: "view",
77742
+ role: "admin"
77743
+ },
76991
77744
  "deviceProvider.getDiscoveryParamsSchema": {
76992
77745
  capName: "device-provider",
76993
77746
  capScope: "system",
@@ -77765,6 +78518,13 @@ Object.freeze({
77765
78518
  access: "create",
77766
78519
  role: "admin"
77767
78520
  },
78521
+ "keychain.releaseRecordRefs": {
78522
+ capName: "keychain",
78523
+ capScope: "system",
78524
+ addonId: null,
78525
+ access: "create",
78526
+ role: "admin"
78527
+ },
77768
78528
  "keychain.resolve": {
77769
78529
  capName: "keychain",
77770
78530
  capScope: "system",
@@ -78185,6 +78945,13 @@ Object.freeze({
78185
78945
  access: "view",
78186
78946
  role: "admin"
78187
78947
  },
78948
+ "localNetwork.getScanSubnets": {
78949
+ capName: "local-network",
78950
+ capScope: "system",
78951
+ addonId: null,
78952
+ access: "view",
78953
+ role: "admin"
78954
+ },
78188
78955
  "localNetwork.getTlsStatus": {
78189
78956
  capName: "local-network",
78190
78957
  capScope: "system",
@@ -78234,6 +79001,13 @@ Object.freeze({
78234
79001
  access: "create",
78235
79002
  role: "admin"
78236
79003
  },
79004
+ "localNetwork.scanNetwork": {
79005
+ capName: "local-network",
79006
+ capScope: "system",
79007
+ addonId: null,
79008
+ access: "create",
79009
+ role: "admin"
79010
+ },
78237
79011
  "localNetwork.setAddressRole": {
78238
79012
  capName: "local-network",
78239
79013
  capScope: "system",
@@ -78255,6 +79029,13 @@ Object.freeze({
78255
79029
  access: "create",
78256
79030
  role: "admin"
78257
79031
  },
79032
+ "localNetwork.setScanSubnets": {
79033
+ capName: "local-network",
79034
+ capScope: "system",
79035
+ addonId: null,
79036
+ access: "create",
79037
+ role: "admin"
79038
+ },
78258
79039
  "localNetwork.setViewerEndpoints": {
78259
79040
  capName: "local-network",
78260
79041
  capScope: "system",
@@ -82476,6 +83257,13 @@ Object.freeze({
82476
83257
  access: "create",
82477
83258
  role: "admin"
82478
83259
  },
83260
+ "storageAdmin.dropOrphanedKind": {
83261
+ capName: "storage-admin",
83262
+ capScope: "system",
83263
+ addonId: null,
83264
+ access: "delete",
83265
+ role: "admin"
83266
+ },
82479
83267
  "storageAdmin.listKinds": {
82480
83268
  capName: "storage-admin",
82481
83269
  capScope: "system",
@@ -82497,6 +83285,27 @@ Object.freeze({
82497
83285
  access: "view",
82498
83286
  role: "admin"
82499
83287
  },
83288
+ "storageAdmin.scanStaleData": {
83289
+ capName: "storage-admin",
83290
+ capScope: "system",
83291
+ addonId: null,
83292
+ access: "create",
83293
+ role: "admin"
83294
+ },
83295
+ "storageAdmin.staleDataReport": {
83296
+ capName: "storage-admin",
83297
+ capScope: "system",
83298
+ addonId: null,
83299
+ access: "view",
83300
+ role: "admin"
83301
+ },
83302
+ "storageAdmin.unsetStaleSettingsKey": {
83303
+ capName: "storage-admin",
83304
+ capScope: "system",
83305
+ addonId: null,
83306
+ access: "create",
83307
+ role: "admin"
83308
+ },
82500
83309
  "storageEvictable.evict": {
82501
83310
  capName: "storage-evictable",
82502
83311
  capScope: "system",
@@ -92770,12 +93579,22 @@ function rewriteBody(body, kind, currentUpstream, route) {
92770
93579
  /**
92771
93580
  * A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
92772
93581
  * 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) => {
93582
+ * `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
93583
+ * also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
93584
+ * `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
93585
+ */
93586
+ function rewriteSetCookie(cookie, route, options = {}) {
93587
+ const parts = cookie.split(";").map((part) => part.trim());
93588
+ const dropped = options.browserSafe === true ? [
93589
+ "domain",
93590
+ "path",
93591
+ "secure",
93592
+ "samesite"
93593
+ ] : ["domain", "path"];
93594
+ const kept = parts.filter((part, index) => {
92776
93595
  if (index === 0) return true;
92777
93596
  const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
92778
- return name !== "domain" && name !== "path";
93597
+ return !dropped.includes(name);
92779
93598
  });
92780
93599
  kept.push(`Path=${route.basePath}/`);
92781
93600
  return kept.join("; ");
@@ -92786,10 +93605,10 @@ function rewriteSetCookie(cookie, route) {
92786
93605
  * one, so a past-dated set that only deleted the other domain's copy would delete the
92787
93606
  * live one the same response just set: that deletion is dropped.
92788
93607
  */
92789
- function rewriteSetCookies(cookies, route) {
93608
+ function rewriteSetCookies(cookies, route, options = {}) {
92790
93609
  const now = Date.now();
92791
93610
  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));
93611
+ 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
93612
  }
92794
93613
  function cookieName(cookie) {
92795
93614
  return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
@@ -92849,6 +93668,99 @@ function rewriteFormBody(body, route) {
92849
93668
  function filterForwardedCookies(cookieHeader) {
92850
93669
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
92851
93670
  }
93671
+ /**
93672
+ * Server-side cookie jar of ONE vendor sign-in attempt (D955).
93673
+ *
93674
+ * A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
93675
+ * without the cookies it set a step earlier. Relying on the browser to store them under the
93676
+ * hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
93677
+ * on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
93678
+ * `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
93679
+ * whatever the browser did. Values never leave this object except as the upstream `Cookie`
93680
+ * header; nothing here logs.
93681
+ */
93682
+ function attributeValue(attrs, name) {
93683
+ const found = attrs.find((a) => a.split("=")[0]?.trim().toLowerCase() === name);
93684
+ if (found === void 0) return void 0;
93685
+ const eq = found.indexOf("=");
93686
+ return eq === -1 ? "" : found.slice(eq + 1).trim();
93687
+ }
93688
+ function hasFlag(attrs, name) {
93689
+ return attrs.some((a) => a.toLowerCase() === name);
93690
+ }
93691
+ function expiryOf(attrs, nowMs) {
93692
+ const maxAge = attributeValue(attrs, "max-age");
93693
+ if (maxAge !== void 0 && maxAge.length > 0 && Number.isFinite(Number(maxAge))) return nowMs + Number(maxAge) * 1e3;
93694
+ const expires = attributeValue(attrs, "expires");
93695
+ if (expires === void 0) return null;
93696
+ const at = Date.parse(expires);
93697
+ return Number.isFinite(at) ? at : null;
93698
+ }
93699
+ function defaultPath(requestPath) {
93700
+ const last = requestPath.lastIndexOf("/");
93701
+ return last <= 0 ? "/" : requestPath.slice(0, last);
93702
+ }
93703
+ function pathMatches(cookiePath, requestPath) {
93704
+ if (requestPath === cookiePath) return true;
93705
+ if (!requestPath.startsWith(cookiePath)) return false;
93706
+ return cookiePath.endsWith("/") || requestPath.charAt(cookiePath.length) === "/";
93707
+ }
93708
+ function hostMatches(cookie, host) {
93709
+ if (cookie.domain === null) return cookie.originHost === host;
93710
+ const bare = hostWithoutPort(host);
93711
+ return bare === cookie.domain || bare.endsWith(`.${cookie.domain}`);
93712
+ }
93713
+ var VendorCookieJar = class {
93714
+ #cookies = /* @__PURE__ */ new Map();
93715
+ /** Store (or delete, when expired) the cookies of one upstream response. */
93716
+ store(setCookies, origin, nowMs) {
93717
+ for (const raw of setCookies) {
93718
+ const [first, ...rest] = raw.split(";").map((part) => part.trim());
93719
+ const eq = first?.indexOf("=") ?? -1;
93720
+ if (first === void 0 || eq <= 0) continue;
93721
+ const name = first.slice(0, eq);
93722
+ const attrs = rest;
93723
+ const domainAttr = attributeValue(attrs, "domain");
93724
+ const domain = domainAttr === void 0 || domainAttr.length === 0 ? null : domainAttr.replace(/^\./, "").toLowerCase();
93725
+ const pathAttr = attributeValue(attrs, "path");
93726
+ const path = pathAttr !== void 0 && pathAttr.startsWith("/") ? pathAttr : defaultPath(origin.pathname);
93727
+ const originBare = hostWithoutPort(origin.host);
93728
+ if (domain !== null && originBare !== domain && !originBare.endsWith(`.${domain}`)) continue;
93729
+ const expiresAtMs = expiryOf(attrs, nowMs);
93730
+ const key = `${name}|${domain ?? origin.host}|${path}`;
93731
+ if (expiresAtMs !== null && expiresAtMs <= nowMs) {
93732
+ this.#cookies.delete(key);
93733
+ continue;
93734
+ }
93735
+ this.#cookies.set(key, {
93736
+ name,
93737
+ value: first.slice(eq + 1),
93738
+ originHost: origin.host,
93739
+ domain,
93740
+ path,
93741
+ secure: hasFlag(attrs, "secure"),
93742
+ expiresAtMs
93743
+ });
93744
+ }
93745
+ }
93746
+ /** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
93747
+ headerFor(target, nowMs) {
93748
+ const secureOk = target.scheme === "https" || isLoopbackHost(target.host);
93749
+ 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);
93750
+ if (matching.length === 0) return null;
93751
+ return matching.map((c) => `${c.name}=${c.value}`).join("; ");
93752
+ }
93753
+ };
93754
+ /** Cookie names carried by a `Cookie` header value. */
93755
+ function cookieHeaderNames(header) {
93756
+ return new Set(header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0));
93757
+ }
93758
+ /** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
93759
+ function mergeCookieHeaders(browser, jar) {
93760
+ if (jar === null) return browser;
93761
+ const shadowed = cookieHeaderNames(jar);
93762
+ return [...browser.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !shadowed.has(part.split("=")[0]?.trim() ?? "")), jar].join("; ");
93763
+ }
92852
93764
  /** A short stable label for the attempt: enough to group lines, useless as a capability. */
92853
93765
  function attemptLabel(attemptId) {
92854
93766
  let hash = 2166136261;
@@ -93044,6 +93956,7 @@ var VendorLoginProxy = class {
93044
93956
  this.#attempts.set(spec.attemptId, {
93045
93957
  expiresAtMs: spec.expiresAtMs,
93046
93958
  upstreams: spec.upstreams,
93959
+ jar: spec.serverCookieJar === true ? new VendorCookieJar() : null,
93047
93960
  evaluate: async (view) => {
93048
93961
  const verdict = await spec.terminal(view);
93049
93962
  if (verdict.kind === "continue") return verdict;
@@ -93140,7 +94053,7 @@ var VendorLoginProxy = class {
93140
94053
  body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
93141
94054
  } else body = read;
93142
94055
  }
93143
- const headers = this.#requestHeaders(req, route, upstream);
94056
+ const headers = this.#requestHeaders(req, route, upstream, attempt.jar, target);
93144
94057
  const upstreamRes = await this.#fetch(target, {
93145
94058
  method,
93146
94059
  headers,
@@ -93149,6 +94062,10 @@ var VendorLoginProxy = class {
93149
94062
  });
93150
94063
  const location = upstreamRes.headers.get("location");
93151
94064
  const setCookies = upstreamRes.headers.getSetCookie();
94065
+ attempt.jar?.store(setCookies, {
94066
+ host: upstream.host,
94067
+ pathname: target.pathname
94068
+ }, this.#now());
93152
94069
  const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
93153
94070
  const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
93154
94071
  const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
@@ -93196,7 +94113,7 @@ var VendorLoginProxy = class {
93196
94113
  upstreamRes.headers.forEach((value, name) => {
93197
94114
  if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
93198
94115
  });
93199
- if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
94116
+ if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route, { browserSafe: attempt.jar !== null });
93200
94117
  if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
93201
94118
  out["cache-control"] = "no-store";
93202
94119
  if (text !== null && kind !== null) {
@@ -93218,15 +94135,19 @@ var VendorLoginProxy = class {
93218
94135
  ...flowTraceMeta(input)
93219
94136
  } });
93220
94137
  }
93221
- #requestHeaders(req, route, upstream) {
94138
+ #requestHeaders(req, route, upstream, jar, target) {
93222
94139
  const headers = {};
93223
94140
  for (const [name, value] of Object.entries(req.headers)) {
93224
94141
  const lower = name.toLowerCase();
93225
94142
  if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
93226
94143
  headers[lower] = Array.isArray(value) ? value.join(", ") : value;
93227
94144
  }
93228
- const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
93229
- if (cookie.length > 0) headers["cookie"] = cookie;
94145
+ const merged = mergeCookieHeaders(filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? ""), jar?.headerFor({
94146
+ host: upstream.host,
94147
+ pathname: target.pathname,
94148
+ scheme: upstream.scheme
94149
+ }, this.#now()) ?? null);
94150
+ if (merged.length > 0) headers["cookie"] = merged;
93230
94151
  if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
93231
94152
  const referer = firstHeader(req.headers["referer"]);
93232
94153
  if (referer !== void 0) {
@@ -136249,10 +137170,6 @@ var AlexaRemoteClient = class {
136249
137170
  if (isAuthError(message)) throw new AlexaAuthError(message);
136250
137171
  throw err;
136251
137172
  }
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
137173
  this.#connected = true;
136257
137174
  await this.#persistSession(remote);
136258
137175
  }
@@ -136988,7 +137905,7 @@ function buildAlexaBrokerProvider(deps) {
136988
137905
  label: "Alexa",
136989
137906
  signIn: {
136990
137907
  basePath: "/addon/import-alexa/login",
136991
- methods: ["device-code", "proxy"]
137908
+ methods: ["device-code"]
136992
137909
  }
136993
137910
  }]
136994
137911
  }],
@@ -138172,6 +139089,15 @@ var BrokerEcho = class {
138172
139089
  try {
138173
139090
  const seed = await this.deps.readSeed();
138174
139091
  const stored = await sessions.get(brokerId);
139092
+ logger.info("alexa speak: broker sign-in material", {
139093
+ ...tags,
139094
+ meta: {
139095
+ seedFound: seed !== null,
139096
+ hasRefreshToken: seed !== null && seed.refreshToken.length > 0,
139097
+ hasDeviceAppName: seed !== null && seed.deviceAppName.length > 0,
139098
+ hasStoredSession: stored.length > 0
139099
+ }
139100
+ });
138175
139101
  if (seed === null && stored.length === 0) {
138176
139102
  this.currentState = "broker-not-signed-in";
138177
139103
  this.currentError = null;
@@ -139774,22 +140700,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
139774
140700
  labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
139775
140701
  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
140702
  }]
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
140703
  }] });
139794
140704
  }
139795
140705
  async supportsManualCreation() {