@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.
- package/dist/core/contracts.d.ts +5 -0
- package/dist/index.js +146 -54
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/index.d.ts +5 -1
- package/dist/model/capabilities/smart-drop.d.ts +20 -0
- package/dist/model/types.d.ts +1 -1
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +10 -7
- package/dist/transport/p2p/live-trace.d.ts +11 -2
- package/dist/transport/p2p/p2p-session.d.ts +38 -11
- package/package.json +1 -1
|
@@ -44,6 +44,7 @@ import type { DisplayActions } from "./display.js";
|
|
|
44
44
|
import type { SuctionActions } from "./suction.js";
|
|
45
45
|
import type { MowerActions } from "./mower.js";
|
|
46
46
|
import type { LocateActions } from "./locate.js";
|
|
47
|
+
import type { SmartDropActions } from "./smart-drop.js";
|
|
47
48
|
import type { DeviceInfo } from "./info.js";
|
|
48
49
|
import type { CusPushAlarmType } from "../push-events.js";
|
|
49
50
|
/**
|
|
@@ -431,6 +432,8 @@ export interface DeviceActionMap {
|
|
|
431
432
|
mower: MowerActions;
|
|
432
433
|
/** RoboVac locate (find-robot beep): `locating`; `locate(on?)`. */
|
|
433
434
|
locate: LocateActions;
|
|
435
|
+
/** SmartDrop parcel box: `openLid()`. */
|
|
436
|
+
smartDrop: SmartDropActions;
|
|
434
437
|
/** Smart Display (read-only): `battery`. No display write is captured. */
|
|
435
438
|
display: DisplayActions;
|
|
436
439
|
/** Identity metadata (read-only): `{ manufacturer, model, serialNumber, name, deviceType?, firmwareVersion?, hardwareVersion? }`. */
|
|
@@ -524,7 +527,7 @@ export type { ActionSpec } from "./types.js";
|
|
|
524
527
|
* `undefined` for an action nothing describes.
|
|
525
528
|
*/
|
|
526
529
|
export { actionSpecOf } from "./access.js";
|
|
527
|
-
export type { PtzActions, LightActions, SmartLightActions, RgbColor, CameraActions, LockActions, SirenActions, ArmingActions, BatteryActions, MotionActions, ContactActions, LeakActions, SmokeActions, CoActions, KeypadActions, RtspActions, VacuumCleanActions, SuctionActions, MowerActions, LocateActions, };
|
|
530
|
+
export type { PtzActions, LightActions, SmartLightActions, RgbColor, CameraActions, LockActions, SirenActions, ArmingActions, BatteryActions, MotionActions, ContactActions, LeakActions, SmokeActions, CoActions, KeypadActions, RtspActions, VacuumCleanActions, SuctionActions, MowerActions, LocateActions, SmartDropActions, };
|
|
528
531
|
/**
|
|
529
532
|
* The member table each `*Actions` type is DERIVED from (`LockActions = Surface<typeof LOCK_MEMBERS>`),
|
|
530
533
|
* so the type names the table and the table is part of the published surface.
|
|
@@ -549,6 +552,7 @@ export { MOTION_MEMBERS } from "./motion.js";
|
|
|
549
552
|
export { PTZ_MEMBERS } from "./ptz.js";
|
|
550
553
|
export { RTSP_MEMBERS } from "./rtsp.js";
|
|
551
554
|
export { SIREN_MEMBERS } from "./siren.js";
|
|
555
|
+
export { SMART_DROP_MEMBERS } from "./smart-drop.js";
|
|
552
556
|
export { SMART_LIGHT_MEMBERS } from "./smart-light.js";
|
|
553
557
|
export { SMOKE_MEMBERS } from "./smoke.js";
|
|
554
558
|
export { DISPLAY_MEMBERS, type DisplayActions } from "./display.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Surface } from "./members.js";
|
|
2
|
+
import type { CapabilityModule } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every `smart_drop` feature, declared once.
|
|
5
|
+
*
|
|
6
|
+
* Exported but NOT published: each entry states its wire id and the evidence it was confirmed on,
|
|
7
|
+
* which the reference site does not carry.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare const SMART_DROP_MEMBERS: {
|
|
11
|
+
/**
|
|
12
|
+
* Opens the lid with `SET_PAYLOAD` 2100 at level 2. Verified on a standalone T8790, whose cipher has
|
|
13
|
+
* only an RSA key: the frame sealed AES-128-ECB opened the lid, the same frame at level 1 did not.
|
|
14
|
+
*/
|
|
15
|
+
readonly openLid: import("./members.js").MethodMember<() => Promise<void>>;
|
|
16
|
+
};
|
|
17
|
+
/** Bound SmartDrop controls — the object returned by `dev.smartDrop()`. */
|
|
18
|
+
export type SmartDropActions = Surface<typeof SMART_DROP_MEMBERS>;
|
|
19
|
+
/** `smart_drop` — a standalone SmartDrop (T8790) parcel box: open the lid. */
|
|
20
|
+
export declare const SMART_DROP: CapabilityModule;
|
package/dist/model/types.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
|
|
|
39
39
|
* it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
|
|
40
40
|
* new capabilities are modelled — adding one never requires a subclass.
|
|
41
41
|
*/
|
|
42
|
-
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "mower" | "smart_light" | "display" | "info";
|
|
42
|
+
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "mower" | "smart_drop" | "smart_light" | "display" | "info";
|
|
43
43
|
/** Value type of a property. */
|
|
44
44
|
export type PropertyValueType = "bool" | "number" | "string" | "enum";
|
|
45
45
|
/**
|
|
@@ -111,6 +111,22 @@ export declare function eciesUnwrap(envelope: Buffer, eccPrivateKeyHex: string,
|
|
|
111
111
|
* Returns the 32-byte key, or undefined if the inputs don't parse.
|
|
112
112
|
*/
|
|
113
113
|
export declare function deriveLevel2KeyFromGatewayInfo(gwPayload: Buffer, eccPrivateKeyHex: string): Buffer | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Derive the P2P **level-2** AES-128-ECB session key from a *decrypted* `CMD_GATEWAYINFO` (1100)
|
|
116
|
+
* payload and the RSA private key of a cipher whose `get_ciphers` answer carries no ECC key.
|
|
117
|
+
*
|
|
118
|
+
* Layout of the decrypted payload: `cipher_id(2 LE) ‖ 0000 ‖ RSA ciphertext ‖ …`, the ciphertext as
|
|
119
|
+
* long as the key's modulus. It is the 16-byte session key under PKCS#1 v1.5 encryption padding
|
|
120
|
+
* (`00 02 ‖ ≥8 non-zero bytes ‖ 00 ‖ key`). The padding is checked here over the raw RSA result so a wrong
|
|
121
|
+
* key fails closed: under OpenSSL's implicit rejection, `RSA_PKCS1_PADDING` answers random bytes for it
|
|
122
|
+
* instead of throwing, and those can be 16 bytes long.
|
|
123
|
+
*
|
|
124
|
+
* `rsaPrivateKey` is the `get_ciphers` `private_key`: base64 PKCS#8 DER.
|
|
125
|
+
*
|
|
126
|
+
* Returns the 16-byte key, or `undefined` when the key does not parse, the payload is short, or the
|
|
127
|
+
* padding or key length is wrong (fails closed, never throws).
|
|
128
|
+
*/
|
|
129
|
+
export declare function deriveLevel2KeyFromRsaGatewayInfo(gwPayload: Buffer, rsaPrivateKey: string): Buffer | undefined;
|
|
114
130
|
/** Local-lookup payload: two zero bytes. */
|
|
115
131
|
export declare function buildLocalLookupPayload(): Buffer;
|
|
116
132
|
/** Cloud-lookup payload (variant 2): `[p2pDid:20][dskKey][0x00000000]`. */
|
|
@@ -108,7 +108,7 @@ export declare class P2PCommandRouter {
|
|
|
108
108
|
* shared — see {@link P2PCommandRouter.openTalkback}.
|
|
109
109
|
*/
|
|
110
110
|
private readonly talkbacks;
|
|
111
|
-
/** cipher_id → ECC private
|
|
111
|
+
/** cipher_id → its ECC and RSA private keys (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
112
112
|
private readonly cipherKeyCache;
|
|
113
113
|
/** The recording download in flight per station; a station serves one at a time. */
|
|
114
114
|
private readonly recordingDownloads;
|
|
@@ -213,7 +213,7 @@ export declare class P2PCommandRouter {
|
|
|
213
213
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
214
214
|
*
|
|
215
215
|
* `resolveCipherKey` auto-negotiates the level-2 session key from `CMD_GATEWAYINFO` by resolving the
|
|
216
|
-
* cipher's
|
|
216
|
+
* cipher's private keys via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
|
|
217
217
|
* cached on the router instance so a lazy re-open (after idle-detach) reuses the lookup. Only a
|
|
218
218
|
* SUCCESSFUL lookup is cached — caching `undefined` after a transient failure would permanently
|
|
219
219
|
* disable level-2 for the session's life.
|
|
@@ -229,11 +229,12 @@ export declare class P2PCommandRouter {
|
|
|
229
229
|
*/
|
|
230
230
|
private makeSession;
|
|
231
231
|
/**
|
|
232
|
-
* The ECC private
|
|
233
|
-
*
|
|
234
|
-
*
|
|
232
|
+
* The ECC and RSA private keys of `cipherId` (`ecc_private_key`, `private_key`), from the router's cache
|
|
233
|
+
* or one `get_ciphers` call. When the cloud answers with another cipher that carries an ECC key and the
|
|
234
|
+
* asked one does not, that one is used and traced on `session`. Only a successful lookup — one that
|
|
235
|
+
* yields either key — is cached.
|
|
235
236
|
*/
|
|
236
|
-
private
|
|
237
|
+
private cipherKeysFor;
|
|
237
238
|
/**
|
|
238
239
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
239
240
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -690,7 +691,9 @@ export declare class P2PCommandRouter {
|
|
|
690
691
|
* HomeBase-attached device takes the GCM signCode-8 form, a standalone one the level-1 form. A
|
|
691
692
|
* standalone camera never negotiates a level-2 key, so pinning this to level 2 makes the envelope
|
|
692
693
|
* unreachable on exactly the devices that serve their own RTSP stream. Verified live: a standalone
|
|
693
|
-
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only
|
|
694
|
+
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only and waits for the
|
|
695
|
+
* key every level-2 sender uses; `level2Sender: "set-payload"` also takes an RSA cipher's AES-128-ECB
|
|
696
|
+
* key (see {@link P2PSession.sendControlLevel2}). A `resolved` session keeps the wait its caller gave it.
|
|
694
697
|
*
|
|
695
698
|
* Both seals REPLAY the frame {@link DIRECT_CMD_SENDS}× at 200ms, as every other fire-and-forget
|
|
696
699
|
* control on this router does: these are unacknowledged datagrams, and a level-1 device is the one
|
|
@@ -118,11 +118,19 @@ export type LiveTrace =
|
|
|
118
118
|
phase: "level2-wait";
|
|
119
119
|
waitMs: number;
|
|
120
120
|
}
|
|
121
|
-
/** The station's level-2 key was negotiated, under the cipher it selected. */
|
|
121
|
+
/** The station's AES-256-GCM level-2 key was negotiated, under the cipher it selected. */
|
|
122
122
|
| {
|
|
123
123
|
phase: "level2-ready";
|
|
124
124
|
cipherId: number;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The station's cipher negotiated only the AES-128-ECB level-2 key of an RSA cipher, under the cipher it
|
|
128
|
+
* selected. That key seals a level-2 set-payload and nothing else, so this is not `level2-ready`.
|
|
129
|
+
*/
|
|
130
|
+
| {
|
|
131
|
+
phase: "level2-set-payload-only";
|
|
132
|
+
cipherId: number;
|
|
133
|
+
}
|
|
126
134
|
/**
|
|
127
135
|
* The station's key is not coming, why, and the cipher where a station named one.
|
|
128
136
|
*
|
|
@@ -145,7 +153,8 @@ export type LiveTrace =
|
|
|
145
153
|
* A station's cipher was answered with material for a DIFFERENT cipher, which was used in its place.
|
|
146
154
|
*
|
|
147
155
|
* The one lookup outcome no other phase accounts for: material for the cipher the station named is followed
|
|
148
|
-
* by `level2-ready` or
|
|
156
|
+
* by `level2-ready`, `level2-set-payload-only` or `level2-unavailable` with `derivation-failed`, an answer
|
|
157
|
+
* holding none by
|
|
149
158
|
* `no-cipher-key`, and a lookup that threw is reported as an error. Substituted material derives to
|
|
150
159
|
* nothing and otherwise reads as a station fault. `cipherId` is the cipher the station asked for,
|
|
151
160
|
* `answeredCipherId` the one whose material was used.
|
|
@@ -46,12 +46,18 @@ export interface P2PSessionConfig {
|
|
|
46
46
|
/** Disable UDP broadcast local lookup (e.g. cloud-only). Default false. */
|
|
47
47
|
noBroadcast?: boolean;
|
|
48
48
|
/**
|
|
49
|
-
* Resolve a station `cipher_id` → its
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
* Resolve a station `cipher_id` → its private keys (from cloud `get_ciphers`): `ecc`, the ECC private
|
|
50
|
+
* key hex (`ecc_private_key`), and `rsa`, the RSA private key (`private_key`). A plain string is the ECC key. Called once per
|
|
51
|
+
* negotiation. When provided, the session auto-negotiates the **level-2** session key on connect: it
|
|
52
|
+
* reads the `cipher_id` + envelope from the `CMD_GATEWAYINFO` reply and derives the key. With an ECC
|
|
53
|
+
* key that is the ECIES unwrap and `setLevel2Key()` — so signCode 2/8 frames (camera info, event DB,
|
|
54
|
+
* event images) decrypt live. A cipher with no ECC key carries the key RSA-encrypted instead, and only
|
|
55
|
+
* {@link P2PSession.sendControlLevel2} seals with it.
|
|
56
|
+
*/
|
|
57
|
+
resolveCipherKey?: (cipherId: number) => Promise<string | {
|
|
58
|
+
ecc?: string;
|
|
59
|
+
rsa?: string;
|
|
60
|
+
} | undefined>;
|
|
55
61
|
/** Diagnostics sink. Omit for silence. */
|
|
56
62
|
logger?: Logger;
|
|
57
63
|
}
|
|
@@ -149,6 +155,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
149
155
|
private readonly level1Key;
|
|
150
156
|
/** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
|
|
151
157
|
private level2Key?;
|
|
158
|
+
/**
|
|
159
|
+
* Negotiated 16-byte level-2 key of a cipher with no ECC key (RSA-encrypted in `CMD_GATEWAYINFO`). Only
|
|
160
|
+
* {@link sendControlLevel2} seals with it, AES-128-ECB in the level-1 layout at encryption type 2; never
|
|
161
|
+
* held beside {@link level2Key}.
|
|
162
|
+
*/
|
|
163
|
+
private level2EcbKey?;
|
|
152
164
|
private level2Seq;
|
|
153
165
|
private rsaPrivateKey?;
|
|
154
166
|
private rsaModulusHex?;
|
|
@@ -204,10 +216,18 @@ export declare class P2PSession extends EventEmitter {
|
|
|
204
216
|
private trace;
|
|
205
217
|
/** Provide the negotiated 32-byte session key so level-2 (signCode 2/8) frames can be decrypted. */
|
|
206
218
|
setLevel2Key(key: Buffer): void;
|
|
207
|
-
/** Whether the level-2 session key has been negotiated/set. */
|
|
219
|
+
/** Whether the AES-256-GCM level-2 session key has been negotiated/set. */
|
|
208
220
|
get hasLevel2Key(): boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Whether the held level-2 key can frame for `sender`: every level-2 sender needs the AES-256-GCM key,
|
|
223
|
+
* and `"set-payload"` ({@link sendControlLevel2}) also takes the AES-128-ECB key of an RSA cipher.
|
|
224
|
+
*/
|
|
225
|
+
private level2KeyServes;
|
|
209
226
|
/**
|
|
210
227
|
* Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
|
|
228
|
+
* `sender` names who will frame with it: by default every level-2 sender, which needs the AES-256-GCM
|
|
229
|
+
* key; `"set-payload"` is {@link sendControlLevel2} alone, which also takes the AES-128-ECB key of an RSA
|
|
230
|
+
* cipher, so `true` for it does not imply {@link hasLevel2Key}.
|
|
211
231
|
*
|
|
212
232
|
* A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
|
|
213
233
|
* egress asks this same session and can proceed without the key on an own-session camera; a per-call
|
|
@@ -232,7 +252,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
232
252
|
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
233
253
|
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
234
254
|
*/
|
|
235
|
-
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session"): Promise<boolean>;
|
|
255
|
+
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session", sender?: "every" | "set-payload"): Promise<boolean>;
|
|
236
256
|
/**
|
|
237
257
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
238
258
|
*
|
|
@@ -253,9 +273,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
253
273
|
private settleLevel2;
|
|
254
274
|
/**
|
|
255
275
|
* Negotiate the level-2 session key from the decrypted CMD_GATEWAYINFO payload: read its
|
|
256
|
-
* `cipher_id`, resolve that cipher's
|
|
257
|
-
* `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`.
|
|
258
|
-
*
|
|
276
|
+
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
277
|
+
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
278
|
+
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
279
|
+
* key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
280
|
+
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` on failure
|
|
281
|
+
* (non-fatal — level-1 traffic keeps working regardless).
|
|
259
282
|
*
|
|
260
283
|
* Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
|
|
261
284
|
* failure final for this connection generation: nothing will retry it there, so a later command must be
|
|
@@ -421,6 +444,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
421
444
|
* mechanism proven for camera selection in media start. Use for control commands routed through a
|
|
422
445
|
* HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
|
|
423
446
|
* yet (caller can fall back to {@link sendStringPayloadCommand}).
|
|
447
|
+
*
|
|
448
|
+
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
449
|
+
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
450
|
+
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
424
451
|
*/
|
|
425
452
|
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
|
|
426
453
|
/**
|
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.70",
|
|
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",
|