@camstack/addon-import-alexa 0.2.163 → 0.2.165

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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/addon.js +353 -412
  3. package/dist/addon.mjs +354 -411
  4. package/dist/index.js +0 -1
  5. package/dist/index.mjs +2 -2
  6. package/package.json +2 -17
  7. package/dist/MaskShapeCanvas-B6q5RDLi.mjs +0 -13807
  8. package/dist/MotionZonesSettings-ByDKuk5o.mjs +0 -309
  9. package/dist/PrivacyMaskSettings-Cn-_9Syv.mjs +0 -368
  10. package/dist/SceneMonitorEditor-jDMtE1yg.mjs +0 -845
  11. package/dist/_stub.js +0 -39538
  12. package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CwP9H68T.mjs +0 -156
  13. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.js-BmoaT3cr.mjs +0 -25
  14. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DE1dtKjc.mjs +0 -26
  15. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-Bct5qZia.mjs +0 -25
  16. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BHqOw8oh.mjs +0 -25
  17. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-B3qxfxBT.mjs +0 -26
  18. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react__loadShare__.js-DwXXYAlk.mjs +0 -70
  19. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CUiVYNMY.mjs +0 -26
  20. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom__loadShare__.js-CIPmAyPi.mjs +0 -25
  21. package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-tXm1x5EZ.mjs +0 -25
  22. package/dist/addon-import-alexa.css +0 -3
  23. package/dist/hostInit-Dy4CxmqQ.mjs +0 -129
  24. package/dist/player-overlays-CcdvNfxc.mjs +0 -51
  25. package/dist/remoteEntry.js +0 -2
  26. package/dist/remoteEntry.ssr.js +0 -33
  27. package/dist/responsive-BmleF5y2.mjs +0 -125
  28. package/dist/rolldown-runtime-HEgqtunE.mjs +0 -20
  29. package/dist/scene-monitor-copy-BEZSY5WG.mjs +0 -240
  30. package/dist/square-DZHhUhzl.mjs +0 -45
  31. package/dist/trash-2-BVrrPiKB.mjs +0 -25
  32. package/dist/ui-text-B5YO4hoJ.mjs +0 -34
  33. package/dist/virtualExposes-lZT06jeg.mjs +0 -2
  34. package/dist/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-DarhMhK_.mjs +0 -2857
  35. package/dist/virtual_mf-exposes-ssr___mfe_internal__addon_import_alexa_page__remoteEntry_js-CViESMdd.mjs +0 -10
  36. package/dist/virtual_mf-exposes___mfe_internal__addon_import_alexa_page__remoteEntry_js-C3qJCjtV.mjs +0 -27
package/dist/addon.js CHANGED
@@ -8865,28 +8865,6 @@ function event(data) {
8865
8865
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
8866
8866
  var VersionOutputSchema$1 = object({ version: string() });
8867
8867
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
8868
- /**
8869
- * Identity — preserves literal types for downstream inference.
8870
- *
8871
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
8872
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
8873
- * the broader unions declared on `CustomActionSpec`'s default generics.
8874
- * Shape validity is enforced separately by the `customAction(...)` helper
8875
- * whose return type is already a `CustomActionSpec<...>`.
8876
- */
8877
- function defineCustomActions(spec) {
8878
- return spec;
8879
- }
8880
- function customAction(input, output, options) {
8881
- return {
8882
- input,
8883
- output,
8884
- kind: options?.kind ?? "query",
8885
- auth: options?.auth ?? "protected",
8886
- scope: options?.scope ?? { kind: "system" },
8887
- ...options?.caller ? { caller: "required" } : {}
8888
- };
8889
- }
8890
8868
  var StaticDirOutputSchema = object({ staticDir: string() });
8891
8869
  var VersionOutputSchema = object({ version: string() });
8892
8870
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -11231,7 +11209,7 @@ function errMsg(err) {
11231
11209
  return String(err);
11232
11210
  }
11233
11211
  //#endregion
11234
- //#region ../types/dist/composition-CZImLpOI.mjs
11212
+ //#region ../types/dist/composition-DwA0uVW5.mjs
11235
11213
  /**
11236
11214
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11237
11215
  *
@@ -16476,24 +16454,7 @@ var AddonPageDeclarationSchema = object({
16476
16454
  /** Display label for a CUSTOM `section` id (ignored for well-known ids). */
16477
16455
  sectionLabel: string().optional()
16478
16456
  });
