@mega-yfue/eufy-sdk 0.5.0-beta.87 → 0.5.0-beta.89
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 +4 -2
- package/dist/core/contracts.d.ts +13 -4
- package/dist/index.js +338 -57
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/battery.d.ts +6 -1
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/keypad.d.ts +20 -23
- package/dist/model/capabilities/lock.d.ts +23 -11
- package/dist/model/device-family.d.ts +12 -0
- package/dist/transport/http/mega-client.d.ts +9 -0
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +73 -0
- package/dist/transport/p2p/keyed-payload.d.ts +112 -0
- package/dist/transport/p2p/p2p-session.d.ts +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2160,6 +2160,32 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2160
2160
|
const shard = this.region.split("-")[0];
|
|
2161
2161
|
return shard === "us" ? "security-app.eufylife.com" : `security-app-${shard}.eufylife.com`;
|
|
2162
2162
|
}
|
|
2163
|
+
/**
|
|
2164
|
+
* The device's long-lived P-256 public key, as the cloud publishes it for key-wrapped commands
|
|
2165
|
+
* (`type=2`): 64 bytes hex with no `04` prefix. A plain authed GET on the security-app host — the
|
|
2166
|
+
* one read here that is neither signed nor body-encrypted, matching how the app fetches it.
|
|
2167
|
+
* Cached per serial for the client's lifetime; the key does not rotate in normal operation.
|
|
2168
|
+
*/
|
|
2169
|
+
async getDevicePublicKey(deviceSn) {
|
|
2170
|
+
const cached2 = this.devicePublicKeys.get(deviceSn);
|
|
2171
|
+
if (cached2)
|
|
2172
|
+
return cached2;
|
|
2173
|
+
const url = `https://${this.securityAppHost()}/v1/app/public_key/query?device_sn=${encodeURIComponent(deviceSn)}&type=2`;
|
|
2174
|
+
const res = await fetch(url, {
|
|
2175
|
+
method: "GET",
|
|
2176
|
+
headers: { ...this.baseHeaders(), ...this.authTokenHeaders() },
|
|
2177
|
+
signal: AbortSignal.timeout(2e4)
|
|
2178
|
+
});
|
|
2179
|
+
const env = parseMaybeJson(await res.text());
|
|
2180
|
+
const key = env?.data?.public_key;
|
|
2181
|
+
if (res.status !== 200 || env?.code !== 0 || typeof key !== "string" || !key.length) {
|
|
2182
|
+
throw new MegaApiError(`public_key/query failed (${res.status}/${env?.code}): ${env?.msg ?? "no public key"}`, env?.code, res.status);
|
|
2183
|
+
}
|
|
2184
|
+
this.devicePublicKeys.set(deviceSn, key);
|
|
2185
|
+
return key;
|
|
2186
|
+
}
|
|
2187
|
+
/** Per-serial cache for {@link getDevicePublicKey}. */
|
|
2188
|
+
devicePublicKeys = /* @__PURE__ */ new Map();
|
|
2163
2189
|
/**
|
|
2164
2190
|
* Signed+encrypted POST to the security-app data host (face recognition, etc.).
|
|
2165
2191
|
* Despite the different host, these endpoints use the SAME algo_ecdh pipeline as
|
|
@@ -3661,6 +3687,7 @@ var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
|
|
|
3661
3687
|
var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
|
|
3662
3688
|
var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
|
|
3663
3689
|
var isVideoSmartLock = (ctx) => ctx.deviceType === DeviceType.LOCK_8530;
|
|
3690
|
+
var isClassicWifiLock = (ctx) => ctx.deviceType === DeviceType.LOCK_WIFI && typeof ctx.serial === "string" && ctx.serial.startsWith("T8520") && ctx.serial.charAt(6) === "2";
|
|
3664
3691
|
|
|
3665
3692
|
// dist/model/capabilities/rtsp.js
|
|
3666
3693
|
var RTSP_PARAM = {
|
|
@@ -4712,6 +4739,10 @@ function cellGated(members) {
|
|
|
4712
4739
|
});
|
|
4713
4740
|
return Object.fromEntries(gated);
|
|
4714
4741
|
}
|
|
4742
|
+
function isChargingState(v) {
|
|
4743
|
+
const n = Number(v);
|
|
4744
|
+
return n !== 0 && n !== 2;
|
|
4745
|
+
}
|
|
4715
4746
|
var BATTERY_MEMBERS = cellGated({
|
|
4716
4747
|
/**
|
|
4717
4748
|
* The headline percentage, and this capability's detection evidence: reporting 1101 is what proves a
|
|
@@ -4737,10 +4768,7 @@ var BATTERY_MEMBERS = cellGated({
|
|
|
4737
4768
|
type: "bool",
|
|
4738
4769
|
kind: "boolean",
|
|
4739
4770
|
provenance: "apk",
|
|
4740
|
-
coerce:
|
|
4741
|
-
const n = Number(v);
|
|
4742
|
-
return n !== 0 && n !== 2;
|
|
4743
|
-
},
|
|
4771
|
+
coerce: isChargingState,
|
|
4744
4772
|
description: "Charging in progress \u2014 derived from BATTERY_STATUS (2111) as `value \u2209 {0,2}`, matching the app's own decode. 0/2 = not charging; a solar/USB charge (incl. a built-in solar panel, which reads 4) is non-zero."
|
|
4745
4773
|
},
|
|
4746
4774
|
/**
|
|
@@ -7534,7 +7562,7 @@ function actuate(engage, ctx, sink) {
|
|
|
7534
7562
|
});
|
|
7535
7563
|
}
|
|
7536
7564
|
return sink.dispatch({
|
|
7537
|
-
kind: "ff09-actuate",
|
|
7565
|
+
kind: isClassicWifiLock(ctx) ? "keyed-payload-actuate" : "ff09-actuate",
|
|
7538
7566
|
engage,
|
|
7539
7567
|
adminUserId: ctx.adminUserId,
|
|
7540
7568
|
username: ctx.accountName ?? "",
|
|
@@ -7579,6 +7607,8 @@ function nativeReport(signal) {
|
|
|
7579
7607
|
const report = signal.json.payload;
|
|
7580
7608
|
return report?.code === 0 ? report : void 0;
|
|
7581
7609
|
}
|
|
7610
|
+
var speaksFf09Settings = (ctx) => !isClassicWifiLock(ctx);
|
|
7611
|
+
var rainModeAvailable = (ctx) => overP2p(ctx) && speaksFf09Settings(ctx);
|
|
7582
7612
|
var LOCK_MEMBERS = {
|
|
7583
7613
|
/**
|
|
7584
7614
|
* The lock's own state, verified on param 6000: "4" = locked, "3" = unlocked.
|
|
@@ -7609,8 +7639,8 @@ var LOCK_MEMBERS = {
|
|
|
7609
7639
|
description: "Lock battery level 0-100 (verified: param 1101, alias 6001)."
|
|
7610
7640
|
},
|
|
7611
7641
|
/**
|
|
7612
|
-
* Link quality in dBm as the lock measures it, on the shared param 1141.
|
|
7613
|
-
* are fire-and-forget, so a weak link is silent rather than an error.
|
|
7642
|
+
* Link quality in dBm as the lock measures it, on the shared param 1141. On the `ff09` wire the
|
|
7643
|
+
* actuations are fire-and-forget, so a weak link is silent rather than an error.
|
|
7614
7644
|
*/
|
|
7615
7645
|
rssi: {
|
|
7616
7646
|
param: 1141,
|
|
@@ -7620,9 +7650,15 @@ var LOCK_MEMBERS = {
|
|
|
7620
7650
|
provenance: "verified",
|
|
7621
7651
|
description: "Lock signal strength (verified: param 1141 = RSSI)."
|
|
7622
7652
|
},
|
|
7623
|
-
/**
|
|
7653
|
+
/**
|
|
7654
|
+
* Lock the deadbolt/door. Resolves after sending, without confirming physical state — except on the
|
|
7655
|
+
* classic Wi-Fi lock, which answers: there it rejects on a device refusal or no reply as well.
|
|
7656
|
+
*/
|
|
7624
7657
|
lock: method(({ ctx, sink }) => () => actuate(true, ctx, sink), "Lock the deadbolt/door.", actuationAvailable),
|
|
7625
|
-
/**
|
|
7658
|
+
/**
|
|
7659
|
+
* Unlock the deadbolt/door. Resolves after sending, without confirming physical state — except on the
|
|
7660
|
+
* classic Wi-Fi lock, which answers: there it rejects on a device refusal or no reply as well.
|
|
7661
|
+
*/
|
|
7626
7662
|
unlock: method(({ ctx, sink }) => () => actuate(false, ctx, sink), "Unlock the deadbolt/door.", actuationAvailable),
|
|
7627
7663
|
/**
|
|
7628
7664
|
* Read-modify-write the auto-lock setting: the transport GETs the device's current settings, changes
|
|
@@ -7644,13 +7680,13 @@ var LOCK_MEMBERS = {
|
|
|
7644
7680
|
enabled,
|
|
7645
7681
|
delaySeconds
|
|
7646
7682
|
});
|
|
7647
|
-
}, "Read-modify-write the auto-lock setting, preserving the fields it does not change.", (ctx) => !isVideoSmartLock(ctx)),
|
|
7683
|
+
}, "Read-modify-write the auto-lock setting, preserving the fields it does not change.", (ctx) => !isVideoSmartLock(ctx) && speaksFf09Settings(ctx)),
|
|
7648
7684
|
/**
|
|
7649
7685
|
* Toggle Rain Mode — a pure blind write, unlike `setAutoLock`: the compact frame carries only this one
|
|
7650
7686
|
* field, so there is no GET pass and it is fire-and-forget. Confirmed on-device end-to-end in both
|
|
7651
7687
|
* directions, with the app UI reflecting the new state afterward. P2P video lock only.
|
|
7652
7688
|
*/
|
|
7653
|
-
setRainMode: method(({ ctx, sink }) => (enabled) => settingToggle(LOCK_SETTING_ID.RAIN_MODE, "setRainMode", enabled, ctx, sink), "Toggle Rain Mode.",
|
|
7689
|
+
setRainMode: method(({ ctx, sink }) => (enabled) => settingToggle(LOCK_SETTING_ID.RAIN_MODE, "setRainMode", enabled, ctx, sink), "Toggle Rain Mode.", rainModeAvailable),
|
|
7654
7690
|
/**
|
|
7655
7691
|
* Lock the door by a single touch on the pad, with no code. First of the six compact toggles: all
|
|
7656
7692
|
* `unverified` (frame shape read out of the app's JS, never captured), so no setter is installed and
|
|
@@ -7809,36 +7845,34 @@ var LOCK = {
|
|
|
7809
7845
|
// dist/model/capabilities/keypad.js
|
|
7810
7846
|
var KEYPAD_MEMBERS = {
|
|
7811
7847
|
/**
|
|
7812
|
-
* `
|
|
7813
|
-
*
|
|
7814
|
-
*
|
|
7815
|
-
*
|
|
7848
|
+
* `APP_CMD_KEYPAD_BATTERY_CAP_STATE` (1653). The app's sensor parser maps it to `sensor_is_low_power`,
|
|
7849
|
+
* the field `APP_CMD_ENTRY_SENSOR_BAT_STATE` and `APP_CMD_MOTION_SENSOR_BAT_STATE` decode as a boolean,
|
|
7850
|
+
* yet gives 1653 itself no decode case. `unexposed` until a device confirms which value means low; in
|
|
7851
|
+
* the schema for `getProperty`, absent from the typed surface.
|
|
7816
7852
|
*/
|
|
7817
7853
|
batteryLow: {
|
|
7818
|
-
param:
|
|
7854
|
+
param: 1653,
|
|
7819
7855
|
type: "bool",
|
|
7820
7856
|
kind: "boolean",
|
|
7821
|
-
provenance: "
|
|
7857
|
+
provenance: "apk",
|
|
7822
7858
|
unexposed: true,
|
|
7823
|
-
description: "Keypad low-battery flag. UNVERIFIED:
|
|
7859
|
+
description: "Keypad low-battery flag (APP_CMD_KEYPAD_BATTERY_CAP_STATE, 1653). UNVERIFIED polarity: the app names it its low-battery field but does not decode its value."
|
|
7824
7860
|
},
|
|
7825
7861
|
/**
|
|
7826
|
-
*
|
|
7827
|
-
*
|
|
7828
|
-
* confirmed value space. In the schema for `getProperty`, absent from the typed surface.
|
|
7862
|
+
* `APP_CMD_KEYPAD_BATTERY_CHARGER_STATE` (1655), decoded the way the app's sensor parser decodes it:
|
|
7863
|
+
* `charging = value ∉ {0,2}` ({@link isChargingState}).
|
|
7829
7864
|
*/
|
|
7830
7865
|
charging: {
|
|
7831
|
-
param:
|
|
7866
|
+
param: 1655,
|
|
7832
7867
|
type: "bool",
|
|
7833
7868
|
kind: "boolean",
|
|
7834
|
-
provenance: "
|
|
7835
|
-
|
|
7836
|
-
description: "Keypad charging
|
|
7869
|
+
provenance: "apk",
|
|
7870
|
+
coerce: isChargingState,
|
|
7871
|
+
description: "Keypad charging \u2014 derived from APP_CMD_KEYPAD_BATTERY_CHARGER_STATE (1655) as `value \u2209 {0,2}`, matching the app's own decode."
|
|
7837
7872
|
},
|
|
7838
7873
|
/**
|
|
7839
|
-
* The keypad's link quality in dBm
|
|
7840
|
-
*
|
|
7841
|
-
* normalised to a bar count.
|
|
7874
|
+
* The keypad's link quality in dBm, on the same param 1141 the other sub-1G sensors report signal
|
|
7875
|
+
* strength on. Reported as the device measures it: raw dBm, never normalised to a bar count.
|
|
7842
7876
|
*/
|
|
7843
7877
|
rssi: {
|
|
7844
7878
|
param: 1141,
|
|
@@ -15141,13 +15175,8 @@ function abortable(work, signal) {
|
|
|
15141
15175
|
]);
|
|
15142
15176
|
}
|
|
15143
15177
|
|
|
15144
|
-
// dist/transport/p2p/
|
|
15145
|
-
import {
|
|
15146
|
-
|
|
15147
|
-
// dist/transport/p2p/p2p-session.js
|
|
15148
|
-
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
15149
|
-
import dgram from "node:dgram";
|
|
15150
|
-
import { constants as constants2, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes2 } from "node:crypto";
|
|
15178
|
+
// dist/transport/p2p/keyed-payload.js
|
|
15179
|
+
import { createCipheriv as createCipheriv3, randomBytes as randomBytes3 } from "node:crypto";
|
|
15151
15180
|
|
|
15152
15181
|
// dist/transport/p2p/codec.js
|
|
15153
15182
|
var codec_exports = {};
|
|
@@ -15174,6 +15203,7 @@ __export(codec_exports, {
|
|
|
15174
15203
|
deriveLevel2KeyFromGatewayInfo: () => deriveLevel2KeyFromGatewayInfo,
|
|
15175
15204
|
deriveLevel2KeyFromRsaGatewayInfo: () => deriveLevel2KeyFromRsaGatewayInfo,
|
|
15176
15205
|
eciesUnwrap: () => eciesUnwrap,
|
|
15206
|
+
eciesWrap: () => eciesWrap,
|
|
15177
15207
|
encryptP2PData: () => encryptP2PData,
|
|
15178
15208
|
frameMessage: () => frameMessage,
|
|
15179
15209
|
gatewayInfoCipherId: () => gatewayInfoCipherId,
|
|
@@ -15186,7 +15216,7 @@ __export(codec_exports, {
|
|
|
15186
15216
|
readNullTerminatedString: () => readNullTerminatedString,
|
|
15187
15217
|
stringWithLength: () => stringWithLength
|
|
15188
15218
|
});
|
|
15189
|
-
import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, timingSafeEqual } from "node:crypto";
|
|
15219
|
+
import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
15190
15220
|
var MAGIC_WORD = "XZYH";
|
|
15191
15221
|
var RequestMessageType = {
|
|
15192
15222
|
LOCAL_LOOKUP: Buffer.from([241, 48]),
|
|
@@ -15323,6 +15353,19 @@ function eciesUnwrap(envelope, eccPrivateKeyHex, options = {}) {
|
|
|
15323
15353
|
return void 0;
|
|
15324
15354
|
}
|
|
15325
15355
|
}
|
|
15356
|
+
function eciesWrap(plaintext, peerPublicKey, options = {}) {
|
|
15357
|
+
const ecdh = createECDH2("prime256v1");
|
|
15358
|
+
if (options.ephemeralPrivateKey)
|
|
15359
|
+
ecdh.setPrivateKey(options.ephemeralPrivateKey);
|
|
15360
|
+
else
|
|
15361
|
+
ecdh.generateKeys();
|
|
15362
|
+
const kdf = eufyKdf(ecdh.computeSecret(peerPublicKey), 48);
|
|
15363
|
+
const iv = options.iv ?? randomBytes2(16);
|
|
15364
|
+
const cipher = createCipheriv2("aes-128-cbc", kdf.subarray(0, 16), iv);
|
|
15365
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
15366
|
+
const tag2 = createHmac2("sha256", kdf.subarray(16, 48)).update(iv).update(ct).digest();
|
|
15367
|
+
return Buffer.concat([ecdh.getPublicKey(null, "compressed"), iv, ct, tag2]);
|
|
15368
|
+
}
|
|
15326
15369
|
function deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivateKeyHex) {
|
|
15327
15370
|
const plain = eciesUnwrap(gwPayload.subarray(4, 4 + 129), eccPrivateKeyHex);
|
|
15328
15371
|
if (!plain)
|
|
@@ -16042,6 +16085,65 @@ function commandName(id) {
|
|
|
16042
16085
|
return COMMAND_NAMES[id] ?? `CMD_${id}`;
|
|
16043
16086
|
}
|
|
16044
16087
|
|
|
16088
|
+
// dist/transport/p2p/keyed-payload.js
|
|
16089
|
+
var KEYED_PAYLOAD_SEQ_ERROR = -151;
|
|
16090
|
+
function serialIv(deviceSn) {
|
|
16091
|
+
const iv = Buffer.alloc(16);
|
|
16092
|
+
Buffer.from(deviceSn, "utf8").copy(iv, 0, 0, 16);
|
|
16093
|
+
return iv;
|
|
16094
|
+
}
|
|
16095
|
+
function spellAesKey(key) {
|
|
16096
|
+
return key.toString("hex").toUpperCase();
|
|
16097
|
+
}
|
|
16098
|
+
function buildKeyBlob(aesKeyHex, devicePublicKeyHex, rnd = {}) {
|
|
16099
|
+
const devicePublicKey = Buffer.concat([Buffer.from([4]), Buffer.from(devicePublicKeyHex, "hex")]);
|
|
16100
|
+
return eciesWrap(Buffer.from(aesKeyHex, "utf8"), devicePublicKey, {
|
|
16101
|
+
ephemeralPrivateKey: rnd.ephemeralPrivateKey,
|
|
16102
|
+
iv: rnd.nonce
|
|
16103
|
+
}).toString("hex");
|
|
16104
|
+
}
|
|
16105
|
+
function sealInnerPayload(innerJson, aesKeyHex, deviceSn) {
|
|
16106
|
+
const cipher = createCipheriv3("aes-128-cbc", Buffer.from(aesKeyHex, "hex"), serialIv(deviceSn));
|
|
16107
|
+
return Buffer.concat([cipher.update(Buffer.from(innerJson, "utf8")), cipher.final()]).toString("base64");
|
|
16108
|
+
}
|
|
16109
|
+
function buildKeyedActuatePayload(id, devicePublicKeyHex, userName, rnd) {
|
|
16110
|
+
const aesKeyHex = spellAesKey(rnd.aesKey ?? randomBytes3(16));
|
|
16111
|
+
const inner = JSON.stringify({
|
|
16112
|
+
shortUserId: id.shortUserId,
|
|
16113
|
+
slOperation: id.engage ? 1 : 0,
|
|
16114
|
+
userId: id.adminUserId,
|
|
16115
|
+
userName,
|
|
16116
|
+
seq_num: rnd.seqNum
|
|
16117
|
+
});
|
|
16118
|
+
return {
|
|
16119
|
+
key: buildKeyBlob(aesKeyHex, devicePublicKeyHex, rnd),
|
|
16120
|
+
payload: sealInnerPayload(inner, aesKeyHex, id.deviceSn)
|
|
16121
|
+
};
|
|
16122
|
+
}
|
|
16123
|
+
function keyedPayloadReportCode(json, engage) {
|
|
16124
|
+
if (json?.cmd !== CommandType.P2P_QUERY_STATUS_IN_LOCK)
|
|
16125
|
+
return void 0;
|
|
16126
|
+
const payload = json.payload;
|
|
16127
|
+
const slState = engage ? "4" : "3";
|
|
16128
|
+
if (payload?.slState !== slState)
|
|
16129
|
+
return void 0;
|
|
16130
|
+
return typeof payload?.code === "number" ? payload.code : void 0;
|
|
16131
|
+
}
|
|
16132
|
+
function keyedPayloadSeqMark(json) {
|
|
16133
|
+
if (json?.cmd !== CommandType.CMD_DOORLOCK_P2P_SEQ)
|
|
16134
|
+
return void 0;
|
|
16135
|
+
const seq = json.payload?.seq_num;
|
|
16136
|
+
return typeof seq === "number" && Number.isFinite(seq) ? seq : void 0;
|
|
16137
|
+
}
|
|
16138
|
+
|
|
16139
|
+
// dist/transport/p2p/command-router.js
|
|
16140
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
16141
|
+
|
|
16142
|
+
// dist/transport/p2p/p2p-session.js
|
|
16143
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
16144
|
+
import dgram from "node:dgram";
|
|
16145
|
+
import { constants as constants2, createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes4 } from "node:crypto";
|
|
16146
|
+
|
|
16045
16147
|
// dist/transport/p2p/live-trace.js
|
|
16046
16148
|
var LIVE_TRACE_MESSAGE = "[live] start trace";
|
|
16047
16149
|
function traceLiveStart(logger, trace, source) {
|
|
@@ -17450,8 +17552,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17450
17552
|
encryptLevel2(plaintext) {
|
|
17451
17553
|
if (!this.level2Key)
|
|
17452
17554
|
return void 0;
|
|
17453
|
-
const nonce =
|
|
17454
|
-
const c =
|
|
17555
|
+
const nonce = randomBytes4(12);
|
|
17556
|
+
const c = createCipheriv4("aes-256-gcm", this.level2Key, nonce);
|
|
17455
17557
|
c.setAAD(GCM_AAD);
|
|
17456
17558
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
17457
17559
|
const sub2 = Buffer.alloc(4);
|
|
@@ -17661,22 +17763,28 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17661
17763
|
* Low-level: send a `CMD_SET_PAYLOAD` (1350) wrapping `{account_id, cmd:<subCmd>, mChannel,
|
|
17662
17764
|
* payload, transaction}` over the level-1 channel. The reply arrives as a `NOTIFY_PAYLOAD`
|
|
17663
17765
|
* (1351) frame (level-1 decrypted, surfaced via the `data` event / `frame.json`).
|
|
17766
|
+
*
|
|
17767
|
+
* `opts.key` makes it a keyed envelope: the sealed per-command key rides ahead of the other fields
|
|
17768
|
+
* as the value's `key`, and the value goes out unencrypted, encryption-type 0, with `=` written
|
|
17769
|
+
* `=` — the form the vendor app sends one in, and the one form the device reads it in.
|
|
17664
17770
|
*/
|
|
17665
17771
|
sendSetPayload(subCmd, payload = {}, opts = {}) {
|
|
17666
17772
|
if (!this.connectAddress)
|
|
17667
17773
|
throw new Error("not connected");
|
|
17668
17774
|
const channel = opts.channel ?? 0;
|
|
17775
|
+
const keyed = opts.key !== void 0;
|
|
17669
17776
|
const inner = opts.rawValue ?? {
|
|
17777
|
+
...keyed ? { key: opts.key } : {},
|
|
17670
17778
|
account_id: opts.accountId ?? "",
|
|
17671
17779
|
cmd: subCmd,
|
|
17672
17780
|
mChannel: channel,
|
|
17673
17781
|
mValue3: 0,
|
|
17674
17782
|
...opts.omitPayload ? {} : { payload }
|
|
17675
17783
|
};
|
|
17676
|
-
const value = JSON.stringify(inner);
|
|
17784
|
+
const value = keyed ? JSON.stringify(inner).replace(/=/g, "\\u003d") : JSON.stringify(inner);
|
|
17677
17785
|
const body = Buffer.concat([
|
|
17678
17786
|
buildCommandHeader(this.seqNumber, opts.wrapCmd ?? CMD_SET_PAYLOAD),
|
|
17679
|
-
buildStringCommandPayload(value, channel, this.level1Key, 1)
|
|
17787
|
+
buildStringCommandPayload(value, channel, keyed ? void 0 : this.level1Key, 1)
|
|
17680
17788
|
]);
|
|
17681
17789
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
17682
17790
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} sendSetPayload subCmd=${subCmd}`);
|
|
@@ -18027,7 +18135,7 @@ function buildDeviceNameBody(channel, name, accountId) {
|
|
|
18027
18135
|
}
|
|
18028
18136
|
|
|
18029
18137
|
// dist/transport/ff09.js
|
|
18030
|
-
import { createCipheriv as
|
|
18138
|
+
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5 } from "node:crypto";
|
|
18031
18139
|
var LOCK_API_COMMAND = {
|
|
18032
18140
|
/** Lock / unlock the deadbolt — video lock AND garage door, both directions (see above). */
|
|
18033
18141
|
ON_OFF_LOCK: 6018,
|
|
@@ -18107,7 +18215,7 @@ function assembleFf09Frame(commandCode, apiCommand, plain, adminUserId, deviceSn
|
|
|
18107
18215
|
const key = Buffer.concat([Buffer.from(adminUserId.slice(-12), "ascii"), u32be(keyTime)]);
|
|
18108
18216
|
const iv = Buffer.alloc(16);
|
|
18109
18217
|
Buffer.from(deviceSn, "ascii").copy(iv);
|
|
18110
|
-
const cipher =
|
|
18218
|
+
const cipher = createCipheriv5("aes-128-cbc", key, iv);
|
|
18111
18219
|
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
18112
18220
|
const body = Buffer.concat([Buffer.from([3, 0, 2]), u16be(16384 | commandCode), ct]);
|
|
18113
18221
|
const size = 2 + 2 + body.length + 1;
|
|
@@ -21815,6 +21923,17 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21815
21923
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
21816
21924
|
/** The recording download in flight per station; a station serves one at a time. */
|
|
21817
21925
|
recordingDownloads = /* @__PURE__ */ new Map();
|
|
21926
|
+
/**
|
|
21927
|
+
* The `seq_num` last sent to each device over the keyed-payload wire. The device holds a mark for the life
|
|
21928
|
+
* of a P2P connection: the first keyed command on a new connection is accepted at whatever number it
|
|
21929
|
+
* carries and becomes the mark, and after that a number not above the mark is refused with the mark
|
|
21930
|
+
* published ({@link keyedPayloadSeqMark}). Each accepted command moves this to its number, a refusal
|
|
21931
|
+
* moves it to the published mark + 1, and a device absent here has not been sent one in this process and
|
|
21932
|
+
* seeds from a small random number — which a new connection takes as sent.
|
|
21933
|
+
*/
|
|
21934
|
+
keyedSequences = /* @__PURE__ */ new Map();
|
|
21935
|
+
/** The keyed-payload command in flight per device, which the next one for that device waits on. */
|
|
21936
|
+
keyedQueue = /* @__PURE__ */ new Map();
|
|
21818
21937
|
constructor(deps) {
|
|
21819
21938
|
this.deps = deps;
|
|
21820
21939
|
this.manager = new SessionManager({
|
|
@@ -22170,6 +22289,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22170
22289
|
case "ff09-actuate":
|
|
22171
22290
|
await this.sendFf09Actuate(sn, cmd);
|
|
22172
22291
|
return;
|
|
22292
|
+
case "keyed-payload-actuate":
|
|
22293
|
+
await this.sendKeyedPayloadActuate(sn, cmd);
|
|
22294
|
+
return;
|
|
22173
22295
|
case "ff09-autolock":
|
|
22174
22296
|
await this.sendFf09Autolock(sn, cmd);
|
|
22175
22297
|
return;
|
|
@@ -23152,6 +23274,163 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23152
23274
|
}
|
|
23153
23275
|
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 }));
|
|
23154
23276
|
}
|
|
23277
|
+
/**
|
|
23278
|
+
* **`keyed-payload-actuate` intent** — the classic Wi-Fi lock's actuation: the sealed fields from
|
|
23279
|
+
* {@link buildKeyedActuatePayload}, sent as a keyed envelope through {@link P2PSession.sendSetPayload}
|
|
23280
|
+
* on the lock's device channel. No session key is waited for — the envelope seals its own per-command
|
|
23281
|
+
* key for the device's public key (fetched from the cloud, cached per serial), so it goes as soon as
|
|
23282
|
+
* the session is up. The device shows the acting member by display name, which lives on the device
|
|
23283
|
+
* record's `member` rather than in the intent, so it is read here and falls back to the account name
|
|
23284
|
+
* the intent carries.
|
|
23285
|
+
*
|
|
23286
|
+
* The device answers, so the outcome is read rather than assumed: {@link awaitKeyedReply} settles each
|
|
23287
|
+
* send on the replies, and resolves a {@link KEYED_PAYLOAD_SEQ_ERROR} refusal with the mark the device
|
|
23288
|
+
* publishes in the refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin. On that mark the command resends once,
|
|
23289
|
+
* at mark + 1; a second refusal, or a refusal that published no mark, throws. Commands run one at a time
|
|
23290
|
+
* per device: the replies carry no correlation id, so one command's answer must never be read as
|
|
23291
|
+
* another's.
|
|
23292
|
+
*/
|
|
23293
|
+
sendKeyedPayloadActuate(sn, cmd) {
|
|
23294
|
+
return this.serializeKeyed(sn, async () => {
|
|
23295
|
+
const dev = await this.deviceFor(sn);
|
|
23296
|
+
const member = (dev.raw ?? {}).member;
|
|
23297
|
+
const userName = typeof member?.nick_name === "string" && member.nick_name ? member.nick_name : cmd.username;
|
|
23298
|
+
const devicePublicKey = await this.deps.mega.getDevicePublicKey(sn);
|
|
23299
|
+
const resolved = await this.resolveSession(sn, { waitLevel2: false });
|
|
23300
|
+
const send = (seqNum) => {
|
|
23301
|
+
const { key, payload } = buildKeyedActuatePayload(cmd, devicePublicKey, userName, { seqNum });
|
|
23302
|
+
resolved.session.sendSetPayload(CommandType.P2P_ON_OFF_LOCK, payload, {
|
|
23303
|
+
accountId: cmd.adminUserId,
|
|
23304
|
+
channel: resolved.channel,
|
|
23305
|
+
key
|
|
23306
|
+
});
|
|
23307
|
+
};
|
|
23308
|
+
await this.sendKeyedAtSequence(sn, resolved.session, resolved.channel, cmd.engage, send);
|
|
23309
|
+
});
|
|
23310
|
+
}
|
|
23311
|
+
/** Run `work` once every keyed-payload command already queued for `sn` has settled. */
|
|
23312
|
+
serializeKeyed(sn, work) {
|
|
23313
|
+
const previous = this.keyedQueue.get(sn) ?? Promise.resolve();
|
|
23314
|
+
const run = previous.then(work);
|
|
23315
|
+
const settled = run.then(() => void 0, () => void 0);
|
|
23316
|
+
this.keyedQueue.set(sn, settled);
|
|
23317
|
+
void settled.then(() => {
|
|
23318
|
+
if (this.keyedQueue.get(sn) === settled)
|
|
23319
|
+
this.keyedQueue.delete(sn);
|
|
23320
|
+
});
|
|
23321
|
+
return run;
|
|
23322
|
+
}
|
|
23323
|
+
/**
|
|
23324
|
+
* Send one keyed command at the device's next sequence and, on a sequence refusal, resend once at the
|
|
23325
|
+
* mark the refusal published.
|
|
23326
|
+
*
|
|
23327
|
+
* A device's first number in a process is a small random seed — the scale the vendor app's own counters
|
|
23328
|
+
* run at — and every later one is the previous plus one. The device takes the first number on a new
|
|
23329
|
+
* connection as sent, and from then on refuses one not above the mark it holds, stating the mark in the
|
|
23330
|
+
* refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin ({@link awaitKeyedReply} resolves the refusal with it).
|
|
23331
|
+
* So a refusal is resolved to that mark and the command resent once at mark + 1, the number the device
|
|
23332
|
+
* accepts after a refusal; a second refusal, or a refusal that carried no mark, throws. The accepted
|
|
23333
|
+
* number is recorded, so the next command starts just above it.
|
|
23334
|
+
*
|
|
23335
|
+
* @param engage whether the command drives to the engaged (locked) state, which its own report states.
|
|
23336
|
+
* @param send dispatches the envelope at the sequence it is given.
|
|
23337
|
+
*/
|
|
23338
|
+
async sendKeyedAtSequence(sn, session, channel, engage, send) {
|
|
23339
|
+
const last = this.keyedSequences.get(sn);
|
|
23340
|
+
const first = last === void 0 ? Math.trunc(Math.random() * 1e3) : last + 1;
|
|
23341
|
+
this.keyedSequences.set(sn, first);
|
|
23342
|
+
const reply = await this.awaitKeyedReply(sn, session, channel, engage, () => send(first));
|
|
23343
|
+
if (reply.code === 0)
|
|
23344
|
+
return;
|
|
23345
|
+
if (reply.code !== KEYED_PAYLOAD_SEQ_ERROR || reply.mark === void 0) {
|
|
23346
|
+
throw new Error(`keyed-payload command rejected by ${sn} with code ${reply.code}`);
|
|
23347
|
+
}
|
|
23348
|
+
const resend = reply.mark + 1;
|
|
23349
|
+
this.keyedSequences.set(sn, resend);
|
|
23350
|
+
const second = await this.awaitKeyedReply(sn, session, channel, engage, () => send(resend));
|
|
23351
|
+
if (second.code === 0)
|
|
23352
|
+
return;
|
|
23353
|
+
throw new Error(`keyed-payload command rejected by ${sn} with code ${second.code}`);
|
|
23354
|
+
}
|
|
23355
|
+
/**
|
|
23356
|
+
* Send one keyed-payload command and read the device's answer for it from the replies on the command's
|
|
23357
|
+
* channel: the `SET_PAYLOAD` echo (a `commandResult`), at once, carrying the result as an int32 — or,
|
|
23358
|
+
* when the echo was not read and the command changed the device's state, the `NOTIFY_PAYLOAD` about two
|
|
23359
|
+
* seconds later carrying the result again as a `P2P_QUERY_STATUS_IN_LOCK` (1955) document. A report
|
|
23360
|
+
* is tied to the command by its `slState`: only one stating the state this command drives to, by
|
|
23361
|
+
* `engage` ({@link keyedPayloadReportCode}), is read as its answer. A report of the other state is a
|
|
23362
|
+
* previous command's, landing after that command settled on its echo, and is ignored.
|
|
23363
|
+
*
|
|
23364
|
+
* - An accepted result (code `0`) settles the command.
|
|
23365
|
+
* - A sequence refusal is stated twice, both about a tenth of a second after the send: the `-151` echo
|
|
23366
|
+
* and a `CMD_DOORLOCK_P2P_SEQ` (1916) notify whose `seq_num` is the mark the device holds. The
|
|
23367
|
+
* resolved value carries that mark, so the caller resends just above it. Collection runs for
|
|
23368
|
+
* {@link KEYED_REFUSAL_TWIN_MS} after the first of the two, so both land and neither is left on the
|
|
23369
|
+
* channel for the resend to read as its own answer, and either one alone still resolves the refusal.
|
|
23370
|
+
* - No reply within {@link KEYED_REPLY_MS} is a command that never landed, and rejects.
|
|
23371
|
+
*
|
|
23372
|
+
* Both listeners are armed before the send, and only replies on `channel` are read. The channel names
|
|
23373
|
+
* the device, not the command: every other P2P write a lock takes waits for a level-2 key this device
|
|
23374
|
+
* never negotiates, so nothing else the SDK sends can draw a reply here, and a future intent on this
|
|
23375
|
+
* wrapper has to share {@link serializeKeyed}.
|
|
23376
|
+
*/
|
|
23377
|
+
awaitKeyedReply(sn, session, channel, engage, send) {
|
|
23378
|
+
return new Promise((resolve, reject) => {
|
|
23379
|
+
let refused = false;
|
|
23380
|
+
let mark;
|
|
23381
|
+
let timer;
|
|
23382
|
+
const settle = (outcome) => {
|
|
23383
|
+
if (timer)
|
|
23384
|
+
clearTimeout(timer);
|
|
23385
|
+
session.off("commandResult", onResult);
|
|
23386
|
+
session.off("data", onData);
|
|
23387
|
+
outcome();
|
|
23388
|
+
};
|
|
23389
|
+
const expire = (ms, onExpiry) => {
|
|
23390
|
+
if (timer)
|
|
23391
|
+
clearTimeout(timer);
|
|
23392
|
+
timer = setTimeout(onExpiry, ms);
|
|
23393
|
+
};
|
|
23394
|
+
const refuse = () => {
|
|
23395
|
+
if (refused)
|
|
23396
|
+
return;
|
|
23397
|
+
refused = true;
|
|
23398
|
+
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() => resolve({ code: KEYED_PAYLOAD_SEQ_ERROR, mark })));
|
|
23399
|
+
};
|
|
23400
|
+
const onResult = (r) => {
|
|
23401
|
+
if (r.channel !== channel)
|
|
23402
|
+
return;
|
|
23403
|
+
if (r.code === KEYED_PAYLOAD_SEQ_ERROR)
|
|
23404
|
+
refuse();
|
|
23405
|
+
else if (!refused)
|
|
23406
|
+
settle(() => resolve({ code: r.code }));
|
|
23407
|
+
};
|
|
23408
|
+
const onData = (f) => {
|
|
23409
|
+
if (f.channel !== channel || f.commandId !== CommandType.CMD_NOTIFY_PAYLOAD)
|
|
23410
|
+
return;
|
|
23411
|
+
const published = keyedPayloadSeqMark(f.json);
|
|
23412
|
+
if (published !== void 0) {
|
|
23413
|
+
mark = published;
|
|
23414
|
+
return refuse();
|
|
23415
|
+
}
|
|
23416
|
+
const code = keyedPayloadReportCode(f.json, engage);
|
|
23417
|
+
if (code !== void 0 && !refused)
|
|
23418
|
+
settle(() => resolve({ code }));
|
|
23419
|
+
};
|
|
23420
|
+
session.on("commandResult", onResult);
|
|
23421
|
+
session.on("data", onData);
|
|
23422
|
+
expire(_P2PCommandRouter.KEYED_REPLY_MS, () => settle(() => reject(new Error(`keyed-payload command to ${sn} drew no reply`))));
|
|
23423
|
+
try {
|
|
23424
|
+
send();
|
|
23425
|
+
} catch (e) {
|
|
23426
|
+
settle(() => reject(e));
|
|
23427
|
+
}
|
|
23428
|
+
});
|
|
23429
|
+
}
|
|
23430
|
+
/** How long a keyed-payload command waits for any reply before it is taken as never having landed. */
|
|
23431
|
+
static KEYED_REPLY_MS = 6e3;
|
|
23432
|
+
/** How long a refusal collects its two statements — the `-151` echo and the mark notify — before resolving. */
|
|
23433
|
+
static KEYED_REFUSAL_TWIN_MS = 500;
|
|
23155
23434
|
/**
|
|
23156
23435
|
* **`ff09-actuate` intent** — build the `ff09` AES-128-CBC frame ({@link buildFf09Frame}, shared with the
|
|
23157
23436
|
* MQTT transport — see `transport/ff09.ts`) and dispatch it in the `1940` TRANSFER_PAYLOAD envelope
|
|
@@ -25289,7 +25568,7 @@ var RtcCommandRouter = class {
|
|
|
25289
25568
|
};
|
|
25290
25569
|
|
|
25291
25570
|
// dist/transport/mqtt/command-router.js
|
|
25292
|
-
import { randomBytes as
|
|
25571
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
25293
25572
|
|
|
25294
25573
|
// dist/transport/mqtt/broker-discovery.js
|
|
25295
25574
|
import { resolve4 } from "node:dns/promises";
|
|
@@ -25672,9 +25951,9 @@ function buildFf09MqttEnvelope(input) {
|
|
|
25672
25951
|
const head = {
|
|
25673
25952
|
version: "1.0.0.1",
|
|
25674
25953
|
client_id: input.clientId,
|
|
25675
|
-
sess_id: input.sessId ??
|
|
25954
|
+
sess_id: input.sessId ?? randomBytes5(2).toString("hex"),
|
|
25676
25955
|
msg_seq: 1,
|
|
25677
|
-
seed: input.seed ??
|
|
25956
|
+
seed: input.seed ?? randomBytes5(16).toString("hex"),
|
|
25678
25957
|
timestamp: input.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
25679
25958
|
cmd_status: 2,
|
|
25680
25959
|
cmd: 9,
|
|
@@ -26135,7 +26414,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26135
26414
|
};
|
|
26136
26415
|
|
|
26137
26416
|
// dist/transport/tuya/account.js
|
|
26138
|
-
import { createCipheriv as
|
|
26417
|
+
import { createCipheriv as createCipheriv6 } from "node:crypto";
|
|
26139
26418
|
var TUYA_USERNAME_PREFIX = "eh-";
|
|
26140
26419
|
var TUYA_PASSWORD_KEY = Buffer.from([
|
|
26141
26420
|
36,
|
|
@@ -26184,7 +26463,7 @@ function zeroPadTo16(s) {
|
|
|
26184
26463
|
return Buffer.concat([Buffer.from("0".repeat(16 - rem), "utf-8"), buf]);
|
|
26185
26464
|
}
|
|
26186
26465
|
function deriveTuyaPassword(eufyUserId) {
|
|
26187
|
-
const cipher =
|
|
26466
|
+
const cipher = createCipheriv6("aes-128-cbc", TUYA_PASSWORD_KEY, TUYA_PASSWORD_IV);
|
|
26188
26467
|
cipher.setAutoPadding(false);
|
|
26189
26468
|
const ct = Buffer.concat([cipher.update(zeroPadTo16(tuyaUsername(eufyUserId))), cipher.final()]);
|
|
26190
26469
|
return ct.toString("hex").toUpperCase();
|
|
@@ -26339,10 +26618,10 @@ var HmacSigner = class {
|
|
|
26339
26618
|
var TUYA_CHKEY = "7cbfe6d8";
|
|
26340
26619
|
|
|
26341
26620
|
// dist/transport/tuya/client.js
|
|
26342
|
-
import { randomBytes as
|
|
26621
|
+
import { randomBytes as randomBytes7, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants3 } from "node:crypto";
|
|
26343
26622
|
|
|
26344
26623
|
// dist/transport/tuya/et3.js
|
|
26345
|
-
import { createCipheriv as
|
|
26624
|
+
import { createCipheriv as createCipheriv7, createDecipheriv as createDecipheriv8, createHmac as createHmac5, randomBytes as randomBytes6 } from "node:crypto";
|
|
26346
26625
|
import { gunzipSync } from "node:zlib";
|
|
26347
26626
|
function deriveBodyKey(requestId, ecode) {
|
|
26348
26627
|
const k = resolveSignKey();
|
|
@@ -26351,8 +26630,8 @@ function deriveBodyKey(requestId, ecode) {
|
|
|
26351
26630
|
return Buffer.from(hex.slice(0, 16), "ascii");
|
|
26352
26631
|
}
|
|
26353
26632
|
function encryptBody2(key, json) {
|
|
26354
|
-
const nonce =
|
|
26355
|
-
const cipher =
|
|
26633
|
+
const nonce = randomBytes6(12);
|
|
26634
|
+
const cipher = createCipheriv7("aes-128-gcm", key, nonce);
|
|
26356
26635
|
const ciphertext = Buffer.concat([cipher.update(Buffer.from(json, "utf-8")), cipher.final()]);
|
|
26357
26636
|
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]).toString("base64");
|
|
26358
26637
|
}
|
|
@@ -26490,7 +26769,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
|
|
|
26490
26769
|
|
|
26491
26770
|
// dist/transport/tuya/client.js
|
|
26492
26771
|
function genDeviceId() {
|
|
26493
|
-
return
|
|
26772
|
+
return randomBytes7(22).toString("hex");
|
|
26494
26773
|
}
|
|
26495
26774
|
function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
26496
26775
|
const bigintToBuffer = (n) => {
|
|
@@ -27612,7 +27891,7 @@ var PushClient = class _PushClient extends EventEmitter13 {
|
|
|
27612
27891
|
};
|
|
27613
27892
|
|
|
27614
27893
|
// dist/transport/push/fcm.js
|
|
27615
|
-
import { randomBytes as
|
|
27894
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
27616
27895
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
27617
27896
|
var FCM = {
|
|
27618
27897
|
PROJECT_ID: "batterycam-3250a",
|
|
@@ -27625,7 +27904,7 @@ var FCM = {
|
|
|
27625
27904
|
SDK_VERSION: "a:16.3.1"
|
|
27626
27905
|
};
|
|
27627
27906
|
function generateFid() {
|
|
27628
|
-
const b =
|
|
27907
|
+
const b = randomBytes8(17);
|
|
27629
27908
|
b[0] = 112 + b[0] % 16;
|
|
27630
27909
|
return b.toString("base64url").slice(0, 22);
|
|
27631
27910
|
}
|
|
@@ -29543,8 +29822,10 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29543
29822
|
* Choose the transport stack for one command — the routing half of {@link commandSinkFor}.
|
|
29544
29823
|
*
|
|
29545
29824
|
* The `ff09-*` kinds share ONE frame that rides either transport, so each stack is asked whether it
|
|
29546
|
-
* drives this device (`claimsDevice`) rather than inferring from the kind
|
|
29547
|
-
*
|
|
29825
|
+
* drives this device (`claimsDevice`) rather than inferring from the kind; `keyed-payload-actuate` (the
|
|
29826
|
+
* classic Wi-Fi lock) is routed the same way, a channel-less actuation claimed by the stack that drives
|
|
29827
|
+
* the lock. `registry.require` throws on an unloaded serial, so a routing decision never silently falls
|
|
29828
|
+
* through to the wrong transport.
|
|
29548
29829
|
* The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
|
|
29549
29830
|
* Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
|
|
29550
29831
|
* (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
|
|
@@ -29555,7 +29836,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29555
29836
|
* media only, and the RTC router refuses a command on it. Everything else is P2P.
|
|
29556
29837
|
*/
|
|
29557
29838
|
routeCommand(sn, cmd) {
|
|
29558
|
-
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
|
|
29839
|
+
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle" || cmd.kind === "keyed-payload-actuate") {
|
|
29559
29840
|
const dev = this.registry.require(sn);
|
|
29560
29841
|
if (P2PCommandRouter.claimsDevice(dev))
|
|
29561
29842
|
return this.p2p.dispatchCommand(sn, cmd);
|