@camstack/addon-import-alexa 0.2.173 → 0.2.175

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,11 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.175] - 2026-11-01
11
+
12
+ ### Changed
13
+
14
+ - Alexa cookie login: the login proxy no longer forwards Amazon's upgrade-insecure-requests directive, which stopped the page from completing over http
15
+ - Echo sign-in through a broker now uses the cookies Amazon returns for the token directly, and a failed sign-in names its step
16
+
17
+ ## [0.2.174] - 2026-11-01
18
+
19
+ ### Changed
20
+
21
+ - Declares the minimum CamStack server it needs; the Addons page shows a held update as requires server >= X
22
+
10
23
  ## [0.2.173] - 2026-11-01
11
24
 
12
25
  ### Changed
13
26
 
14
27
  - The Amazon device-code sign-in's refresh token and session cookie are declared secret
28
+ - Declares the minimum CamStack server it needs; the Addons page shows a held update as requires server >= X
15
29
 
16
30
  ## [0.2.172] - 2026-10-31
17
31
 
package/dist/addon.js CHANGED
@@ -6027,7 +6027,7 @@ function number(params) {
6027
6027
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6028
6028
  }
6029
6029
  //#endregion
6030
- //#region ../types/dist/sleep-BHS4ImRX.mjs
6030
+ //#region ../types/dist/sleep-CSYuxssh.mjs
6031
6031
  /**
6032
6032
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6033
6033
  * window to float samples (D455).
@@ -9494,7 +9494,7 @@ var ProfileRtspEntrySchema = object({
9494
9494
  /**
9495
9495
  * The model weight formats a catalog entry can ship. Schema-free on purpose:
9496
9496
  * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9497
- * (the model downloader in `@camstack/system`) needs only the list and must
9497
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9498
9498
  * reach it through `@camstack/types/addon` without building the cap schema
9499
9499
  * graph (D28).
9500
9500
  */
@@ -12869,6 +12869,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12869
12869
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12870
12870
  new Set(EVENT_OWNER_TYPES);
12871
12871
  /**
12872
+ * What a published package needs from the rest of the install (D989).
12873
+ *
12874
+ * One published framework package (`@camstack/server`) carries the kernel, the
12875
+ * builtins and the capability contracts, so "which addon runs on which
12876
+ * framework" is a question of version, and the answer is DECLARED by the
12877
+ * package that knows it:
12878
+ *
12879
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12880
+ * - the server says the oldest addon it works with
12881
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12882
+ * around it (`requires.wrapper: { desktop, image }`);
12883
+ * - a wrapper (the desktop app, the container starter) says which server
12884
+ * range it can host, and reports its own kind and version.
12885
+ *
12886
+ * The publish workflow STAMPS these after the version bump
12887
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12888
+ *
12889
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12890
+ * version counts as below the minimum, and an unreadable running version is
12891
+ * named in the result (a failed read is never a pass).
12892
+ */
12893
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12894
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12895
+ object({
12896
+ /** The oldest `@camstack/server` this package runs on. */
12897
+ server: MinVersionSchema.optional(),
12898
+ /** The oldest installed version of each named addon package. */
12899
+ addons: record(string(), MinVersionSchema).optional(),
12900
+ /** The oldest shell around the server, per shell kind. */
12901
+ wrapper: object({
12902
+ desktop: MinVersionSchema.optional(),
12903
+ image: MinVersionSchema.optional()
12904
+ }).optional()
12905
+ });
12906
+ /** One unmet requirement; each names the versions on both sides. */
12907
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12908
+ object({
12909
+ kind: literal("server-too-old"),
12910
+ required: string(),
12911
+ /** `null` — the running server version could not be read. */
12912
+ running: string().nullable()
12913
+ }),
12914
+ object({
12915
+ kind: literal("addon-too-old"),
12916
+ packageName: string(),
12917
+ required: string(),
12918
+ /** `null` — the addon is not installed (or its version is unreadable). */
12919
+ running: string().nullable()
12920
+ }),
12921
+ object({
12922
+ kind: literal("wrapper-too-old"),
12923
+ wrapper: _enum(["desktop", "image"]),
12924
+ required: string(),
12925
+ /** `null` — the wrapper reported no version. */
12926
+ running: string().nullable()
12927
+ })
12928
+ ]);
12929
+ var RequiredAddonUpdateSchema = object({
12930
+ packageName: string(),
12931
+ /** The minimum the target server declares. */
12932
+ required: string(),
12933
+ /** What this node has; `null` when the addon is not installed. */
12934
+ installed: string().nullable()
12935
+ });
12936
+ /**
12872
12937
  * Which integration instance a settings form is for. An addon that serves several instances
12873
12938
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12874
12939
  * needs its connection, the default integration needs the addon's own sections.
@@ -43423,7 +43488,7 @@ var notificationOutputCapability = {
43423
43488
  * rebuilds without manual reload.
43424
43489
  *
43425
43490
  * The hub-local builtin `addon-pages-aggregator` (see
43426
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43491
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
43427
43492
  * provider. Splitting the public aggregator from the raw collection
43428
43493
  * keeps both ends in codegen — there's no hand-written
43429
43494
  * `addon-pages.router.ts` wrapper anymore.
@@ -43513,7 +43578,7 @@ var addonRoutesCapability = {
43513
43578
  */
