@mega-yfue/eufy-sdk 0.2.0-beta.0 → 0.2.0-beta.2
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/README.md +17 -41
- package/dist/client/eufy-mega.d.ts +28 -1
- package/dist/index.js +112 -44
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +44 -16
- package/dist/model/capabilities/index.d.ts +2 -2
- package/dist/transport/http/mega-client.d.ts +10 -2
- package/dist/transport/mqtt/app-client-id.d.ts +9 -3
- package/dist/transport/mqtt/command-router.d.ts +0 -3
- package/dist/transport/mqtt/secure-mqtt.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,11 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> [!IMPORTANT]
|
|
27
|
-
> **Not usable yet.** This repository is being set up: the scaffold, the CI gate and the docs pipeline
|
|
28
|
-
> are in place, the library source lands next. `0.0.1` exists on npm only to prove the release
|
|
29
|
-
> pipeline works — it is an empty package. Wait for `0.1.0`.
|
|
30
|
-
|
|
31
26
|
## What it is
|
|
32
27
|
|
|
33
28
|
A TypeScript SDK for the Anker eufy cloud that the current eufy app speaks. It logs in (captcha and 2FA
|
|
@@ -37,14 +32,18 @@ you drive through a **typed, fluent API**:
|
|
|
37
32
|
```ts
|
|
38
33
|
const dev = await eufy.getDevice(sn);
|
|
39
34
|
|
|
40
|
-
const stored = await dev.camera()?.snapshotStored?.(); // latest retained push JPEG
|
|
41
|
-
const fresh = await dev.camera()?.snapshotLive(); // explicit fresh live capture
|
|
42
|
-
await dev.
|
|
43
|
-
await dev.light()?.setBrightness(60);
|
|
35
|
+
const stored = await dev.camera?.()?.snapshotStored?.(); // latest retained push JPEG
|
|
36
|
+
const fresh = await dev.camera?.()?.snapshotLive?.(); // explicit fresh live capture
|
|
37
|
+
await dev.ptz?.()?.rotate(PtzDirection.left);
|
|
38
|
+
await dev.light?.()?.setBrightness(60);
|
|
44
39
|
|
|
45
|
-
eufy.on("motion", (e) => console.log(e.
|
|
40
|
+
eufy.on("motion", (e) => console.log(e.deviceSn, "saw something"));
|
|
46
41
|
```
|
|
47
42
|
|
|
43
|
+
Accessors and methods are optional because both are **evidence-gated**: a device exposes exactly the
|
|
44
|
+
features it reported, so the optionality states that one may be absent. An **unverified write path
|
|
45
|
+
throws** rather than send a frame it cannot stand behind.
|
|
46
|
+
|
|
48
47
|
Realtime arrives over **P2P** (cameras and HomeBases), **secure MQTT** (appliances) and **push**
|
|
49
48
|
(events), all surfaced as typed semantic events. Live **video streaming** works, with one shared pull
|
|
50
49
|
fanned out to every consumer.
|
|
@@ -56,12 +55,10 @@ code path and an unlisted or future device resolves the same way as a known one.
|
|
|
56
55
|
## Install
|
|
57
56
|
|
|
58
57
|
```bash
|
|
59
|
-
npm install @mega-yfue/eufy-sdk
|
|
58
|
+
npm install @mega-yfue/eufy-sdk # latest stable
|
|
59
|
+
npm install @mega-yfue/eufy-sdk@beta # the prerelease of the version in review
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Releases go to **npmjs**, published from CI with provenance. Prereleases ship on the `beta` channel
|
|
63
|
-
(`npm install @mega-yfue/eufy-sdk@beta`) while a version is still under review.
|
|
64
|
-
|
|
65
62
|
**Node.js ≥ 24.5.0** is required, not just recommended (see [`.nvmrc`](./.nvmrc)). `ffmpeg` is
|
|
66
63
|
optional — only the live JPEG snapshot and one-shot mp4 record paths use it,
|
|
67
64
|
and a host that ships its own build names it with `new EufyMega({ ffmpegPath })` rather than needing
|
|
@@ -69,34 +66,13 @@ one on `PATH`.
|
|
|
69
66
|
|
|
70
67
|
## Documentation
|
|
71
68
|
|
|
72
|
-
The guides at **<https://mega-yfue.github.io/>**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
in [`examples/`](./examples/).
|
|
76
|
-
|
|
77
|
-
## Design
|
|
78
|
-
|
|
79
|
-
Four layers, one dependency direction — `core` → `transport` → `model` → `client`:
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
src/
|
|
83
|
-
core/ shared floor: crypto, cross-layer contracts, value types, session store
|
|
84
|
-
transport/ every byte-on-a-wire module: http, mqtt, p2p, push, tuya
|
|
85
|
-
model/ Device + one self-contained module per capability
|
|
86
|
-
client/ the facade: login, device registry, event fan-out
|
|
87
|
-
index.ts public surface — one `export *` per layer barrel
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Capability ↔ transport decorrelation is a hard, CI-enforced rule:** `model/` never imports
|
|
91
|
-
`transport/` and vice versa. A capability describes what a value MEANS; a transport moves bytes and
|
|
92
|
-
never names a feature. Anything genuinely shared is a contract in `core/`. That rule and the rest of
|
|
93
|
-
the code practice are in [AGENTS.md](./AGENTS.md).
|
|
94
|
-
|
|
95
|
-
Three runtime dependencies — `mqtt`, `protobufjs`, `jpeg-js` — and that is deliberate. HTTP is native
|
|
96
|
-
`fetch`, hashing and ciphers are `node:crypto`, 64-bit integers are `BigInt`.
|
|
69
|
+
The guides at **<https://mega-yfue.github.io/>** cover installing and logging in, devices and
|
|
70
|
+
capabilities, events and realtime transports, consuming live media, and the generated API reference.
|
|
71
|
+
Runnable, typechecked samples live in [`examples/`](./examples/).
|
|
97
72
|
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
The architecture — four layers with one dependency direction, and the CI-enforced rule that keeps
|
|
74
|
+
capabilities and transports from importing each other — is in [AGENTS.md](./AGENTS.md), with the rest of
|
|
75
|
+
the code practice.
|
|
100
76
|
|
|
101
77
|
## Develop
|
|
102
78
|
|
|
@@ -707,11 +707,38 @@ export declare class EufyMega extends EventEmitter {
|
|
|
707
707
|
* {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
|
|
708
708
|
* lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
|
|
709
709
|
*
|
|
710
|
+
* Identified by a client id built from this client's `openudid`, not by the certificate's name:
|
|
711
|
+
* that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
|
|
712
|
+
* duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
|
|
713
|
+
* the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
|
|
714
|
+
* the broker grants on the `eufy_security` credential.
|
|
715
|
+
*
|
|
716
|
+
* It separates two clients exactly as far as their `openudid` does: a caller that supplies none
|
|
717
|
+
* gets the value derived from the account, which every such client shares — the same condition
|
|
718
|
+
* under which their logins already displace each other (`MegaClientConfig.openudid`).
|
|
719
|
+
*
|
|
720
|
+
* A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
|
|
721
|
+
* none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
|
|
722
|
+
* that fails for any other reason rejects the bring-up, which clears its memo in
|
|
723
|
+
* {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
|
|
724
|
+
* socket must not be what moves a process onto the shared name for good.
|
|
725
|
+
*/
|
|
726
|
+
private startMqtt;
|
|
727
|
+
/**
|
|
728
|
+
* Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
|
|
729
|
+
* omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
|
|
730
|
+
*
|
|
731
|
+
* The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
|
|
732
|
+
* broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
|
|
733
|
+
* the connect it just completed is announced here instead. Errors raised while connecting are held
|
|
734
|
+
* only to keep an emitter without an `error` listener from throwing, and are reported once the
|
|
735
|
+
* transport is one a consumer owns.
|
|
736
|
+
*
|
|
710
737
|
* The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
|
|
711
738
|
* runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
|
|
712
739
|
* transport — so a capability reads tags without owning any framing.
|
|
713
740
|
*/
|
|
714
|
-
private
|
|
741
|
+
private connectMqtt;
|
|
715
742
|
/**
|
|
716
743
|
* Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
|
|
717
744
|
* unreachable device must not stop the rest of the roster from coming up).
|
package/dist/index.js
CHANGED
|
@@ -967,7 +967,15 @@ var MegaHttpClient = class {
|
|
|
967
967
|
* restored-session short-circuit must NOT treat it as a usable session. Cleared on Ok/reset. */
|
|
968
968
|
pending2fa = false;
|
|
969
969
|
tokenExpiresAt = 0;
|
|
970
|
-
/**
|
|
970
|
+
/**
|
|
971
|
+
* Stable per-install device id: `openudid` as configured, as restored from the session store, or as
|
|
972
|
+
* derived from the ACCOUNT when neither supplied one — two clients that configure none therefore
|
|
973
|
+
* share it, and are one install as far as everything keyed on this is concerned.
|
|
974
|
+
*
|
|
975
|
+
* Two things are keyed on it, and both fail the same way when it is shared: the auth token is bound
|
|
976
|
+
* to it, so each login displaces the other's session, and the secure-MQTT client id is built from it,
|
|
977
|
+
* so each connection evicts the other's channel.
|
|
978
|
+
*/
|
|
971
979
|
openudid;
|
|
972
980
|
/** The device model reported to the cloud (explicit `phoneModel`, else a stable random one). */
|
|
973
981
|
phoneModel;
|
|
@@ -1805,6 +1813,9 @@ function parseSecureTopic(topic) {
|
|
|
1805
1813
|
|
|
1806
1814
|
// dist/transport/mqtt/secure-mqtt.js
|
|
1807
1815
|
var SUBACK_FAILURE = 128;
|
|
1816
|
+
function isNotAuthorized(err) {
|
|
1817
|
+
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
1818
|
+
}
|
|
1808
1819
|
var SecureMqtt = class extends EventEmitter {
|
|
1809
1820
|
kind = "smqtt";
|
|
1810
1821
|
client;
|
|
@@ -1864,6 +1875,7 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
1864
1875
|
}
|
|
1865
1876
|
this.emit("connect");
|
|
1866
1877
|
});
|
|
1878
|
+
client.on("connect", () => this.logger.info(`[smqtt] connected (${c.app_name ?? "default"})`));
|
|
1867
1879
|
client.on("reconnect", () => this.logger.warn("[smqtt] reconnecting"));
|
|
1868
1880
|
client.on("close", () => this.emit("disconnect", "close"));
|
|
1869
1881
|
client.on("error", (err) => {
|
|
@@ -1932,6 +1944,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
1932
1944
|
}
|
|
1933
1945
|
};
|
|
1934
1946
|
|
|
1947
|
+
// dist/transport/mqtt/app-client-id.js
|
|
1948
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1949
|
+
function buildAppShapedClientId(input) {
|
|
1950
|
+
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
1951
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
1952
|
+
}
|
|
1953
|
+
function mqttUuidFrom(installId) {
|
|
1954
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1935
1957
|
// dist/core/util.js
|
|
1936
1958
|
var Timer = class {
|
|
1937
1959
|
handle;
|
|
@@ -6868,6 +6890,16 @@ var KEYPAD = {
|
|
|
6868
6890
|
// dist/model/capabilities/arming.js
|
|
6869
6891
|
var STATION_CHANNEL2 = 255;
|
|
6870
6892
|
var ArmingMode = {
|
|
6893
|
+
/** Armed — full protection, nobody home (wire value 0). */
|
|
6894
|
+
away: "away",
|
|
6895
|
+
/** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
|
|
6896
|
+
home: "home",
|
|
6897
|
+
/** Custom 1 — a user-defined posture configured in the app (wire value 3). */
|
|
6898
|
+
custom1: "custom1",
|
|
6899
|
+
/** Disarmed — no alarms; sensors still report state (wire value 63). */
|
|
6900
|
+
disarmed: "disarmed"
|
|
6901
|
+
};
|
|
6902
|
+
var AlarmDelayMode = {
|
|
6871
6903
|
/** Armed — full protection, nobody home (wire value 0). */
|
|
6872
6904
|
away: "away",
|
|
6873
6905
|
/** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
|
|
@@ -6883,10 +6915,11 @@ var ARMING_CMD = {
|
|
|
6883
6915
|
* mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
|
|
6884
6916
|
*
|
|
6885
6917
|
* ⚠️ Only 3 of the 9 modes were exercised in that capture — `mode_type` 0 (away), 63 (disarmed), 1
|
|
6886
|
-
* (home), all confirmed byte-exact
|
|
6887
|
-
*
|
|
6888
|
-
*
|
|
6889
|
-
* them. See `ARMING_MODE_WIRE` for the
|
|
6918
|
+
* (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
|
|
6919
|
+
* within ~5s of the write. `custom1` 3 joined {@link ArmingMode} on a live confirmation rather than a
|
|
6920
|
+
* capture, making four settable in total. The remaining five are named by the app but never observed
|
|
6921
|
+
* leaving it, so this capability reads them and refuses to send them. See `ARMING_MODE_WIRE` for the
|
|
6922
|
+
* per-value breakdown.
|
|
6890
6923
|
*/
|
|
6891
6924
|
SET_ARMING: 1224,
|
|
6892
6925
|
/**
|
|
@@ -6922,7 +6955,6 @@ var ARMING_MODE_WIRE = {
|
|
|
6922
6955
|
schedule: 2,
|
|
6923
6956
|
// ⚠️ reportable, NOT settable — see the doc comment above
|
|
6924
6957
|
custom1: 3,
|
|
6925
|
-
// ⚠️ reportable, NOT settable — see the doc comment above
|
|
6926
6958
|
custom2: 4,
|
|
6927
6959
|
// ⚠️ reportable, NOT settable — see the doc comment above
|
|
6928
6960
|
custom3: 5,
|
|
@@ -6965,9 +6997,9 @@ function alarmDelayCommand(mode, config, ctx) {
|
|
|
6965
6997
|
var ARMING_MEMBERS = {
|
|
6966
6998
|
/**
|
|
6967
6999
|
* The one member whose write domain is NARROWER than its read: `enumValues` names all nine modes a
|
|
6968
|
-
* station can report, and the argument's `values` publishes only the
|
|
6969
|
-
* argument IS the domain the derived setter enforces and the refusal names, so an
|
|
6970
|
-
* refused by naming the
|
|
7000
|
+
* station can report, and the argument's `values` publishes only the four whose write is confirmed. That
|
|
7001
|
+
* argument IS the domain the derived setter enforces and the refusal names, so an unconfirmed mode is
|
|
7002
|
+
* refused by naming the four that work — nine labels for the read and four for the write, off one
|
|
6971
7003
|
* declaration.
|
|
6972
7004
|
*
|
|
6973
7005
|
* `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
|
|
@@ -6988,7 +7020,7 @@ var ARMING_MEMBERS = {
|
|
|
6988
7020
|
enumValues: ARMING_MODE_LABELS,
|
|
6989
7021
|
provenance: "verified",
|
|
6990
7022
|
args: [{ name: "mode", kind: "enum", values: SETTABLE_MODES }],
|
|
6991
|
-
description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the
|
|
7023
|
+
description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the 4 whose write is confirmed (away/home/custom1/disarmed) \u2014 schedule/custom2/custom3/off/geo are named by the app but no capture shows one being sent, so they are refused rather than guessed; see ARMING_MODE_WIRE in arming.ts for the breakdown.",
|
|
6992
7024
|
observation: {
|
|
6993
7025
|
event: "armingModeChanged",
|
|
6994
7026
|
reflects: (value) => ({ param: ARMING_CMD.SET_ARMING, expected: ARMING_MODE_WIRE[armingModeOf(value)] }),
|
|
@@ -7009,9 +7041,10 @@ var ARMING_MEMBERS = {
|
|
|
7009
7041
|
* this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
|
|
7010
7042
|
* misconfigure which sensors arm/trigger for real.
|
|
7011
7043
|
*
|
|
7012
|
-
* Takes {@link
|
|
7013
|
-
* captured
|
|
7014
|
-
* `
|
|
7044
|
+
* Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
|
|
7045
|
+
* integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
|
|
7046
|
+
* that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
|
|
7047
|
+
* would be the same unverified guess `setMode` refuses.
|
|
7015
7048
|
*/
|
|
7016
7049
|
setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config) => {
|
|
7017
7050
|
try {
|
|
@@ -20168,17 +20201,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20168
20201
|
};
|
|
20169
20202
|
|
|
20170
20203
|
// dist/transport/mqtt/command-router.js
|
|
20171
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
20172
|
-
|
|
20173
|
-
// dist/transport/mqtt/app-client-id.js
|
|
20174
20204
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
20175
|
-
function buildAppShapedClientId(input) {
|
|
20176
|
-
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
20177
|
-
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
20178
|
-
}
|
|
20179
|
-
function generateMqttUuid() {
|
|
20180
|
-
return randomBytes3(8).toString("hex");
|
|
20181
|
-
}
|
|
20182
20205
|
|
|
20183
20206
|
// dist/transport/mqtt/broker-discovery.js
|
|
20184
20207
|
import { resolve4 } from "node:dns/promises";
|
|
@@ -20556,9 +20579,9 @@ function buildFf09MqttEnvelope(input) {
|
|
|
20556
20579
|
const head = {
|
|
20557
20580
|
version: "1.0.0.1",
|
|
20558
20581
|
client_id: input.clientId,
|
|
20559
|
-
sess_id: input.sessId ??
|
|
20582
|
+
sess_id: input.sessId ?? randomBytes3(2).toString("hex"),
|
|
20560
20583
|
msg_seq: 1,
|
|
20561
|
-
seed: input.seed ??
|
|
20584
|
+
seed: input.seed ?? randomBytes3(16).toString("hex"),
|
|
20562
20585
|
timestamp: input.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
20563
20586
|
cmd_status: 2,
|
|
20564
20587
|
cmd: 9,
|
|
@@ -20569,9 +20592,6 @@ function buildFf09MqttEnvelope(input) {
|
|
|
20569
20592
|
var MqttCommandRouter = class _MqttCommandRouter {
|
|
20570
20593
|
deps;
|
|
20571
20594
|
logger;
|
|
20572
|
-
/** Stable per-process install id for the app-shaped MQTT client_id (see {@link buildAppShapedClientId}) —
|
|
20573
|
-
* generated once, reused for every security-MQTT connect this router makes. */
|
|
20574
|
-
mqttUuid;
|
|
20575
20595
|
constructor(deps) {
|
|
20576
20596
|
this.deps = deps;
|
|
20577
20597
|
this.logger = deps.logger ?? noopLogger;
|
|
@@ -20678,9 +20698,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
20678
20698
|
*/
|
|
20679
20699
|
async ensureSecurityMqttFor(dev) {
|
|
20680
20700
|
const creds = await this.deps.mega.getUserMqttInfo("eufy_security");
|
|
20681
|
-
|
|
20682
|
-
this.mqttUuid = generateMqttUuid();
|
|
20683
|
-
const mqttUuid = this.mqttUuid;
|
|
20701
|
+
const mqttUuid = mqttUuidFrom(this.deps.mega.openudid);
|
|
20684
20702
|
const clientIdFor = () => buildAppShapedClientId({ appName: "eufy_security", uid: creds.user_id ?? "", mqttUuid });
|
|
20685
20703
|
const results = await discoverReachableInstance({
|
|
20686
20704
|
hostname: creds.endpoint_addr,
|
|
@@ -21175,7 +21193,7 @@ function deriveTuyaAccount(eufyUserId, phoneCode) {
|
|
|
21175
21193
|
}
|
|
21176
21194
|
|
|
21177
21195
|
// dist/transport/tuya/sign.js
|
|
21178
|
-
import { createHash as
|
|
21196
|
+
import { createHash as createHash5, createHmac as createHmac3 } from "node:crypto";
|
|
21179
21197
|
var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
21180
21198
|
"a",
|
|
21181
21199
|
"v",
|
|
@@ -21199,7 +21217,7 @@ var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
|
21199
21217
|
"sp"
|
|
21200
21218
|
]);
|
|
21201
21219
|
function swapMd5(postData) {
|
|
21202
|
-
const h =
|
|
21220
|
+
const h = createHash5("md5").update(postData, "utf-8").digest("hex");
|
|
21203
21221
|
return h.slice(8, 16) + h.slice(0, 8) + h.slice(24, 32) + h.slice(16, 24);
|
|
21204
21222
|
}
|
|
21205
21223
|
function buildSignPreimage(params) {
|
|
@@ -21224,7 +21242,7 @@ var HmacSigner = class {
|
|
|
21224
21242
|
var TUYA_CHKEY = "7cbfe6d8";
|
|
21225
21243
|
|
|
21226
21244
|
// dist/transport/tuya/client.js
|
|
21227
|
-
import { randomBytes as
|
|
21245
|
+
import { randomBytes as randomBytes4, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
|
|
21228
21246
|
|
|
21229
21247
|
// dist/transport/tuya/request.js
|
|
21230
21248
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -21336,7 +21354,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
|
|
|
21336
21354
|
|
|
21337
21355
|
// dist/transport/tuya/client.js
|
|
21338
21356
|
function genDeviceId() {
|
|
21339
|
-
return
|
|
21357
|
+
return randomBytes4(22).toString("hex");
|
|
21340
21358
|
}
|
|
21341
21359
|
function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
21342
21360
|
const bigintToBuffer = (n) => {
|
|
@@ -21351,7 +21369,7 @@ function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
|
21351
21369
|
},
|
|
21352
21370
|
format: "jwk"
|
|
21353
21371
|
});
|
|
21354
|
-
const md5Hex =
|
|
21372
|
+
const md5Hex = createHash6("md5").update(password).digest("hex");
|
|
21355
21373
|
return publicEncrypt({ key: rsaKey, padding: constants2.RSA_PKCS1_PADDING }, Buffer.from(md5Hex)).toString("hex");
|
|
21356
21374
|
}
|
|
21357
21375
|
var TuyaClient = class {
|
|
@@ -22443,7 +22461,7 @@ var PushClient = class _PushClient extends EventEmitter8 {
|
|
|
22443
22461
|
};
|
|
22444
22462
|
|
|
22445
22463
|
// dist/transport/push/fcm.js
|
|
22446
|
-
import { randomBytes as
|
|
22464
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
22447
22465
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
22448
22466
|
var FCM = {
|
|
22449
22467
|
PROJECT_ID: "batterycam-3250a",
|
|
@@ -22456,7 +22474,7 @@ var FCM = {
|
|
|
22456
22474
|
SDK_VERSION: "a:16.3.1"
|
|
22457
22475
|
};
|
|
22458
22476
|
function generateFid() {
|
|
22459
|
-
const b =
|
|
22477
|
+
const b = randomBytes5(17);
|
|
22460
22478
|
b[0] = 112 + b[0] % 16;
|
|
22461
22479
|
return b.toString("base64url").slice(0, 22);
|
|
22462
22480
|
}
|
|
@@ -24750,9 +24768,21 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24750
24768
|
* {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
|
|
24751
24769
|
* lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
|
|
24752
24770
|
*
|
|
24753
|
-
*
|
|
24754
|
-
*
|
|
24755
|
-
*
|
|
24771
|
+
* Identified by a client id built from this client's `openudid`, not by the certificate's name:
|
|
24772
|
+
* that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
|
|
24773
|
+
* duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
|
|
24774
|
+
* the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
|
|
24775
|
+
* the broker grants on the `eufy_security` credential.
|
|
24776
|
+
*
|
|
24777
|
+
* It separates two clients exactly as far as their `openudid` does: a caller that supplies none
|
|
24778
|
+
* gets the value derived from the account, which every such client shares — the same condition
|
|
24779
|
+
* under which their logins already displace each other (`MegaClientConfig.openudid`).
|
|
24780
|
+
*
|
|
24781
|
+
* A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
|
|
24782
|
+
* none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
|
|
24783
|
+
* that fails for any other reason rejects the bring-up, which clears its memo in
|
|
24784
|
+
* {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
|
|
24785
|
+
* socket must not be what moves a process onto the shared name for good.
|
|
24756
24786
|
*/
|
|
24757
24787
|
async startMqtt(scope) {
|
|
24758
24788
|
const auth = this.mega.auth;
|
|
@@ -24761,7 +24791,41 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24761
24791
|
if (!this.registry.list().length)
|
|
24762
24792
|
await this.getDevices();
|
|
24763
24793
|
const creds = await this.getUserMqttInfo(mqttAppName(scope));
|
|
24764
|
-
const
|
|
24794
|
+
const ownClientId = buildAppShapedClientId({
|
|
24795
|
+
appName: creds.app_name ?? mqttAppName(scope) ?? "eufy_mega",
|
|
24796
|
+
uid: creds.user_id ?? auth.userId,
|
|
24797
|
+
mqttUuid: mqttUuidFrom(this.mega.openudid)
|
|
24798
|
+
});
|
|
24799
|
+
try {
|
|
24800
|
+
return await this.connectMqtt(creds, ownClientId);
|
|
24801
|
+
} catch (e) {
|
|
24802
|
+
if (!isNotAuthorized(e))
|
|
24803
|
+
throw e;
|
|
24804
|
+
this.opts.logger?.warn("[smqtt] the broker refused this client's own id; connecting under the certificate's name, which another client signed in to this account takes over", e);
|
|
24805
|
+
return await this.connectMqtt(creds);
|
|
24806
|
+
}
|
|
24807
|
+
}
|
|
24808
|
+
/**
|
|
24809
|
+
* Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
|
|
24810
|
+
* omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
|
|
24811
|
+
*
|
|
24812
|
+
* The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
|
|
24813
|
+
* broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
|
|
24814
|
+
* the connect it just completed is announced here instead. Errors raised while connecting are held
|
|
24815
|
+
* only to keep an emitter without an `error` listener from throwing, and are reported once the
|
|
24816
|
+
* transport is one a consumer owns.
|
|
24817
|
+
*
|
|
24818
|
+
* The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
|
|
24819
|
+
* runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
|
|
24820
|
+
* transport — so a capability reads tags without owning any framing.
|
|
24821
|
+
*/
|
|
24822
|
+
async connectMqtt(creds, clientId) {
|
|
24823
|
+
const transport = new SecureMqtt({ credentials: creds, clientId, logger: this.opts.logger });
|
|
24824
|
+
const whileConnecting = [];
|
|
24825
|
+
const hold = (e) => void whileConnecting.push(e);
|
|
24826
|
+
transport.on("error", hold);
|
|
24827
|
+
await transport.connect();
|
|
24828
|
+
transport.off("error", hold);
|
|
24765
24829
|
transport.on("connect", () => this.emit("connect"));
|
|
24766
24830
|
transport.on("disconnect", (r) => this.emit("disconnect", r));
|
|
24767
24831
|
transport.on("message", (m) => {
|
|
@@ -24800,7 +24864,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24800
24864
|
this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
|
|
24801
24865
|
});
|
|
24802
24866
|
transport.on("error", (e) => this.reportError(e));
|
|
24803
|
-
|
|
24867
|
+
this.emit("connect");
|
|
24868
|
+
for (const e of whileConnecting)
|
|
24869
|
+
this.reportError(e);
|
|
24804
24870
|
return transport;
|
|
24805
24871
|
}
|
|
24806
24872
|
/**
|
|
@@ -25257,6 +25323,7 @@ export {
|
|
|
25257
25323
|
AUDIO_MEMBERS,
|
|
25258
25324
|
AccessUnitAssembler,
|
|
25259
25325
|
AiDetectType,
|
|
25326
|
+
AlarmDelayMode,
|
|
25260
25327
|
ArmingMode,
|
|
25261
25328
|
BATTERY_MEMBERS,
|
|
25262
25329
|
BIZ_CHANNEL,
|
|
@@ -25443,7 +25510,6 @@ export {
|
|
|
25443
25510
|
freshestLanIp,
|
|
25444
25511
|
genId,
|
|
25445
25512
|
generateFid,
|
|
25446
|
-
generateMqttUuid,
|
|
25447
25513
|
getCapabilityModule,
|
|
25448
25514
|
getIdSuffix,
|
|
25449
25515
|
getImageBaseCode,
|
|
@@ -25456,6 +25522,7 @@ export {
|
|
|
25456
25522
|
inferName,
|
|
25457
25523
|
inspectParams,
|
|
25458
25524
|
isKnownValueKind,
|
|
25525
|
+
isNotAuthorized,
|
|
25459
25526
|
isPrivateIpv4,
|
|
25460
25527
|
isSessionValid,
|
|
25461
25528
|
isV1Image,
|
|
@@ -25471,6 +25538,7 @@ export {
|
|
|
25471
25538
|
mergeProperties,
|
|
25472
25539
|
mqttAppName,
|
|
25473
25540
|
mqttScopeFor,
|
|
25541
|
+
mqttUuidFrom,
|
|
25474
25542
|
namespaceForCodec,
|
|
25475
25543
|
needsRealtimeInit,
|
|
25476
25544
|
noopLogger,
|