@mega-yfue/eufy-sdk 0.5.0-beta.69 → 0.5.0-beta.70

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.
@@ -329,6 +329,11 @@ export type Command = {
329
329
  channel: number;
330
330
  mValue3?: number;
331
331
  form?: ScalarForm;
332
+ /**
333
+ * `"set-payload"`: a level-2 key that seals only this envelope (an RSA cipher's AES-128-ECB key)
334
+ * may seal it. Absent, the send waits for the key every level-2 sender uses.
335
+ */
336
+ level2Sender?: "set-payload";
332
337
  } | {
333
338
  kind: "p2p-privacy-burst";
334
339
  enabled: boolean;
package/dist/index.js CHANGED
@@ -11097,6 +11097,30 @@ var LOCATE = {
11097
11097
  }
11098
11098
  };
11099
11099
 
11100
+ // dist/model/capabilities/smart-drop.js
11101
+ var OPEN_LID_CMD = 2100;
11102
+ var SMART_DROP_MEMBERS = {
11103
+ /**
11104
+ * Opens the lid with `SET_PAYLOAD` 2100 at level 2. Verified on a standalone T8790, whose cipher has
11105
+ * only an RSA key: the frame sealed AES-128-ECB opened the lid, the same frame at level 1 did not.
11106
+ */
11107
+ openLid: method(({ ctx, sink }) => () => sink.dispatch({
11108
+ kind: "set-payload",
11109
+ cmd: OPEN_LID_CMD,
11110
+ payload: { isOpen: 1, userIndex: 0 },
11111
+ channel: ctx.channel,
11112
+ mValue3: 0,
11113
+ level2Sender: "set-payload"
11114
+ }), "Open the SmartDrop lid.")
11115
+ };
11116
+ var SMART_DROP = {
11117
+ capability: "smart_drop",
11118
+ description: "SmartDrop parcel box lid control.",
11119
+ members: SMART_DROP_MEMBERS,
11120
+ properties: propertiesOf(SMART_DROP_MEMBERS),
11121
+ detection: { detect: (record) => record.model === "T8790" && record.parentSn === void 0 }
11122
+ };
11123
+
11100
11124
  // dist/model/capabilities/info.js
11101
11125
  var MANUFACTURER = "eufy";