43514
43579
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
43515
43580
  },
43516
- /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
43581
+ /** BIG PLAN 2: declarative mount hint — read by `@camstack/server/system` `buildCapRouters`. */
43517
43582
  mount: { kind: "skip" }
43518
43583
  };
43519
43584
  var ConfigTabDeclarationSchema = object({
@@ -43742,7 +43807,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
43742
43807
  * manual reload — same scheme used by `addon-pages`.
43743
43808
  *
43744
43809
  * The hub-local builtin `addon-widgets-aggregator` (see
43745
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43810
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
43746
43811
  * provider. Splitting the public aggregator from the raw collection
43747
43812
  * keeps both ends in codegen — there's no hand-written wrapper.
43748
43813
  */
@@ -61878,7 +61943,18 @@ var ServerUpdateActionResultSchema = object({
61878
61943
  * store (D877), present when the pre-flight refused the update
61879
61944
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
61880
61945
  */
61881
- blockers: array(UpdateBlockerSchema).optional()
61946
+ blockers: array(UpdateBlockerSchema).optional(),
61947
+ /**
61948
+ * The minimums the target server declares and this node does not meet
61949
+ * (D989), present when the update was refused with
61950
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61951
+ */
61952
+ unmet: array(UnmetRequirementSchema).optional(),
61953
+ /**
61954
+ * The addons this node has below the minimum the staged server declares —
61955
+ * the ones to update WITH the server. Present on an accepted update.
61956
+ */
61957
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
61882
61958
  });
61883
61959
  var LaunchOverridesSchema = object({
61884
61960
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -64422,7 +64498,9 @@ var PackageUpdateSchema = object({
64422
64498
  latestVersion: string(),
64423
64499
  category: _enum(["addon", "core"]),
64424
64500
  requiresRestart: boolean(),
64425
- isSystem: boolean()
64501
+ isSystem: boolean(),
64502
+ /** Held: what the newer version needs and this node does not meet (D989). */
64503
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
64426
64504
  });
64427
64505
  var PackageVersionInfoSchema = object({
64428
64506
  version: string(),
@@ -69722,7 +69800,7 @@ var lawnMowerControlCapability = {
69722
69800
  * clients — they reverse-connect to the hub. Exposing their interfaces
69723
69801
  * via the same surface would leak internal topology with no upside.
69724
69802
  *
69725
- * Implementation in `@camstack/system/builtins/local-network/`.
69803
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
69726
69804
  */
69727
69805
  /** Coarse classification derived from the interface name + IP range. */
69728
69806
  var InterfaceKindEnum = _enum([
@@ -71610,7 +71688,7 @@ onStatusChanged: { data: object({
71610
71688
  * jitter, and observed/peak bandwidth per device + per client.
71611
71689
  *
71612
71690
  * Implementation lives in the server's `NetworkQualityService` (thin
71613
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71691
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
71614
71692
  * The provider is registered from `trpc.router.ts` against the existing
71615
71693
  * service instance — no addon owns this state.
71616
71694
  *
@@ -73761,7 +73839,7 @@ var ptzAutotrackCapability = {
73761
73839
  * - **Native**: the camera's own firmware autotrack, registered per device by
73762
73840
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
73763
73841
  * model-dependent).
73764
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73842
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
73765
73843
  * which follows a subject with `ptz.move` pulses driven by the detection
73766
73844
  * pipeline. `defaultActive: false` — the operator binds it per camera in
73767
73845
  * the bindings UI.
@@ -133162,6 +133240,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
133162
133240
  _options.proxyLogLevel === "debug" && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Request: (modified:${modified})${customStringify(proxyReq, null, 2)}`);
133163
133241
  }
133164
133242
  function onProxyRes(proxyRes, req, res) {
133243
+ stripUpgradeInsecureRequestsHeaders(proxyRes);
133165
133244
  const url = req.originalUrl || req.url;
133166
133245
  if (url.endsWith(".ico") || url.endsWith(".js") || url.endsWith(".ttf") || url.endsWith(".svg") || url.endsWith(".png") || url.endsWith(".appcache")) return;
133167
133246
  if (url.startsWith("/ap/uedata")) return;
@@ -133254,7 +133333,18 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
133254
133333
  callbackListening = null;
133255
133334
  });
133256
133335
  }
133336
+ function stripUpgradeInsecureRequestsHeaders(proxyRes) {
133337
+ const headers = proxyRes && proxyRes.headers;
133338
+ if (!headers) return;
133339
+ for (const name of ["content-security-policy", "content-security-policy-report-only"]) {
133340
+ if (headers[name] === void 0) continue;
133341
+ const kept = (Array.isArray(headers[name]) ? headers[name] : [headers[name]]).map((v) => String(v).split(";").map((d) => d.trim()).filter((d) => d && !/^upgrade-insecure-requests$/i.test(d)).join("; ")).filter((v) => v);
133342
+ if (kept.length) headers[name] = kept.length === 1 ? kept[0] : kept;
133343
+ else delete headers[name];
133344
+ }
133345
+ }
133257
133346
  module.exports.initAmazonProxy = initAmazonProxy;
133347
+ module.exports.stripUpgradeInsecureRequestsHeaders = stripUpgradeInsecureRequestsHeaders;
133258
133348
  }));
133259
133349
  //#endregion
133260
133350
  //#region ../../node_modules/alexa-cookie2/alexa-cookie.js
@@ -140876,7 +140966,7 @@ var BrokerEcho = class {
140876
140966
  try {
140877
140967
  this.currentDevices = [...parseDevices(await this.deps.worker.request({ op: "devices" }))];
140878
140968
  } catch (err) {
140879
- this.deps.logger.warn("alexa speak: device refresh failed", {
140969
+ this.deps.logger.warn("alexa speak: step 3 of 3, device list failed after a good sign-in", {
140880
140970
  tags: { brokerId: this.deps.brokerId },
140881
140971
  meta: { error: err instanceof Error ? err.message : String(err) }
140882
140972
  });
@@ -141406,6 +141496,7 @@ function createAlexaNotificationProvider(ports) {
141406
141496
  return found;
141407
141497
  },
141408
141498
  async discoverTargets(input) {
141499
+ for (const broker of ports.brokers()) if (!broker.ready) ports.logger.warn(`alexa echo discovery: no Echo listed for '${broker.name}', the Amazon account is not signed in for this broker`, { tags: { brokerId: broker.id } });
141409
141500
  const named = readString(input.config ?? {}, "brokerId");
141410
141501
  if (named.length > 0) {
141411
141502
  const owner = ports.brokers().find((broker) => broker.id === named);
package/dist/addon.mjs CHANGED
@@ -6025,7 +6025,7 @@ function number(params) {
6025
6025
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6026
6026
  }
6027
6027
  //#endregion
6028
- //#region ../types/dist/sleep-BHS4ImRX.mjs
6028
+ //#region ../types/dist/sleep-CSYuxssh.mjs
6029
6029
  /**
6030
6030
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6031
6031
  * window to float samples (D455).
@@ -9492,7 +9492,7 @@ var ProfileRtspEntrySchema = object({
9492
9492
  /**
9493
9493
  * The model weight formats a catalog entry can ship. Schema-free on purpose:
9494
9494
  * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9495
- * (the model downloader in `@camstack/system`) needs only the list and must
9495
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9496
9496
  * reach it through `@camstack/types/addon` without building the cap schema
9497
9497
  * graph (D28).
9498
9498
  */
@@ -12867,6 +12867,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12867
12867
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12868
12868
  new Set(EVENT_OWNER_TYPES);
12869
12869
  /**
12870
+ * What a published package needs from the rest of the install (D989).
12871
+ *
12872
+ * One published framework package (`@camstack/server`) carries the kernel, the
12873
+ * builtins and the capability contracts, so "which addon runs on which
12874
+ * framework" is a question of version, and the answer is DECLARED by the
12875
+ * package that knows it:
12876
+ *
12877
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12878
+ * - the server says the oldest addon it works with
12879
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12880
+ * around it (`requires.wrapper: { desktop, image }`);
12881
+ * - a wrapper (the desktop app, the container starter) says which server
12882
+ * range it can host, and reports its own kind and version.
12883
+ *
12884
+ * The publish workflow STAMPS these after the version bump
12885
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12886
+ *
12887
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12888
+ * version counts as below the minimum, and an unreadable running version is
12889
+ * named in the result (a failed read is never a pass).
12890
+ */
12891
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12892
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12893
+ object({
12894
+ /** The oldest `@camstack/server` this package runs on. */
12895
+ server: MinVersionSchema.optional(),
12896
+ /** The oldest installed version of each named addon package. */
12897
+ addons: record(string(), MinVersionSchema).optional(),
12898
+ /** The oldest shell around the server, per shell kind. */
12899
+ wrapper: object({
12900
+ desktop: MinVersionSchema.optional(),
12901
+ image: MinVersionSchema.optional()
12902
+ }).optional()
12903
+ });
12904
+ /** One unmet requirement; each names the versions on both sides. */
12905
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12906
+ object({
12907
+ kind: literal("server-too-old"),
12908
+ required: string(),
12909
+ /** `null` — the running server version could not be read. */
12910
+ running: string().nullable()
12911
+ }),
12912
+ object({
12913
+ kind: literal("addon-too-old"),
12914
+ packageName: string(),
12915
+ required: string(),
12916
+ /** `null` — the addon is not installed (or its version is unreadable). */
12917
+ running: string().nullable()
12918
+ }),
12919
+ object({
12920
+ kind: literal("wrapper-too-old"),
12921
+ wrapper: _enum(["desktop", "image"]),
12922
+ required: string(),
12923
+ /** `null` — the wrapper reported no version. */
12924
+ running: string().nullable()
12925
+ })
12926
+ ]);
12927
+ var RequiredAddonUpdateSchema = object({
12928
+ packageName: string(),
12929
+ /** The minimum the target server declares. */
12930
+ required: string(),
12931
+ /** What this node has; `null` when the addon is not installed. */
12932
+ installed: string().nullable()
12933
+ });
12934
+ /**
12870
12935
  * Which integration instance a settings form is for. An addon that serves several instances
12871
12936
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12872
12937
  * needs its connection, the default integration needs the addon's own sections.
@@ -43421,7 +43486,7 @@ var notificationOutputCapability = {
43421
43486
  * rebuilds without manual reload.
43422
43487
  *
43423
43488
  * The hub-local builtin `addon-pages-aggregator` (see
43424
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43489
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
43425
43490
  * provider. Splitting the public aggregator from the raw collection
43426
43491
  * keeps both ends in codegen — there's no hand-written
43427
43492
  * `addon-pages.router.ts` wrapper anymore.
@@ -43511,7 +43576,7 @@ var addonRoutesCapability = {
43511
43576
  */
43512
43577
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
43513
43578
  },
43514
- /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
43579
+ /** BIG PLAN 2: declarative mount hint — read by `@camstack/server/system` `buildCapRouters`. */
43515
43580
  mount: { kind: "skip" }
43516
43581
  };
43517
43582
  var ConfigTabDeclarationSchema = object({
@@ -43740,7 +43805,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
43740
43805
  * manual reload — same scheme used by `addon-pages`.
43741
43806
  *
43742
43807
  * The hub-local builtin `addon-widgets-aggregator` (see
43743
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43808
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
43744
43809
  * provider. Splitting the public aggregator from the raw collection
43745
43810
  * keeps both ends in codegen — there's no hand-written wrapper.
43746
43811
  */
@@ -61876,7 +61941,18 @@ var ServerUpdateActionResultSchema = object({
61876
61941
  * store (D877), present when the pre-flight refused the update
61877
61942
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
61878
61943
  */
61879
- blockers: array(UpdateBlockerSchema).optional()
61944
+ blockers: array(UpdateBlockerSchema).optional(),
61945
+ /**
61946
+ * The minimums the target server declares and this node does not meet
61947
+ * (D989), present when the update was refused with
61948
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61949
+ */
61950
+ unmet: array(UnmetRequirementSchema).optional(),
61951
+ /**
61952
+ * The addons this node has below the minimum the staged server declares —
61953
+ * the ones to update WITH the server. Present on an accepted update.
61954
+ */
61955
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
61880
61956
  });
61881
61957
  var LaunchOverridesSchema = object({
61882
61958
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -64420,7 +64496,9 @@ var PackageUpdateSchema = object({
64420
64496
  latestVersion: string(),
64421
64497
  category: _enum(["addon", "core"]),
64422
64498
  requiresRestart: boolean(),
64423
- isSystem: boolean()
64499
+ isSystem: boolean(),
64500
+ /** Held: what the newer version needs and this node does not meet (D989). */
64501
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
64424
64502
  });
64425
64503
  var PackageVersionInfoSchema = object({
64426
64504
  version: string(),
@@ -69720,7 +69798,7 @@ var lawnMowerControlCapability = {
69720
69798
  * clients — they reverse-connect to the hub. Exposing their interfaces
69721
69799
  * via the same surface would leak internal topology with no upside.
69722
69800
  *
69723
- * Implementation in `@camstack/system/builtins/local-network/`.
69801
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
69724
69802
  */
69725
69803
  /** Coarse classification derived from the interface name + IP range. */
69726
69804
  var InterfaceKindEnum = _enum([
@@ -71608,7 +71686,7 @@ onStatusChanged: { data: object({
71608
71686
  * jitter, and observed/peak bandwidth per device + per client.
71609
71687
  *
71610
71688
  * Implementation lives in the server's `NetworkQualityService` (thin
71611
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71689
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
71612
71690
  * The provider is registered from `trpc.router.ts` against the existing
71613
71691
  * service instance — no addon owns this state.
71614
71692
  *
@@ -73759,7 +73837,7 @@ var ptzAutotrackCapability = {
73759
73837
  * - **Native**: the camera's own firmware autotrack, registered per device by
73760
73838
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
73761
73839
  * model-dependent).
73762
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73840
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
73763
73841
  * which follows a subject with `ptz.move` pulses driven by the detection
73764
73842
  * pipeline. `defaultActive: false` — the operator binds it per camera in
73765
73843
  * the bindings UI.
@@ -133160,6 +133238,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
133160
133238
  _options.proxyLogLevel === "debug" && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Request: (modified:${modified})${customStringify(proxyReq, null, 2)}`);
133161
133239
  }
133162
133240
  function onProxyRes(proxyRes, req, res) {
133241
+ stripUpgradeInsecureRequestsHeaders(proxyRes);
133163
133242
  const url = req.originalUrl || req.url;
133164
133243
  if (url.endsWith(".ico") || url.endsWith(".js") || url.endsWith(".ttf") || url.endsWith(".svg") || url.endsWith(".png") || url.endsWith(".appcache")) return;
133165
133244
  if (url.startsWith("/ap/uedata")) return;
@@ -133252,7 +133331,18 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
133252
133331
  callbackListening = null;
133253
133332
  });
133254
133333
  }
133334
+ function stripUpgradeInsecureRequestsHeaders(proxyRes) {
133335
+ const headers = proxyRes && proxyRes.headers;
133336
+ if (!headers) return;
133337
+ for (const name of ["content-security-policy", "content-security-policy-report-only"]) {
133338
+ if (headers[name] === void 0) continue;
133339
+ const kept = (Array.isArray(headers[name]) ? headers[name] : [headers[name]]).map((v) => String(v).split(";").map((d) => d.trim()).filter((d) => d && !/^upgrade-insecure-requests$/i.test(d)).join("; ")).filter((v) => v);
133340
+ if (kept.length) headers[name] = kept.length === 1 ? kept[0] : kept;
133341
+ else delete headers[name];
133342
+ }
133343
+ }
133255
133344
  module.exports.initAmazonProxy = initAmazonProxy;
133345
+ module.exports.stripUpgradeInsecureRequestsHeaders = stripUpgradeInsecureRequestsHeaders;
133256
133346
  }));
133257
133347
  //#endregion
133258
133348
  //#region ../../node_modules/alexa-cookie2/alexa-cookie.js
@@ -140874,7 +140964,7 @@ var BrokerEcho = class {
140874
140964
  try {
140875
140965
  this.currentDevices = [...parseDevices(await this.deps.worker.request({ op: "devices" }))];
140876
140966
  } catch (err) {
140877
- this.deps.logger.warn("alexa speak: device refresh failed", {
140967
+ this.deps.logger.warn("alexa speak: step 3 of 3, device list failed after a good sign-in", {
140878
140968
  tags: { brokerId: this.deps.brokerId },
140879
140969
  meta: { error: err instanceof Error ? err.message : String(err) }
140880
140970
  });
@@ -141404,6 +141494,7 @@ function createAlexaNotificationProvider(ports) {
141404
141494
  return found;
141405
141495
  },
141406
141496
  async discoverTargets(input) {
141497
+ for (const broker of ports.brokers()) if (!broker.ready) ports.logger.warn(`alexa echo discovery: no Echo listed for '${broker.name}', the Amazon account is not signed in for this broker`, { tags: { brokerId: broker.id } });
141407
141498
  const named = readString(input.config ?? {}, "brokerId");
141408
141499
  if (named.length > 0) {
141409
141500
  const owner = ports.brokers().find((broker) => broker.id === named);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.173",
3
+ "version": "0.2.175",
4
4
  "description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -66,7 +66,10 @@
66
66
  "requirements": "./python/requirements.txt"
67
67
  }
68
68
  }
69
- ]
69
+ ],
70
+ "requires": {
71
+ "server": ">=1.2.723"
72
+ }
70
73
  },
71
74
  "files": [
72
75
  "dist",
@@ -119,14 +119,32 @@ class AlexaPyGateway:
119
119
  bare_domain(domain), label, "", session_output(session_path), False, oauth=oauth
120
120
  )
121
121
  self._login = login
122
+ cookies: dict = {}
122
123
  if refresh_token != "":
123
124
  exchanged = await _exchange_refresh_token(login, device_app_name)
124
125
  if not exchanged:
125
- return {"status": "failed", "error": "Amazon did not accept the broker's token"}
126
+ return {
127
+ "status": "failed",
128
+ "error": "step 1 of 3, token exchange: Amazon refused the broker's token",
129
+ }
126
130
  await login.save_cookiefile()
127
- cookies = await login.load_cookie()
128
- if not cookies:
129
- return {"status": "failed", "error": "no Amazon session to start from"}
131
+ # The exchange put its cookies in the live session; use them as they are. Reading
132
+ # them back through the cookie file filters them by host and can come back empty.
133
+ cookies = _session_cookies(login)
134
+ if not cookies:
135
+ cookies = await login.load_cookie() or {}
136
+ if not cookies:
137
+ return {
138
+ "status": "failed",
139
+ "error": "step 2 of 3, cookies: the token exchange was accepted but returned no cookies",
140
+ }
141
+ else:
142
+ cookies = await login.load_cookie() or {}
143
+ if not cookies:
144
+ return {
145
+ "status": "failed",
146
+ "error": "step 2 of 3, cookies: no stored Amazon session and no broker token to start from",
147
+ }
130
148
  await login.login(cookies=cookies)
131
149
  await self._persist_session()
132
150
  return self._status_result()
@@ -213,6 +231,22 @@ def _import_alexapy():
213
231
  return AlexaLogin, AlexaAPI
214
232
 
215
233
 
234
+ def _session_cookies(login) -> dict:
235
+ """Every cookie the login's live session holds, by name. Values only leave this process
236
+ inside the library; nothing here logs them."""
237
+ session = getattr(login, "_session", None)
238
+ jar = getattr(session, "cookie_jar", None)
239
+ found: dict = {}
240
+ if jar is None:
241
+ return found
242
+ for morsel in jar:
243
+ name = getattr(morsel, "key", None)
244
+ value = getattr(morsel, "value", None)
245
+ if isinstance(name, str) and isinstance(value, str) and name != "":
246
+ found[name] = value.strip('"')
247
+ return found
248
+
249
+
216
250
  async def _exchange_refresh_token(login, device_app_name: str) -> bool:
217
251
  """Trade the broker's refresh token for cookies, under the name that token was registered with."""
218
252
  import alexapy.alexalogin as module
@@ -19,7 +19,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
19
19
  from alexa_gateway import AlexaPyGateway, bare_domain, normalize_device, session_output
20
20
 
21
21
 
22
- def install_fake_alexapy(exchange_ok: bool = True, stored_cookies: dict | None = None):
22
+ def install_fake_alexapy(
23
+ exchange_ok: bool = True, stored_cookies: dict | None = None, file_readback: bool = True
24
+ ):
23
25
  """Put a fake `alexapy` (and `alexapy.alexalogin`) in sys.modules; return the call log."""
24
26
  calls: dict = {"exchange_app_names": [], "logins": [], "saved": 0}
25
27
  module = types.ModuleType("alexapy.alexalogin")
@@ -34,10 +36,13 @@ def install_fake_alexapy(exchange_ok: bool = True, stored_cookies: dict | None =
34
36
  calls["jar_path"] = outputpath(".storage/alexa_media.x.pickle")
35
37
  self.status = {"login_successful": True}
36
38
  self.refresh_token = (oauth or {}).get("refresh_token")
39
+ self._session = types.SimpleNamespace(cookie_jar=[])
37
40
 
38
41
  async def exchange_token_for_cookies(self):
39
42
  calls["exchange_app_names"].append(module.APP_NAME)
40
43
  calls["exchanged_token"] = self.refresh_token
44
+ if exchange_ok:
45
+ self._session.cookie_jar.append(types.SimpleNamespace(key="session-id", value="1"))
41
46
  return exchange_ok
42
47
 
43
48
  async def save_cookiefile(self):
@@ -45,7 +50,9 @@ def install_fake_alexapy(exchange_ok: bool = True, stored_cookies: dict | None =
45
50
  Path(calls["jar_path"]).write_bytes(b"jar")
46
51
 
47
52
  async def load_cookie(self):
48
- return {"session-id": "1"} if (exchange_ok or stored_cookies) else {}
53
+ if stored_cookies:
54
+ return stored_cookies
55
+ return {"session-id": "1"} if (exchange_ok and file_readback) else {}
49
56
 
50
57
  async def login(self, cookies=None, data=None):
51
58
  calls["logins"].append(cookies)
@@ -68,6 +75,10 @@ def run(coro):
68
75
  return asyncio.run(coro)
69
76
 
70
77
 
78
+ async def _accept_without_cookies(self):
79
+ return True
80
+
81
+
71
82
  class GatewayTests(unittest.TestCase):
72
83
  def tearDown(self) -> None:
73
84
  remove_fake_alexapy()
@@ -113,6 +124,27 @@ class GatewayTests(unittest.TestCase):
113
124
  self.assertEqual(calls["logins"], [])
114
125
  self.assertEqual(module.APP_NAME, "Alexa Media Player")
115
126
 
127
+ def test_cookies_from_the_exchange_are_used_when_the_file_reads_back_empty(self) -> None:
128
+ calls, _ = install_fake_alexapy(file_readback=False)
129
+ with tempfile.TemporaryDirectory() as directory:
130
+ result = run(
131
+ AlexaPyGateway().login_broker("alexa_002", "t", "", "amazon.com", "NA", str(Path(directory) / "s"))
132
+ )
133
+ self.assertEqual(result, {"status": "ok"})
134
+ self.assertEqual(calls["logins"], [{"session-id": "1"}])
135
+
136
+ def test_an_exchange_that_yields_no_cookies_names_its_step(self) -> None:
137
+ calls, _ = install_fake_alexapy(file_readback=False)
138
+ sys.modules["alexapy"].AlexaLogin.exchange_token_for_cookies = _accept_without_cookies
139
+ with tempfile.TemporaryDirectory() as directory:
140
+ result = run(
141
+ AlexaPyGateway().login_broker("alexa_002", "t", "", "amazon.com", "NA", str(Path(directory) / "s"))
142
+ )
143
+ self.assertEqual(result["status"], "failed")
144
+ self.assertIn("cookies", result["error"])
145
+ self.assertIn("exchange was accepted", result["error"])
146
+ self.assertEqual(calls["logins"], [])
147
+
116
148
  def test_with_no_token_only_the_stored_jar_is_tried(self) -> None:
117
149
  calls, _ = install_fake_alexapy(stored_cookies={"session-id": "1"})
118
150
  with tempfile.TemporaryDirectory() as directory: