@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 +14 -0
- package/dist/addon.js +102 -11
- package/dist/addon.mjs +102 -11
- package/package.json +5 -2
- package/python/alexa_gateway.py +38 -4
- package/python/test_alexa_gateway.py +34 -2
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-
|
|
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
|
|
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-
|
|
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
|
|
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.
|
|
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",
|
package/python/alexa_gateway.py
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|