11102
11126
  function buildDeviceInfo(ctx) {
@@ -11178,6 +11202,7 @@ var MODULES = [
11178
11202
  SUCTION,
11179
11203
  MOWER,
11180
11204
  LOCATE,
11205
+ SMART_DROP,
11181
11206
  DISPLAY,
11182
11207
  INFO
11183
11208
  ];
@@ -14980,7 +15005,7 @@ import { setTimeout as sleep2 } from "node:timers/promises";
14980
15005
  // dist/transport/p2p/p2p-session.js
14981
15006
  import { EventEmitter as EventEmitter2 } from "node:events";
14982
15007
  import dgram from "node:dgram";
14983
- import { constants, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt, randomBytes as randomBytes2 } from "node:crypto";
15008
+ import { constants as constants2, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes2 } from "node:crypto";
14984
15009
 
14985
15010
  // dist/transport/p2p/codec.js
14986
15011
  var codec_exports = {};
@@ -15005,6 +15030,7 @@ __export(codec_exports, {
15005
15030
  decodeP2PCloudIPs: () => decodeP2PCloudIPs,
15006
15031
  decryptP2PData: () => decryptP2PData,
15007
15032
  deriveLevel2KeyFromGatewayInfo: () => deriveLevel2KeyFromGatewayInfo,
15033
+ deriveLevel2KeyFromRsaGatewayInfo: () => deriveLevel2KeyFromRsaGatewayInfo,
15008
15034
  eciesUnwrap: () => eciesUnwrap,
15009
15035
  encryptP2PData: () => encryptP2PData,
15010
15036
  frameMessage: () => frameMessage,
@@ -15018,7 +15044,7 @@ __export(codec_exports, {
15018
15044
  readNullTerminatedString: () => readNullTerminatedString,
15019
15045
  stringWithLength: () => stringWithLength
15020
15046
  });
15021
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, timingSafeEqual } from "node:crypto";
15047
+ import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, timingSafeEqual } from "node:crypto";
15022
15048
  var MAGIC_WORD = "XZYH";
15023
15049
  var RequestMessageType = {
15024
15050
  LOCAL_LOOKUP: Buffer.from([241, 48]),
@@ -15162,6 +15188,27 @@ function deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivateKeyHex) {
15162
15188
  const key = plain.subarray(0, 32);
15163
15189
  return key.length === 32 ? Buffer.from(key) : void 0;
15164
15190
  }
15191
+ function deriveLevel2KeyFromRsaGatewayInfo(gwPayload, rsaPrivateKey) {
15192
+ try {
15193
+ const key = createPrivateKey({ key: Buffer.from(rsaPrivateKey, "base64"), format: "der", type: "pkcs8" });
15194
+ const modulusBits = key.asymmetricKeyDetails?.modulusLength;
15195
+ if (!modulusBits)
15196
+ return void 0;
15197
+ const len = Math.ceil(modulusBits / 8);
15198
+ if (gwPayload.length < 4 + len)
15199
+ return void 0;
15200
+ const block = privateDecrypt({ key, padding: constants.RSA_NO_PADDING }, gwPayload.subarray(4, 4 + len));
15201
+ if (block[0] !== 0 || block[1] !== 2)
15202
+ return void 0;
15203
+ const separator = block.indexOf(0, 2);
15204
+ if (separator < 10)
15205
+ return void 0;
15206
+ const sessionKey = block.subarray(separator + 1);
15207
+ return sessionKey.length === 16 ? Buffer.from(sessionKey) : void 0;
15208
+ } catch {
15209
+ return void 0;
15210
+ }
15211
+ }
15165
15212
  function buildLocalLookupPayload() {
15166
15213
  return Buffer.from([0, 0]);
15167
15214
  }
@@ -16023,6 +16070,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16023
16070
  level1Key;
16024
16071
  /** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
16025
16072
  level2Key;
16073
+ /**
16074
+ * Negotiated 16-byte level-2 key of a cipher with no ECC key (RSA-encrypted in `CMD_GATEWAYINFO`). Only
16075
+ * {@link sendControlLevel2} seals with it, AES-128-ECB in the level-1 layout at encryption type 2; never
16076
+ * held beside {@link level2Key}.
16077
+ */
16078
+ level2EcbKey;
16026
16079
  level2Seq = 0;
16027
16080
  rsaPrivateKey;
16028
16081
  rsaModulusHex;
@@ -16103,12 +16156,22 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16103
16156
  this.level2Key = key;
16104
16157
  this.settleLevel2();
16105
16158
  }
16106
- /** Whether the level-2 session key has been negotiated/set. */
16159
+ /** Whether the AES-256-GCM level-2 session key has been negotiated/set. */
16107
16160
  get hasLevel2Key() {
16108
16161
  return !!this.level2Key;
16109
16162
  }
16163
+ /**
16164
+ * Whether the held level-2 key can frame for `sender`: every level-2 sender needs the AES-256-GCM key,
16165
+ * and `"set-payload"` ({@link sendControlLevel2}) also takes the AES-128-ECB key of an RSA cipher.
16166
+ */
16167
+ level2KeyServes(sender) {
16168
+ return !!this.level2Key || sender === "set-payload" && !!this.level2EcbKey;
16169
+ }
16110
16170
  /**
16111
16171
  * Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
16172
+ * `sender` names who will frame with it: by default every level-2 sender, which needs the AES-256-GCM
16173
+ * key; `"set-payload"` is {@link sendControlLevel2} alone, which also takes the AES-128-ECB key of an RSA
16174
+ * cipher, so `true` for it does not imply {@link hasLevel2Key}.
16112
16175
  *
16113
16176
  * A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
16114
16177
  * egress asks this same session and can proceed without the key on an own-session camera; a per-call
@@ -16133,12 +16196,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16133
16196
  * `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
16134
16197
  * cipher and the cause are known, and re-stating it here would double every settled negotiation.
16135
16198
  */
16136
- async awaitLevel2Key(graceMs, graceFrom = "call") {
16199
+ async awaitLevel2Key(graceMs, graceFrom = "call", sender = "every") {
16137
16200
  if (this.closed) {
16138
16201
  this.trace({ phase: "level2-unavailable", reason: "session-closed" });
16139
16202
  return false;
16140
16203
  }
16141
- if (this.level2Key)
16204
+ if (this.level2KeyServes(sender))
16142
16205
  return true;
16143
16206
  if (!this.level2Pending) {
16144
16207
  this.trace({ phase: "level2-unavailable", reason: "not-negotiating" });
@@ -16172,16 +16235,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16172
16235
  deadline.unref?.();
16173
16236
  waiters.push(wake);
16174
16237
  });
16238
+ const served = outcome === "key" || outcome === "terminal" && this.level2KeyServes(sender);
16175
16239
  if (outcome === "timeout") {
16176
16240
  this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key did not arrive within its grace`);
16177
16241
  this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: remaining });
16178
- } else if (outcome === "terminal") {
16242
+ } else if (outcome === "terminal" && !served) {
16179
16243
  this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 negotiation concluded without a key`);
16180
16244
  } else if (outcome === "closed") {
16181
16245
  this.logger.debug(`[p2p] ${this.cfg.stationSn} session closed before the level-2 key arrived`);
16182
16246
  this.trace({ phase: "level2-unavailable", reason: "session-closed" });
16183
16247
  }
