@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.
@@ -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 startMqtt;
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
- /** Stable per-install device id — the auth token is bound to it. */
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 ?? randomBytes4(2).toString("hex"),
20571
+ sess_id: input.sessId ?? randomBytes3(2).toString("hex"),
20560
20572
  msg_seq: 1,
20561
- seed: input.seed ?? randomBytes4(16).toString("hex"),
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
- if (!this.mqttUuid)
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 createHash4, createHmac as createHmac3 } from "node:crypto";
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 = createHash4("md5").update(postData, "utf-8").digest("hex");
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 randomBytes5, createHash as createHash5, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
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 randomBytes5(22).toString("hex");
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 = createHash5("md5").update(password).digest("hex");
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 randomBytes6 } from "node:crypto";
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 = randomBytes6(17);
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
- * The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
24754
- * runs against another's traffic, and the DP frame is unwrapped here the layer that may import the
24755
- * transport so a capability reads tags without owning any framing.
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 transport = new SecureMqtt({ credentials: creds, logger: this.opts.logger });
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
- await transport.connect();
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,