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