16184
- return outcome === "key";
16248
+ return served;
16185
16249
  }
16186
16250
  /**
16187
16251
  * Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
@@ -16199,7 +16263,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16199
16263
  * use this: re-prompting on their behalf would be noise for an answer they do not need.
16200
16264
  */
16201
16265
  repromptLevel2Key() {
16202
- if (this.closed || this.level2Key || this.level2Reprompted)
16266
+ if (this.closed || this.level2KeyServes("set-payload") || this.level2Reprompted)
16203
16267
  return false;
16204
16268
  if (!this.cfg.resolveCipherKey || !this.connectAddress)
16205
16269
  return false;
@@ -16218,9 +16282,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16218
16282
  }
16219
16283
  /**
16220
16284
  * Negotiate the level-2 session key from the decrypted CMD_GATEWAYINFO payload: read its
16221
- * `cipher_id`, resolve that cipher's ECC private key (cloud `get_ciphers`, via the configured
16222
- * `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`. One-shot; emits `level2Ready`
16223
- * on success and `error` on failure (non-fatal — level-1 traffic keeps working regardless).
16285
+ * `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
16286
+ * `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
16287
+ * ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
16288
+ * key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
16289
+ * `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` on failure
16290
+ * (non-fatal — level-1 traffic keeps working regardless).
16224
16291
  *
16225
16292
  * Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
16226
16293
  * failure final for this connection generation: nothing will retry it there, so a later command must be
@@ -16233,26 +16300,36 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16233
16300
  this.trace({ phase: "level2-negotiating", cipherId });
16234
16301
  void (async () => {
16235
16302
  try {
16236
- const eccPrivHex = await this.cfg.resolveCipherKey?.(cipherId);
16303
+ const resolved = await this.cfg.resolveCipherKey?.(cipherId);
16237
16304
  if (this.closed || generation !== this.connectionGeneration)
16238
16305
  return;
16239
- if (!eccPrivHex) {
16240
- this.logger.debug(`[p2p] ${this.cfg.stationSn} no ECC key for cipher_id ${cipherId}`);
16306
+ const keys = typeof resolved === "string" ? { ecc: resolved } : resolved;
16307
+ const eccPrivHex = keys?.ecc;
16308
+ const rsaPrivKey = eccPrivHex ? void 0 : keys?.rsa;
16309
+ if (!eccPrivHex && !rsaPrivKey) {
16310
+ this.logger.debug(`[p2p] ${this.cfg.stationSn} no ECC or RSA key for cipher_id ${cipherId}`);
16241
16311
  this.trace({ phase: "level2-unavailable", reason: "no-cipher-key", cipherId });
16242
16312
  this.settleLevel2();
16243
16313
  return;
16244
16314
  }
16245
- const key = deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex);
16315
+ const key = eccPrivHex ? deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex) : deriveLevel2KeyFromRsaGatewayInfo(gwPayload, rsaPrivKey);
16246
16316
  if (!key) {
16247
16317
  this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
16248
16318
  this.settleLevel2();
16249
16319
  this.emit("error", new Error(`level-2 key derivation failed (cipher_id ${cipherId})`));
16250
16320
  return;
16251
16321
  }
16252
- this.setLevel2Key(key);
16253
- this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key negotiated (cipher_id ${cipherId})`);
16254
- this.trace({ phase: "level2-ready", cipherId });
16255
- this.emit("level2Ready", { cipherId });
16322
+ if (eccPrivHex) {
16323
+ this.setLevel2Key(key);
16324
+ this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key negotiated (cipher_id ${cipherId})`);
16325
+ this.trace({ phase: "level2-ready", cipherId });
16326
+ this.emit("level2Ready", { cipherId });
16327
+ } else {
16328
+ this.level2EcbKey = key;
16329
+ this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 set-payload key negotiated (cipher_id ${cipherId})`);
16330
+ this.trace({ phase: "level2-set-payload-only", cipherId });
16331
+ this.settleLevel2();
16332
+ }
16256
16333
  } catch (e) {
16257
16334
  if (this.closed || generation !== this.connectionGeneration)
16258
16335
  return;
@@ -16322,7 +16399,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16322
16399
  this.closed = false;
16323
16400
  this.connectionGeneration += 1;
16324
16401
  this.resetInboundSequencing();
16325
- if (!this.level2Key) {
16402
+ if (!this.level2KeyServes("set-payload")) {
16326
16403
  this.level2Pending = this.cfg.resolveCipherKey !== void 0;
16327
16404
  this.level2Negotiating = false;
16328
16405
  this.level2Reprompted = false;
@@ -16805,18 +16882,25 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16805
16882
  * mechanism proven for camera selection in media start. Use for control commands routed through a
16806
16883
  * HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
16807
16884
  * yet (caller can fall back to {@link sendStringPayloadCommand}).
16885
+ *
16886
+ * On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
16887
+ * layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
16888
+ * byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
16808
16889
  */
