@camstack/addon-import-alexa 0.2.148 → 0.2.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/MaskShapeCanvas-B6q5RDLi.mjs +13807 -0
  2. package/dist/MotionZonesSettings-BD57OAIs.mjs +309 -0
  3. package/dist/PrivacyMaskSettings-ZxdPEelf.mjs +368 -0
  4. package/dist/SceneMonitorEditor--KqtKX1B.mjs +846 -0
  5. package/dist/_stub.js +39665 -0
  6. package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CdRL7bFZ.mjs +156 -0
  7. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.js-BmoaT3cr.mjs +25 -0
  8. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-I8eNxvdp.mjs +26 -0
  9. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-Bct5qZia.mjs +25 -0
  10. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BHqOw8oh.mjs +25 -0
  11. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-B3qxfxBT.mjs +26 -0
  12. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react__loadShare__.js-DwXXYAlk.mjs +70 -0
  13. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CUiVYNMY.mjs +26 -0
  14. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom__loadShare__.js-CIPmAyPi.mjs +25 -0
  15. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-tXm1x5EZ.mjs +25 -0
  16. package/dist/addon-import-alexa.css +3 -0
  17. package/dist/addon.js +2211 -160
  18. package/dist/addon.mjs +2227 -179
  19. package/dist/hostInit-_k3o3MHN.mjs +129 -0
  20. package/dist/index.js +1 -0
  21. package/dist/index.mjs +2 -2
  22. package/dist/player-overlays-CJBC2I_E.mjs +51 -0
  23. package/dist/remoteEntry.js +2 -0
  24. package/dist/remoteEntry.ssr.js +33 -0
  25. package/dist/responsive-N99niSJu.mjs +125 -0
  26. package/dist/rolldown-runtime-HEgqtunE.mjs +20 -0
  27. package/dist/scene-monitor-copy-DeuPVYmS.mjs +227 -0
  28. package/dist/square-CnLcG0Zc.mjs +45 -0
  29. package/dist/trash-2-BzimUQsl.mjs +25 -0
  30. package/dist/ui-text-B5YO4hoJ.mjs +34 -0
  31. package/dist/virtualExposes-lZT06jeg.mjs +2 -0
  32. package/dist/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-CsdMNhUS.mjs +2857 -0
  33. package/dist/virtual_mf-exposes-ssr___mfe_internal__addon_import_alexa_page__remoteEntry_js-CViESMdd.mjs +10 -0
  34. package/dist/virtual_mf-exposes___mfe_internal__addon_import_alexa_page__remoteEntry_js-C3qJCjtV.mjs +27 -0
  35. package/i18n/en.json +6 -1
  36. package/i18n/it.json +6 -1
  37. package/package.json +26 -4
  38. package/python/NOTICE +4 -0
  39. package/python/alexa_gateway.py +199 -0
  40. package/python/alexa_protocol.py +110 -0
  41. package/python/alexa_sequences.py +74 -0
  42. package/python/alexa_worker.py +50 -0
  43. package/python/requirements.txt +3 -0
  44. package/python/test_alexa_protocol.py +168 -0
package/dist/addon.mjs CHANGED
@@ -1,8 +1,15 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { randomBytes } from "node:crypto";
3
- import { networkInterfaces } from "node:os";
4
- import { promises } from "node:fs";
5
- import path from "node:path";
3
+ import * as os from "node:os";
4
+ import { networkInterfaces, userInfo } from "node:os";
5
+ import * as fs$1 from "node:fs";
6
+ import { existsSync, promises } from "node:fs";
7
+ import * as path$1 from "node:path";
8
+ import path, { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import * as fs from "node:fs/promises";
11
+ import { execFileSync, spawn } from "node:child_process";
12
+ import "node:zlib";
6
13
  //#region \0rolldown/runtime.js
7
14
  var __create = Object.create;
8
15
  var __defProp = Object.defineProperty;
@@ -5614,7 +5621,7 @@ var ZodIssueCode = {
5614
5621
  var ZodFirstPartyTypeKind;
5615
5622
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5616
5623
  //#endregion
5617
- //#region ../types/dist/sleep-bykKrTgu.mjs
5624
+ //#region ../types/dist/sleep-0sD_LHLI.mjs
5618
5625
  /**
5619
5626
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5620
5627
  * window to float samples (D455).
@@ -8852,6 +8859,28 @@ function event(data) {
8852
8859
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8853
8860
  var VersionOutputSchema$1 = object({ version: string() });
8854
8861
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8862
+ /**
8863
+ * Identity — preserves literal types for downstream inference.
8864
+ *
8865
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
8866
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
8867
+ * the broader unions declared on `CustomActionSpec`'s default generics.
8868
+ * Shape validity is enforced separately by the `customAction(...)` helper
8869
+ * whose return type is already a `CustomActionSpec<...>`.
8870
+ */
8871
+ function defineCustomActions(spec) {
8872
+ return spec;
8873
+ }
8874
+ function customAction(input, output, options) {
8875
+ return {
8876
+ input,
8877
+ output,
8878
+ kind: options?.kind ?? "query",
8879
+ auth: options?.auth ?? "protected",
8880
+ scope: options?.scope ?? { kind: "system" },
8881
+ ...options?.caller ? { caller: "required" } : {}
8882
+ };
8883
+ }
8855
8884
  var StaticDirOutputSchema = object({ staticDir: string() });
8856
8885
  var VersionOutputSchema = object({ version: string() });
8857
8886
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -9017,6 +9046,7 @@ var CAP_NAMES = Object.freeze([
9017
9046
  "login-method",
9018
9047
  "media-player",
9019
9048
  "mesh-network",
9049
+ "meter",
9020
9050
  "metrics-provider",
9021
9051
  "model-convert",
9022
9052
  "model-distributor",
@@ -10074,6 +10104,10 @@ Object.freeze({
10074
10104
  ]
10075
10105
  }
10076
10106
  ],
10107
+ "meter": [{
10108
+ "path": "value",
10109
+ "valueType": "number"
10110
+ }],
10077
10111
  "motion": [
10078
10112
  {
10079
10113
  "path": "detected",
@@ -10817,7 +10851,7 @@ object({
10817
10851
  params: record(string(), union([string(), number()])).optional()
10818
10852
  });
10819
10853
  //#endregion
10820
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10854
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10821
10855
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10822
10856
  DeviceType["Camera"] = "camera";
10823
10857
  DeviceType["Hub"] = "hub";
@@ -10911,8 +10945,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10911
10945
  DeviceType["PetFeeder"] = "pet-feeder";
10912
10946
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10913
10947
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10914
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10948
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10949
+ * A plug-built dishwasher carries running-state and power instead. */
10915
10950
  DeviceType["Dishwasher"] = "dishwasher";
10951
+ /** Washing machine. A plug-built one is running while its power stays
10952
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10953
+ DeviceType["Washer"] = "washer";
10954
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10955
+ DeviceType["Dryer"] = "dryer";
10916
10956
  return DeviceType;
10917
10957
  }({});
10918
10958
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11170,7 +11210,7 @@ function errMsg(err) {
11170
11210
  return String(err);
11171
11211
  }
11172
11212
  //#endregion
11173
- //#region ../types/dist/composition-OhpuespA.mjs
11213
+ //#region ../types/dist/composition-CpETZ4HL.mjs
11174
11214
  /**
11175
11215
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11176
11216
  *
@@ -11926,6 +11966,97 @@ function validateExpressionSource(src) {
11926
11966
  return null;
11927
11967
  }
11928
11968
  /**
11969
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11970
+ *
11971
+ * Standby of the control board is under 5 W; a motor or a heater is far
11972
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11973
+ * a cycle, so running starts only after 30 s above the line.
11974
+ *
11975
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11976
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11977
+ * report "finished" in the middle of the program, so the stop dwell is
11978
+ * 10 minutes on every type. The number is stored per type so one machine
11979
+ * can move later without moving the others.
11980
+ */
11981
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11982
+ var APPLIANCE_KINDS = [
11983
+ "washer",
11984
+ "dryer",
11985
+ "dishwasher"
11986
+ ];
11987
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11988
+ /**
11989
+ * Statistics and integrals over a history read (D944).
11990
+ *
11991
+ * Pure: the same points always give the same answer, so a restart recomputes
11992
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11993
+ * never the mean of the samples. A hole with no held value is not integrated
11994
+ * as zero. A truncated read is not published.
11995
+ */
11996
+ var STATISTIC_STATS = [
11997
+ "min",
11998
+ "max",
11999
+ "mean",
12000
+ "sum",
12001
+ "count",
12002
+ "change"
12003
+ ];
12004
+ /**
12005
+ * The range a statistics or integral helper asks history for (D944).
12006
+ *
12007
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
12008
+ * or 25 hours across a DST change. A tier is eligible only when its retention
12009
+ * is strictly longer than the longest that range can be. Forever qualifies.
12010
+ * Equal to a tier's retention is a refusal, not a short answer.
12011
+ */
12012
+ var MINUTE_MS = 6e4;
12013
+ var HOUR_MS = 60 * MINUTE_MS;
12014
+ var DAY_MS = 24 * HOUR_MS;
12015
+ var ROLLING_SPANS = [
12016
+ "1h",
12017
+ "24h",
12018
+ "2d",
12019
+ "7d",
12020
+ "30d",
12021
+ "custom"
12022
+ ];
12023
+ var CALENDAR_PERIODS = [
12024
+ "today",
12025
+ "yesterday",
12026
+ "this-week",
12027
+ "this-month"
12028
+ ];
12029
+ var RESET_PERIODS = [
12030
+ "never",
12031
+ "daily",
12032
+ "weekly",
12033
+ "monthly"
12034
+ ];
12035
+ 5 * MINUTE_MS;
12036
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
12037
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
12038
+ var StatisticWindowSchema = discriminatedUnion("kind", [object({
12039
+ kind: literal("rolling"),
12040
+ span: _enum(ROLLING_SPANS),
12041
+ customMinutes: number().int().min(5).optional()
12042
+ }).check((payload) => {
12043
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12044
+ code: "custom",
12045
+ message: "a custom range needs a duration in minutes",
12046
+ path: ["customMinutes"],
12047
+ input: payload.value
12048
+ });
12049
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12050
+ code: "custom",
12051
+ message: "only a custom range carries a duration",
12052
+ path: ["customMinutes"],
12053
+ input: payload.value
12054
+ });
12055
+ }), object({
12056
+ kind: literal("calendar"),
12057
+ period: _enum(CALENDAR_PERIODS)
12058
+ })]);
12059
+ /**
11929
12060
  * A composition source identity: `(addonId, stableId)`, never a numeric device
11930
12061
  * id. Split out so helper schemas can name it without a cycle through
11931
12062
  * `composition.ts` (D659).
@@ -11981,14 +12112,37 @@ var HelperMemberSchema = object({
11981
12112
  cap: string().min(1),
11982
12113
  fieldPath: string().min(1)
11983
12114
  });
11984
- var HelperSpecSchema = discriminatedUnion("kind", [object({
11985
- kind: literal("group"),
11986
- templateId: GroupTemplateIdSchema,
11987
- aggregate: AggregateReducerSchema,
11988
- members: array(HelperMemberSchema).min(1).max(64),
11989
- /** Actuator groups only. Readings leave this unset. */
11990
- active: ActiveCriterionSchema.optional()
11991
- })]);
12115
+ var HelperSpecSchema = discriminatedUnion("kind", [
12116
+ object({
12117
+ kind: literal("group"),
12118
+ templateId: GroupTemplateIdSchema,
12119
+ aggregate: AggregateReducerSchema,
12120
+ members: array(HelperMemberSchema).min(1).max(64),
12121
+ /** Actuator groups only. Readings leave this unset. */
12122
+ active: ActiveCriterionSchema.optional()
12123
+ }),
12124
+ object({
12125
+ kind: literal("appliance"),
12126
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12127
+ appliance: _enum(APPLIANCE_KINDS),
12128
+ thresholdWatts: number().positive(),
12129
+ startDwellMs: number().int().nonnegative(),
12130
+ stopDwellMs: number().int().nonnegative(),
12131
+ member: HelperMemberSchema
12132
+ }),
12133
+ object({
12134
+ kind: literal("statistic"),
12135
+ stat: _enum(STATISTIC_STATS),
12136
+ window: StatisticWindowSchema,
12137
+ source: HelperMemberSchema
12138
+ }),
12139
+ object({
12140
+ kind: literal("integral"),
12141
+ reset: _enum(RESET_PERIODS),
12142
+ output: _enum(["energy", "meter"]),
12143
+ source: HelperMemberSchema
12144
+ })
12145
+ ]);
11992
12146
  /** A composed device's stableId is this prefix plus the block id. */
11993
12147
  var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11994
12148
  /** Longest snippet a `code` source may carry (slice 4). */
@@ -12062,6 +12216,26 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12062
12216
  "omit",
12063
12217
  "offline"
12064
12218
  ])
12219
+ }),
12220
+ object({
12221
+ kind: literal("series"),
12222
+ read: CompositionFieldReadSchema,
12223
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12224
+ publish: _enum([
12225
+ "value",
12226
+ "included",
12227
+ "excluded",
12228
+ "total"
12229
+ ]),
12230
+ /**
12231
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12232
+ * device unseeded until history answers, so a restart cannot publish 0.
12233
+ */
12234
+ empty: _enum([
12235
+ "null",
12236
+ "omit",
12237
+ "offline"
12238
+ ])
12065
12239
  })
12066
12240
  ]);
12067
12241
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
@@ -15822,7 +15996,24 @@ var AddonPageDeclarationSchema = object({
15822
15996
  /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
15823
15997
  sectionLabel: string().optional()
15824
15998
  });
