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