16809
16890
  sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd) {
16810
- if (!this.connectAddress || !this.level2Key)
16811
- return false;
16812
- const value = JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload });
16813
- const body = this.encryptLevel2(Buffer.from(value, "utf-8"));
16814
- if (!body)
16891
+ if (!this.connectAddress || !this.level2KeyServes("set-payload"))
16815
16892
  return false;
16816
- const data = Buffer.concat([
16817
- buildCommandHeader(this.seqNumber, CMD_SET_PAYLOAD),
16818
- buildRawCommandPayload(body, channel, 8, [8, 0], 0)
16819
- ]);
16893
+ const value = Buffer.from(JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload }), "utf-8");
16894
+ let sealed;
16895
+ if (this.level2Key) {
16896
+ const body = this.encryptLevel2(value);
16897
+ if (!body)
16898
+ return false;
16899
+ sealed = buildRawCommandPayload(body, channel, 8, [8, 0], 0);
16900
+ } else {
16901
+ sealed = buildRawCommandPayload(encryptP2PData(paddingP2PData(value), this.level2EcbKey), channel, 2);
16902
+ }
16903
+ const data = Buffer.concat([buildCommandHeader(this.seqNumber, CMD_SET_PAYLOAD), sealed]);
16820
16904
  this.seqNumber = this.seqNumber + 1 & 65535;
16821
16905
  this.send(this.connectAddress, RequestMessageType.DATA, data);
16822
16906
  return true;
@@ -17152,7 +17236,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17152
17236
  if (!this.rsaPrivateKey)
17153
17237
  return void 0;
17154
17238
  try {
17155
- return privateDecrypt({ key: this.rsaPrivateKey, padding: constants.RSA_PKCS1_PADDING }, wrapped);
17239
+ return privateDecrypt2({ key: this.rsaPrivateKey, padding: constants2.RSA_PKCS1_PADDING }, wrapped);
17156
17240
  } catch {
17157
17241
  return void 0;
17158
17242
  }
@@ -17621,7 +17705,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17621
17705
  } catch {
17622
17706
  }
17623
17707
  }