15825
- method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
15999
+ var addonPagesSourceCapability = {
16000
+ name: "addon-pages-source",
16001
+ scope: "system",
16002
+ mode: "collection",
16003
+ internal: true,
16004
+ methods: {
16005
+ /**
16006
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
16007
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
16008
+ * authenticated session at the default `auth: 'protected'`. The only
16009
+ * consumer, the `addon-pages-aggregator` builtin, reads it via
16010
+ * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
16011
+ * only, never tRPC (per `ctx.capabilities` seeing only the local
16012
+ * process). The public, enriched listing admin-ui actually consumes is
16013
+ * the separate `addon-pages` cap, unaffected by this change.
16014
+ */
16015
+ listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
16016
+ };
15826
16017
  var AddonHttpRouteSchema = object({
15827
16018
  method: _enum([
15828
16019
  "GET",
@@ -18511,7 +18702,8 @@ var CompositionProblemSchema = object({
18511
18702
  "native-capability-replace",
18512
18703
  "mixed-quantity",
18513
18704
  "helper-mismatch",
18514
- "member-constraint"
18705
+ "member-constraint",
18706
+ "retention"
18515
18707
  ]),
18516
18708
  path: string(),
18517
18709
  message: string()
@@ -18751,7 +18943,7 @@ Object.freeze({
18751
18943
  * One catalog row per base reading a group helper can build (D944 §3).
18752
18944
  * Data, not a form default written into the block.
18753
18945
  */
18754
- var NOW = {
18946
+ var NOW$1 = {
18755
18947
  fieldPath: "lastFetchedAt",
18756
18948
  expr: "now"
18757
18949
  };
@@ -18797,7 +18989,7 @@ var GROUP_TEMPLATES = [
18797
18989
  quantity: "temperature",
18798
18990
  defaultAggregate: "mean",
18799
18991
  empty: "null",
18800
- companions: [NOW]
18992
+ companions: [NOW$1]
18801
18993
  }),
18802
18994
  numeric({
18803
18995
  id: "humidity",
@@ -18808,7 +19000,7 @@ var GROUP_TEMPLATES = [
18808
19000
  quantity: "humidity",
18809
19001
  defaultAggregate: "mean",
18810
19002
  empty: "null",
18811
- companions: [NOW]
19003
+ companions: [NOW$1]
18812
19004
  }),
18813
19005
  numeric({
18814
19006
  id: "pressure",
@@ -18819,7 +19011,7 @@ var GROUP_TEMPLATES = [
18819
19011
  quantity: "pressure",
18820
19012
  defaultAggregate: "mean",
18821
19013
  empty: "null",
18822
- companions: [NOW]
19014
+ companions: [NOW$1]
18823
19015
  }),
18824
19016
  numeric({
18825
19017
  id: "illuminance",
@@ -18830,7 +19022,7 @@ var GROUP_TEMPLATES = [
18830
19022
  quantity: "illuminance",
18831
19023
  defaultAggregate: "mean",
18832
19024
  empty: "null",
18833
- companions: [NOW]
19025
+ companions: [NOW$1]
18834
19026
  }),
18835
19027
  numeric({
18836
19028
  id: "power",
@@ -18841,7 +19033,7 @@ var GROUP_TEMPLATES = [
18841
19033
  quantity: "power",
18842
19034
  defaultAggregate: "sum",
18843
19035
  empty: "omit",
18844
- companions: [NOW]
19036
+ companions: [NOW$1]
18845
19037
  }),
18846
19038
  numeric({
18847
19039
  id: "energy",
@@ -18852,7 +19044,7 @@ var GROUP_TEMPLATES = [
18852
19044
  quantity: "energy",
18853
19045
  defaultAggregate: "sum",
18854
19046
  empty: "omit",
18855
- companions: [NOW]
19047
+ companions: [NOW$1]
18856
19048
  }),
18857
19049
  numeric({
18858
19050
  id: "battery",
@@ -18887,7 +19079,7 @@ var GROUP_TEMPLATES = [
18887
19079
  quantity: "device",
18888
19080
  defaultAggregate: "mean",
18889
19081
  empty: "null",
18890
- companions: [NOW]
19082
+ companions: [NOW$1]
18891
19083
  }),
18892
19084
  binary({
18893
19085
  id: "motion",
@@ -19016,6 +19208,7 @@ function actuator(input) {
19016
19208
  }
19017
19209
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19018
19210
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19211
+ DeviceType.Camera;
19019
19212
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
19020
19213
  /**
19021
19214
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -44581,57 +44774,79 @@ var ProbeTargetConfigResultSchema = object({
44581
44774
  stateInstance: string().optional(),
44582
44775
  error: string().optional()
44583
44776
  });
44584
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44585
- kind: string(),
44586
- config: record(string(), unknown()).optional(),
44587
- /**
44588
- * When editing / duplicating, the blank password field never round-trips
44589
- * the stored token. Pass the source target id so the provider can restore
44590
- * secrets server-side before dialling (same merge as upsert).
44591
- */
44592
- targetId: string().optional()
44593
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44594
- kind: string(),
44595
- config: record(string(), unknown()),
44596
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44597
- targetId: string().optional()
44598
- }), ProbeTargetConfigResultSchema, {
44599
- kind: "mutation",
44600
- auth: "admin"
44601
- }), method(object({
44602
- targetId: string(),
44603
- notification: NotificationSchema
44604
- }), SendResultSchema, {
44605
- kind: "mutation",
44606
- auth: "admin"
44607
- }), method(object({
44608
- targetId: string(),
44609
- sample: NotificationSchema.optional()
44610
- }), TestResultSchema, {
44611
- kind: "mutation",
44612
- auth: "admin"
44613
- }), method(object({
44614
- target: TargetSchema,
44615
- /**
44616
- * Duplicate flow: create a NEW target id while copying secrets from an
44617
- * existing row. The UI never holds the stored token; blank / redacted
44618
- * password fields are restored from this source (D634 still applies if
44619
- * destinations diverge without a re-typed secret).
44620
- */
44621
- cloneSecretsFrom: string().optional()
44622
- }), TargetSchema, {
44623
- kind: "mutation",
44624
- auth: "admin"
44625
- }), method(object({ targetId: string() }), _void(), {
44626
- kind: "mutation",
44627
- auth: "admin"
44628
- }), method(object({
44629
- targetId: string(),
44630
- enabled: boolean()
44631
- }), _void(), {
44632
- kind: "mutation",
44633
- auth: "admin"
44634
- });
44777
+ var notificationOutputCapability = {
44778
+ name: "notification-output",
44779
+ scope: "system",
44780
+ mode: "collection",
44781
+ methods: {
44782
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
44783
+ listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
44784
+ /**
44785
+ * One target WITH secrets — admin edit / duplicate hydration only.
44786
+ * `listTargets` stays redacted; the editor loads this so password fields
44787
+ * are filled (masked in the input) and a save can round-trip the token
44788
+ * when a destination changes (Duplicate with a new chatId, etc.).
44789
+ */
44790
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
44791
+ discoverTargets: method(object({
44792
+ kind: string(),
44793
+ config: record(string(), unknown()).optional(),
44794
+ /**
44795
+ * When editing / duplicating, the blank password field never round-trips
44796
+ * the stored token. Pass the source target id so the provider can restore
44797
+ * secrets server-side before dialling (same merge as upsert).
44798
+ */
44799
+ targetId: string().optional()
44800
+ }), array(DiscoveredTargetSchema), { auth: "admin" }),
44801
+ probeTargetConfig: method(object({
44802
+ kind: string(),
44803
+ config: record(string(), unknown()),
44804
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44805
+ targetId: string().optional()
44806
+ }), ProbeTargetConfigResultSchema, {
44807
+ kind: "mutation",
44808
+ auth: "admin"
44809
+ }),
44810
+ send: method(object({
44811
+ targetId: string(),
44812
+ notification: NotificationSchema
44813
+ }), SendResultSchema, {
44814
+ kind: "mutation",
44815
+ auth: "admin"
44816
+ }),
44817
+ testTarget: method(object({
44818
+ targetId: string(),
44819
+ sample: NotificationSchema.optional()
44820
+ }), TestResultSchema, {
44821
+ kind: "mutation",
44822
+ auth: "admin"
44823
+ }),
44824
+ upsertTarget: method(object({
44825
+ target: TargetSchema,
44826
+ /**
44827
+ * Duplicate flow: create a NEW target id while copying secrets from an
44828
+ * existing row. The UI never holds the stored token; blank / redacted
44829
+ * password fields are restored from this source (D634 still applies if
44830
+ * destinations diverge without a re-typed secret).
44831
+ */
44832
+ cloneSecretsFrom: string().optional()
44833
+ }), TargetSchema, {
44834
+ kind: "mutation",
44835
+ auth: "admin"
44836
+ }),
44837
+ deleteTarget: method(object({ targetId: string() }), _void(), {
44838
+ kind: "mutation",
44839
+ auth: "admin"
44840
+ }),
44841
+ setTargetEnabled: method(object({
44842
+ targetId: string(),
44843
+ enabled: boolean()
44844
+ }), _void(), {
44845
+ kind: "mutation",
44846
+ auth: "admin"
44847
+ })
44848
+ }
44849
+ };
44635
44850
  new Set([
44636
44851
  {
44637
44852
  id: "person",
@@ -61881,6 +62096,44 @@ var lockControlCapability = {
61881
62096
  volatileStateFields: ["lastChangedAt"]
61882
62097
  };
61883
62098
  /**
62099
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
62100
+ * so dashboards keep one series; every other instantaneous reading lands here.
62101
+ * `value` is the time integral in the source quantity's accumulated base
62102
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
62103
+ */
62104
+ var MeterStatusSchema = object({
62105
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
62106
+ lastFetchedAt: number()
62107
+ });
62108
+ var meterCapability = {
62109
+ name: "meter",
62110
+ scope: "device",
62111
+ deviceNative: true,
62112
+ mode: "singleton",
62113
+ deviceTypes: [DeviceType.Sensor],
62114
+ methods: {},
62115
+ status: {
62116
+ schema: MeterStatusSchema,
62117
+ kind: "push"
62118
+ },
62119
+ runtimeState: MeterStatusSchema,
62120
+ /**
62121
+ * Runtime-state durability: **restored** — the total is a cache of the
62122
+ * history integral. A restart shows it only until the history pass
62123
+ * recomputes the open period; it is not an in-memory buffer.
62124
+ *
62125
+ * See `RuntimeStateDurability`. Enforced by
62126
+ * `scripts/check-runtime-state-durability.ts`.
62127
+ */
62128
+ durability: "restored",
62129
+ history: { fields: { value: {
62130
+ kind: "counter",
62131
+ deadband: .001,
62132
+ minIntervalMs: 6e4
62133
+ } } },
62134
+ volatileStateFields: ["lastFetchedAt"]
62135
+ };
62136
+ /**
61884
62137
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61885
62138
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61886
62139
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -64407,7 +64660,12 @@ var powerMeterCapability = {
64407
64660
  scope: "device",
64408
64661
  deviceNative: true,
64409
64662
  mode: "singleton",
64410
- deviceTypes: [DeviceType.Sensor],
64663
+ deviceTypes: [
64664
+ DeviceType.Sensor,
64665
+ DeviceType.Washer,
64666
+ DeviceType.Dryer,
64667
+ DeviceType.Dishwasher
64668
+ ],
64411
64669
  methods: {},
64412
64670
  status: {
64413
64671
  schema: PowerMeterStatusSchema,
@@ -65145,6 +65403,8 @@ var runningCustomize = {
65145
65403
  stateField: "running"
65146
65404
  },
65147
65405
  deviceTypes: [
65406
+ DeviceType.Washer,
65407
+ DeviceType.Dryer,
65148
65408
  DeviceType.Dishwasher,
65149
65409
  DeviceType.Vacuum,
65150
65410
  DeviceType.LawnMower,
@@ -69848,7 +70108,7 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69848
70108
  auth: "admin"
69849
70109
  });
69850
70110
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69851
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
70111
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
69852
70112
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69853
70113
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69854
70114
  new Set(CAP_NAMES);
@@ -70188,6 +70448,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
70188
70448
  lawnMowerControl: lawnMowerControlCapability,
70189
70449
  lockControl: lockControlCapability,
70190
70450
  mediaPlayer: mediaPlayerCapability,
70451
+ meter: meterCapability,
70191
70452
  motion: motionCapability,
70192
70453
  motionTrigger: motionTriggerCapability,
70193
70454
  motionZones: motionZonesCapability,
@@ -71447,7 +71708,7 @@ var BaseDeviceProvider = class extends BaseAddon {
71447
71708
  return toDeviceSummary(device, this.addonId);
71448
71709
  }
71449
71710
  };
71450
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
71711
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
71451
71712
  new Set(Object.values(DeviceType));
71452
71713
  Object.freeze({
71453
71714
  "accessories.getStatus": {
@@ -75636,6 +75897,13 @@ Object.freeze({
75636
75897
  access: "create",
75637
75898
  role: "admin"
75638
75899
  },
75900
+ "meter.getStatus": {
75901
+ capName: "meter",
75902
+ capScope: "device",
75903
+ addonId: null,
75904
+ access: "view",
75905
+ role: "viewer"
75906
+ },
75639
75907
  "metricsProvider.collectSnapshot": {
75640
75908
  capName: "metrics-provider",
75641
75909
  capScope: "system",
@@ -83734,7 +84002,39 @@ var APPLIANCE_CAP_VOCABULARY = {
83734
84002
  phrase: t("When {device} finishes", "Quando {device} finisce"),
83735
84003
  icon: "square"
83736
84004
  }
83737
- }
84005
+ },
84006
+ overrides: { byType: {
84007
+ [DeviceType.Washer]: {
84008
+ state: { running: {
84009
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
84010
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
84011
+ } },
84012
+ events: {
84013
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
84014
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
84015
+ }
84016
+ },
84017
+ [DeviceType.Dryer]: {
84018
+ state: { running: {
84019
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
84020
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
84021
+ } },
84022
+ events: {
84023
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
84024
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
84025
+ }
84026
+ },
84027
+ [DeviceType.Dishwasher]: {
84028
+ state: { running: {
84029
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
84030
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
84031
+ } },
84032
+ events: {
84033
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
84034
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
84035
+ }
84036
+ }
84037
+ } }
83738
84038
  },
83739
84039
  update: {
83740
84040
  scope: "device",
@@ -84562,6 +84862,17 @@ var SENSOR_CAP_VOCABULARY = {
84562
84862
  }],
84563
84863
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
84564
84864
  },
84865
+ meter: {
84866
+ scope: "device",
84867
+ noun: t("Meter", "Contatore"),
84868
+ state: [{
84869
+ kind: "numeric",
84870
+ field: "value",
84871
+ quantity: t("the total", "il totale"),
84872
+ label: t("Total", "Totale"),
84873
+ icon: "gauge"
84874
+ }]
84875
+ },
84565
84876
  "numeric-sensor": {
84566
84877
  scope: "device",
84567
84878
  noun: t("Sensor", "Sensore"),
@@ -85034,8 +85345,47 @@ function projectAction(cap, action) {
85034
85345
  };
85035
85346
  }
85036
85347
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
85037
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
85348
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
85038
85349
  DeviceFeature.BatteryOperated;
85350
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85351
+ var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
85352
+ var HOMEKIT_REASON = "HomeKit has no laundry-appliance service. The export tab would pair an accessory with nothing on it: running-state is withheld until a null reading can be unavailable, and power is not a HomeKit service this export honours.";
85353
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85354
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85355
+ var MATTER_REASON = "No Matter export is built. The closest device types are Laundry Washer (0x0073) and Laundry Dryer (0x0074); a power threshold is not the Laundry Washer Mode cluster.";
85356
+ function withheld(reason) {
85357
+ return {
85358
+ [DeviceType.Washer]: {
85359
+ exported: false,
85360
+ reason
85361
+ },
85362
+ [DeviceType.Dryer]: {
85363
+ exported: false,
85364
+ reason
85365
+ },
85366
+ [DeviceType.Dishwasher]: {
85367
+ exported: false,
85368
+ reason
85369
+ }
85370
+ };
85371
+ }
85372
+ function offered(received) {
85373
+ return {
85374
+ [DeviceType.Washer]: {
85375
+ exported: true,
85376
+ as: received
85377
+ },
85378
+ [DeviceType.Dryer]: {
85379
+ exported: true,
85380
+ as: received
85381
+ },
85382
+ [DeviceType.Dishwasher]: {
85383
+ exported: true,
85384
+ as: received
85385
+ }
85386
+ };
85387
+ }
85388
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
85039
85389
  Object.freeze({
85040
85390
  "accessories.getStatus": [{
85041
85391
  name: "deviceId",
@@ -86212,6 +86562,11 @@ Object.freeze({
86212
86562
  form: "single",
86213
86563
  optional: false
86214
86564
  }],
86565
+ "meter.getStatus": [{
86566
+ name: "deviceId",
86567
+ form: "single",
86568
+ optional: false
86569
+ }],
86215
86570
  "motion.getStatus": [{
86216
86571
  name: "deviceId",
86217
86572
  form: "single",
@@ -87819,7 +88174,6 @@ Object.freeze({
87819
88174
  "network-access": "ingress",
87820
88175
  "smtp-provider": "email"
87821
88176
  });
87822
- DeviceType.Camera;
87823
88177
  /**
87824
88178
  * A saved Customize assignment (D936). The schema is the type: callers use
87825
88179
  * `z.infer`, they do not restate the shape.
@@ -87965,6 +88319,213 @@ var NC_AUDIO_DEFAULTS = {
87965
88319
  samplingSeconds: 10
87966
88320
  };
87967
88321
  new Set(["devices", "classes"]);
88322
+ /** Strip a small, safe subset of Markdown down to plain text. */
88323
+ function markdownToText(md) {
88324
+ return md.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]*)`/g, "$1").replace(/^\s{0,3}#{1,6}\s*/gm, "").replace(/^\s{0,3}>\s?/gm, "").replace(/^\s{0,3}[-*+]\s+/gm, "").replace(/^\s{0,3}\d+\.\s+/gm, "").trim();
88325
+ }
88326
+ var HTML_ENTITIES = [
88327
+ [/&nbsp;/g, " "],
88328
+ [/&amp;/g, "&"],
88329
+ [/&lt;/g, "<"],
88330
+ [/&gt;/g, ">"],
88331
+ [/&quot;/g, "\""],
88332
+ [/&#39;/g, "'"]
88333
+ ];
88334
+ /** Strip HTML tags, decode a few entities, map block/br elements to newlines. */
88335
+ function htmlToText(html) {
88336
+ let out = html.replace(/<\s*br\s*\/?\s*>/gi, "\n").replace(/<\s*\/\s*(p|div|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]+>/g, "");
88337
+ for (const [re, rep] of HTML_ENTITIES) out = out.replace(re, rep);
88338
+ return out.replace(/\n{3,}/g, "\n\n").trim();
88339
+ }
88340
+ var HTML_ESCAPES = [
88341
+ [/&/g, "&amp;"],
88342
+ [/</g, "&lt;"],
88343
+ [/>/g, "&gt;"],
88344
+ [/"/g, "&quot;"],
88345
+ [/'/g, "&#39;"]
88346
+ ];
88347
+ function escapeHtml(text) {
88348
+ let out = text;
88349
+ for (const [re, rep] of HTML_ESCAPES) out = out.replace(re, rep);
88350
+ return out;
88351
+ }
88352
+ /** Plain text → HTML-lite (escape + newlines to <br>). */
88353
+ function textToHtml(text) {
88354
+ return escapeHtml(text).replace(/\n/g, "<br>");
88355
+ }
88356
+ /** Markdown → HTML-lite (bold/italic/code/links; used e.g. for Pushover). */
88357
+ function markdownToHtmlLite(md) {
88358
+ return escapeHtml(md).replace(/(\*\*|__)(.*?)\1/g, "<b>$2</b>").replace(/(\*|_)(.*?)\1/g, "<i>$2</i>").replace(/`([^`]*)`/g, "<code>$1</code>").replace(/\[([^\]]*)\]\(([^)]*)\)/g, "<a href=\"$2\">$1</a>").replace(/\n/g, "<br>");
88359
+ }
88360
+ /**
88361
+ * Preference order when the source format is NOT supported — first supported
88362
+ * entry wins. Encodes the Apprise-style "degrade DOWN, but preserve rendering
88363
+ * where a richer target exists" policy:
88364
+ * - markdown → html (html-lite) before text (strip)
88365
+ * - html → text before markdown
88366
+ * - text → markdown before html (text is valid markdown)
88367
+ */
88368
+ var DEGRADE_ORDER = {
88369
+ text: [
88370
+ "text",
88371
+ "markdown",
88372
+ "html"
88373
+ ],
88374
+ markdown: [
88375
+ "markdown",
88376
+ "html",
88377
+ "text"
88378
+ ],
88379
+ html: [
88380
+ "html",
88381
+ "text",
88382
+ "markdown"
88383
+ ]
88384
+ };
88385
+ /**
88386
+ * Pick the best target format the kind supports for a given source format.
88387
+ *
88388
+ * TOLERANT of a source outside the union. `NotificationSchema.format` carries
88389
+ * `.default('text')`, but that default only runs where the schema is PARSED —
88390
+ * the tRPC router. A cap call arriving from another addon (`ctx.api`) skips
88391
+ * that parse, so `format` reaches here `undefined`, and the bare
88392
+ * `DEGRADE_ORDER[source]` threw `is not iterable`. That broke the one promise
88393
+ * the degrade path makes ("never throws — degradation is reported, never
88394
+ * surfaced as an error") and dead-lettered every Notification-Center delivery
88395
+ * for a day (2026-07-30). An unknown source degrades like `text`.
88396
+ */
88397
+ function resolveFormat(source, supported) {
88398
+ for (const candidate of DEGRADE_ORDER[source] ?? DEGRADE_ORDER.text) if (supported.includes(candidate)) return candidate;
88399
+ return supported[0] ?? "text";
88400
+ }
88401
+ /** Transcode a body between formats. */
88402
+ function transcodeBody(body, from, to) {
88403
+ if (from === to) return body;
88404
+ if (to === "text") {
88405
+ if (from === "markdown") return markdownToText(body);
88406
+ if (from === "html") return htmlToText(body);
88407
+ }
88408
+ if (to === "html") {
88409
+ if (from === "text") return textToHtml(body);
88410
+ if (from === "markdown") return markdownToHtmlLite(body);
88411
+ }
88412
+ if (to === "markdown") {
88413
+ if (from === "text") return body;
88414
+ if (from === "html") return htmlToText(body);
88415
+ }
88416
+ return body;
88417
+ }
88418
+ function toResolvedLevel(level) {
88419
+ return {
88420
+ id: level.id,
88421
+ ordinal: level.ordinal,
88422
+ ...level.flags ? { flags: level.flags } : {},
88423
+ ...level.requires ? { requires: level.requires } : {}
88424
+ };
88425
+ }
88426
+ function resolveLevel(levels, n) {
88427
+ if (levels.length === 0) return null;
88428
+ if (n.level !== void 0) {
88429
+ const explicit = levels.find((l) => l.id === n.level);
88430
+ if (explicit) return toResolvedLevel(explicit);
88431
+ }
88432
+ const withOrdinal = levels.filter((l) => l.ordinal !== null);
88433
+ if (withOrdinal.length === 0) return toResolvedLevel(levels[0]);
88434
+ const priority = n.priority;
88435
+ return toResolvedLevel([...withOrdinal].sort((a, b) => {
88436
+ const da = Math.abs((a.ordinal ?? 0) - priority);
88437
+ const db = Math.abs((b.ordinal ?? 0) - priority);
88438
+ if (da !== db) return da - db;
88439
+ return (b.ordinal ?? 0) - (a.ordinal ?? 0);
88440
+ })[0]);
88441
+ }
88442
+ function splitBody(body, maxLen) {
88443
+ if (!Number.isFinite(maxLen) || maxLen <= 0 || body.length <= maxLen) return [body];
88444
+ const parts = [];
88445
+ for (let i = 0; i < body.length; i += maxLen) parts.push(body.slice(i, i + maxLen));
88446
+ return parts;
88447
+ }
88448
+ /**
88449
+ * Prepare a canonical notification for one kind. Never throws — degradation is
88450
+ * always reported in `renderedAs`, never surfaced as an error.
88451
+ */
88452
+ function prepareNotification(caps, n) {
88453
+ const dropped = [];
88454
+ const sourceFormat = n.format;
88455
+ const format = resolveFormat(sourceFormat, caps.format);
88456
+ if (format !== sourceFormat) dropped.push("format");
88457
+ const transcoded = transcodeBody(n.body, sourceFormat, format);
88458
+ const level = resolveLevel(caps.levels, n);
88459
+ const inAttachments = n.attachments ?? [];
88460
+ const attachments = [];
88461
+ for (const att of inAttachments) {
88462
+ if (!caps.attachments.mediaTypes.includes(att.mediaType)) {
88463
+ dropped.push(`attachment:${att.mediaType}`);
88464
+ continue;
88465
+ }
88466
+ if (caps.attachments.mode === "url" && att.url === void 0) {
88467
+ dropped.push("attachment:noUrl");
88468
+ continue;
88469
+ }
88470
+ if (att.bytes !== void 0 && caps.attachments.maxBytes !== void 0 && att.bytes.length > caps.attachments.maxBytes) {
88471
+ dropped.push("attachment:maxBytes");
88472
+ continue;
88473
+ }
88474
+ if (attachments.length >= caps.attachments.max) {
88475
+ dropped.push("attachment:max");
88476
+ continue;
88477
+ }
88478
+ const needsFetch = caps.attachments.mode === "bytes" && att.bytes === void 0 && att.url !== void 0;
88479
+ attachments.push({
88480
+ mediaType: att.mediaType,
88481
+ ...att.url !== void 0 ? { url: att.url } : {},
88482
+ ...att.bytes !== void 0 ? { bytes: att.bytes } : {},
88483
+ ...att.mime !== void 0 ? { mime: att.mime } : {},
88484
+ ...att.name !== void 0 ? { name: att.name } : {},
88485
+ needsFetch
88486
+ });
88487
+ }
88488
+ const inActions = n.actions ?? [];
88489
+ const kept = inActions.slice(0, Math.max(0, caps.actions));
88490
+ if (inActions.length > kept.length) dropped.push("actions");
88491
+ const iconsSupported = caps.actionIcons === true;
88492
+ if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
88493
+ const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
88494
+ let clickUrl = null;
88495
+ if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
88496
+ else dropped.push("clickUrl");
88497
+ let sound = null;
88498
+ if (n.sound !== void 0) if (caps.sound) sound = n.sound;
88499
+ else dropped.push("sound");
88500
+ let ttl = null;
88501
+ if (n.ttl !== void 0) if (caps.ttl) ttl = n.ttl;
88502
+ else dropped.push("ttl");
88503
+ const tag = n.tag ?? null;
88504
+ const bodyParts = splitBody(transcoded, caps.bodyMaxLen);
88505
+ const truncated = bodyParts.length > 1;
88506
+ const renderedAs = {
88507
+ level: level?.id ?? "",
88508
+ format,
88509
+ attachmentsSent: attachments.length,
88510
+ actionsSent: actions.length,
88511
+ truncated,
88512
+ dropped
88513
+ };
88514
+ return {
88515
+ body: bodyParts[0] ?? "",
88516
+ ...n.title !== void 0 ? { title: n.title } : {},
88517
+ format,
88518
+ level,
88519
+ attachments,
88520
+ actions,
88521
+ clickUrl,
88522
+ sound,
88523
+ ttl,
88524
+ tag,
88525
+ bodyParts,
88526
+ renderedAs
88527
+ };
88528
+ }
87968
88529
  new Set(NC_LEGACY_SYSTEM_EVENT_KINDS);
87969
88530
  /**
87970
88531
  * Every filter, for the callers that must cover all of them (guards, tests).
@@ -89186,12 +89747,55 @@ var alexaProxyLoginSchema = object({
89186
89747
  proxyHost: string().default("").describe("Host/IP the browser uses to reach the login proxy (blank = auto-detect)"),
89187
89748
  proxyPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(3456)).describe("Port the local Amazon-login proxy listens on (must be reachable)")
89188
89749
  });
89750
+ /** Amazon account used to speak on Echo devices. The session blob is the cookie jar. */
89751
+ var alexaSpeakSettingsSchema = object({
89752
+ email: string().default(""),
89753
+ password: string().default("").describe("Amazon password").meta({ secret: true }),
89754
+ region: _enum([
89755
+ "NA",
89756
+ "EU",
89757
+ "FE"
89758
+ ]).default("EU"),
89759
+ session: string().default("").describe("Alexa session blob").meta({ secret: true }),
89760
+ targets: array(object({
89761
+ id: string(),
89762
+ name: string(),
89763
+ enabled: boolean(),
89764
+ echoId: string(),
89765
+ mode: _enum(["speak", "announce"]),
89766
+ volume: number(),
89767
+ respectDnd: boolean()
89768
+ })).default([])
89769
+ });
89770
+ function isRecord$2(value) {
89771
+ return typeof value === "object" && value !== null && !Array.isArray(value);
89772
+ }
89773
+ /** Resolved settings. A keychain marker (an object) is an empty secret, not a password. */
89774
+ function readSpeakSettings(config) {
89775
+ const raw = isRecord$2(config.speak) ? config.speak : {};
89776
+ const region = raw["region"];
89777
+ const targets = raw["targets"];
89778
+ return alexaSpeakSettingsSchema.parse({
89779
+ email: typeof raw["email"] === "string" ? raw["email"] : "",
89780
+ password: typeof raw["password"] === "string" ? raw["password"] : "",
89781
+ session: typeof raw["session"] === "string" ? raw["session"] : "",
89782
+ ...region === "NA" || region === "EU" || region === "FE" ? { region } : {},
89783
+ ...Array.isArray(targets) ? { targets } : {}
89784
+ });
89785
+ }
89189
89786
  /** Top-level addon config — broker entries + the proxy-login settings. */
89190
89787
  var alexaAddonConfigSchema = object({
89191
89788
  brokers: array(AlexaBrokerEntrySchema).default([]),
89192
89789
  proxyLogin: alexaProxyLoginSchema.default({
89193
89790
  proxyHost: "",
89194
89791
  proxyPort: 3456
89792
+ }),
89793
+ speak: alexaSpeakSettingsSchema.default({
89794
+ email: "",
89795
+ password: "",
89796
+ region: "EU",
89797
+ session: "",
89798
+ targets: []
89195
89799
  })
89196
89800
  });
89197
89801
  /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
@@ -112965,17 +113569,17 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
112965
113569
  * @private
112966
113570
  */
112967
113571
  var debug = require_src$2()("express:view");
112968
- var path$7 = __require("path");
112969
- var fs$3 = __require("fs");
113572
+ var path$8 = __require("path");
113573
+ var fs$5 = __require("fs");
112970
113574
  /**
112971
113575
  * Module variables.
112972
113576
  * @private
112973
113577
  */
112974
- var dirname = path$7.dirname;
112975
- var basename = path$7.basename;
112976
- var extname = path$7.extname;
112977
- var join = path$7.join;
112978
- var resolve = path$7.resolve;
113578
+ var dirname = path$8.dirname;
113579
+ var basename = path$8.basename;
113580
+ var extname = path$8.extname;
113581
+ var join = path$8.join;
113582
+ var resolve = path$8.resolve;
112979
113583
  /**
112980
113584
  * Module exports.
112981
113585
  * @public
@@ -113072,7 +113676,7 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
113072
113676
  function tryStat(path) {
113073
113677
  debug("stat \"%s\"", path);
113074
113678
  try {
113075
- return fs$3.statSync(path);
113679
+ return fs$5.statSync(path);
113076
113680
  } catch (e) {
113077
113681
  return;
113078
113682
  }
@@ -115242,7 +115846,7 @@ var init_types = __esmMin((() => {
115242
115846
  //#region ../../node_modules/send/node_modules/mime/mime.js
115243
115847
  var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
115244
115848
  __require("path");
115245
- var fs$2 = __require("fs");
115849
+ var fs$4 = __require("fs");
115246
115850
  function Mime() {
115247
115851
  this.types = Object.create(null);
115248
115852
  this.extensions = Object.create(null);
@@ -115277,7 +115881,7 @@ var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
115277
115881
  Mime.prototype.load = function(file) {
115278
115882
  this._loading = file;
115279
115883
  var map = {};
115280
- fs$2.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
115884
+ fs$4.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
115281
115885
  var fields = line.replace(/\s*#.*|^\s*|\s*$/g, "").split(/\s+/);
115282
115886
  map[fields.shift()] = fields;
115283
115887
  });
@@ -115553,12 +116157,12 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
115553
116157
  var escapeHtml = require_escape_html();
115554
116158
  var etag = require_etag();
115555
116159
  var fresh = require_fresh();
115556
- var fs$1 = __require("fs");
116160
+ var fs$3 = __require("fs");
115557
116161
  var mime = require_mime();
115558
116162
  var ms = require_ms();
115559
116163
  var onFinished = require_on_finished();
115560
116164
  var parseRange = require_range_parser();
115561
- var path$6 = __require("path");
116165
+ var path$7 = __require("path");
115562
116166
  var statuses = require_statuses();
115563
116167
  var Stream = __require("stream");
115564
116168
  var util$4 = __require("util");
@@ -115566,11 +116170,11 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
115566
116170
  * Path function references.
115567
116171
  * @private
115568
116172
  */
115569
- var extname = path$6.extname;
115570
- var join = path$6.join;
115571
- var normalize = path$6.normalize;
115572
- var resolve = path$6.resolve;
115573
- var sep = path$6.sep;
116173
+ var extname = path$7.extname;
116174
+ var join = path$7.join;
116175
+ var normalize = path$7.normalize;
116176
+ var resolve = path$7.resolve;
116177
+ var sep = path$7.sep;
115574
116178
  /**
115575
116179
  * Regular expression for identifying a bytes Range header.
115576
116180
  * @private
@@ -116018,7 +116622,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
116018
116622
  var i = 0;
116019
116623
  var self = this;
116020
116624
  debug("stat \"%s\"", path);
116021
- fs$1.stat(path, function onstat(err, stat) {
116625
+ fs$3.stat(path, function onstat(err, stat) {
116022
116626
  if (err && err.code === "ENOENT" && !extname(path) && path[path.length - 1] !== sep) return next(err);
116023
116627
  if (err) return self.onStatError(err);
116024
116628
  if (stat.isDirectory()) return self.redirect(path);
@@ -116029,7 +116633,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
116029
116633
  if (self._extensions.length <= i) return err ? self.onStatError(err) : self.error(404);
116030
116634
  var p = path + "." + self._extensions[i++];
116031
116635
  debug("stat \"%s\"", p);
116032
- fs$1.stat(p, function(err, stat) {
116636
+ fs$3.stat(p, function(err, stat) {
116033
116637
  if (err) return next(err);
116034
116638
  if (stat.isDirectory()) return next();
116035
116639
  self.emit("file", p, stat);
@@ -116053,7 +116657,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
116053
116657
  }
116054
116658
  var p = join(path, self._index[i]);
116055
116659
  debug("stat \"%s\"", p);
116056
- fs$1.stat(p, function(err, stat) {
116660
+ fs$3.stat(p, function(err, stat) {
116057
116661
  if (err) return next(err);
116058
116662
  if (stat.isDirectory()) return next();
116059
116663
  self.emit("file", p, stat);
@@ -116072,7 +116676,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
116072
116676
  SendStream.prototype.stream = function stream(path, options) {
116073
116677
  var self = this;
116074
116678
  var res = this.res;
116075
- var stream = fs$1.createReadStream(path, options);
116679
+ var stream = fs$3.createReadStream(path, options);
116076
116680
  this.emit("stream", stream);
116077
116681
  stream.pipe(res);
116078
116682
  function cleanup() {
@@ -117512,7 +118116,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
117512
118116
  var deprecate = require_depd()("express");
117513
118117
  var flatten = require_array_flatten();
117514
118118
  var merge = require_utils_merge();
117515
- var resolve$1 = __require("path").resolve;
118119
+ var resolve$2 = __require("path").resolve;
117516
118120
  var setPrototypeOf = require_setprototypeof();
117517
118121
  /**
117518
118122
  * Module variables.
@@ -117575,7 +118179,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
117575
118179
  this.mountpath = "/";
117576
118180
  this.locals.settings = this.settings;
117577
118181
  this.set("view", View);
117578
- this.set("views", resolve$1("views"));
118182
+ this.set("views", resolve$2("views"));
117579
118183
  this.set("jsonp callback name", "callback");
117580
118184
  if (env === "production") this.enable("view cache");
117581
118185
  Object.defineProperty(this, "router", { get: function() {
@@ -119593,7 +120197,7 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
119593
120197
  var http$3 = __require("http");
119594
120198
  var isAbsolute = require_utils$2().isAbsolute;
119595
120199
  var onFinished = require_on_finished();
119596
- var path$5 = __require("path");
120200
+ var path$6 = __require("path");
119597
120201
  var statuses = require_statuses();
119598
120202
  var merge = require_utils_merge();
119599
120203
  var sign = require_cookie_signature().sign;
@@ -119602,9 +120206,9 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
119602
120206
  var setCharset = require_utils$2().setCharset;
119603
120207
  var cookie = require_cookie();
119604
120208
  var send = require_send();
119605
- var extname = path$5.extname;
120209
+ var extname = path$6.extname;
119606
120210
  var mime = send.mime;
119607
- var resolve = path$5.resolve;
120211
+ var resolve = path$6.resolve;
119608
120212
  var vary = require_vary();
119609
120213
  /**
119610
120214
  * Response prototype.
@@ -120436,7 +121040,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
120436
121040
  var encodeUrl = require_encodeurl();
120437
121041
  var escapeHtml = require_escape_html();
120438
121042
  var parseUrl = require_parseurl();
120439
- var resolve = __require("path").resolve;
121043
+ var resolve$1 = __require("path").resolve;
120440
121044
  var send = require_send();
120441
121045
  var url$6 = __require("url");
120442
121046
  /**
@@ -120460,7 +121064,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
120460
121064
  var setHeaders = opts.setHeaders;
120461
121065
  if (setHeaders && typeof setHeaders !== "function") throw new TypeError("option setHeaders must be function");
120462
121066
  opts.maxage = opts.maxage || opts.maxAge || 0;
120463
- opts.root = resolve(root);
121067
+ opts.root = resolve$1(root);
120464
121068
  var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
120465
121069
  return function serveStatic(req, res, next) {
120466
121070
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -121586,7 +122190,7 @@ var require_has_flag = /* @__PURE__ */ __commonJSMin(((exports, module) => {
121586
122190
  //#endregion
121587
122191
  //#region ../../node_modules/supports-color/index.js
121588
122192
  var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) => {
121589
- var os$2 = __require("os");
122193
+ var os$3 = __require("os");
121590
122194
  var tty$1 = __require("tty");
121591
122195
  var hasFlag = require_has_flag();
121592
122196
  var { env } = process;
@@ -121622,7 +122226,7 @@ var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) =>
121622
122226
  const min = forceColor || 0;
121623
122227
  if (env.TERM === "dumb") return min;
121624
122228
  if (process.platform === "win32") {
121625
- const osRelease = os$2.release().split(".");
122229
+ const osRelease = os$3.release().split(".");
121626
122230
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
121627
122231
  return 1;
121628
122232
  }
@@ -124129,7 +124733,7 @@ var require_braces = /* @__PURE__ */ __commonJSMin(((exports, module) => {
124129
124733
  //#endregion
124130
124734
  //#region ../../node_modules/micromatch/node_modules/picomatch/lib/constants.js
124131
124735
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
124132
- var path$4 = __require("path");
124736
+ var path$5 = __require("path");
124133
124737
  var WIN_SLASH = "\\\\/";
124134
124738
  var WIN_NO_SLASH = `[^${WIN_SLASH}]`;
124135
124739
  var DEFAULT_MAX_EXTGLOB_RECURSION = 0;
@@ -124254,7 +124858,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
124254
124858
  CHAR_UNDERSCORE: 95,
124255
124859
  CHAR_VERTICAL_LINE: 124,
124256
124860
  CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279,
124257
- SEP: path$4.sep,
124861
+ SEP: path$5.sep,
124258
124862
  /**
124259
124863
  * Create EXTGLOB_CHARS
124260
124864
  */
@@ -124298,7 +124902,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
124298
124902
  //#endregion
124299
124903
  //#region ../../node_modules/micromatch/node_modules/picomatch/lib/utils.js
124300
124904
  var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
124301
- var path$3 = __require("path");
124905
+ var path$4 = __require("path");
124302
124906
  var win32 = process.platform === "win32";
124303
124907
  var { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = require_constants();
124304
124908
  exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
@@ -124318,7 +124922,7 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
124318
124922
  };
124319
124923
  exports.isWindows = (options) => {
124320
124924
  if (options && typeof options.windows === "boolean") return options.windows;
124321
- return win32 === true || path$3.sep === "\\";
124925
+ return win32 === true || path$4.sep === "\\";
124322
124926
  };
124323
124927
  exports.escapeLast = (input, char, lastIdx) => {
124324
124928
  const idx = input.lastIndexOf(char, lastIdx);
@@ -125688,7 +126292,7 @@ var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
125688
126292
  //#endregion
125689
126293
  //#region ../../node_modules/micromatch/node_modules/picomatch/lib/picomatch.js
125690
126294
  var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
125691
- var path$2 = __require("path");
126295
+ var path$3 = __require("path");
125692
126296
  var scan = require_scan();
125693
126297
  var parse = require_parse();
125694
126298
  var utils = require_utils();
@@ -125827,7 +126431,7 @@ var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
125827
126431
  * @api public
125828
126432
  */
125829
126433
  picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => {
125830
- return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$2.basename(input));
126434
+ return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$3.basename(input));
125831
126435
  };
125832
126436
  /**
125833
126437
  * Returns true if **any** of the given glob `patterns` match the specified `string`.
@@ -126912,8 +127516,8 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
126912
127516
  var proxy = require_dist().createProxyMiddleware;
126913
127517
  var querystring$2 = __require("querystring");
126914
127518
  var cookieTools = require_cookie$1();
126915
- var fs = __require("fs");
126916
- var path$1 = __require("path");
127519
+ var fs$2 = __require("fs");
127520
+ var path$2 = __require("path");
126917
127521
  var crypto$1 = __require("crypto");
126918
127522
  var FORMERDATA_STORE_VERSION = 4;
126919
127523
  function addCookies(Cookie, headers) {
@@ -126944,11 +127548,11 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
126944
127548
  }
126945
127549
  function initAmazonProxy(_options, callbackCookie, callbackListening) {
126946
127550
  const initialCookies = {};
126947
- const formerDataStorePath = _options.formerDataStorePath || path$1.join(__dirname, "formerDataStore.json");
127551
+ const formerDataStorePath = _options.formerDataStorePath || path$2.join(__dirname, "formerDataStore.json");
126948
127552
  let formerDataStoreValid = false;
126949
127553
  if (!_options.formerRegistrationData) try {
126950
- if (fs.existsSync(formerDataStorePath)) {
126951
- const formerDataStore = JSON.parse(fs.readFileSync(path$1.join(__dirname, "formerDataStore.json"), "utf8"));
127554
+ if (fs$2.existsSync(formerDataStorePath)) {
127555
+ const formerDataStore = JSON.parse(fs$2.readFileSync(path$2.join(__dirname, "formerDataStore.json"), "utf8"));
126952
127556
  if (typeof formerDataStore === "object" && formerDataStore.storeVersion === FORMERDATA_STORE_VERSION) {
126953
127557
  _options.formerRegistrationData = _options.formerRegistrationData || {};
126954
127558
  _options.formerRegistrationData.frc = _options.formerRegistrationData.frc || formerDataStore.frc;
@@ -126989,7 +127593,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
126989
127593
  "map-md": initialCookies["map-md"],
126990
127594
  "frc": initialCookies.frc
126991
127595
  };
126992
- fs.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
127596
+ fs$2.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
126993
127597
  } catch (_err) {}
126994
127598
  function base64URLEncode(str) {
126995
127599
  return str.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
@@ -127199,7 +127803,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
127199
127803
  var https$1 = __require("https");
127200
127804
  var querystring$1 = __require("querystring");
127201
127805
  var url = __require("url");
127202
- var os$1 = __require("os");
127806
+ var os$2 = __require("os");
127203
127807
  var cookieTools = require_cookie$1();
127204
127808
  var amazonProxy = require_proxy();
127205
127809
  var defaultAmazonPage = "amazon.de";
@@ -127312,7 +127916,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
127312
127916
  else if (amazonDomain !== "com") _options.baseAmazonPageHandle = "";
127313
127917
  else _options.baseAmazonPageHandle = "";
127314
127918
  }
127315
- if (!_options.userAgent) if (os$1.platform() === "win32") _options.userAgent = defaultUserAgent;
127919
+ if (!_options.userAgent) if (os$2.platform() === "win32") _options.userAgent = defaultUserAgent;
127316
127920
  else _options.userAgent = defaultUserAgentLinux;
127317
127921
  _options.logger && _options.logger(`Alexa-Cookie: Use as User-Agent: ${_options.userAgent}`);
127318
127922
  _options.acceptLanguage = _options.acceptLanguage || defaultAcceptLanguage;
@@ -127836,7 +128440,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
127836
128440
  var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
127837
128441
  var https = __require("https");
127838
128442
  var querystring = __require("querystring");
127839
- var os = __require("os");
128443
+ var os$1 = __require("os");
127840
128444
  var extend = require_extend();
127841
128445
  var AlexaHttp2Push = require_alexa_http2push();
127842
128446
  var { v1: uuidv1 } = require_cjs();
@@ -127981,7 +128585,7 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
127981
128585
  init(cookie, callback) {
127982
128586
  if (typeof cookie === "object") {
127983
128587
  this._options = cookie;
127984
- if (!this._options.userAgent) if (os.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
128588
+ if (!this._options.userAgent) if (os$1.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
127985
128589
  else this._options.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
127986
128590
  if (this._options.apiUserAgentPostfix === void 0) this._options.apiUserAgentPostfix = `AlexaRemote/${__require(fsPath.join(__dirname, "package.json")).version}`;
127987
128591
  this._options.amazonPage = this._options.amazonPage || "amazon.de";
@@ -132679,13 +133283,1365 @@ var AlexaBrokerStore = class AlexaBrokerStore {
132679
133283
  }
132680
133284
  };
132681
133285
  //#endregion
133286
+ //#region src/speak/constants.ts
133287
+ /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
133288
+ var ALEXA_SPEAK_ADDON_ID = "import-alexa";
133289
+ var ALEXA_SPEAK_KIND = "alexa-echo";
133290
+ var DEVICE_REFRESH_MS = 5 * 6e4;
133291
+ var OTP_ALERT_ID = "import-alexa.speak-otp-required";
133292
+ var ALEXA_REGIONS = [
133293
+ "NA",
133294
+ "EU",
133295
+ "FE"
133296
+ ];
133297
+ //#endregion
133298
+ //#region src/speak/worker-protocol.ts
133299
+ /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
133300
+ var EchoDeviceSchema = object({
133301
+ id: string().min(1),
133302
+ name: string(),
133303
+ family: string(),
133304
+ online: boolean(),
133305
+ kind: _enum(["device", "group"]),
133306
+ dnd: boolean(),
133307
+ deviceType: string()
133308
+ });
133309
+ var LoginStatusSchema = _enum([
133310
+ "ok",
133311
+ "otp_needed",
133312
+ "captcha",
133313
+ "failed"
133314
+ ]);
133315
+ var SessionStatusSchema = object({
133316
+ loggedIn: boolean(),
133317
+ ageSec: number().int().nonnegative(),
133318
+ lastSuccessAt: number().int().nullable()
133319
+ });
133320
+ var TargetDeliverySchema = object({
133321
+ targetId: string(),
133322
+ ok: boolean(),
133323
+ error: string().optional()
133324
+ });
133325
+ var WorkerLineSchema = union([object({
133326
+ id: string(),
133327
+ ok: literal(true),
133328
+ result: unknown().optional()
133329
+ }), object({
133330
+ id: string(),
133331
+ ok: literal(false),
133332
+ error: string()
133333
+ })]);
133334
+ function requestLine(id, request) {
133335
+ return `${JSON.stringify({
133336
+ id,
133337
+ ...request
133338
+ })}\n`;
133339
+ }
133340
+ function parseLoginResult(result) {
133341
+ const parsed = object({
133342
+ status: LoginStatusSchema,
133343
+ error: string().optional()
133344
+ }).safeParse(result);
133345
+ if (!parsed.success) return {
133346
+ status: "failed",
133347
+ error: "worker returned an unexpected login result"
133348
+ };
133349
+ return parsed.data;
133350
+ }
133351
+ function parseDevices(result) {
133352
+ const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
133353
+ if (!parsed.success) throw new Error("worker returned an unexpected device list");
133354
+ return parsed.data.devices;
133355
+ }
133356
+ function parseDeliveries(result) {
133357
+ const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
133358
+ if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
133359
+ return parsed.data.outcomes;
133360
+ }
133361
+ function parseSessionStatus(result) {
133362
+ const parsed = SessionStatusSchema.safeParse(result);
133363
+ if (!parsed.success) throw new Error("worker returned an unexpected session status");
133364
+ return parsed.data;
133365
+ }
133366
+ //#endregion
133367
+ //#region src/speak/actions.ts
133368
+ var SpeakStatusSchema = object({
133369
+ phase: _enum([
133370
+ "sign-in",
133371
+ "otp",
133372
+ "captcha",
133373
+ "devices"
133374
+ ]),
133375
+ email: string(),
133376
+ region: _enum(ALEXA_REGIONS),
133377
+ devices: array(EchoDeviceSchema),
133378
+ session: SessionStatusSchema
133379
+ });
133380
+ var LoginResultSchema = object({
133381
+ status: LoginStatusSchema,
133382
+ error: string().optional()
133383
+ });
133384
+ var alexaSpeakActions = defineCustomActions({
133385
+ speakStatus: customAction(object({}), SpeakStatusSchema, {
133386
+ kind: "mutation",
133387
+ auth: "admin"
133388
+ }),
133389
+ loginStart: customAction(object({
133390
+ email: string().min(1),
133391
+ password: string().min(1).meta({ secret: true }),
133392
+ region: _enum(ALEXA_REGIONS)
133393
+ }), LoginResultSchema, {
133394
+ kind: "mutation",
133395
+ auth: "admin"
133396
+ }),
133397
+ loginOtp: customAction(object({ code: string().min(1).max(12) }), LoginResultSchema, {
133398
+ kind: "mutation",
133399
+ auth: "admin"
133400
+ }),
133401
+ sendTest: customAction(object({
133402
+ echoIds: array(string().min(1)).min(1),
133403
+ mode: _enum(["speak", "announce"]),
133404
+ volume: number().int().min(0).max(100),
133405
+ text: string().min(1).max(450)
133406
+ }), object({ outcomes: array(object({
133407
+ echoId: string(),
133408
+ ok: boolean(),
133409
+ error: string().optional()
133410
+ })) }), {
133411
+ kind: "mutation",
133412
+ auth: "admin"
133413
+ })
133414
+ });
133415
+ //#endregion
133416
+ //#region src/speak/otp-alert.ts
133417
+ /**
133418
+ * A login that needs a new one-time code raises ONE operator alert.
133419
+ * Further failures in the same episode do not raise another.
133420
+ */
133421
+ function buildOtpAlert(now, reason) {
133422
+ const otp = reason === "otp";
133423
+ return {
133424
+ id: OTP_ALERT_ID,
133425
+ category: EventCategory.AddonError,
133426
+ severity: "error",
133427
+ title: otp ? "Alexa needs a new sign-in code" : "Alexa sign-in needs a captcha",
133428
+ message: otp ? "Amazon asked for a one-time code before Echo devices can speak. Open Alexa → Speak on Echo and enter the code." : "Amazon asked for a captcha. Wait a few minutes, then sign in again on Alexa → Speak on Echo.",
133429
+ action: {
133430
+ label: "Sign in",
133431
+ href: "/addon/import-alexa",
133432
+ navigation: "app"
133433
+ },
133434
+ status: "active",
133435
+ read: false,
133436
+ createdAt: now,
133437
+ updatedAt: now,
133438
+ source: {
133439
+ type: "addon",
133440
+ id: "import-alexa"
133441
+ },
133442
+ metadata: { episodeKey: reason }
133443
+ };
133444
+ }
133445
+ var OtpAttention = class {
133446
+ ports;
133447
+ open = null;
133448
+ constructor(ports) {
133449
+ this.ports = ports;
133450
+ }
133451
+ /** First need in an episode emits the alert. A repeat does not. */
133452
+ async needed(reason) {
133453
+ if (this.open !== null) return "already-open";
133454
+ this.open = reason;
133455
+ await this.ports.emit(buildOtpAlert(this.ports.now(), reason));
133456
+ return "raised";
133457
+ }
133458
+ async cleared() {
133459
+ if (this.open === null) return;
133460
+ this.open = null;
133461
+ await this.ports.resolve(OTP_ALERT_ID);
133462
+ }
133463
+ };
133464
+ //#endregion
133465
+ //#region src/speak/rate-limit.ts
133466
+ /**
133467
+ * Per-Echo admission. A burst is dropped here, not queued: a queue would be
133468
+ * minutes of speech long after the event.
133469
+ */
133470
+ function emptySpeechLedger() {
133471
+ return { byEcho: {} };
133472
+ }
133473
+ /**
133474
+ * Identical text inside the collapse window is one utterance.
133475
+ * A different text inside the gap is refused until the gap elapses.
133476
+ * Admitted calls replace that Echo's memory; refusals leave it as it was.
133477
+ */
133478
+ function admitSpeech(ledger, echoId, text, now) {
133479
+ const previous = ledger.byEcho[echoId];
133480
+ if (previous !== void 0 && previous.lastText === text && now - previous.lastSpokenAt < 6e4) return {
133481
+ decision: "collapsed",
133482
+ ledger
133483
+ };
133484
+ if (previous !== void 0 && now - previous.lastSpokenAt < 2e4) return {
133485
+ decision: "rate_limited",
133486
+ ledger
133487
+ };
133488
+ return {
133489
+ decision: "admit",
133490
+ ledger: { byEcho: {
133491
+ ...ledger.byEcho,
133492
+ [echoId]: {
133493
+ lastSpokenAt: now,
133494
+ lastText: text
133495
+ }
133496
+ } }
133497
+ };
133498
+ }
133499
+ //#endregion
133500
+ //#region src/speak/speech-text.ts
133501
+ var ALEXA_SPEECH_CAPS = {
133502
+ attachments: {
133503
+ mediaTypes: [],
133504
+ mode: "url",
133505
+ max: 0
133506
+ },
133507
+ actions: 0,
133508
+ actionIcons: false,
133509
+ levels: [{
133510
+ id: "default",
133511
+ label: "Default",
133512
+ ordinal: 3
133513
+ }],
133514
+ format: ["text"],
133515
+ clickUrl: false,
133516
+ sound: false,
133517
+ ttl: false,
133518
+ bodyMaxLen: 450
133519
+ };
133520
+ function collapseWhitespace(value) {
133521
+ return value.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
133522
+ }
133523
+ /** Markup that survived the degrade engine must not be read aloud. */
133524
+ function toPlainSpeech(value) {
133525
+ return collapseWhitespace(htmlToText(markdownToText(value)));
133526
+ }
133527
+ /**
133528
+ * Title then body, as one sentence, never longer than {@link SPEECH_MAX_CHARS}.
133529
+ * An empty result is an empty string — the caller refuses to send it.
133530
+ */
133531
+ function speechFromNotification(notification) {
133532
+ const prepared = prepareNotification(ALEXA_SPEECH_CAPS, notification);
133533
+ const title = prepared.title !== void 0 ? toPlainSpeech(prepared.title) : "";
133534
+ const body = toPlainSpeech(prepared.body);
133535
+ const joined = title.length > 0 && body.length > 0 ? `${title}. ${body}` : title || body;
133536
+ const text = joined.length > 450 ? joined.slice(0, 450).trimEnd() : joined;
133537
+ const truncated = prepared.renderedAs.truncated || text.length < joined.length;
133538
+ return {
133539
+ text,
133540
+ renderedAs: {
133541
+ ...prepared.renderedAs,
133542
+ truncated,
133543
+ format: "text"
133544
+ }
133545
+ };
133546
+ }
133547
+ //#endregion
133548
+ //#region src/speak/provider.ts
133549
+ function quietLogger$1() {
133550
+ const logger = {
133551
+ debug() {},
133552
+ info() {},
133553
+ warn() {},
133554
+ error() {},
133555
+ child() {
133556
+ return logger;
133557
+ },
133558
+ withTags() {
133559
+ return logger;
133560
+ }
133561
+ };
133562
+ return logger;
133563
+ }
133564
+ function readString(config, key) {
133565
+ const value = config[key];
133566
+ return typeof value === "string" ? value : "";
133567
+ }
133568
+ function readMode(config) {
133569
+ return config["mode"] === "announce" ? "announce" : "speak";
133570
+ }
133571
+ function readVolume(config) {
133572
+ const value = config["volume"];
133573
+ if (typeof value !== "number" || !Number.isFinite(value)) return 40;
133574
+ return Math.max(0, Math.min(100, Math.round(value)));
133575
+ }
133576
+ function respectDnd(config) {
133577
+ return config["respectDnd"] !== false;
133578
+ }
133579
+ function configSchema() {
133580
+ return { sections: [{
133581
+ id: "echo",
133582
+ title: "Echo",
133583
+ titleKey: "import-alexa:echoTarget.s_echo.title",
133584
+ fields: [
133585
+ {
133586
+ type: "text",
133587
+ key: "echoId",
133588
+ label: "Echo id",
133589
+ labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
133590
+ required: true
133591
+ },
133592
+ {
133593
+ type: "select",
133594
+ key: "mode",
133595
+ label: "Mode",
133596
+ labelKey: "import-alexa:echoTarget.s_echo.mode.label",
133597
+ default: "speak",
133598
+ options: [{
133599
+ value: "speak",
133600
+ label: "Speak (quiet hours apply)"
133601
+ }, {
133602
+ value: "announce",
133603
+ label: "Announce (chime, then speech)"
133604
+ }]
133605
+ },
133606
+ {
133607
+ type: "number",
133608
+ key: "volume",
133609
+ label: "Volume",
133610
+ labelKey: "import-alexa:echoTarget.s_echo.volume.label",
133611
+ min: 0,
133612
+ max: 100,
133613
+ default: 40
133614
+ },
133615
+ {
133616
+ type: "boolean",
133617
+ key: "respectDnd",
133618
+ label: "Respect Do Not Disturb",
133619
+ labelKey: "import-alexa:echoTarget.s_echo.respectDnd.label",
133620
+ default: true
133621
+ }
133622
+ ]
133623
+ }] };
133624
+ }
133625
+ function kindDescriptor() {
133626
+ return {
133627
+ kind: ALEXA_SPEAK_KIND,
133628
+ label: "Alexa",
133629
+ icon: "megaphone",
133630
+ addonId: ALEXA_SPEAK_ADDON_ID,
133631
+ supportsDiscovery: true,
133632
+ destinationKeys: ["echoId"],
133633
+ configSchema: configSchema(),
133634
+ caps: {
133635
+ attachments: {
133636
+ mediaTypes: [],
133637
+ mode: "url",
133638
+ max: 0
133639
+ },
133640
+ actions: 0,
133641
+ actionIcons: false,
133642
+ levels: [{
133643
+ id: "default",
133644
+ label: "Default",
133645
+ ordinal: 3
133646
+ }],
133647
+ format: ["text"],
133648
+ clickUrl: false,
133649
+ sound: false,
133650
+ ttl: false,
133651
+ bodyMaxLen: 450
133652
+ }
133653
+ };
133654
+ }
133655
+ function discoveredFrom(device) {
133656
+ return {
133657
+ kind: ALEXA_SPEAK_KIND,
133658
+ suggestedName: device.name.length > 0 ? device.name : device.id,
133659
+ config: {
133660
+ echoId: device.id,
133661
+ mode: "speak",
133662
+ volume: 40,
133663
+ respectDnd: true,
133664
+ family: device.family,
133665
+ deviceKind: device.kind
133666
+ }
133667
+ };
133668
+ }
133669
+ function createAlexaNotificationProvider(ports) {
133670
+ const logger = ports.logger ?? quietLogger$1();
133671
+ let targets = ports.loadTargets();
133672
+ let ledger = emptySpeechLedger();
133673
+ let deliveries = [];
133674
+ function record(row) {
133675
+ deliveries = [...deliveries, row];
133676
+ }
133677
+ function deviceFor(echoId) {
133678
+ return ports.devices().find((device) => device.id === echoId);
133679
+ }
133680
+ function persist(next) {
133681
+ targets = next;
133682
+ ports.saveTargets(next);
133683
+ }
133684
+ async function deliver(target, notification) {
133685
+ const speech = speechFromNotification(notification);
133686
+ const echoId = readString(target.config, "echoId");
133687
+ const deviceId = notification.deviceId;
133688
+ const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
133689
+ if (echoId.length === 0) {
133690
+ record({
133691
+ targetId: target.id,
133692
+ outcome: "failed",
133693
+ at: ports.now(),
133694
+ error: "target has no echo id"
133695
+ });
133696
+ logger.warn("alexa speak dropped: target has no echo id", {
133697
+ ...tags,
133698
+ meta: { targetId: target.id }
133699
+ });
133700
+ return {
133701
+ success: false,
133702
+ error: "target has no echo id",
133703
+ renderedAs: speech.renderedAs
133704
+ };
133705
+ }
133706
+ if (speech.text.length === 0) {
133707
+ record({
133708
+ targetId: target.id,
133709
+ outcome: "empty",
133710
+ at: ports.now()
133711
+ });
133712
+ logger.info("alexa speak dropped: empty text", {
133713
+ ...tags,
133714
+ meta: {
133715
+ targetId: target.id,
133716
+ echoId
133717
+ }
133718
+ });
133719
+ return {
133720
+ success: true,
133721
+ renderedAs: speech.renderedAs
133722
+ };
133723
+ }
133724
+ const device = deviceFor(echoId);
133725
+ if (device !== void 0 && respectDnd(target.config) && device.dnd) {
133726
+ record({
133727
+ targetId: target.id,
133728
+ outcome: "dnd",
133729
+ at: ports.now()
133730
+ });
133731
+ logger.info("alexa speak skipped: do not disturb", {
133732
+ ...tags,
133733
+ meta: {
133734
+ targetId: target.id,
133735
+ echoId
133736
+ }
133737
+ });
133738
+ return {
133739
+ success: true,
133740
+ renderedAs: speech.renderedAs
133741
+ };
133742
+ }
133743
+ if (device !== void 0 && !device.online) {
133744
+ record({
133745
+ targetId: target.id,
133746
+ outcome: "offline",
133747
+ at: ports.now()
133748
+ });
133749
+ logger.info("alexa speak not delivered: echo offline", {
133750
+ ...tags,
133751
+ meta: {
133752
+ targetId: target.id,
133753
+ echoId
133754
+ }
133755
+ });
133756
+ return {
133757
+ success: false,
133758
+ error: "echo offline",
133759
+ renderedAs: speech.renderedAs
133760
+ };
133761
+ }
133762
+ const admitted = admitSpeech(ledger, echoId, speech.text, ports.now());
133763
+ ledger = admitted.ledger;
133764
+ if (admitted.decision !== "admit") {
133765
+ record({
133766
+ targetId: target.id,
133767
+ outcome: admitted.decision,
133768
+ at: ports.now()
133769
+ });
133770
+ logger.info("alexa speak skipped", {
133771
+ ...tags,
133772
+ meta: {
133773
+ targetId: target.id,
133774
+ echoId,
133775
+ outcome: admitted.decision
133776
+ }
133777
+ });
133778
+ return {
133779
+ success: true,
133780
+ renderedAs: speech.renderedAs
133781
+ };
133782
+ }
133783
+ const mode = readMode(target.config);
133784
+ try {
133785
+ const outcomes = await ports.speak({
133786
+ mode,
133787
+ targets: [echoId],
133788
+ text: speech.text,
133789
+ volume: readVolume(target.config)
133790
+ });
133791
+ const one = outcomes.find((row) => row.targetId === echoId) ?? outcomes[0];
133792
+ if (one === void 0 || !one.ok) {
133793
+ const error = one?.error ?? "speak failed";
133794
+ record({
133795
+ targetId: target.id,
133796
+ outcome: "failed",
133797
+ at: ports.now(),
133798
+ error
133799
+ });
133800
+ logger.warn("alexa speak failed", {
133801
+ ...tags,
133802
+ meta: {
133803
+ targetId: target.id,
133804
+ echoId,
133805
+ error
133806
+ }
133807
+ });
133808
+ return {
133809
+ success: false,
133810
+ error,
133811
+ renderedAs: speech.renderedAs
133812
+ };
133813
+ }
133814
+ const outcome = mode === "announce" ? "announced" : "spoken";
133815
+ record({
133816
+ targetId: target.id,
133817
+ outcome,
133818
+ at: ports.now()
133819
+ });
133820
+ logger.info("alexa speak delivered", {
133821
+ ...tags,
133822
+ meta: {
133823
+ targetId: target.id,
133824
+ echoId,
133825
+ outcome
133826
+ }
133827
+ });
133828
+ return {
133829
+ success: true,
133830
+ renderedAs: speech.renderedAs
133831
+ };
133832
+ } catch (err) {
133833
+ const error = err instanceof Error ? err.message : String(err);
133834
+ record({
133835
+ targetId: target.id,
133836
+ outcome: "failed",
133837
+ at: ports.now(),
133838
+ error
133839
+ });
133840
+ logger.warn("alexa speak failed", {
133841
+ ...tags,
133842
+ meta: {
133843
+ targetId: target.id,
133844
+ echoId,
133845
+ error
133846
+ }
133847
+ });
133848
+ return {
133849
+ success: false,
133850
+ error,
133851
+ renderedAs: speech.renderedAs
133852
+ };
133853
+ }
133854
+ }
133855
+ return {
133856
+ get deliveries() {
133857
+ return deliveries;
133858
+ },
133859
+ async listTargetKinds() {
133860
+ return [kindDescriptor()];
133861
+ },
133862
+ async listTargets() {
133863
+ return [...targets];
133864
+ },
133865
+ async getTarget(input) {
133866
+ const found = targets.find((target) => target.id === input.targetId);
133867
+ if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
133868
+ return found;
133869
+ },
133870
+ async discoverTargets() {
133871
+ return ports.devices().map(discoveredFrom);
133872
+ },
133873
+ async probeTargetConfig() {
133874
+ return ports.loggedIn() ? {
133875
+ ok: true,
133876
+ resolvedUrl: "alexa://session"
133877
+ } : {
133878
+ ok: false,
133879
+ resolvedUrl: "alexa://session",
133880
+ error: "not signed in"
133881
+ };
133882
+ },
133883
+ async send(input) {
133884
+ const target = targets.find((row) => row.id === input.targetId);
133885
+ if (target === void 0) return {
133886
+ success: false,
133887
+ error: `target not found: ${input.targetId}`
133888
+ };
133889
+ if (!target.enabled) return {
133890
+ success: false,
133891
+ error: "target disabled"
133892
+ };
133893
+ return deliver(target, input.notification);
133894
+ },
133895
+ async testTarget(input) {
133896
+ const found = targets.find((row) => row.id === input.targetId);
133897
+ if (found === void 0) return {
133898
+ success: false,
133899
+ error: `target not found: ${input.targetId}`
133900
+ };
133901
+ if (!found.enabled) return {
133902
+ success: false,
133903
+ error: "target disabled"
133904
+ };
133905
+ return deliver(found, input.sample ?? {
133906
+ body: "This is a CamStack test notification.",
133907
+ title: "CamStack",
133908
+ format: "text",
133909
+ priority: 3
133910
+ });
133911
+ },
133912
+ async upsertTarget(input) {
133913
+ const target = {
133914
+ ...input.target,
133915
+ addonId: ALEXA_SPEAK_ADDON_ID,
133916
+ kind: ALEXA_SPEAK_KIND
133917
+ };
133918
+ persist([...targets.filter((row) => row.id !== target.id), target]);
133919
+ return target;
133920
+ },
133921
+ async deleteTarget(input) {
133922
+ persist(targets.filter((row) => row.id !== input.targetId));
133923
+ },
133924
+ async setTargetEnabled(input) {
133925
+ persist(targets.map((row) => row.id === input.targetId ? {
133926
+ ...row,
133927
+ enabled: input.enabled
133928
+ } : row));
133929
+ }
133930
+ };
133931
+ }
133932
+ //#endregion
133933
+ //#region src/speak/python-paths.ts
133934
+ /** Locate bundled `python/alexa_worker.py` from dist/ or from src/ under test. */
133935
+ function resolveAlexaWorkerScript(fromModuleUrl = import.meta.url) {
133936
+ const here = dirname(fileURLToPath(fromModuleUrl));
133937
+ const candidates = [
133938
+ join(here, "../python/alexa_worker.py"),
133939
+ join(here, "../../python/alexa_worker.py"),
133940
+ join(here, "../../../python/alexa_worker.py")
133941
+ ];
133942
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
133943
+ throw new Error(`import-alexa: alexa_worker.py not found. Searched:\n${candidates.join("\n")}`);
133944
+ }
133945
+ process.platform, process.env["SystemRoot"];
133946
+ /**
133947
+ * Files only their owner may read: the one place a secret-bearing file is
133948
+ * restricted, on every OS.
133949
+ *
133950
+ * `mode: 0o600` is a POSIX idea. On Windows Node maps it to the read-only
133951
+ * attribute and nothing else, and `chmod` is a no-op for permissions, so a
133952
+ * "0600" `agent.json` (the mesh private key) would have
133953
+ * been readable by every local user. Windows has its own tool for this:
133954
+ * `icacls`. This module removes the inherited ACL entries and grants access to
133955
+ * the current user and SYSTEM only — the Windows reading of "0600".
133956
+ *
133957
+ * ZERO imports beyond Node built-ins, on purpose: the Electron shell bundles
133958
+ * this single file (see `packages/desktop/tsup.config.ts`) instead of the
133959
+ * whole `@camstack/types` barrel.
133960
+ *
133961
+ * Failure is NOT swallowed. A secret file that could not be restricted is a
133962
+ * secret file the caller must know about, so `OwnerOnlyError` propagates; the
133963
+ * write order below makes sure nothing sensitive is in the file yet when that
133964
+ * happens (the temp file is empty until it has been restricted).
133965
+ *
133966
+ * Out of scope on purpose: files that are only tidy, not secret (the job
133967
+ * journal, the addon manifest) keep a plain `mode`, since one `icacls` per
133968
+ * write would be a process spawn on a hot path.
133969
+ */
133970
+ /** `*S-1-5-18` is SYSTEM in every locale — a name like "SYSTEM" is not. */
133971
+ var WINDOWS_SYSTEM_SID = "*S-1-5-18";
133972
+ var FILE_MODE = 384;
133973
+ var DIR_MODE = 448;
133974
+ var OwnerOnlyError = class extends Error {
133975
+ target;
133976
+ constructor(target, detail) {
133977
+ super(`could not restrict ${target} to its owner: ${detail}`);
133978
+ this.target = target;
133979
+ this.name = "OwnerOnlyError";
133980
+ }
133981
+ };
133982
+ function currentPrincipal() {
133983
+ const user = process.env["USERNAME"] ?? userInfo().username;
133984
+ const domain = process.env["USERDOMAIN"];
133985
+ return domain !== void 0 && domain.length > 0 ? `${domain}\\${user}` : user;
133986
+ }
133987
+ /** Resolved per call, not at import: a platform stubbed by a test must be seen. */
133988
+ function resolveDeps(overrides) {
133989
+ return {
133990
+ platform: process.platform,
133991
+ exec: (file, args) => {
133992
+ execFileSync(file, [...args], {
133993
+ stdio: "pipe",
133994
+ windowsHide: true
133995
+ });
133996
+ },
133997
+ principal: currentPrincipal,
133998
+ systemRoot: process.env["SystemRoot"],
133999
+ ...overrides
134000
+ };
134001
+ }
134002
+ /** The icacls argv for one path: no inheritance, owner + SYSTEM only. */
134003
+ function icaclsArgs(target, principal, isDirectory) {
134004
+ const inherit = isDirectory ? "(OI)(CI)" : "";
134005
+ return [
134006
+ target,
134007
+ "/inheritance:r",
134008
+ "/grant:r",
134009
+ `${principal}:${inherit}F`,
134010
+ "/grant:r",
134011
+ `${WINDOWS_SYSTEM_SID}:${inherit}F`
134012
+ ];
134013
+ }
134014
+ function restrictWindows(target, isDirectory, deps) {
134015
+ const icacls = path$1.win32.join(deps.systemRoot ?? "C:\\Windows", "System32", "icacls.exe");
134016
+ try {
134017
+ deps.exec(icacls, icaclsArgs(target, deps.principal(), isDirectory));
134018
+ } catch (err) {
134019
+ throw new OwnerOnlyError(target, err instanceof Error ? err.message : String(err));
134020
+ }
134021
+ }
134022
+ /** Restrict an existing file to its owner (chmod 0600 / owner+SYSTEM ACL). */
134023
+ function restrictFileToOwnerSync(target, overrides = {}) {
134024
+ const deps = resolveDeps(overrides);
134025
+ if (deps.platform === "win32") {
134026
+ restrictWindows(target, false, deps);
134027
+ return;
134028
+ }
134029
+ fs$1.chmodSync(target, FILE_MODE);
134030
+ }
134031
+ /** Restrict an existing directory to its owner (chmod 0700 / owner+SYSTEM, inherited). */
134032
+ function restrictDirToOwnerSync(target, overrides = {}) {
134033
+ const deps = resolveDeps(overrides);
134034
+ if (deps.platform === "win32") {
134035
+ restrictWindows(target, true, deps);
134036
+ return;
134037
+ }
134038
+ fs$1.chmodSync(target, DIR_MODE);
134039
+ }
134040
+ /**
134041
+ * Atomically write a secret-bearing file that only its owner can read.
134042
+ *
134043
+ * Order matters: the temp file is created EMPTY, restricted, and only then
134044
+ * given its content, so there is no moment at which the secret exists under a
134045
+ * permissive ACL. The rename carries the (already restricted) ACL with it.
134046
+ */
134047
+ function writeOwnerOnlyFileSync(target, data, overrides = {}) {
134048
+ fs$1.mkdirSync(path$1.dirname(target), { recursive: true });
134049
+ const tmp = `${target}.tmp.${process.pid}`;
134050
+ fs$1.writeFileSync(tmp, "", { mode: FILE_MODE });
134051
+ try {
134052
+ restrictFileToOwnerSync(tmp, overrides);
134053
+ fs$1.writeFileSync(tmp, data);
134054
+ fs$1.renameSync(tmp, target);
134055
+ } catch (err) {
134056
+ fs$1.rmSync(tmp, { force: true });
134057
+ throw err;
134058
+ }
134059
+ }
134060
+ //#endregion
134061
+ //#region src/speak/secret-file.ts
134062
+ /**
134063
+ * Hand a password or a saved Amazon session to the Python worker as an
134064
+ * owner-only file. The path travels on stdin. The bytes never travel on
134065
+ * argv or in the environment, and they are removed when the call settles.
134066
+ */
134067
+ var REDACTED = "‹redacted›";
134068
+ function decodeSession(sessionB64) {
134069
+ if (sessionB64.length === 0) return Buffer.alloc(0);
134070
+ return Buffer.from(sessionB64, "base64");
134071
+ }
134072
+ async function readSessionB64(sessionPath) {
134073
+ const bytes = await fs.readFile(sessionPath);
134074
+ if (bytes.length === 0) return "";
134075
+ return bytes.toString("base64");
134076
+ }
134077
+ /**
134078
+ * Create a 0700 directory, write the password and the session 0600, run
134079
+ * `body`, then delete the directory whether `body` succeeded or threw.
134080
+ * Returns the session file as base64 so the caller can store it in the keychain.
134081
+ */
134082
+ async function withSpeakSecretFiles(input, body) {
134083
+ const dir = await fs.mkdtemp(path$1.join(os.tmpdir(), "camstack-alexa-"));
134084
+ const paths = {
134085
+ dir,
134086
+ passwordPath: path$1.join(dir, "password"),
134087
+ sessionPath: path$1.join(dir, "session")
134088
+ };
134089
+ try {
134090
+ restrictDirToOwnerSync(dir);
134091
+ writeOwnerOnlyFileSync(paths.passwordPath, input.password);
134092
+ writeOwnerOnlyFileSync(paths.sessionPath, decodeSession(input.sessionB64));
134093
+ await body(paths);
134094
+ return { sessionB64: await readSessionB64(paths.sessionPath) };
134095
+ } finally {
134096
+ await fs.rm(dir, {
134097
+ recursive: true,
134098
+ force: true
134099
+ });
134100
+ }
134101
+ }
134102
+ /** Replace every occurrence of each non-empty secret. Used before a log line is emitted. */
134103
+ function scrubSecrets(text, secrets) {
134104
+ let out = text;
134105
+ for (const secret of secrets) {
134106
+ if (secret.length === 0) continue;
134107
+ out = out.split(secret).join(REDACTED);
134108
+ }
134109
+ return out;
134110
+ }
134111
+ //#endregion
134112
+ //#region src/speak/setup-flow.ts
134113
+ /** Where the next screen is. A failed password stays on sign-in. A failed code stays on the code step. */
134114
+ function reduceSetup(phase, event) {
134115
+ if (event.type === "reset") return "sign-in";
134116
+ if (event.status === "ok") return "devices";
134117
+ if (event.status === "otp_needed") return "otp";
134118
+ if (event.status === "captcha") return "captcha";
134119
+ return phase;
134120
+ }
134121
+ //#endregion
134122
+ //#region src/speak/worker-client.ts
134123
+ /**
134124
+ * One Python process for the life of the addon. Requests are one JSON line
134125
+ * on stdin; responses are one JSON line on stdout, matched by id.
134126
+ * A crash restarts the process until the same bound CrashSupervisor uses,
134127
+ * then requests fail instead of looping.
134128
+ */
134129
+ /** Pure: a new timestamp list, never a mutated one. */
134130
+ function recordWorkerCrash(times, now, windowMs, maxCrashes) {
134131
+ const cutoff = now - windowMs;
134132
+ const recent = [...times.filter((t) => t >= cutoff), now];
134133
+ if (recent.length >= maxCrashes) return {
134134
+ action: "failed",
134135
+ times: recent
134136
+ };
134137
+ return {
134138
+ action: "respawn",
134139
+ times: recent
134140
+ };
134141
+ }
134142
+ function quietLogger() {
134143
+ const logger = {
134144
+ debug() {},
134145
+ info() {},
134146
+ warn() {},
134147
+ error() {},
134148
+ child() {
134149
+ return logger;
134150
+ },
134151
+ withTags() {
134152
+ return logger;
134153
+ }
134154
+ };
134155
+ return logger;
134156
+ }
134157
+ function wrapChild(child) {
134158
+ const stdin = child.stdin;
134159
+ const stdout = child.stdout;
134160
+ const stderr = child.stderr;
134161
+ if (stdin === null || stdout === null || stderr === null) throw new Error("alexa worker spawn did not provide pipes");
134162
+ return {
134163
+ stdin: { write(chunk) {
134164
+ stdin.write(chunk);
134165
+ } },
134166
+ stdout: { on(event, listener) {
134167
+ stdout.on(event, listener);
134168
+ } },
134169
+ stderr: { on(event, listener) {
134170
+ stderr.on(event, listener);
134171
+ } },
134172
+ on(event, listener) {
134173
+ if (event === "close") child.on("close", listener);
134174
+ else child.on("error", listener);
134175
+ },
134176
+ kill(signal) {
134177
+ child.kill(signal);
134178
+ }
134179
+ };
134180
+ }
134181
+ function defaultSpawn(pythonPath, scriptPath, env) {
134182
+ return wrapChild(spawn(pythonPath, [scriptPath], {
134183
+ stdio: [
134184
+ "pipe",
134185
+ "pipe",
134186
+ "pipe"
134187
+ ],
134188
+ env
134189
+ }));
134190
+ }
134191
+ function startAlexaWorker(deps) {
134192
+ const logger = deps.logger ?? quietLogger();
134193
+ const now = deps.now ?? (() => Date.now());
134194
+ const schedule = deps.schedule ?? ((fn, delayMs) => {
134195
+ setTimeout(fn, delayMs);
134196
+ });
134197
+ const spawnChild = deps.spawnChild ?? defaultSpawn;
134198
+ const maxCrashes = deps.maxCrashes ?? 5;
134199
+ const windowMs = deps.windowMs ?? 6e4;
134200
+ const env = deps.env ?? process.env;
134201
+ let child = null;
134202
+ let buffer = "";
134203
+ let nextId = 1;
134204
+ let stopped = false;
134205
+ let failed = false;
134206
+ let crashTimes = [];
134207
+ let secrets = [];
134208
+ let pending = null;
134209
+ const queue = [];
134210
+ function scrub(text) {
134211
+ return scrubSecrets(text, secrets);
134212
+ }
134213
+ function failPending(err) {
134214
+ const current = pending;
134215
+ pending = null;
134216
+ current?.reject(err);
134217
+ }
134218
+ function attach(spawned) {
134219
+ child = spawned;
134220
+ buffer = "";
134221
+ let settled = false;
134222
+ const finish = (code) => {
134223
+ if (settled || child !== spawned) return;
134224
+ settled = true;
134225
+ onExit(code);
134226
+ };
134227
+ spawned.stdout.on("data", (chunk) => {
134228
+ buffer += chunk.toString("utf8");
134229
+ const lines = buffer.split("\n");
134230
+ buffer = lines.pop() ?? "";
134231
+ for (const line of lines) {
134232
+ if (line.trim().length === 0) continue;
134233
+ acceptLine(line);
134234
+ }
134235
+ });
134236
+ spawned.stderr.on("data", (chunk) => {
134237
+ const text = scrub(chunk.toString("utf8")).trim();
134238
+ if (text.length === 0) return;
134239
+ logger.warn("alexa worker stderr", { meta: { text: text.slice(0, 500) } });
134240
+ });
134241
+ spawned.on("error", (err) => {
134242
+ logger.warn("alexa worker spawn error", { meta: { error: scrub(err.message) } });
134243
+ finish(null);
134244
+ });
134245
+ spawned.on("close", (code) => {
134246
+ finish(code);
134247
+ });
134248
+ }
134249
+ function acceptLine(line) {
134250
+ let parsed;
134251
+ try {
134252
+ parsed = JSON.parse(line);
134253
+ } catch (err) {
134254
+ logger.warn("alexa worker non-JSON line", { meta: { error: err instanceof Error ? err.message : String(err) } });
134255
+ return;
134256
+ }
134257
+ const checked = WorkerLineSchema.safeParse(parsed);
134258
+ if (!checked.success) {
134259
+ logger.warn("alexa worker line failed schema", { meta: { error: checked.error.message } });
134260
+ return;
134261
+ }
134262
+ const message = checked.data;
134263
+ if (pending === null || pending.id !== message.id) {
134264
+ logger.warn("alexa worker line for an unknown request", { meta: { id: message.id } });
134265
+ return;
134266
+ }
134267
+ const current = pending;
134268
+ pending = null;
134269
+ if (message.ok) current.resolve(message.result);
134270
+ else current.reject(new Error(scrub(message.error)));
134271
+ pump();
134272
+ }
134273
+ function onExit(code) {
134274
+ child = null;
134275
+ if (stopped) {
134276
+ failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
134277
+ return;
134278
+ }
134279
+ failPending(/* @__PURE__ */ new Error(`alexa worker exited (${code ?? "null"})`));
134280
+ const decision = recordWorkerCrash(crashTimes, now(), windowMs, maxCrashes);
134281
+ crashTimes = decision.times;
134282
+ if (decision.action === "failed") {
134283
+ failed = true;
134284
+ logger.error("alexa worker restart bound reached", { meta: { crashes: crashTimes.length } });
134285
+ const waiting = queue.splice(0, queue.length);
134286
+ for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
134287
+ return;
134288
+ }
134289
+ logger.warn("alexa worker exited; restarting", { meta: {
134290
+ code,
134291
+ crashes: crashTimes.length
134292
+ } });
134293
+ schedule(() => {
134294
+ if (stopped || failed) return;
134295
+ launch();
134296
+ pump();
134297
+ }, Math.min(3e4, 500 * 2 ** Math.min(6, crashTimes.length - 1)));
134298
+ }
134299
+ function launch() {
134300
+ if (stopped || failed) return;
134301
+ attach(spawnChild(deps.pythonPath, deps.scriptPath, env));
134302
+ }
134303
+ function pump() {
134304
+ if (pending !== null || child === null || failed || stopped) return;
134305
+ const next = queue.shift();
134306
+ if (next === void 0) return;
134307
+ pending = next.pending;
134308
+ child.stdin.write(requestLine(next.pending.id, next.request));
134309
+ }
134310
+ function request(body) {
134311
+ if (failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
134312
+ if (stopped) return Promise.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
134313
+ const id = String(nextId);
134314
+ nextId += 1;
134315
+ return new Promise((resolve, reject) => {
134316
+ queue.push({
134317
+ request: body,
134318
+ pending: {
134319
+ id,
134320
+ resolve,
134321
+ reject
134322
+ }
134323
+ });
134324
+ if (child === null) launch();
134325
+ pump();
134326
+ });
134327
+ }
134328
+ return {
134329
+ request,
134330
+ noteSecrets(next) {
134331
+ secrets = [...next];
134332
+ },
134333
+ stop() {
134334
+ stopped = true;
134335
+ const current = child;
134336
+ child = null;
134337
+ current?.kill("SIGTERM");
134338
+ failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
134339
+ const waiting = queue.splice(0, queue.length);
134340
+ for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
134341
+ },
134342
+ get failed() {
134343
+ return failed;
134344
+ },
134345
+ get crashesInWindow() {
134346
+ return crashTimes.length;
134347
+ }
134348
+ };
134349
+ }
134350
+ //#endregion
134351
+ //#region src/speak/controller.ts
134352
+ /**
134353
+ * Owns the one Python worker, the keychain session, the Echo cache and the
134354
+ * notification provider. A login that needs a code raises one alert.
134355
+ */
134356
+ function targetsFrom(settings) {
134357
+ return settings.targets.map((row) => ({
134358
+ id: row.id,
134359
+ name: row.name,
134360
+ kind: ALEXA_SPEAK_KIND,
134361
+ addonId: ALEXA_SPEAK_ADDON_ID,
134362
+ enabled: row.enabled,
134363
+ config: {
134364
+ echoId: row.echoId,
134365
+ mode: row.mode,
134366
+ volume: row.volume,
134367
+ respectDnd: row.respectDnd
134368
+ }
134369
+ }));
134370
+ }
134371
+ function withTargets(settings, targets) {
134372
+ return {
134373
+ ...settings,
134374
+ targets: targets.map((target) => ({
134375
+ id: target.id,
134376
+ name: target.name,
134377
+ enabled: target.enabled,
134378
+ echoId: typeof target.config["echoId"] === "string" ? target.config["echoId"] : "",
134379
+ mode: target.config["mode"] === "announce" ? "announce" : "speak",
134380
+ volume: typeof target.config["volume"] === "number" ? target.config["volume"] : 40,
134381
+ respectDnd: target.config["respectDnd"] !== false
134382
+ }))
134383
+ };
134384
+ }
134385
+ function usablePassword(value) {
134386
+ if (value.length === 0 || isSecretMarker(value)) return "";
134387
+ return value;
134388
+ }
134389
+ var AlexaSpeakController = class {
134390
+ host;
134391
+ provider;
134392
+ settings;
134393
+ worker = null;
134394
+ devices = [];
134395
+ phase = "sign-in";
134396
+ timer = null;
134397
+ attention;
134398
+ now;
134399
+ constructor(host) {
134400
+ this.host = host;
134401
+ this.now = host.now ?? (() => Date.now());
134402
+ this.settings = readSpeakSettings(host.readConfig());
134403
+ this.phase = this.settings.session.length > 0 ? "devices" : "sign-in";
134404
+ this.attention = new OtpAttention({
134405
+ now: this.now,
134406
+ emit: (alert) => host.emitAlert(alert),
134407
+ resolve: (id) => host.resolveAlert(id)
134408
+ });
134409
+ this.provider = createAlexaNotificationProvider({
134410
+ devices: () => this.devices,
134411
+ loggedIn: () => this.phase === "devices",
134412
+ now: this.now,
134413
+ logger: host.logger,
134414
+ loadTargets: () => targetsFrom(this.settings),
134415
+ saveTargets: (targets) => {
134416
+ this.settings = withTargets(this.settings, targets);
134417
+ this.persist();
134418
+ },
134419
+ speak: (command) => this.utter(command.mode, command.targets, command.text, command.volume)
134420
+ });
134421
+ }
134422
+ async start() {
134423
+ const pythonPath = await this.host.ensurePython();
134424
+ if (pythonPath === null) {
134425
+ this.host.logger.warn("alexa speak: embedded Python unavailable; Echo speech will not run");
134426
+ return;
134427
+ }
134428
+ this.worker = startAlexaWorker({
134429
+ pythonPath,
134430
+ scriptPath: resolveAlexaWorkerScript(),
134431
+ logger: this.host.logger,
134432
+ now: this.now
134433
+ });
134434
+ this.noteSecrets();
134435
+ if (this.settings.session.length > 0) await this.loginWithFiles().catch((err) => {
134436
+ this.host.logger.warn("alexa speak: stored session did not restore", { meta: { error: err instanceof Error ? err.message : String(err) } });
134437
+ });
134438
+ this.timer = setInterval(() => {
134439
+ this.refreshDevices();
134440
+ }, DEVICE_REFRESH_MS);
134441
+ }
134442
+ stop() {
134443
+ if (this.timer !== null) clearInterval(this.timer);
134444
+ this.timer = null;
134445
+ this.worker?.stop();
134446
+ this.worker = null;
134447
+ }
134448
+ handlers() {
134449
+ return {
134450
+ speakStatus: async () => this.status(),
134451
+ loginStart: async (input) => {
134452
+ this.settings = {
134453
+ ...this.settings,
134454
+ email: input.email,
134455
+ password: input.password,
134456
+ region: input.region
134457
+ };
134458
+ await this.persist();
134459
+ this.noteSecrets();
134460
+ return this.loginWithFiles();
134461
+ },
134462
+ loginOtp: async (input) => this.otpWithFiles(input.code),
134463
+ sendTest: async (input) => this.sendTest(input)
134464
+ };
134465
+ }
134466
+ noteSecrets() {
134467
+ const password = usablePassword(this.settings.password);
134468
+ this.worker?.noteSecrets(password.length > 0 ? [password] : []);
134469
+ }
134470
+ async persist() {
134471
+ try {
134472
+ await this.host.writeSpeak(this.settings);
134473
+ } catch (err) {
134474
+ this.host.logger.warn("alexa speak: could not store settings", { meta: { error: err instanceof Error ? err.message : String(err) } });
134475
+ }
134476
+ }
134477
+ workerRequest(request) {
134478
+ const worker = this.worker;
134479
+ if (worker === null || worker.failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker is not running"));
134480
+ return worker.request(request);
134481
+ }
134482
+ async loginWithFiles() {
134483
+ let outcome = {
134484
+ status: "failed",
134485
+ error: "alexa worker is not running"
134486
+ };
134487
+ const held = await withSpeakSecretFiles({
134488
+ password: usablePassword(this.settings.password),
134489
+ sessionB64: this.settings.session
134490
+ }, async (paths) => {
134491
+ outcome = await this.applyLogin(await this.loginRequest(paths));
134492
+ });
134493
+ if (held.sessionB64 !== this.settings.session) {
134494
+ this.settings = {
134495
+ ...this.settings,
134496
+ session: held.sessionB64
134497
+ };
134498
+ await this.persist();
134499
+ }
134500
+ return outcome;
134501
+ }
134502
+ async otpWithFiles(code) {
134503
+ let outcome = {
134504
+ status: "failed",
134505
+ error: "alexa worker is not running"
134506
+ };
134507
+ const held = await withSpeakSecretFiles({
134508
+ password: "",
134509
+ sessionB64: this.settings.session
134510
+ }, async (paths) => {
134511
+ outcome = await this.applyLogin(await this.workerRequest({
134512
+ op: "login_otp",
134513
+ code,
134514
+ sessionPath: paths.sessionPath
134515
+ }));
134516
+ });
134517
+ if (held.sessionB64 !== this.settings.session) {
134518
+ this.settings = {
134519
+ ...this.settings,
134520
+ session: held.sessionB64
134521
+ };
134522
+ await this.persist();
134523
+ }
134524
+ return outcome;
134525
+ }
134526
+ loginRequest(paths) {
134527
+ return this.workerRequest({
134528
+ op: "login_start",
134529
+ email: this.settings.email,
134530
+ passwordPath: paths.passwordPath,
134531
+ sessionPath: paths.sessionPath,
134532
+ region: this.settings.region
134533
+ });
134534
+ }
134535
+ async applyLogin(raw) {
134536
+ const outcome = parseLoginResult(raw);
134537
+ this.phase = reduceSetup(this.phase, {
134538
+ type: "login",
134539
+ status: outcome.status
134540
+ });
134541
+ if (outcome.status === "otp_needed") await this.attention.needed("otp");
134542
+ else if (outcome.status === "captcha") await this.attention.needed("captcha");
134543
+ else if (outcome.status === "ok") {
134544
+ await this.attention.cleared();
134545
+ await this.refreshDevices();
134546
+ }
134547
+ return outcome;
134548
+ }
134549
+ async refreshDevices() {
134550
+ if (this.worker === null || this.phase !== "devices") return;
134551
+ try {
134552
+ this.devices = [...parseDevices(await this.workerRequest({ op: "devices" }))];
134553
+ } catch (err) {
134554
+ this.host.logger.warn("alexa speak: device refresh failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
134555
+ }
134556
+ }
134557
+ async utter(mode, targets, text, volume) {
134558
+ const op = mode === "announce" ? "announce" : "speak";
134559
+ return parseDeliveries(await this.workerRequest({
134560
+ op,
134561
+ targets,
134562
+ text,
134563
+ volume
134564
+ }));
134565
+ }
134566
+ async session() {
134567
+ if (this.worker === null) return {
134568
+ loggedIn: false,
134569
+ ageSec: 0,
134570
+ lastSuccessAt: null
134571
+ };
134572
+ try {
134573
+ return parseSessionStatus(await this.workerRequest({ op: "session_status" }));
134574
+ } catch {
134575
+ return {
134576
+ loggedIn: this.phase === "devices",
134577
+ ageSec: 0,
134578
+ lastSuccessAt: null
134579
+ };
134580
+ }
134581
+ }
134582
+ async status() {
134583
+ return {
134584
+ phase: this.phase,
134585
+ email: this.settings.email,
134586
+ region: this.settings.region,
134587
+ devices: [...this.devices],
134588
+ session: await this.session()
134589
+ };
134590
+ }
134591
+ async sendTest(input) {
134592
+ const outcomes = await this.utter(input.mode, input.echoIds, input.text, input.volume);
134593
+ const known = new Map(this.devices.map((device) => [device.id, device]));
134594
+ const next = [...targetsFrom(this.settings)];
134595
+ for (const echoId of input.echoIds) {
134596
+ if (next.some((row) => row.config["echoId"] === echoId)) continue;
134597
+ const device = known.get(echoId);
134598
+ next.push({
134599
+ id: `echo-${echoId}`,
134600
+ name: device?.name ?? echoId,
134601
+ kind: ALEXA_SPEAK_KIND,
134602
+ addonId: ALEXA_SPEAK_ADDON_ID,
134603
+ enabled: true,
134604
+ config: {
134605
+ echoId,
134606
+ mode: input.mode,
134607
+ volume: input.volume,
134608
+ respectDnd: true
134609
+ }
134610
+ });
134611
+ }
134612
+ this.settings = withTargets(this.settings, next);
134613
+ await this.persist();
134614
+ return { outcomes: outcomes.map((row) => ({
134615
+ echoId: row.targetId,
134616
+ ok: row.ok,
134617
+ ...row.error !== void 0 ? { error: row.error } : {}
134618
+ })) };
134619
+ }
134620
+ };
134621
+ //#endregion
132682
134622
  //#region src/addon.ts
134623
+ var SPEAK_PAGE = {
134624
+ id: "speak",
134625
+ label: "Speak on Echo",
134626
+ icon: "megaphone",
134627
+ path: "/addon/import-alexa",
134628
+ remoteName: "addon_import_alexa_page",
134629
+ bundle: "remoteEntry.js",
134630
+ section: "administration"
134631
+ };
132683
134632
  /** Default config — a fresh install starts with no accounts. */
132684
134633
  var DEFAULTS = {
132685
134634
  brokers: [],
132686
134635
  proxyLogin: {
132687
134636
  proxyHost: "",
132688
134637
  proxyPort: 3456
134638
+ },
134639
+ speak: {
134640
+ email: "",
134641
+ password: "",
134642
+ region: "EU",
134643
+ session: "",
134644
+ targets: []
132689
134645
  }
132690
134646
  };
132691
134647
  /** Device types an imported Alexa appliance can materialise as. */
@@ -132727,6 +134683,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
132727
134683
  proxyLogin = null;
132728
134684
  brokerStore = null;
132729
134685
  activeBrokers = [];
134686
+ speak = null;
134687
+ secretStatePaths = ["speak.password", "speak.session"];
132730
134688
  constructor() {
132731
134689
  super({ ...DEFAULTS });
132732
134690
  }
@@ -132780,21 +134738,62 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
132780
134738
  proxyLogin: this.proxyLogin,
132781
134739
  commitCookie: (input) => this.commitCapturedCookie(input)
132782
134740
  });
132783
- return [
132784
- ...regs,
132785
- {
132786
- capability: brokerCapability,
132787
- provider: this.buildBrokerProvider()
134741
+ const speak = new AlexaSpeakController({
134742
+ logger: this.ctx.logger.child("speak"),
134743
+ readConfig: () => this.config,
134744
+ writeSpeak: async (next) => {
134745
+ await this.updateGlobalSettings({ speak: next });
132788
134746
  },
132789
- {
132790
- capability: deviceAdoptionCapability,
132791
- provider: this.buildAdoptionProvider()
134747
+ emitAlert: async (alert) => {
134748
+ const emit = this.ctx.api.alerts?.emit;
134749
+ if (emit === void 0) {
134750
+ this.ctx.logger.warn("alexa speak: OTP alert skipped — alerts capability missing");
134751
+ return;
134752
+ }
134753
+ await emit.mutate(alert);
132792
134754
  },
132793
- {
132794
- capability: addonRoutesCapability,
132795
- provider: buildAddonRouteProvider("import-alexa", loginRoutes)
132796
- }
132797
- ];
134755
+ resolveAlert: async (alertId) => {
134756
+ const dismiss = this.ctx.api.alerts?.dismiss;
134757
+ if (dismiss === void 0) return;
134758
+ await dismiss.mutate({ alertId });
134759
+ },
134760
+ ensurePython: () => this.ctx.deps.ensurePython()
134761
+ });
134762
+ this.speak = speak;
134763
+ speak.start().catch((err) => {
134764
+ this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
134765
+ });
134766
+ const pages = {
134767
+ id: this.addonId,
134768
+ listPages: () => [SPEAK_PAGE]
134769
+ };
134770
+ return {
134771
+ providers: [
134772
+ ...regs,
134773
+ {
134774
+ capability: brokerCapability,
134775
+ provider: this.buildBrokerProvider()
134776
+ },
134777
+ {
134778
+ capability: deviceAdoptionCapability,
134779
+ provider: this.buildAdoptionProvider()
134780
+ },
134781
+ {
134782
+ capability: addonRoutesCapability,
134783
+ provider: buildAddonRouteProvider("import-alexa", loginRoutes)
134784
+ },
134785
+ {
134786
+ capability: notificationOutputCapability,
134787
+ provider: speak.provider
134788
+ },
134789
+ {
134790
+ capability: addonPagesSourceCapability,
134791
+ provider: pages
134792
+ }
134793
+ ],
134794
+ customActions: alexaSpeakActions,
134795
+ actionHandlers: speak.handlers()
134796
+ };
132798
134797
  }
132799
134798
  async loadPersistedBrokers() {
132800
134799
  const store = this.brokerStore;
@@ -132859,6 +134858,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
132859
134858
  }
132860
134859
  }
132861
134860
  async onShutdown() {
134861
+ this.speak?.stop();
134862
+ this.speak = null;
132862
134863
  alexaCommands.clear();
132863
134864
  try {
132864
134865
  await this.proxyLogin?.dispose();
@@ -133093,43 +135094,90 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
133093
135094
  });
133094
135095
  }
133095
135096
  globalSettingsSchema() {
133096
- return this.schema({ sections: [{
133097
- id: "alexa-account",
133098
- title: "Amazon Alexa account",
133099
- titleKey: "import-alexa:globalSettings.s_alexa-account.title",
133100
- description: "Alexa accounts are managed in the External systems → Brokers tab.",
133101
- descriptionKey: "import-alexa:globalSettings.s_alexa-account.description",
133102
- columns: 1,
133103
- fields: [{
133104
- type: "info",
133105
- key: "accountHelp",
133106
- label: "Accounts are managed separately",
133107
- labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
133108
- 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."
133109
- }]
133110
- }, {
133111
- id: "alexa-proxy-login",
133112
- title: "Interactive login (proxy)",
133113
- titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
133114
- description: "The \"Login to Amazon\" button in onboarding opens a local proxy that captures the cookie after you complete the real Amazon login (incl. 2FA). Because Amazon URLs are rewritten to this exact host:port, it must be a directly-reachable address — leave the host blank to auto-detect the server IP, and make sure the port is reachable from the browser you log in from.",
133115
- descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
133116
- columns: 2,
133117
- fields: [{
133118
- type: "text",
133119
- key: "proxyLogin.proxyHost",
133120
- label: "Login proxy host/IP (blank = auto-detect)",
133121
- labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyHost.label",
133122
- placeholder: "auto-detect"
133123
- }, {
133124
- type: "number",
133125
- key: "proxyLogin.proxyPort",
133126
- label: "Login proxy port",
133127
- labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
133128
- min: 1,
133129
- max: 65535,
133130
- default: 3456
133131
- }]
133132
- }] });
135097
+ return this.schema({ sections: [
135098
+ {
135099
+ id: "alexa-account",
135100
+ title: "Amazon Alexa account",
135101
+ titleKey: "import-alexa:globalSettings.s_alexa-account.title",
135102
+ description: "Alexa accounts are managed in the External systems → Brokers tab.",
135103
+ descriptionKey: "import-alexa:globalSettings.s_alexa-account.description",
135104
+ columns: 1,
135105
+ fields: [{
135106
+ type: "info",
135107
+ key: "accountHelp",
135108
+ label: "Accounts are managed separately",
135109
+ labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
135110
+ 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."
135111
+ }]
135112
+ },
135113
+ {
135114
+ id: "alexa-proxy-login",
135115
+ title: "Interactive login (proxy)",
135116
+ titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
135117
+ description: "The \"Login to Amazon\" button in onboarding opens a local proxy that captures the cookie after you complete the real Amazon login (incl. 2FA). Because Amazon URLs are rewritten to this exact host:port, it must be a directly-reachable address — leave the host blank to auto-detect the server IP, and make sure the port is reachable from the browser you log in from.",
135118
+ descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
135119
+ columns: 2,
135120
+ fields: [{
135121
+ type: "text",
135122
+ key: "proxyLogin.proxyHost",
135123
+ label: "Login proxy host/IP (blank = auto-detect)",
135124
+ labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyHost.label",
135125
+ placeholder: "auto-detect"
135126
+ }, {
135127
+ type: "number",
135128
+ key: "proxyLogin.proxyPort",
135129
+ label: "Login proxy port",
135130
+ labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
135131
+ min: 1,
135132
+ max: 65535,
135133
+ default: 3456
135134
+ }]
135135
+ },
135136
+ {
135137
+ id: "alexa-speak",
135138
+ title: "Speak on Echo",
135139
+ titleKey: "import-alexa:globalSettings.s_alexa-speak.title",
135140
+ description: "Sign in on the Speak on Echo page, pick the devices, and use them as a notification target. The text is the rule template, read aloud.",
135141
+ descriptionKey: "import-alexa:globalSettings.s_alexa-speak.description",
135142
+ columns: 1,
135143
+ fields: [
135144
+ {
135145
+ type: "text",
135146
+ key: "speak.email",
135147
+ label: "Amazon email",
135148
+ labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_email.label",
135149
+ inputType: "email"
135150
+ },
135151
+ {
135152
+ type: "password",
135153
+ key: "speak.password",
135154
+ label: "Amazon password",
135155
+ labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_password.label"
135156
+ },
135157
+ {
135158
+ type: "select",
135159
+ key: "speak.region",
135160
+ label: "Region",
135161
+ labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_region.label",
135162
+ default: "EU",
135163
+ options: [
135164
+ {
135165
+ value: "NA",
135166
+ label: "North America"
135167
+ },
135168
+ {
135169
+ value: "EU",
135170
+ label: "Europe"
135171
+ },
135172
+ {
135173
+ value: "FE",
135174
+ label: "Far East"
135175
+ }
135176
+ ]
135177
+ }
135178
+ ]
135179
+ }
135180
+ ] });
133133
135181
  }
133134
135182
  async supportsManualCreation() {
133135
135183
  return false;
@@ -133163,4 +135211,4 @@ function capsEqual(a, b) {
133163
135211
  return true;
133164
135212
  }
133165
135213
  //#endregion
133166
- export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
135214
+ export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, alexaSpeakActions as customActions, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };