@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 +82 -19
- package/dist/index.js.map +4 -4
- package/dist/transport/tuya/account.d.ts +8 -5
- package/dist/transport/tuya/client.d.ts +16 -3
- package/dist/transport/tuya/command-router.d.ts +1 -1
- package/dist/transport/tuya/et3.d.ts +22 -0
- package/dist/transport/tuya/request.d.ts +16 -6
- package/dist/transport/tuya/sign.d.ts +7 -0
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Username/plaintext prefix the app prepends to the eufy user id. The production app uses `eh-`;
|
|
3
|
+
* `eufyhome-` is the prefix of its QA environment only.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TUYA_USERNAME_PREFIX = "eh-";
|
|
3
6
|
/** AES-128-CBC key used to derive the Tuya password (16 bytes). */
|
|
4
7
|
export declare const TUYA_PASSWORD_KEY: Buffer<ArrayBuffer>;
|
|
5
8
|
/** AES-128-CBC IV used to derive the Tuya password (16 bytes). */
|
|
6
9
|
export declare const TUYA_PASSWORD_IV: Buffer<ArrayBuffer>;
|
|
7
|
-
/** The Tuya username for a eufy user id (`"
|
|
10
|
+
/** The Tuya username for a eufy user id (`"eh-" + eufyUserId`). */
|
|
8
11
|
export declare function tuyaUsername(eufyUserId: string): string;
|
|
9
12
|
/**
|
|
10
13
|
* Derive the Tuya password for a eufy user id:
|
|
11
|
-
* `UPPERCASE-HEX( AES-128-CBC-NoPadding( pad16("
|
|
14
|
+
* `UPPERCASE-HEX( AES-128-CBC-NoPadding( pad16("eh-"+eufyUserId), KEY, IV ) )`.
|
|
12
15
|
* Deterministic (fixed KEY/IV) — the same user id always yields the same password.
|
|
13
16
|
*/
|
|
14
17
|
export declare function deriveTuyaPassword(eufyUserId: string): string;
|
|
@@ -29,7 +32,7 @@ export declare function isoToDialCode(iso: string): string | undefined;
|
|
|
29
32
|
export declare function resolveCountryCode(phoneCode?: string, region?: string, isoCode?: string): string;
|
|
30
33
|
/** A derived Tuya login identity — everything needed to call the uid token/password login actions. */
|
|
31
34
|
export interface TuyaAccount {
|
|
32
|
-
/** `"
|
|
35
|
+
/** `"eh-" + eufyUserId`. */
|
|
33
36
|
username: string;
|
|
34
37
|
/** AES-derived password (uppercase hex). See {@link deriveTuyaPassword}. */
|
|
35
38
|
password: string;
|
|
@@ -50,8 +50,21 @@ export declare class TuyaClient {
|
|
|
50
50
|
* Requires a working signer (the sign step).
|
|
51
51
|
*/
|
|
52
52
|
buildRequest(action: TuyaAction): Record<string, string>;
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Build + POST an action, returning the parsed envelope. Requires a working signer.
|
|
55
|
+
*
|
|
56
|
+
* On an `et=3` reply the envelope's `result` is a base64 AES-128-GCM blob, not an object; this
|
|
57
|
+
* decrypts it (keyed off the request's `requestId` and the session's `ecode`) and returns the
|
|
58
|
+
* decrypted inner envelope, so callers read `result` as an object regardless. A non-string `result`
|
|
59
|
+
* (an error, or an already-plain reply) passes through untouched.
|
|
60
|
+
*/
|
|
54
61
|
call<T = unknown>(action: TuyaAction): Promise<TuyaEnvelope<T>>;
|
|
62
|
+
/**
|
|
63
|
+
* Decode an et=3 reply. When `result` is a non-empty string it is the encrypted blob: decrypt and
|
|
64
|
+
* parse it (a wrong key or tampered blob fails the GCM tag in {@link decryptReply} and throws). The
|
|
65
|
+
* plaintext is itself the standard `{success, t, result}` envelope, which becomes the returned one.
|
|
66
|
+
*/
|
|
67
|
+
private decodeReply;
|
|
55
68
|
/**
|
|
56
69
|
* Log into the Tuya cloud from a eufy user id (wire-confirmed from the eufy Security app).
|
|
57
70
|
*
|
|
@@ -68,12 +81,12 @@ export declare class TuyaClient {
|
|
|
68
81
|
*/
|
|
69
82
|
login(eufyUserId: string, phoneCode?: string): Promise<TuyaLoginResult>;
|
|
70
83
|
/**
|
|
71
|
-
* READ/dump a device's cached data-points (`
|
|
84
|
+
* READ/dump a device's cached data-points (`smartlife.m.device.cache.dp.get`).
|
|
72
85
|
* Builds the request without needing a working signer; the signer is only exercised on send.
|
|
73
86
|
*/
|
|
74
87
|
getDeviceDps<T = unknown>(devId: string, dpCacheType?: number): Promise<TuyaEnvelope<T>>;
|
|
75
88
|
/**
|
|
76
|
-
* CONTROL: publish data-points to a device (`
|
|
89
|
+
* CONTROL: publish data-points to a device (`smartlife.m.device.dp.publish`). `dps` is
|
|
77
90
|
* `{ "<dpId>": <value> }`. `gwId` is the gateway/parent id (equals `devId` for a standalone gw).
|
|
78
91
|
*
|
|
79
92
|
* ⚠️ UNVERIFIED write — refuses to send by default. The `dp.publish` param shape
|
|
@@ -56,7 +56,7 @@ export declare class TuyaCommandRouter {
|
|
|
56
56
|
registerDevice(sn: string, devId: string, gwId?: string): void;
|
|
57
57
|
private ensureLoggedIn;
|
|
58
58
|
/**
|
|
59
|
-
* Fetch a device's cached DPs from the Tuya cloud (`
|
|
59
|
+
* Fetch a device's cached DPs from the Tuya cloud (`smartlife.m.device.cache.dp.get`) and deliver
|
|
60
60
|
* the raw DP map to the caller. Used for initial state hydration after MQTT subscribe — gets the
|
|
61
61
|
* last-known state without waiting for the first realtime push.
|
|
62
62
|
*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-request AES-128 key, `getEncryptoKey(requestId, ecode)`: take the lowercase hex of
|
|
3
|
+
* `HMAC-SHA256(key = requestId, msg = K[+ "_" + ecode])`, keep the first 16 hex characters, and use
|
|
4
|
+
* those characters as 16 ASCII bytes. `K` is the app HMAC key ({@link resolveSignKey}).
|
|
5
|
+
*
|
|
6
|
+
* `ecode` is the session's per-login encryption code (`User.ecode`, read from the decrypted
|
|
7
|
+
* `login.reg` reply). `ecode === null` selects the pre-session variant `msg = K` used to open the
|
|
8
|
+
* `token.get` and `login.reg` replies themselves, before any `ecode` is known.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveBodyKey(requestId: string, ecode: string | null): Buffer;
|
|
11
|
+
/**
|
|
12
|
+
* Encrypt an `et=3` request `postData`: AES-128-GCM under a random 12-byte nonce, laid out
|
|
13
|
+
* `nonce(12) ‖ ciphertext ‖ tag(16)` then base64 — the inverse of {@link decryptReply}, keyed by the
|
|
14
|
+
* same {@link deriveBodyKey}. No AAD. The request is signed over this blob.
|
|
15
|
+
*/
|
|
16
|
+
export declare function encryptBody(key: Buffer, json: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Decrypt an `et=3` reply `result`: base64 → AES-128-GCM-decrypt-verify (`nonce = blob[0:12]`,
|
|
19
|
+
* `tag = blob[-16:]`, ciphertext between), gunzip when the plaintext is gzip-framed, and return the
|
|
20
|
+
* plaintext JSON string. Throws when the blob is too short or the GCM tag fails to authenticate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function decryptReply(key: Buffer, resultB64: string): string;
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Tuya / Thingclips `api.json` request assembly + transport.
|
|
3
3
|
*
|
|
4
4
|
* Every call is a single `POST https://a1.tuyaeu.com/api.json` (EU) whose body is
|
|
5
|
-
* **form-urlencoded
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* **form-urlencoded params** plus a `sign`. Under `et=3` the `postData` param is an AES-128-GCM blob
|
|
6
|
+
* and the reply `result` is encrypted the same way, with the same per-request key. This module builds
|
|
7
|
+
* that param map for a given action, form-encodes it, POSTs it, and parses Tuya's response envelope.
|
|
8
|
+
* The `sign` digest is delegated to a {@link TuyaSigner}.
|
|
8
9
|
*/
|
|
9
10
|
/** Injectable POST transport — takes (url, form-body), returns the parsed JSON envelope. Native `fetch` by default. */
|
|
10
11
|
export type TuyaHttpPost = (url: string, body: string) => Promise<unknown>;
|
|
@@ -24,7 +25,10 @@ export declare const TUYA_SDK_VERSION = "7.5.0";
|
|
|
24
25
|
export declare const TUYA_APP_VERSION = "6.0.51_26722";
|
|
25
26
|
/** A single action to call: the `a` (action) + `v` (version) + optional pre-serialized `postData`. */
|
|
26
27
|
export interface TuyaAction {
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Action name as it goes on the wire, e.g. `smartlife.m.device.dp.publish`. The vendor SDK names
|
|
30
|
+
* its actions `thing.*` and rewrites a leading `thing` to `smartlife` before sending.
|
|
31
|
+
*/
|
|
28
32
|
a: string;
|
|
29
33
|
/** Per-action version, e.g. `"1.0"` / `"2.0"`. */
|
|
30
34
|
v: string;
|
|
@@ -43,6 +47,11 @@ export interface TuyaSession {
|
|
|
43
47
|
* `"7cbfe6d8"` for this app's appId. Hardcode it.
|
|
44
48
|
*/
|
|
45
49
|
chKey: string;
|
|
50
|
+
/**
|
|
51
|
+
* Per-login encryption code (`User.ecode`), read from the decrypted `login.reg` reply. Absent until
|
|
52
|
+
* login completes; it keys et=3 body encryption and reply decryption for every session call.
|
|
53
|
+
*/
|
|
54
|
+
ecode?: string;
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
57
|
* The constant / environment fields of a request. All are plaintext and (except the sign-relevant
|
|
@@ -127,11 +136,11 @@ export interface SendOptions {
|
|
|
127
136
|
export declare function sendApiRequest<T = unknown>(params: Record<string, string>, opts?: SendOptions): Promise<TuyaEnvelope<T>>;
|
|
128
137
|
/**
|
|
129
138
|
* CONTROL: publish device data-points. `dps` is `{ "<dpId>": <value> }`; note it is JSON-stringified
|
|
130
|
-
* INSIDE the postData (Tuya nests the dps map as a string). Action `
|
|
139
|
+
* INSIDE the postData (Tuya nests the dps map as a string). Action `smartlife.m.device.dp.publish` v2.0.
|
|
131
140
|
*/
|
|
132
141
|
export declare function buildPublishDpsAction(devId: string, gwId: string, dps: Record<string, unknown>): TuyaAction;
|
|
133
142
|
/**
|
|
134
|
-
* READ/dump: fetch a device's cached data-points. Action `
|
|
143
|
+
* READ/dump: fetch a device's cached data-points. Action `smartlife.m.device.cache.dp.get` v2.0.
|
|
135
144
|
* TODO(verify): `dpCacheType` default (1) was not pinned from the capture.
|
|
136
145
|
*/
|
|
137
146
|
export declare function buildGetDeviceDpsAction(devId: string, dpCacheType?: number): TuyaAction;
|
|
@@ -146,6 +155,7 @@ export declare function buildUsernameTokenGetAction(countryCode: string, usernam
|
|
|
146
155
|
* LOGIN step 2: uid password login + auto-register.
|
|
147
156
|
* Wire-confirmed from the eufy Security app: wire action
|
|
148
157
|
* `smartlife.m.user.uid.password.login.reg`, v=1.0. `passwd` = hex of RSA-PKCS1-encrypt(MD5hex(aesPassword)).
|
|
158
|
+
* `options` is the JSON string `{"group": 1}`, as the app sends it.
|
|
149
159
|
* On success returns `{ sid, uid }`.
|
|
150
160
|
*/
|
|
151
161
|
export declare function buildPasswordLoginRegAction(countryCode: string, uid: string, passwd: string, token: string): TuyaAction;
|
|
@@ -44,6 +44,13 @@ export declare class StubSigner implements TuyaSigner {
|
|
|
44
44
|
* The env var `TUYA_SIGN_KEY` can override this for non-standard builds.
|
|
45
45
|
*/
|
|
46
46
|
export declare const TUYA_SIGN_K: string;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the app-wide HMAC key {@link TUYA_SIGN_K}: an explicit `override` wins, then the
|
|
49
|
+
* `TUYA_SIGN_KEY` env var (for custom builds), then the built-in constant. Shared by the request
|
|
50
|
+
* signer ({@link HmacSigner}) and the et=3 body-key derivation, which key off the same `K`.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveSignKey(override?: string): string;
|
|
47
54
|
/**
|
|
48
55
|
* The real {@link TuyaSigner}: `sign = HMAC-SHA256(K, preimage)` as lowercase hex.
|
|
49
56
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.51",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|