17624
- if (header.commandId === CMD_GATEWAYINFO && header.signCode <= 1 && !this.level2Key && !this.level2Negotiating && this.cfg.resolveCipherKey) {
17708
+ if (header.commandId === CMD_GATEWAYINFO && header.signCode <= 1 && !this.level2KeyServes("set-payload") && !this.level2Negotiating && this.cfg.resolveCipherKey) {
17625
17709
  this.negotiateLevel2Key(data);
17626
17710
  }
17627
17711
  if (header.commandId === CMD_DATABASE && header.signCode > 0) {
@@ -17684,6 +17768,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17684
17768
  this.stopPunchProbes();
17685
17769
  this.connectionGeneration += 1;
17686
17770
  this.level2Key = void 0;
17771
+ this.level2EcbKey = void 0;
17687
17772
  this.level2Seq = 0;
17688
17773
  this.settleLevel2("closed");
17689
17774
  this.connected = false;
@@ -21565,7 +21650,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
21565
21650
  * shared — see {@link P2PCommandRouter.openTalkback}.
21566
21651
  */
21567
21652
  talkbacks = /* @__PURE__ */ new Map();
21568
- /** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
21653
+ /** cipher_id → its ECC and RSA private keys (one eufylife get_ciphers call per cipher), shared across (re)opens. */
21569
21654
  cipherKeyCache = /* @__PURE__ */ new Map();
21570
21655
  /** The recording download in flight per station; a station serves one at a time. */
21571
21656
  recordingDownloads = /* @__PURE__ */ new Map();
@@ -21757,7 +21842,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
21757
21842
  * Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
21758
21843
  *
21759
21844
  * `resolveCipherKey` auto-negotiates the level-2 session key from `CMD_GATEWAYINFO` by resolving the
21760
- * cipher's ECC private key via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
21845
+ * cipher's private keys via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
21761
21846
  * cached on the router instance so a lazy re-open (after idle-detach) reuses the lookup. Only a
21762
21847
  * SUCCESSFUL lookup is cached — caching `undefined` after a transient failure would permanently
21763
21848
  * disable level-2 for the session's life.
@@ -21781,7 +21866,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
21781
21866
  localAddress,
21782
21867
  dskKey,
21783
21868
  noBroadcast: this.deps.noBroadcast,
21784
- resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
21869
+ resolveCipherKey: (cipherId) => this.cipherKeysFor(session, cipherId, adminUserId, stationSn),
21785
21870
  logger: this.deps.logger ?? noopLogger
21786
21871
  });
21787
21872
  session.on("error", (e) => this.reportError(e));
@@ -21806,19 +21891,20 @@ var P2PCommandRouter = class _P2PCommandRouter {
21806
21891
  return session;
21807
21892
  }
21808
21893
  /**
21809
- * The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
21810
- * answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
21811
- * cached.
21894
+ * The ECC and RSA private keys of `cipherId` (`ecc_private_key`, `private_key`), from the router's cache
21895
+ * or one `get_ciphers` call. When the cloud answers with another cipher that carries an ECC key and the
21896
+ * asked one does not, that one is used and traced on `session`. Only a successful lookup — one that
21897
+ * yields either key — is cached.
21812
21898
  */
21813
- async cipherKeyFor(session, cipherId, userId, stationSn) {
21899
+ async cipherKeysFor(session, cipherId, userId, stationSn) {
21814
21900
  if (this.cipherKeyCache.has(cipherId))
21815
21901
  return this.cipherKeyCache.get(cipherId);
21816
- let ecc;
21902
+ let cipher;
21817
21903
  try {
21818
21904
  const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
21819
- ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
21820
- if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
21821
- ecc = ciphers[0].ecc_private_key;
21905
+ cipher = ciphers.find((c) => Number(c.cipher_id) === cipherId);
21906
+ if (cipher?.ecc_private_key === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
21907
+ cipher = ciphers[0];
21822
21908
  this.traceOnStation(session, {
21823
21909
  phase: "cipher-fallback",
21824
21910
  cipherId,
@@ -21828,9 +21914,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21828
21914
  } catch (e) {
21829
21915
  this.deps.onError(e instanceof Error ? e : new Error(String(e)));
21830
21916
  }
21831
- if (ecc !== void 0)
21832
- this.cipherKeyCache.set(cipherId, ecc);
21833
- return ecc;
21917
+ if (cipher?.ecc_private_key === void 0 && cipher?.private_key === void 0)
21918
+ return void 0;
21919
+ const keys = { ecc: cipher.ecc_private_key, rsa: cipher.private_key };
21920
+ this.cipherKeyCache.set(cipherId, keys);
21921
+ return keys;
21834
21922
  }
21835
21923
  /**
21836
21924
  * Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
@@ -21906,7 +21994,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
21906
21994
  await this.sendJsonRaw(sn, cmd.cmd, cmd.data, cmd.channel);
21907
21995
  return;
21908
21996
  case "set-payload":
21909
- await this.sendSetPayloadEnvelope(sn, cmd.cmd, cmd.payload, cmd.channel, cmd.mValue3, void 0, cmd.form);
21997
+ await this.sendSetPayloadEnvelope(sn, cmd.cmd, cmd.payload, cmd.channel, cmd.mValue3, void 0, cmd.form, cmd.level2Sender);
21910
21998
  return;
21911
21999
  case "p2p-station-scalar":
21912
22000
  await this.sendStationScalar(sn, cmd.cmd, cmd.value, cmd.channel);
@@ -22050,7 +22138,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
22050
22138
  const path = homeBase2RecordingPath(channel, opts.recording);
22051
22139
  if (!path)
22052
22140
  throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
22053
- const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
22141
+ const eccKey = (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc;
22054
22142
  if (!eccKey) {
22055
22143
  throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
22056
22144
  }
@@ -22791,9 +22879,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
22791
22879
  const required = opts.waitLevel2 !== "soft" || opts.requireLevel2ForAttached === true && homeBaseAttached;
22792
22880
  if (!required)
22793
22881
  return { session, parentSn, channel, accountId, homeBaseAttached };
22794
- let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), opts.signal);
22882
+ const sender = opts.level2Sender ?? "every";
22883
+ let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
22795
22884
  if (!ready && session.repromptLevel2Key()) {
22796
- ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), opts.signal);
22885
+ ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
22797
22886
  }
22798
22887
  if (!ready)
22799
22888
  throw new StationKeyUnavailableError(parentSn);
@@ -22868,7 +22957,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
22868
22957
  * HomeBase-attached device takes the GCM signCode-8 form, a standalone one the level-1 form. A
22869
22958
  * standalone camera never negotiates a level-2 key, so pinning this to level 2 makes the envelope
22870
22959
  * unreachable on exactly the devices that serve their own RTSP stream. Verified live: a standalone
22871
- * camera accepts the level-1 form. With no `form` (default) it stays level-2 only.
22960
+ * camera accepts the level-1 form. With no `form` (default) it stays level-2 only and waits for the
22961
+ * key every level-2 sender uses; `level2Sender: "set-payload"` also takes an RSA cipher's AES-128-ECB
22962
+ * key (see {@link P2PSession.sendControlLevel2}). A `resolved` session keeps the wait its caller gave it.
22872
22963
  *
22873
22964
  * Both seals REPLAY the frame {@link DIRECT_CMD_SENDS}× at 200ms, as every other fire-and-forget
22874
22965
  * control on this router does: these are unacknowledged datagrams, and a level-1 device is the one
@@ -22877,7 +22968,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
22877
22968
  * throwing (`sendSetPayload` throws when the session has no address) rather than by returning a
22878
22969
  * boolean, so the first pass carries the failure and the rest are repeats.
22879
22970
  */
22880
- async sendSetPayloadEnvelope(sn, cmd, payload, channel, mValue3, resolved, form) {
22971
+ async sendSetPayloadEnvelope(sn, cmd, payload, channel, mValue3, resolved, form, level2Sender) {
22881
22972
  if (form === "auto") {
22882
22973
  await this.sendBySessionLevel(sn, {
22883
22974
  l1: async ({ session, accountId }) => {
@@ -22894,7 +22985,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
22894
22985
  });
22895
22986
  return;
22896
22987
  }
22897
- await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved);
22988
+ await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
22898
22989
  }
22899
22990
  /**
22900
22991
  * **`ff09-actuate` intent** — build the `ff09` AES-128-CBC frame ({@link buildFf09Frame}, shared with the
@@ -26016,7 +26107,7 @@ var HmacSigner = class {
26016
26107
  var TUYA_CHKEY = "7cbfe6d8";
26017
26108
 
26018
26109
  // dist/transport/tuya/client.js
26019
- import { randomBytes as randomBytes5, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
26110
+ import { randomBytes as randomBytes5, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants3 } from "node:crypto";
26020
26111
 
26021
26112
  // dist/transport/tuya/et3.js
26022
26113
  import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv8, createHmac as createHmac5, randomBytes as randomBytes4 } from "node:crypto";
@@ -26183,7 +26274,7 @@ function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
26183
26274
  format: "jwk"
26184
26275
  });
26185
26276
  const md5Hex2 = createHash6("md5").update(password).digest("hex");
26186
- return publicEncrypt({ key: rsaKey, padding: constants2.RSA_PKCS1_PADDING }, Buffer.from(md5Hex2)).toString("hex");
26277
+ return publicEncrypt({ key: rsaKey, padding: constants3.RSA_PKCS1_PADDING }, Buffer.from(md5Hex2)).toString("hex");
26187
26278
  }
26188
26279
  var TuyaClient = class {
26189
26280
  signer;
@@ -31558,6 +31649,7 @@ export {
31558
31649
  SECURITY_PARAMS,
31559
31650
  SERVER_STATIC_PUBLIC_KEY_HEX,
31560
31651
  SIREN_MEMBERS,
31652
+ SMART_DROP_MEMBERS,
31561
31653
  SMART_LIGHT_MEMBERS,
31562
31654
  SMOKE_MEMBERS,
31563
31655
  SOLIX_ENERGY_METER_MEMBERS,