16479
- var addonPagesSourceCapability = {
16480
- name: "addon-pages-source",
16481
- scope: "system",
16482
- mode: "collection",
16483
- internal: true,
16484
- methods: {
16485
- /**
16486
- * `internal: true` did not gate the mount (see the 2026-08-26 note on
16487
- * `data-store-provider`) — this was reachable on the AppRouter by ANY
16488
- * authenticated session at the default `auth: 'protected'`. The only
16489
- * consumer, the `addon-pages-aggregator` builtin, reads it via
16490
- * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
16491
- * only, never tRPC (per `ctx.capabilities` seeing only the local
16492
- * process). The public, enriched listing admin-ui actually consumes is
16493
- * the separate `addon-pages` cap, unaffected by this change.
16494
- */
16495
- listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
16496
- };
16457
+ method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" });
16497
16458
  var AddonHttpRouteSchema = object({
16498
16459
  method: _enum([
16499
16460
  "GET",
@@ -19451,12 +19412,13 @@ var CHANGED = {
19451
19412
  expr: "now"
19452
19413
  };
19453
19414
  function numeric(input) {
19415
+ const { alsoFrom, ...base } = input;
19454
19416
  return {
19455
- ...input,
19417
+ ...base,
19456
19418
  family: "numeric",
19457
19419
  deviceType: DeviceType.Sensor,
19458
19420
  memberDeviceType: DeviceType.Sensor,
19459
- memberDeviceTypes: [DeviceType.Sensor],
19421
+ memberDeviceTypes: [DeviceType.Sensor, ...alsoFrom?.deviceTypes ?? []],
19460
19422
  activeWhen: void 0,
19461
19423
  commandCaps: [],
19462
19424
  baseUnit: baseUnitOf(input.quantity),
@@ -19464,9 +19426,10 @@ function numeric(input) {
19464
19426
  accept: [{
19465
19427
  cap: input.outputCap,
19466
19428
  fieldPath: input.outputField
19467
- }]
19429
+ }, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
19468
19430
  };
19469
19431
  }
19432
+ var CLIMATE_DEVICES = [DeviceType.Thermostat, DeviceType.Climate];
19470
19433
  function binary(input) {
19471
19434
  return {
19472
19435
  ...input,
@@ -19492,7 +19455,14 @@ var GROUP_TEMPLATES = [
19492
19455
  quantity: "temperature",
19493
19456
  defaultAggregate: "mean",
19494
19457
  empty: "null",
19495
- companions: [NOW$2]
19458
+ companions: [NOW$2],
19459
+ alsoFrom: {
19460
+ deviceTypes: CLIMATE_DEVICES,
19461
+ accept: {
19462
+ cap: "climate-control",
19463
+ fieldPath: "currentTemp"
19464
+ }
19465
+ }
19496
19466
  }),
19497
19467
  numeric({
19498
19468
  id: "humidity",
@@ -19503,7 +19473,14 @@ var GROUP_TEMPLATES = [
19503
19473
  quantity: "humidity",
19504
19474
  defaultAggregate: "mean",
19505
19475
  empty: "null",
19506
- companions: [NOW$2]
19476
+ companions: [NOW$2],
19477
+ alsoFrom: {
19478
+ deviceTypes: CLIMATE_DEVICES,
19479
+ accept: {
19480
+ cap: "climate-control",
19481
+ fieldPath: "currentHumidity"
19482
+ }
19483
+ }
19507
19484
  }),
19508
19485
  numeric({
19509
19486
  id: "pressure",
@@ -43930,6 +43907,17 @@ var LlmProfileSchema = object({
43930
43907
  * field in its editor only when its descriptor lists it in `profileFields`. */
43931
43908
  topK: number().int().positive().optional(),
43932
43909
  maxTokens: number().int().positive().optional(),
43910
+ /** How hard the model "thinks" before it answers. Omitted = the model's own
43911
+ * default. `off` matters for structured output: a thinking model can spend
43912
+ * the whole completion on reasoning and return an empty `content`. Carried
43913
+ * only by the wires that have it — a kind offers the field in its editor
43914
+ * only when its descriptor lists it in `profileFields`. */
43915
+ reasoningEffort: _enum([
43916
+ "off",
43917
+ "low",
43918
+ "medium",
43919
+ "high"
43920
+ ]).optional(),
43933
43921
  /** Prompt context window. Advisory for cloud kinds (they enforce their own). */
43934
43922
  contextLength: number().int().positive().optional(),
43935
43923
  /** Default system prompt. A caller's `system` REPLACES it (never appends —
@@ -43968,6 +43956,7 @@ var LlmProfileFieldSchema = _enum([
43968
43956
  "topP",
43969
43957
  "topK",
43970
43958
  "maxTokens",
43959
+ "reasoningEffort",
43971
43960
  "contextLength",
43972
43961
  "systemPrompt",
43973
43962
  "timeoutMs",
@@ -45218,6 +45207,43 @@ var NotificationActionSchema = object({
45218
45207
  */
45219
45208
  mode: _enum(["navigate", "background"]).optional()
45220
45209
  });
45210
+ /** A delivery parameter value: what a notifier declared it takes. */
45211
+ var DeliveryParamValueSchema = union([
45212
+ string(),
45213
+ number(),
45214
+ boolean()
45215
+ ]);
45216
+ /**
45217
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
45218
+ * addresses a target of that kind may set it; a kind that declares nothing is
45219
+ * never handed one. The admin UI derives its control from this descriptor —
45220
+ * no editor knows a service by name.
45221
+ *
45222
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
45223
+ * run on the addon cap path).
45224
+ */
45225
+ var DeliveryParamSchema = object({
45226
+ key: string().min(1),
45227
+ label: string(),
45228
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
45229
+ labelKey: string().optional(),
45230
+ type: _enum([
45231
+ "enum",
45232
+ "number",
45233
+ "boolean",
45234
+ "string"
45235
+ ]),
45236
+ /** `enum` only: the closed set of accepted values. */
45237
+ options: array(object({
45238
+ value: string(),
45239
+ label: string()
45240
+ })).optional(),
45241
+ /** `number` only. */
45242
+ min: number().optional(),
45243
+ max: number().optional(),
45244
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
45245
+ default: DeliveryParamValueSchema.optional()
45246
+ });
45221
45247
  /**
45222
45248
  * The canonical notification. `body` is the only hard field (Apprise model).
45223
45249
  * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
@@ -45240,7 +45266,13 @@ var NotificationSchema = object({
45240
45266
  tag: string().optional(),
45241
45267
  deviceId: number().optional(),
45242
45268
  eventId: string().optional(),
45243
- metadata: record(string(), unknown()).optional()
45269
+ metadata: record(string(), unknown()).optional(),
45270
+ /**
45271
+ * Values for the parameters the target's kind DECLARES in
45272
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
45273
+ * declaration by the sender; a notifier reads only the keys it declared.
45274
+ */
45275
+ delivery: record(string(), DeliveryParamValueSchema).optional()
45244
45276
  });
45245
45277
  /** One declared native severity/priority level for a kind. */
45246
45278
  var TargetKindLevelSchema = object({
@@ -45346,6 +45378,11 @@ var TargetKindSchema = object({
45346
45378
  */
45347
45379
  destinationKeys: array(string()).optional(),
45348
45380
  supportsDiscovery: boolean(),
45381
+ /**
45382
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
45383
+ * Absent = none; the rule editor then offers nothing.
45384
+ */
45385
+ deliveryParams: array(DeliveryParamSchema).optional(),
45349
45386
  caps: TargetKindCapsSchema
45350
45387
  });
45351
45388
  /**
@@ -57659,7 +57696,9 @@ var ManifestShapeSchema = object({
57659
57696
  icon: string().optional()
57660
57697
  }).optional(),
57661
57698
  protected: boolean().optional(),
57662
- removable: boolean().optional()
57699
+ removable: boolean().optional(),
57700
+ /** Declared Integrations-page block (mirror of `AddonDeclaration.integrationSection`). */
57701
+ integrationSection: _enum(["system", "providers"]).optional()
57663
57702
  });
57664
57703
  var AddonProcessSchema = object({
57665
57704
  pid: number().optional(),
@@ -90791,6 +90830,29 @@ function rewriteSetCookie(cookie, route) {
90791
90830
  return kept.join("; ");
90792
90831
  }
90793
90832
  /**
90833
+ * Every Set-Cookie of one vendor response, rewritten. Dropping `Domain` folds cookies
90834
+ * the vendor keeps apart (`session-id` on `.amazon.it` and on `.www.amazon.it`) into
90835
+ * one, so a past-dated set that only deleted the other domain's copy would delete the
90836
+ * live one the same response just set: that deletion is dropped.
90837
+ */
90838
+ function rewriteSetCookies(cookies, route) {
90839
+ const now = Date.now();
90840
+ const liveNames = new Set(cookies.filter((c) => !isExpiredCookie(c, now)).map(cookieName));
90841
+ return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).map((c) => rewriteSetCookie(c, route));
90842
+ }
90843
+ function cookieName(cookie) {
90844
+ return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
90845
+ }
90846
+ function isExpiredCookie(cookie, now) {
90847
+ const attrs = cookie.split(";").slice(1).map((part) => part.trim());
90848
+ const maxAge = attrs.find((a) => /^max-age=/i.test(a));
90849
+ if (maxAge !== void 0) return Number(maxAge.slice(8)) <= 0;
90850
+ const expires = attrs.find((a) => /^expires=/i.test(a));
90851
+ if (expires === void 0) return false;
90852
+ const at = Date.parse(expires.slice(8));
90853
+ return Number.isFinite(at) && at <= now;
90854
+ }
90855
+ /**
90794
90856
  * A browser `Referer` that points into this proxy, turned back into the vendor URL it
90795
90857
  * stands for. A Referer that is not ours is dropped (null): it names the hub.
90796
90858
  */
@@ -90809,6 +90871,29 @@ function rewriteReferer(referer, route) {
90809
90871
  if (upstream === void 0) return null;
90810
90872
  return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
90811
90873
  }
90874
+ function decodeFormComponent(raw) {
90875
+ try {
90876
+ return decodeURIComponent(raw.replace(/\+/g, " "));
90877
+ } catch {
90878
+ return null;
90879
+ }
90880
+ }
90881
+ /**
90882
+ * An `application/x-www-form-urlencoded` body whose field VALUES that are proxy URLs of
90883
+ * this attempt (absolute or root-relative) are turned back into the vendor URL they stand
90884
+ * for — the page's hidden inputs were rewritten on the way out (`rewriteBody`), and the
90885
+ * vendor must get its own URL back. Field order and every untouched pair stay byte-identical.
90886
+ */
90887
+ function rewriteFormBody(body, route) {
90888
+ return body.split("&").map((pair) => {
90889
+ const eq = pair.indexOf("=");
90890
+ if (eq === -1) return pair;
90891
+ const value = decodeFormComponent(pair.slice(eq + 1));
90892
+ if (value === null || !/^(?:https?:\/\/|\/)/i.test(value)) return pair;
90893
+ const back = rewriteReferer(value, route);
90894
+ return back === null ? pair : `${pair.slice(0, eq + 1)}${encodeURIComponent(back)}`;
90895
+ }).join("&");
90896
+ }
90812
90897
  /** Cookies the browser holds for the hub itself must never reach a vendor. */
90813
90898
  function filterForwardedCookies(cookieHeader) {
90814
90899
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
@@ -90867,6 +90952,9 @@ function bodyKindOf(contentType) {
90867
90952
  if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
90868
90953
  return null;
90869
90954
  }
90955
+ function isFormEncoded(contentType) {
90956
+ return (firstHeader(contentType) ?? "").toLowerCase().startsWith("application/x-www-form-urlencoded");
90957
+ }
90870
90958
  function firstHeader(value) {
90871
90959
  return Array.isArray(value) ? value[0] : value;
90872
90960
  }
@@ -90881,6 +90969,26 @@ async function readRequestBody(req) {
90881
90969
  }
90882
90970
  return Buffer.concat(chunks);
90883
90971
  }
90972
+ /** The path of the Referer sent upstream, or why none was sent — the query is never logged. */
90973
+ function forwardedRefererPath(forwarded, received) {
90974
+ if (forwarded !== void 0) return URL.canParse(forwarded) ? new URL(forwarded).pathname : "unparsable";
90975
+ return received === void 0 ? "absent" : "unmapped";
90976
+ }
90977
+ /** The form targets a vendor page posts to, path only (no query), so a lost path prefix is visible. */
90978
+ function formActionPaths(html) {
90979
+ const actions = [...html.matchAll(/<form\b[^>]*\baction\s*=\s*["']([^"']*)["']/gi)].map((m) => (m[1] ?? "").split("?")[0] ?? "").slice(0, 5);
90980
+ return actions.length === 0 ? "none" : actions.join(" | ");
90981
+ }
90982
+ /** Cookie NAMES only — values are session secrets and are never logged. */
90983
+ function cookieNames(header) {
90984
+ if (header === void 0 || header.length === 0) return "none";
90985
+ return header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").join(",");
90986
+ }
90987
+ /** A Set-Cookie's name and attributes without its value, so a cookie the browser refuses is visible. */
90988
+ function setCookieShape(cookie) {
90989
+ const [first, ...attrs] = cookie.split(";").map((part) => part.trim());
90990
+ return [first?.split("=")[0] ?? "", ...attrs.filter((a) => !/^expires=/i.test(a))].join(";");
90991
+ }
90884
90992
  function sendPlain(res, status, text) {
90885
90993
  res.writeHead(status, {
90886
90994
  "content-type": "text/plain; charset=utf-8",
@@ -90997,7 +91105,7 @@ var VendorLoginProxy = class {
90997
91105
  sendPlain(res, 413, "Request too large");
90998
91106
  return;
90999
91107
  }
91000
- body = read;
91108
+ body = isFormEncoded(req.headers["content-type"]) ? Buffer.from(rewriteFormBody(read.toString("utf8"), route), "utf8") : read;
91001
91109
  }
91002
91110
  const headers = this.#requestHeaders(req, route, upstream);
91003
91111
  const upstreamRes = await this.#fetch(target, {
@@ -91011,11 +91119,16 @@ var VendorLoginProxy = class {
91011
91119
  const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
91012
91120
  const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
91013
91121
  const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
91014
- this.#logger.debug("vendor login proxy: upstream answered", { meta: {
91122
+ this.#logger.info("vendor login proxy: upstream answered", { meta: {
91015
91123
  addonId: this.#addonId,
91016
91124
  host: upstream.host,
91125
+ path: target.pathname,
91017
91126
  status: upstreamRes.status,
91018
- method
91127
+ method,
91128
+ refererPath: forwardedRefererPath(headers["referer"], req.headers["referer"]),
91129
+ formActions: kind === "html" && text !== null ? formActionPaths(text) : void 0,
91130
+ sentNames: cookieNames(headers["cookie"]),
91131
+ vendorSets: setCookies.map(setCookieShape).join(" | ")
91019
91132
  } });
91020
91133
  const settled = await attempt.evaluate({
91021
91134
  status: upstreamRes.status,
@@ -91037,7 +91150,7 @@ var VendorLoginProxy = class {
91037
91150
  upstreamRes.headers.forEach((value, name) => {
91038
91151
  if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
91039
91152
  });
91040
- if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
91153
+ if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
91041
91154
  if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
91042
91155
  out["cache-control"] = "no-store";
91043
91156
  if (text !== null && kind !== null) {
@@ -91146,59 +91259,37 @@ var AlexaBrokerEntrySchema = object({
91146
91259
  */
91147
91260
  var alexaProxyLoginSchema = object({ proxyPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(3456)).describe("Loopback port the local Amazon-login proxy listens on (never exposed)") });
91148
91261
  /**
91149
- * Echo speech settings. Speech signs in as an import broker, so the targets
91150
- * are what lives here. `email`, `password`, `region` and `session` are the OLD
91151
- * standalone login: they are read once to migrate it onto the single broker
91152
- * (see `speak/migration.ts`) and written back empty.
91153
- */
91154
- var alexaSpeakSettingsSchema = object({
91155
- email: string().default(""),
91156
- password: string().default("").describe("Legacy Amazon password").meta({ secret: true }),
91157
- region: _enum([
91158
- "NA",
91159
- "EU",
91160
- "FE"
91161
- ]).default("EU"),
91162
- session: string().default("").describe("Legacy Alexa session blob").meta({ secret: true }),
91163
- targets: array(object({
91164
- id: string(),
91165
- name: string(),
91166
- enabled: boolean(),
91167
- /** The broker (Amazon account) the Echo belongs to; '' until one is chosen. */
91168
- brokerId: string().default(""),
91169
- echoId: string(),
91170
- mode: _enum(["speak", "announce"]),
91171
- volume: number(),
91172
- respectDnd: boolean()
91173
- })).default([])
91262
+ * The Echo notification targets this addon owns. Speech signs in as an import
91263
+ * broker, so a target is only the Echo it speaks on and how.
91264
+ */
91265
+ var alexaEchoTargetSchema = object({
91266
+ id: string(),
91267
+ name: string(),
91268
+ enabled: boolean(),
91269
+ /** The broker (Amazon account) the Echo belongs to; '' until one is chosen. */
91270
+ brokerId: string().default(""),
91271
+ echoId: string(),
91272
+ mode: _enum(["speak", "announce"]),
91273
+ volume: number(),
91274
+ respectDnd: boolean()
91174
91275
  });
91175
91276
  function isRecord$3(value) {
91176
91277
  return typeof value === "object" && value !== null && !Array.isArray(value);
91177
91278
  }
91178
- /** Resolved settings. A keychain marker (an object) is an empty secret, not a password. */
91179
- function readSpeakSettings(config) {
91180
- const raw = isRecord$3(config.speak) ? config.speak : {};
91181
- const region = raw["region"];
91182
- const targets = raw["targets"];
91183
- return alexaSpeakSettingsSchema.parse({
91184
- email: typeof raw["email"] === "string" ? raw["email"] : "",
91185
- password: typeof raw["password"] === "string" ? raw["password"] : "",
91186
- session: typeof raw["session"] === "string" ? raw["session"] : "",
91187
- ...region === "NA" || region === "EU" || region === "FE" ? { region } : {},
91188
- ...Array.isArray(targets) ? { targets } : {}
91279
+ /** The stored Echo targets; a row that no longer parses is dropped, never guessed. */
91280
+ function readEchoTargets(config) {
91281
+ const raw = config.echoTargets;
91282
+ if (!Array.isArray(raw)) return [];
91283
+ return raw.flatMap((row) => {
91284
+ const parsed = alexaEchoTargetSchema.safeParse(isRecord$3(row) ? row : {});
91285
+ return parsed.success ? [parsed.data] : [];
91189
91286
  });
91190
91287
  }
91191
91288
  /** Top-level addon config — broker entries + the proxy-login settings. */
91192
91289
  var alexaAddonConfigSchema = object({
91193
91290
  brokers: array(AlexaBrokerEntrySchema).default([]),
91194
91291
  proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
91195
- speak: alexaSpeakSettingsSchema.default({
91196
- email: "",
91197
- password: "",
91198
- region: "EU",
91199
- session: "",
91200
- targets: []
91201
- })
91292
+ echoTargets: array(alexaEchoTargetSchema).default([])
91202
91293
  });
91203
91294
  /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
91204
91295
  function toRecord(value) {
@@ -132745,6 +132836,33 @@ var ALEXA_API_USER_AGENT_POSTFIX = `AlexaRemote/${{
132745
132836
  readmeFilename: "readme.md"
132746
132837
  }.version}`;
132747
132838
  //#endregion
132839
+ //#region src/alexa-region-options.ts
132840
+ var DEFAULT_LOCALE = "en-US";
132841
+ var LOCALE_BY_DOMAIN = {
132842
+ "amazon.com": "en-US",
132843
+ "amazon.it": "it-IT",
132844
+ "amazon.de": "de-DE",
132845
+ "amazon.co.uk": "en-GB",
132846
+ "amazon.fr": "fr-FR",
132847
+ "amazon.es": "es-ES",
132848
+ "amazon.ca": "en-CA",
132849
+ "amazon.com.au": "en-AU",
132850
+ "amazon.co.jp": "ja-JP",
132851
+ "amazon.in": "en-IN",
132852
+ "amazon.com.br": "pt-BR",
132853
+ "amazon.com.mx": "es-MX"
132854
+ };
132855
+ function alexaRegionOptions(amazonDomain) {
132856
+ const domain = amazonDomain.length > 0 ? amazonDomain : "amazon.com";
132857
+ const locale = LOCALE_BY_DOMAIN[domain] ?? DEFAULT_LOCALE;
132858
+ return {
132859
+ baseAmazonPage: domain,
132860
+ amazonPage: domain,
132861
+ acceptLanguage: locale,
132862
+ amazonPageProxyLanguage: locale.replace("-", "_")
132863
+ };
132864
+ }
132865
+ //#endregion
132748
132866
  //#region src/alexa-proxy-trace.ts
132749
132867
  /**
132750
132868
  * Sanitised one-line trace of the alexa-cookie2 proxy's own log lines.
@@ -132877,7 +132995,7 @@ var AlexaProxyLoginSession = class {
132877
132995
  async start() {
132878
132996
  const remote = new import_alexa_remote.default();
132879
132997
  this.#remote = remote;
132880
- const amazonPage = this.#opts.amazonDomain.length > 0 ? this.#opts.amazonDomain : "amazon.com";
132998
+ const region = alexaRegionOptions(this.#opts.amazonDomain);
132881
132999
  remote.on("cookie", () => this.#onCookieEvent(remote));
132882
133000
  remote.init({
132883
133001
  proxyOnly: true,
@@ -132885,7 +133003,7 @@ var AlexaProxyLoginSession = class {
132885
133003
  proxyListenBind: ALEXA_PROXY_LOOPBACK_HOST,
132886
133004
  proxyPort: this.#opts.proxyPort,
132887
133005
  proxyLogLevel: "warn",
132888
- amazonPage,
133006
+ ...region,
132889
133007
  apiUserAgentPostfix: ALEXA_API_USER_AGENT_POSTFIX,
132890
133008
  logger: (...args) => {
132891
133009
  const line = args.map(String).join(" ");
@@ -135151,126 +135269,6 @@ var AlexaBrokerStore = class AlexaBrokerStore {
135151
135269
  }
135152
135270
  };
135153
135271
  //#endregion
135154
- //#region src/speak/constants.ts
135155
- /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
135156
- var ALEXA_SPEAK_ADDON_ID = "import-alexa";
135157
- var ALEXA_SPEAK_KIND = "alexa-echo";
135158
- var DEVICE_REFRESH_MS = 5 * 6e4;
135159
- /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
135160
- var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
135161
- //#endregion
135162
- //#region src/speak/worker-protocol.ts
135163
- /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
135164
- var EchoDeviceSchema = object({
135165
- id: string().min(1),
135166
- name: string(),
135167
- family: string(),
135168
- online: boolean(),
135169
- kind: _enum(["device", "group"]),
135170
- dnd: boolean(),
135171
- deviceType: string(),
135172
- /** The room the account files the device under, when Amazon names one. */
135173
- room: string().optional()
135174
- });
135175
- var LoginStatusSchema = _enum(["ok", "failed"]);
135176
- var SessionStatusSchema = object({
135177
- loggedIn: boolean(),
135178
- ageSec: number().int().nonnegative(),
135179
- lastSuccessAt: number().int().nullable()
135180
- });
135181
- var TargetDeliverySchema = object({
135182
- targetId: string(),
135183
- ok: boolean(),
135184
- error: string().optional()
135185
- });
135186
- var WorkerLineSchema = union([object({
135187
- id: string(),
135188
- ok: literal(true),
135189
- result: unknown().optional()
135190
- }), object({
135191
- id: string(),
135192
- ok: literal(false),
135193
- error: string()
135194
- })]);
135195
- function requestLine(id, request) {
135196
- return `${JSON.stringify({
135197
- id,
135198
- ...request
135199
- })}\n`;
135200
- }
135201
- function parseLoginResult(result) {
135202
- const parsed = object({
135203
- status: LoginStatusSchema,
135204
- error: string().optional()
135205
- }).safeParse(result);
135206
- if (!parsed.success) return {
135207
- status: "failed",
135208
- error: "worker returned an unexpected login result"
135209
- };
135210
- return parsed.data;
135211
- }
135212
- function parseDevices(result) {
135213
- const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
135214
- if (!parsed.success) throw new Error("worker returned an unexpected device list");
135215
- return parsed.data.devices;
135216
- }
135217
- function parseDeliveries(result) {
135218
- const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
135219
- if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
135220
- return parsed.data.outcomes;
135221
- }
135222
- function parseSessionStatus(result) {
135223
- const parsed = SessionStatusSchema.safeParse(result);
135224
- if (!parsed.success) throw new Error("worker returned an unexpected session status");
135225
- return parsed.data;
135226
- }
135227
- //#endregion
135228
- //#region src/speak/actions.ts
135229
- var BrokerEchoStateSchema = _enum([
135230
- "starting",
135231
- "ready",
135232
- "broker-not-signed-in",
135233
- "failed"
135234
- ]);
135235
- var SpeakStatusSchema = object({
135236
- brokers: array(object({
135237
- id: string(),
135238
- name: string(),
135239
- state: BrokerEchoStateSchema,
135240
- error: string().optional(),
135241
- devices: array(EchoDeviceSchema),
135242
- session: SessionStatusSchema
135243
- })),
135244
- /** Targets that name no Amazon account yet (they cannot speak until one is chosen). */
135245
- unassignedTargets: number().int().nonnegative(),
135246
- /** What the old standalone login left behind, named; '' when nothing is pending. */
135247
- notice: _enum([
135248
- "",
135249
- "no-broker",
135250
- "choose-broker"
135251
- ])
135252
- });
135253
- var alexaSpeakActions = defineCustomActions({
135254
- speakStatus: customAction(object({}), SpeakStatusSchema, {
135255
- kind: "mutation",
135256
- auth: "admin"
135257
- }),
135258
- sendTest: customAction(object({
135259
- brokerId: string().min(1),
135260
- echoIds: array(string().min(1)).min(1),
135261
- mode: _enum(["speak", "announce"]),
135262
- volume: number().int().min(0).max(100),
135263
- text: string().min(1).max(450)
135264
- }), object({ outcomes: array(object({
135265
- echoId: string(),
135266
- ok: boolean(),
135267
- error: string().optional()
135268
- })) }), {
135269
- kind: "mutation",
135270
- auth: "admin"
135271
- })
135272
- });
135273
- //#endregion
135274
135272
  //#region src/speak/broker-seed.ts
135275
135273
  /**
135276
135274
  * What the Echo worker needs from an import broker to act as the same Amazon
@@ -135505,6 +135503,72 @@ function scrubSecrets(text, secrets) {
135505
135503
  return out;
135506
135504
  }
135507
135505
  //#endregion
135506
+ //#region src/speak/worker-protocol.ts
135507
+ /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
135508
+ var EchoDeviceSchema = object({
135509
+ id: string().min(1),
135510
+ name: string(),
135511
+ family: string(),
135512
+ online: boolean(),
135513
+ kind: _enum(["device", "group"]),
135514
+ dnd: boolean(),
135515
+ deviceType: string(),
135516
+ /** The room the account files the device under, when Amazon names one. */
135517
+ room: string().optional()
135518
+ });
135519
+ var LoginStatusSchema = _enum(["ok", "failed"]);
135520
+ var SessionStatusSchema = object({
135521
+ loggedIn: boolean(),
135522
+ ageSec: number().int().nonnegative(),
135523
+ lastSuccessAt: number().int().nullable()
135524
+ });
135525
+ var TargetDeliverySchema = object({
135526
+ targetId: string(),
135527
+ ok: boolean(),
135528
+ error: string().optional()
135529
+ });
135530
+ var WorkerLineSchema = union([object({
135531
+ id: string(),
135532
+ ok: literal(true),
135533
+ result: unknown().optional()
135534
+ }), object({
135535
+ id: string(),
135536
+ ok: literal(false),
135537
+ error: string()
135538
+ })]);
135539
+ function requestLine(id, request) {
135540
+ return `${JSON.stringify({
135541
+ id,
135542
+ ...request
135543
+ })}\n`;
135544
+ }
135545
+ function parseLoginResult(result) {
135546
+ const parsed = object({
135547
+ status: LoginStatusSchema,
135548
+ error: string().optional()
135549
+ }).safeParse(result);
135550
+ if (!parsed.success) return {
135551
+ status: "failed",
135552
+ error: "worker returned an unexpected login result"
135553
+ };
135554
+ return parsed.data;
135555
+ }
135556
+ function parseDevices(result) {
135557
+ const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
135558
+ if (!parsed.success) throw new Error("worker returned an unexpected device list");
135559
+ return parsed.data.devices;
135560
+ }
135561
+ function parseDeliveries(result) {
135562
+ const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
135563
+ if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
135564
+ return parsed.data.outcomes;
135565
+ }
135566
+ function parseSessionStatus(result) {
135567
+ const parsed = SessionStatusSchema.safeParse(result);
135568
+ if (!parsed.success) throw new Error("worker returned an unexpected session status");
135569
+ return parsed.data;
135570
+ }
135571
+ //#endregion
135508
135572
  //#region src/speak/broker-echo.ts
135509
135573
  var BrokerEcho = class {
135510
135574
  deps;
@@ -135633,40 +135697,13 @@ var BrokerEcho = class {
135633
135697
  }
135634
135698
  };
135635
135699
  //#endregion
135636
- //#region src/speak/migration.ts
135637
- function legacyLoginOf(settings) {
135638
- return {
135639
- email: settings.email,
135640
- password: settings.password,
135641
- session: settings.session
135642
- };
135643
- }
135644
- function planLegacyMigration(legacy, brokerIds, targets) {
135645
- const present = legacy.email.length > 0 || legacy.password.length > 0 || legacy.session.length > 0;
135646
- const only = brokerIds.length === 1 ? brokerIds[0] ?? null : null;
135647
- if (!present || only === null) return {
135648
- present,
135649
- attachTo: null,
135650
- targets
135651
- };
135652
- return {
135653
- present,
135654
- attachTo: legacy.session.length > 0 ? only : null,
135655
- targets: targets.map((row) => row.brokerId.length > 0 ? row : {
135656
- ...row,
135657
- brokerId: only
135658
- })
135659
- };
135660
- }
135661
- /** What the page must tell the operator about targets that name no account. */
135662
- function noticeFor(targets, brokerIds) {
135663
- if (!targets.some((row) => row.brokerId.length === 0)) return "";
135664
- if (brokerIds.length === 0) return "no-broker";
135665
- return brokerIds.length > 1 ? "choose-broker" : "";
135666
- }
135667
- function unassignedCount(targets) {
135668
- return targets.filter((row) => row.brokerId.length === 0).length;
135669
- }
135700
+ //#region src/speak/constants.ts
135701
+ /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
135702
+ var ALEXA_SPEAK_ADDON_ID = "import-alexa";
135703
+ var ALEXA_SPEAK_KIND = "alexa-echo";
135704
+ var DEVICE_REFRESH_MS = 5 * 6e4;
135705
+ /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
135706
+ var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
135670
135707
  //#endregion
135671
135708
  //#region src/speak/rate-limit.ts
135672
135709
  /**
@@ -135771,14 +135808,42 @@ function readString(config, key) {
135771
135808
  const value = config[key];
135772
135809
  return typeof value === "string" ? value : "";
135773
135810
  }
135774
- function readMode(config) {
135775
- return config["mode"] === "announce" ? "announce" : "speak";
135811
+ /** The rule's value for this send wins; the target's saved config is the fallback. */
135812
+ function readMode(config, delivery) {
135813
+ return (delivery?.["mode"] ?? config["mode"]) === "announce" ? "announce" : "speak";
135776
135814
  }
135777
- function readVolume(config) {
135778
- const value = config["volume"];
135815
+ function readVolume(config, delivery) {
135816
+ const ruled = delivery?.["volume"];
135817
+ const value = typeof ruled === "number" ? ruled : config["volume"];
135779
135818
  if (typeof value !== "number" || !Number.isFinite(value)) return 40;
135780
135819
  return Math.max(0, Math.min(100, Math.round(value)));
135781
135820
  }
135821
+ /**
135822
+ * The delivery parameters a rule may set for an Echo target (D938). The
135823
+ * target's own config holds the default; these override it per notification.
135824
+ */
135825
+ var DELIVERY_PARAMS = [{
135826
+ key: "mode",
135827
+ label: "Mode",
135828
+ labelKey: "import-alexa:echoTarget.s_echo.mode.label",
135829
+ type: "enum",
135830
+ options: [{
135831
+ value: "speak",
135832
+ label: "Speak (quiet hours apply)"
135833
+ }, {
135834
+ value: "announce",
135835
+ label: "Announce (chime, then speech)"
135836
+ }],
135837
+ default: "speak"
135838
+ }, {
135839
+ key: "volume",
135840
+ label: "Volume",
135841
+ labelKey: "import-alexa:echoTarget.s_echo.volume.label",
135842
+ type: "number",
135843
+ min: 0,
135844
+ max: 100,
135845
+ default: 40
135846
+ }];
135782
135847
  function respectDnd(config) {
135783
135848
  return config["respectDnd"] !== false;
135784
135849
  }
@@ -135879,6 +135944,7 @@ function alexaTargetKind(brokers) {
135879
135944
  supportsDiscovery: true,
135880
135945
  destinationKeys: ["brokerId", "echoId"],
135881
135946
  configSchema: configSchema(brokers),
135947
+ deliveryParams: DELIVERY_PARAMS,
135882
135948
  caps: {
135883
135949
  attachments: {
135884
135950
  mediaTypes: [],
@@ -136061,14 +136127,14 @@ function createAlexaNotificationProvider(ports) {
136061
136127
  renderedAs: speech.renderedAs
136062
136128
  };
136063
136129
  }
136064
- const mode = readMode(target.config);
136130
+ const mode = readMode(target.config, notification.delivery);
136065
136131
  try {
136066
136132
  const outcomes = await ports.speak({
136067
136133
  brokerId: broker.id,
136068
136134
  mode,
136069
136135
  targets: [echoId],
136070
136136
  text: speech.text,
136071
- volume: readVolume(target.config)
136137
+ volume: readVolume(target.config, notification.delivery)
136072
136138
  });
136073
136139
  const one = outcomes.find((row) => row.targetId === echoId) ?? outcomes[0];
136074
136140
  if (one === void 0 || !one.ok) {
@@ -136468,8 +136534,8 @@ function startAlexaWorker(deps) {
136468
136534
  }
136469
136535
  //#endregion
136470
136536
  //#region src/speak/controller.ts
136471
- function targetsFrom(settings) {
136472
- return settings.targets.map((row) => ({
136537
+ function targetsFrom(rows) {
136538
+ return rows.map((row) => ({
136473
136539
  id: row.id,
136474
136540
  name: row.name,
136475
136541
  kind: ALEXA_SPEAK_KIND,
@@ -136499,7 +136565,7 @@ function rowsFrom(targets) {
136499
136565
  var AlexaSpeakController = class {
136500
136566
  host;
136501
136567
  provider;
136502
- settings;
136568
+ rows;
136503
136569
  pythonPath = null;
136504
136570
  echoes = /* @__PURE__ */ new Map();
136505
136571
  timer = null;
@@ -136508,17 +136574,14 @@ var AlexaSpeakController = class {
136508
136574
  constructor(host) {
136509
136575
  this.host = host;
136510
136576
  this.now = host.now ?? (() => Date.now());
136511
- this.settings = readSpeakSettings(host.readConfig());
136577
+ this.rows = readEchoTargets(host.readConfig());
136512
136578
  this.provider = createAlexaNotificationProvider({
136513
136579
  brokers: () => this.brokerViews(),
136514
136580
  now: this.now,
136515
136581
  logger: host.logger,
136516
- loadTargets: () => targetsFrom(this.settings),
136582
+ loadTargets: () => targetsFrom(this.rows),
136517
136583
  saveTargets: (targets) => {
136518
- this.settings = {
136519
- ...this.settings,
136520
- targets: [...rowsFrom(targets)]
136521
- };
136584
+ this.rows = rowsFrom(targets);
136522
136585
  this.persist();
136523
136586
  },
136524
136587
  speak: (command) => this.utter(command.brokerId, command.mode, command.targets, command.text, command.volume)
@@ -136530,7 +136593,6 @@ var AlexaSpeakController = class {
136530
136593
  this.host.logger.warn("alexa speak: embedded Python unavailable; Echo speech will not run");
136531
136594
  return;
136532
136595
  }
136533
- await this.migrateLegacyLogin();
136534
136596
  await this.brokersChanged();
136535
136597
  this.timer = setInterval(() => {
136536
136598
  this.refreshAll();
@@ -136547,12 +136609,6 @@ var AlexaSpeakController = class {
136547
136609
  this.queue = this.queue.then(() => this.reconcile()).catch(() => void 0);
136548
136610
  return this.queue;
136549
136611
  }
136550
- handlers() {
136551
- return {
136552
- speakStatus: async () => this.status(),
136553
- sendTest: async (input) => this.sendTest(input)
136554
- };
136555
- }
136556
136612
  brokerViews() {
136557
136613
  return this.host.listBrokers().map((ref) => {
136558
136614
  const echo = this.echoes.get(ref.id);
@@ -136566,36 +136622,11 @@ var AlexaSpeakController = class {
136566
136622
  }
136567
136623
  async persist() {
136568
136624
  try {
136569
- await this.host.writeSpeak(this.settings);
136625
+ await this.host.writeTargets(this.rows);
136570
136626
  } catch (err) {
136571
- this.host.logger.warn("alexa speak: could not store settings", { meta: { error: err instanceof Error ? err.message : String(err) } });
136627
+ this.host.logger.warn("alexa speak: could not store targets", { meta: { error: err instanceof Error ? err.message : String(err) } });
136572
136628
  }
136573
136629
  }
136574
- /**
136575
- * Drain the old standalone login once. With exactly one broker its jar and
136576
- * its targets move onto that broker; otherwise the jar is dropped and the
136577
- * targets wait for the operator to choose (the status names that state).
136578
- */
136579
- async migrateLegacyLogin() {
136580
- const brokers = this.host.listBrokers();
136581
- const plan = planLegacyMigration(legacyLoginOf(this.settings), brokers.map((ref) => ref.id), this.settings.targets);
136582
- if (!plan.present) return;
136583
- if (plan.attachTo !== null) await this.host.sessions.put(plan.attachTo, this.settings.session);
136584
- this.settings = {
136585
- ...this.settings,
136586
- email: "",
136587
- password: "",
136588
- session: "",
136589
- targets: [...plan.targets]
136590
- };
136591
- await this.persist();
136592
- await this.host.clearLegacySecrets();
136593
- this.host.logger.info("alexa speak: the old standalone login was retired", { meta: {
136594
- attachedTo: plan.attachTo ?? "none",
136595
- brokers: brokers.length,
136596
- targets: plan.targets.length
136597
- } });
136598
- }
136599
136630
  makeWorker(brokerId, pythonPath) {
136600
136631
  if (this.host.startWorker !== void 0) return this.host.startWorker(brokerId, pythonPath);
136601
136632
  return startAlexaWorker({
@@ -136645,59 +136676,6 @@ var AlexaSpeakController = class {
136645
136676
  if (echo === void 0) throw new Error("no Echo session for this Amazon account");
136646
136677
  return echo.utter(mode === "announce" ? "announce" : "speak", targets, text, volume);
136647
136678
  }
136648
- async status() {
136649
- const brokers = this.host.listBrokers();
136650
- const rows = await Promise.all(brokers.map(async (ref) => {
136651
- const echo = this.echoes.get(ref.id);
136652
- return {
136653
- id: ref.id,
136654
- name: ref.name,
136655
- state: echo?.state ?? "failed",
136656
- ...echo === void 0 ? { error: "embedded Python unavailable" } : echo.error === null ? {} : { error: echo.error },
136657
- devices: [...echo?.devices ?? []],
136658
- session: echo === void 0 ? {
136659
- loggedIn: false,
136660
- ageSec: 0,
136661
- lastSuccessAt: null
136662
- } : await echo.session()
136663
- };
136664
- }));
136665
- const targets = this.settings.targets;
136666
- const ids = brokers.map((ref) => ref.id);
136667
- return {
136668
- brokers: rows,
136669
- unassignedTargets: unassignedCount(targets),
136670
- notice: noticeFor(targets, ids)
136671
- };
136672
- }
136673
- async sendTest(input) {
136674
- const outcomes = await this.utter(input.brokerId, input.mode, input.echoIds, input.text, input.volume);
136675
- const known = new Map((this.echoes.get(input.brokerId)?.devices ?? []).map((device) => [device.id, device]));
136676
- const next = [...this.settings.targets];
136677
- for (const echoId of input.echoIds) {
136678
- if (next.some((row) => row.echoId === echoId && row.brokerId === input.brokerId)) continue;
136679
- next.push({
136680
- id: `echo-${input.brokerId}-${echoId}`,
136681
- name: known.get(echoId)?.name ?? echoId,
136682
- enabled: true,
136683
- brokerId: input.brokerId,
136684
- echoId,
136685
- mode: input.mode,
136686
- volume: input.volume,
136687
- respectDnd: true
136688
- });
136689
- }
136690
- this.settings = {
136691
- ...this.settings,
136692
- targets: next
136693
- };
136694
- await this.persist();
136695
- return { outcomes: outcomes.map((row) => ({
136696
- echoId: row.targetId,
136697
- ok: row.ok,
136698
- ...row.error !== void 0 ? { error: row.error } : {}
136699
- })) };
136700
- }
136701
136679
  };
136702
136680
  //#endregion
136703
136681
  //#region src/speak/icon-routes.ts
@@ -136750,26 +136728,11 @@ var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
136750
136728
  };
136751
136729
  //#endregion
136752
136730
  //#region src/addon.ts
136753
- var SPEAK_PAGE = {
136754
- id: "speak",
136755
- label: "Speak on Echo",
136756
- icon: "megaphone",
136757
- path: "/addon/import-alexa",
136758
- remoteName: "addon_import_alexa_page",
136759
- bundle: "remoteEntry.js",
136760
- section: "administration"
136761
- };
136762
136731
  /** Default config — a fresh install starts with no accounts. */
136763
136732
  var DEFAULTS = {
136764
136733
  brokers: [],
136765
136734
  proxyLogin: { proxyPort: 3456 },
136766
- speak: {
136767
- email: "",
136768
- password: "",
136769
- region: "EU",
136770
- session: "",
136771
- targets: []
136772
- }
136735
+ echoTargets: []
136773
136736
  };
136774
136737
  /** Device types an imported Alexa appliance can materialise as. */
136775
136738
  var ALEXA_DEVICE_TYPES = [
@@ -136813,7 +136776,6 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136813
136776
  brokerStore = null;
136814
136777
  activeBrokers = [];
136815
136778
  speak = null;
136816
- secretStatePaths = ["speak.password", "speak.session"];
136817
136779
  constructor() {
136818
136780
  super({ ...DEFAULTS });
136819
136781
  }
@@ -136868,8 +136830,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136868
136830
  const speak = new AlexaSpeakController({
136869
136831
  logger: this.ctx.logger.child("speak"),
136870
136832
  readConfig: () => this.config,
136871
- writeSpeak: async (next) => {
136872
- await this.updateGlobalSettings({ speak: next });
136833
+ writeTargets: async (next) => {
136834
+ await this.updateGlobalSettings({ echoTargets: [...next] });
136873
136835
  },
136874
136836
  listBrokers: () => this.activeBrokers.map((entry) => ({
136875
136837
  id: entry.id,
@@ -136881,50 +136843,31 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136881
136843
  return entry === void 0 ? null : readBrokerSeed(this.ctx.dataDir, entry);
136882
136844
  },
136883
136845
  sessions: RecordsSpeakSessionStore.of(records),
136884
- clearLegacySecrets: async () => {
136885
- await this.updateGlobalSettingsAsOperator({ speak: {
136886
- ...this.config.speak,
136887
- password: SECRET_CLEAR,
136888
- session: SECRET_CLEAR
136889
- } });
136890
- },
136891
136846
  ensurePython: () => this.ctx.deps.ensurePython()
136892
136847
  });
136893
136848
  this.speak = speak;
136894
136849
  speak.start().catch((err) => {
136895
136850
  this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
136896
136851
  });
136897
- const pages = {
136898
- id: this.addonId,
136899
- listPages: () => [SPEAK_PAGE]
136900
- };
136901
- return {
136902
- providers: [
136903
- ...regs,
136904
- {
136905
- capability: brokerCapability,
136906
- provider: this.buildBrokerProvider()
136907
- },
136908
- {
136909
- capability: deviceAdoptionCapability,
136910
- provider: this.buildAdoptionProvider()
136911
- },
136912
- {
136913
- capability: addonRoutesCapability,
136914
- provider: buildAddonRouteProvider("import-alexa", loginRoutes)
136915
- },
136916
- {
136917
- capability: notificationOutputCapability,
136918
- provider: speak.provider
136919
- },
136920
- {
136921
- capability: addonPagesSourceCapability,
136922
- provider: pages
136923
- }
136924
- ],
136925
- customActions: alexaSpeakActions,
136926
- actionHandlers: speak.handlers()
136927
- };
136852
+ return { providers: [
136853
+ ...regs,
136854
+ {
136855
+ capability: brokerCapability,
136856
+ provider: this.buildBrokerProvider()
136857
+ },
136858
+ {
136859
+ capability: deviceAdoptionCapability,
136860
+ provider: this.buildAdoptionProvider()
136861
+ },
136862
+ {
136863
+ capability: addonRoutesCapability,
136864
+ provider: buildAddonRouteProvider("import-alexa", loginRoutes)
136865
+ },
136866
+ {
136867
+ capability: notificationOutputCapability,
136868
+ provider: speak.provider
136869
+ }
136870
+ ] };
136928
136871
  }
136929
136872
  async loadPersistedBrokers() {
136930
136873
  const store = this.brokerStore;
@@ -137315,12 +137258,10 @@ exports.StubAlexaClient = StubAlexaClient;
137315
137258
  exports.__toCommonJS = __toCommonJS;
137316
137259
  exports.alexaAddonConfigSchema = alexaAddonConfigSchema;
137317
137260
  exports.alexaConnectionSchema = alexaConnectionSchema;
137318
- exports.alexaSpeakActions = alexaSpeakActions;
137319
137261
  exports.buildAlexaApplianceCandidate = buildAlexaApplianceCandidate;
137320
137262
  exports.buildConnectionFormSchema = buildConnectionFormSchema;
137321
137263
  exports.connectionToCredentials = connectionToCredentials;
137322
137264
  exports.createAlexaClient = createAlexaClient;
137323
- exports.customActions = alexaSpeakActions;
137324
137265
  exports.mapAlexaCapabilitiesToCamStack = mapAlexaCapabilitiesToCamStack;
137325
137266
  exports.mappableAppliance = mappableAppliance;
137326
137267
  exports.settingsToAlexaConnection = settingsToAlexaConnection;