@mega-yfue/eufy-sdk 0.1.2 → 0.2.0-beta.1
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/dist/client/eufy-mega.d.ts +28 -1
- package/dist/index.js +88 -32
- package/dist/index.js.map +4 -4
- 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
|
@@ -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;
|
|
@@ -20168,17 +20190,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20168
20190
|
};
|
|
20169
20191
|
|
|
20170
20192
|
// dist/transport/mqtt/command-router.js
|
|
20171
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
20172
|
-
|
|
20173
|
-
// dist/transport/mqtt/app-client-id.js
|
|
20174
20193
|
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
20194
|
|
|
20183
20195
|
// dist/transport/mqtt/broker-discovery.js
|
|
20184
20196
|
import { resolve4 } from "node:dns/promises";
|
|
@@ -20556,9 +20568,9 @@ function buildFf09MqttEnvelope(input) {
|
|
|
20556
20568
|
const head = {
|
|
20557
20569
|
version: "1.0.0.1",
|
|
20558
20570
|
client_id: input.clientId,
|
|
20559
|
-
sess_id: input.sessId ??
|
|
20571
|
+
sess_id: input.sessId ?? randomBytes3(2).toString("hex"),
|
|
20560
20572
|
msg_seq: 1,
|
|
20561
|
-
seed: input.seed ??
|
|
20573
|
+
seed: input.seed ?? randomBytes3(16).toString("hex"),
|
|
20562
20574
|
timestamp: input.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
20563
20575
|
cmd_status: 2,
|
|
20564
20576
|
cmd: 9,
|
|
@@ -20569,9 +20581,6 @@ function buildFf09MqttEnvelope(input) {
|
|
|
20569
20581
|
var MqttCommandRouter = class _MqttCommandRouter {
|
|
20570
20582
|
deps;
|
|
20571
20583
|
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
20584
|
constructor(deps) {
|
|
20576
20585
|
this.deps = deps;
|
|
20577
20586
|
this.logger = deps.logger ?? noopLogger;
|
|
@@ -20678,9 +20687,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
20678
20687
|
*/
|
|
20679
20688
|
async ensureSecurityMqttFor(dev) {
|
|
20680
20689
|
const creds = await this.deps.mega.getUserMqttInfo("eufy_security");
|
|
20681
|
-
|
|
20682
|
-
this.mqttUuid = generateMqttUuid();
|
|
20683
|
-
const mqttUuid = this.mqttUuid;
|
|
20690
|
+
const mqttUuid = mqttUuidFrom(this.deps.mega.openudid);
|
|
20684
20691
|
const clientIdFor = () => buildAppShapedClientId({ appName: "eufy_security", uid: creds.user_id ?? "", mqttUuid });
|
|
20685
20692
|
const results = await discoverReachableInstance({
|
|
20686
20693
|
hostname: creds.endpoint_addr,
|
|
@@ -21175,7 +21182,7 @@ function deriveTuyaAccount(eufyUserId, phoneCode) {
|
|
|
21175
21182
|
}
|
|
21176
21183
|
|
|
21177
21184
|
// dist/transport/tuya/sign.js
|
|
21178
|
-
import { createHash as
|
|
21185
|
+
import { createHash as createHash5, createHmac as createHmac3 } from "node:crypto";
|
|
21179
21186
|
var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
21180
21187
|
"a",
|
|
21181
21188
|
"v",
|
|
@@ -21199,7 +21206,7 @@ var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
|
21199
21206
|
"sp"
|
|
21200
21207
|
]);
|
|
21201
21208
|
function swapMd5(postData) {
|
|
21202
|
-
const h =
|
|
21209
|
+
const h = createHash5("md5").update(postData, "utf-8").digest("hex");
|
|
21203
21210
|
return h.slice(8, 16) + h.slice(0, 8) + h.slice(24, 32) + h.slice(16, 24);
|
|
21204
21211
|
}
|
|
21205
21212
|
function buildSignPreimage(params) {
|
|
@@ -21224,7 +21231,7 @@ var HmacSigner = class {
|
|
|
21224
21231
|
var TUYA_CHKEY = "7cbfe6d8";
|
|
21225
21232
|
|
|
21226
21233
|
// dist/transport/tuya/client.js
|
|
21227
|
-
import { randomBytes as
|
|
21234
|
+
import { randomBytes as randomBytes4, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
|
|
21228
21235
|
|
|
21229
21236
|
// dist/transport/tuya/request.js
|
|
21230
21237
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -21336,7 +21343,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
|
|
|
21336
21343
|
|
|
21337
21344
|
// dist/transport/tuya/client.js
|
|
21338
21345
|
function genDeviceId() {
|
|
21339
|
-
return
|
|
21346
|
+
return randomBytes4(22).toString("hex");
|
|
21340
21347
|
}
|
|
21341
21348
|
function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
21342
21349
|
const bigintToBuffer = (n) => {
|
|
@@ -21351,7 +21358,7 @@ function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
|
21351
21358
|
},
|
|
21352
21359
|
format: "jwk"
|
|
21353
21360
|
});
|
|
21354
|
-
const md5Hex =
|
|
21361
|
+
const md5Hex = createHash6("md5").update(password).digest("hex");
|
|
21355
21362
|
return publicEncrypt({ key: rsaKey, padding: constants2.RSA_PKCS1_PADDING }, Buffer.from(md5Hex)).toString("hex");
|
|
21356
21363
|
}
|
|
21357
21364
|
var TuyaClient = class {
|
|
@@ -22443,7 +22450,7 @@ var PushClient = class _PushClient extends EventEmitter8 {
|
|
|
22443
22450
|
};
|
|
22444
22451
|
|
|
22445
22452
|
// dist/transport/push/fcm.js
|
|
22446
|
-
import { randomBytes as
|
|
22453
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
22447
22454
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
22448
22455
|
var FCM = {
|
|
22449
22456
|
PROJECT_ID: "batterycam-3250a",
|
|
@@ -22456,7 +22463,7 @@ var FCM = {
|
|
|
22456
22463
|
SDK_VERSION: "a:16.3.1"
|
|
22457
22464
|
};
|
|
22458
22465
|
function generateFid() {
|
|
22459
|
-
const b =
|
|
22466
|
+
const b = randomBytes5(17);
|
|
22460
22467
|
b[0] = 112 + b[0] % 16;
|
|
22461
22468
|
return b.toString("base64url").slice(0, 22);
|
|
22462
22469
|
}
|
|
@@ -24750,9 +24757,21 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24750
24757
|
* {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
|
|
24751
24758
|
* lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
|
|
24752
24759
|
*
|
|
24753
|
-
*
|
|
24754
|
-
*
|
|
24755
|
-
*
|
|
24760
|
+
* Identified by a client id built from this client's `openudid`, not by the certificate's name:
|
|
24761
|
+
* that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
|
|
24762
|
+
* duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
|
|
24763
|
+
* the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
|
|
24764
|
+
* the broker grants on the `eufy_security` credential.
|
|
24765
|
+
*
|
|
24766
|
+
* It separates two clients exactly as far as their `openudid` does: a caller that supplies none
|
|
24767
|
+
* gets the value derived from the account, which every such client shares — the same condition
|
|
24768
|
+
* under which their logins already displace each other (`MegaClientConfig.openudid`).
|
|
24769
|
+
*
|
|
24770
|
+
* A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
|
|
24771
|
+
* none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
|
|
24772
|
+
* that fails for any other reason rejects the bring-up, which clears its memo in
|
|
24773
|
+
* {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
|
|
24774
|
+
* socket must not be what moves a process onto the shared name for good.
|
|
24756
24775
|
*/
|
|
24757
24776
|
async startMqtt(scope) {
|
|
24758
24777
|
const auth = this.mega.auth;
|
|
@@ -24761,7 +24780,41 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24761
24780
|
if (!this.registry.list().length)
|
|
24762
24781
|
await this.getDevices();
|
|
24763
24782
|
const creds = await this.getUserMqttInfo(mqttAppName(scope));
|
|
24764
|
-
const
|
|
24783
|
+
const ownClientId = buildAppShapedClientId({
|
|
24784
|
+
appName: creds.app_name ?? mqttAppName(scope) ?? "eufy_mega",
|
|
24785
|
+
uid: creds.user_id ?? auth.userId,
|
|
24786
|
+
mqttUuid: mqttUuidFrom(this.mega.openudid)
|
|
24787
|
+
});
|
|
24788
|
+
try {
|
|
24789
|
+
return await this.connectMqtt(creds, ownClientId);
|
|
24790
|
+
} catch (e) {
|
|
24791
|
+
if (!isNotAuthorized(e))
|
|
24792
|
+
throw e;
|
|
24793
|
+
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);
|
|
24794
|
+
return await this.connectMqtt(creds);
|
|
24795
|
+
}
|
|
24796
|
+
}
|
|
24797
|
+
/**
|
|
24798
|
+
* Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
|
|
24799
|
+
* omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
|
|
24800
|
+
*
|
|
24801
|
+
* The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
|
|
24802
|
+
* broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
|
|
24803
|
+
* the connect it just completed is announced here instead. Errors raised while connecting are held
|
|
24804
|
+
* only to keep an emitter without an `error` listener from throwing, and are reported once the
|
|
24805
|
+
* transport is one a consumer owns.
|
|
24806
|
+
*
|
|
24807
|
+
* The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
|
|
24808
|
+
* runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
|
|
24809
|
+
* transport — so a capability reads tags without owning any framing.
|
|
24810
|
+
*/
|
|
24811
|
+
async connectMqtt(creds, clientId) {
|
|
24812
|
+
const transport = new SecureMqtt({ credentials: creds, clientId, logger: this.opts.logger });
|
|
24813
|
+
const whileConnecting = [];
|
|
24814
|
+
const hold = (e) => void whileConnecting.push(e);
|
|
24815
|
+
transport.on("error", hold);
|
|
24816
|
+
await transport.connect();
|
|
24817
|
+
transport.off("error", hold);
|
|
24765
24818
|
transport.on("connect", () => this.emit("connect"));
|
|
24766
24819
|
transport.on("disconnect", (r) => this.emit("disconnect", r));
|
|
24767
24820
|
transport.on("message", (m) => {
|
|
@@ -24800,7 +24853,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24800
24853
|
this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
|
|
24801
24854
|
});
|
|
24802
24855
|
transport.on("error", (e) => this.reportError(e));
|
|
24803
|
-
|
|
24856
|
+
this.emit("connect");
|
|
24857
|
+
for (const e of whileConnecting)
|
|
24858
|
+
this.reportError(e);
|
|
24804
24859
|
return transport;
|
|
24805
24860
|
}
|
|
24806
24861
|
/**
|
|
@@ -25443,7 +25498,6 @@ export {
|
|
|
25443
25498
|
freshestLanIp,
|
|
25444
25499
|
genId,
|
|
25445
25500
|
generateFid,
|
|
25446
|
-
generateMqttUuid,
|
|
25447
25501
|
getCapabilityModule,
|
|
25448
25502
|
getIdSuffix,
|
|
25449
25503
|
getImageBaseCode,
|
|
@@ -25456,6 +25510,7 @@ export {
|
|
|
25456
25510
|
inferName,
|
|
25457
25511
|
inspectParams,
|
|
25458
25512
|
isKnownValueKind,
|
|
25513
|
+
isNotAuthorized,
|
|
25459
25514
|
isPrivateIpv4,
|
|
25460
25515
|
isSessionValid,
|
|
25461
25516
|
isV1Image,
|
|
@@ -25471,6 +25526,7 @@ export {
|
|
|
25471
25526
|
mergeProperties,
|
|
25472
25527
|
mqttAppName,
|
|
25473
25528
|
mqttScopeFor,
|
|
25529
|
+
mqttUuidFrom,
|
|
25474
25530
|
namespaceForCodec,
|
|
25475
25531
|
needsRealtimeInit,
|
|
25476
25532
|
noopLogger,
|