@mega-yfue/eufy-sdk 0.5.0-beta.50 → 0.5.0-beta.51

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/index.js CHANGED
@@ -25210,7 +25210,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
25210
25210
 
25211
25211
  // dist/transport/tuya/account.js
25212
25212
  import { createCipheriv as createCipheriv5 } from "node:crypto";
25213
- var TUYA_USERNAME_PREFIX = "eufyhome-";
25213
+ var TUYA_USERNAME_PREFIX = "eh-";
25214
25214
  var TUYA_PASSWORD_KEY = Buffer.from([
25215
25215
  36,
25216
25216
  78,
@@ -25398,10 +25398,13 @@ var StubSigner = class {
25398
25398
  }
25399
25399
  };
25400
25400
  var TUYA_SIGN_K = "com.oceanwing.battery.cam_16:6C:23:45:57:B7:76:CA:D8:AC:94:C9:79:37:9E:48:DF:38:7D:4D:8F:96:A3:43:DF:40:FC:D9:05:BF:F6:86_dn9erpyp7nmeuvah8ktghqsgpay87maa_pt585qhmt75hwcynchnps9dnxh9suhwd";
25401
+ function resolveSignKey(override) {
25402
+ return override ?? process.env.TUYA_SIGN_KEY ?? TUYA_SIGN_K;
25403
+ }
25401
25404
  var HmacSigner = class {
25402
25405
  key;
25403
25406
  constructor(key) {
25404
- this.key = key ?? process.env.TUYA_SIGN_KEY ?? TUYA_SIGN_K;
25407
+ this.key = resolveSignKey(key);
25405
25408
  }
25406
25409
  sign(preimage) {
25407
25410
  return createHmac4("sha256", this.key).update(preimage, "utf-8").digest("hex");
@@ -25410,7 +25413,37 @@ var HmacSigner = class {
25410
25413
  var TUYA_CHKEY = "7cbfe6d8";
25411
25414
 
25412
25415
  // dist/transport/tuya/client.js
25413
- import { randomBytes as randomBytes4, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
25416
+ import { randomBytes as randomBytes5, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
25417
+
25418
+ // dist/transport/tuya/et3.js
25419
+ import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv8, createHmac as createHmac5, randomBytes as randomBytes4 } from "node:crypto";
25420
+ import { gunzipSync } from "node:zlib";
25421
+ function deriveBodyKey(requestId, ecode) {
25422
+ const k = resolveSignKey();
25423
+ const msg = ecode === null ? k : `${k}_${ecode}`;
25424
+ const hex = createHmac5("sha256", requestId).update(msg, "utf-8").digest("hex");
25425
+ return Buffer.from(hex.slice(0, 16), "ascii");
25426
+ }
25427
+ function encryptBody2(key, json) {
25428
+ const nonce = randomBytes4(12);
25429
+ const cipher = createCipheriv6("aes-128-gcm", key, nonce);
25430
+ const ciphertext = Buffer.concat([cipher.update(Buffer.from(json, "utf-8")), cipher.final()]);
25431
+ return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]).toString("base64");
25432
+ }
25433
+ function decryptReply(key, resultB64) {
25434
+ const blob = Buffer.from(resultB64, "base64");
25435
+ if (blob.length < 12 + 16) {
25436
+ throw new Error("tuya et=3 reply too short to hold a 12-byte nonce + 16-byte tag");
25437
+ }
25438
+ const nonce = blob.subarray(0, 12);
25439
+ const tag2 = blob.subarray(blob.length - 16);
25440
+ const ciphertext = blob.subarray(12, blob.length - 16);
25441
+ const decipher = createDecipheriv8("aes-128-gcm", key, nonce);
25442
+ decipher.setAuthTag(tag2);
25443
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
25444
+ const text2 = plain.length >= 2 && plain[0] === 31 && plain[1] === 139 ? gunzipSync(plain) : plain;
25445
+ return text2.toString("utf-8");
25446
+ }
25414
25447
 
25415
25448
  // dist/transport/tuya/request.js
25416
25449
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -25465,8 +25498,9 @@ function buildApiParams(action, opts) {
25465
25498
  nd: env.nd,
25466
25499
  deviceCoreVersion: env.deviceCoreVersion
25467
25500
  };
25468
- if (action.postData !== void 0)
25469
- params.postData = action.postData;
25501
+ if (action.postData !== void 0) {
25502
+ params.postData = env.et === "3" ? encryptBody2(deriveBodyKey(params.requestId, opts.session.ecode ?? null), action.postData) : action.postData;
25503
+ }
25470
25504
  const sign = opts.signer.sign(buildSignPreimage(params));
25471
25505
  return { ...params, sign };
25472
25506
  }
@@ -25486,14 +25520,14 @@ async function sendApiRequest(params, opts = {}) {
25486
25520
  }
25487
25521
  function buildPublishDpsAction(devId, gwId, dps) {
25488
25522
  return {
25489
- a: "thing.m.device.dp.publish",
25523
+ a: "smartlife.m.device.dp.publish",
25490
25524
  v: "2.0",
25491
25525
  postData: JSON.stringify({ gwId, devId, dps: JSON.stringify(dps) })
25492
25526
  };
25493
25527
  }
25494
25528
  function buildGetDeviceDpsAction(devId, dpCacheType = 1) {
25495
25529
  return {
25496
- a: "thing.m.device.cache.dp.get",
25530
+ a: "smartlife.m.device.cache.dp.get",
25497
25531
  v: "2.0",
25498
25532
  postData: JSON.stringify({ devId, dpCacheType })
25499
25533
  };
@@ -25509,7 +25543,15 @@ function buildPasswordLoginRegAction(countryCode, uid, passwd, token) {
25509
25543
  return {
25510
25544
  a: "smartlife.m.user.uid.password.login.reg",
25511
25545
  v: "1.0",
25512
- postData: JSON.stringify({ countryCode, uid, passwd, token, ifencrypt: 1, createGroup: true })
25546
+ postData: JSON.stringify({
25547
+ countryCode,
25548
+ uid,
25549
+ passwd,
25550
+ token,
25551
+ ifencrypt: 1,
25552
+ createGroup: true,
25553
+ options: '{"group": 1}'
25554
+ })
25513
25555
  };
25514
25556
  }
25515
25557
  function buildUidTokenCreateAction(countryCode, uid) {
@@ -25522,7 +25564,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
25522
25564
 
25523
25565
  // dist/transport/tuya/client.js
25524
25566
  function genDeviceId() {
25525
- return randomBytes4(22).toString("hex");
25567
+ return randomBytes5(22).toString("hex");
25526
25568
  }
25527
25569
  function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
25528
25570
  const bigintToBuffer = (n) => {
@@ -25572,9 +25614,29 @@ var TuyaClient = class {
25572
25614
  buildRequest(action) {
25573
25615
  return buildApiParams(action, { session: this.session, signer: this.signer, env: this.env });
25574
25616
  }
25575
- /** Build + POST an action, returning the parsed envelope. Requires a working signer. */
25617
+ /**
25618
+ * Build + POST an action, returning the parsed envelope. Requires a working signer.
25619
+ *
25620
+ * On an `et=3` reply the envelope's `result` is a base64 AES-128-GCM blob, not an object; this
25621
+ * decrypts it (keyed off the request's `requestId` and the session's `ecode`) and returns the
25622
+ * decrypted inner envelope, so callers read `result` as an object regardless. A non-string `result`
25623
+ * (an error, or an already-plain reply) passes through untouched.
25624
+ */
25576
25625
  async call(action) {
25577
- return sendApiRequest(this.buildRequest(action), { endpoint: this.endpoint, http: this.http });
25626
+ const params = this.buildRequest(action);
25627
+ const env = await sendApiRequest(params, { endpoint: this.endpoint, http: this.http });
25628
+ return this.decodeReply(env, params.requestId);
25629
+ }
25630
+ /**
25631
+ * Decode an et=3 reply. When `result` is a non-empty string it is the encrypted blob: decrypt and
25632
+ * parse it (a wrong key or tampered blob fails the GCM tag in {@link decryptReply} and throws). The
25633
+ * plaintext is itself the standard `{success, t, result}` envelope, which becomes the returned one.
25634
+ */
25635
+ decodeReply(env, requestId) {
25636
+ if (typeof env.result !== "string" || env.result === "")
25637
+ return env;
25638
+ const key = deriveBodyKey(requestId, this.session.ecode ?? null);
25639
+ return JSON.parse(decryptReply(key, env.result));
25578
25640
  }
25579
25641
  /**
25580
25642
  * Log into the Tuya cloud from a eufy user id (wire-confirmed from the eufy Security app).
@@ -25607,7 +25669,7 @@ var TuyaClient = class {
25607
25669
  const encPasswd = rsaEncryptPassword(password, publicKey, exponent);
25608
25670
  const loginRes = await this.call(buildPasswordLoginRegAction(account.countryCode, account.username, encPasswd, token));
25609
25671
  if (loginRes.success && loginRes.result?.sid && loginRes.result?.uid) {
25610
- return { sid: loginRes.result.sid, uid: loginRes.result.uid };
25672
+ return { sid: loginRes.result.sid, uid: loginRes.result.uid, ecode: loginRes.result.ecode };
25611
25673
  }
25612
25674
  if ((loginRes.errorCode ?? loginRes.errorMsg) === "USER_PASSWD_WRONG")
25613
25675
  return "PASSWD_WRONG";
@@ -25619,18 +25681,18 @@ var TuyaClient = class {
25619
25681
  if (result === "PASSWD_WRONG") {
25620
25682
  throw new Error("tuya login: USER_PASSWD_WRONG on both derived password and fallback '12345678'");
25621
25683
  }
25622
- this.session = { ...this.session, sid: result.sid };
25623
- return result;
25684
+ this.session = { ...this.session, sid: result.sid, ecode: result.ecode };
25685
+ return { sid: result.sid, uid: result.uid };
25624
25686
  }
25625
25687
  /**
25626
- * READ/dump a device's cached data-points (`thing.m.device.cache.dp.get`).
25688
+ * READ/dump a device's cached data-points (`smartlife.m.device.cache.dp.get`).
25627
25689
  * Builds the request without needing a working signer; the signer is only exercised on send.
25628
25690
  */
25629
25691
  async getDeviceDps(devId, dpCacheType) {
25630
25692
  return this.call(buildGetDeviceDpsAction(devId, dpCacheType));
25631
25693
  }
25632
25694
  /**
25633
- * CONTROL: publish data-points to a device (`thing.m.device.dp.publish`). `dps` is
25695
+ * CONTROL: publish data-points to a device (`smartlife.m.device.dp.publish`). `dps` is
25634
25696
  * `{ "<dpId>": <value> }`. `gwId` is the gateway/parent id (equals `devId` for a standalone gw).
25635
25697
  *
25636
25698
  * ⚠️ UNVERIFIED write — refuses to send by default. The `dp.publish` param shape
@@ -25769,7 +25831,7 @@ var TuyaCommandRouter = class {
25769
25831
  return this.loginOnce;
25770
25832
  }
25771
25833
  /**
25772
- * Fetch a device's cached DPs from the Tuya cloud (`thing.m.device.cache.dp.get`) and deliver
25834
+ * Fetch a device's cached DPs from the Tuya cloud (`smartlife.m.device.cache.dp.get`) and deliver
25773
25835
  * the raw DP map to the caller. Used for initial state hydration after MQTT subscribe — gets the
25774
25836
  * last-known state without waiting for the first realtime push.
25775
25837
  *
@@ -26647,7 +26709,7 @@ var PushClient = class _PushClient extends EventEmitter12 {
26647
26709
  };
26648
26710
 
26649
26711
  // dist/transport/push/fcm.js
26650
- import { randomBytes as randomBytes5 } from "node:crypto";
26712
+ import { randomBytes as randomBytes6 } from "node:crypto";
26651
26713
  import { setTimeout as sleep3 } from "node:timers/promises";
26652
26714
  var FCM = {
26653
26715
  PROJECT_ID: "batterycam-3250a",
@@ -26660,7 +26722,7 @@ var FCM = {
26660
26722
  SDK_VERSION: "a:16.3.1"
26661
26723
  };
26662
26724
  function generateFid() {
26663
- const b = randomBytes5(17);
26725
+ const b = randomBytes6(17);
26664
26726
  b[0] = 112 + b[0] % 16;
26665
26727
  return b.toString("base64url").slice(0, 22);
26666
26728
  }
@@ -30787,6 +30849,7 @@ __export(tuya_exports, {
30787
30849
  genDeviceId: () => genDeviceId,
30788
30850
  isoToDialCode: () => isoToDialCode,
30789
30851
  resolveCountryCode: () => resolveCountryCode,
30852
+ resolveSignKey: () => resolveSignKey,
30790
30853
  sendApiRequest: () => sendApiRequest,
30791
30854
  swapMd5: () => swapMd5,
30792
30855
  tuyaUsername: () => tuyaUsername