@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.
@@ -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;
@@ -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 key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
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 ECC private key via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
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 key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
233
- * answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
234
- * cached.
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 cipherKeyFor;
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 by `level2-unavailable` with `derivation-failed`, an answer holding none by
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 ECC private key hex (from cloud `get_ciphers`). When
50
- * provided, the session auto-negotiates the **level-2** session key on connect: it reads the
51
- * `cipher_id` + ECIES envelope from the `CMD_GATEWAYINFO` reply, derives the key, and calls
52
- * `setLevel2Key()` — so signCode 2/8 frames (camera info, event DB, event images) decrypt live.
53
- */
54
- resolveCipherKey?: (cipherId: number) => Promise<string | undefined>;
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 ECC private key (cloud `get_ciphers`, via the configured
257
- * `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`. One-shot; emits `level2Ready`
258
- * on success and `error` on failure (non-fatal — level-1 traffic keeps working regardless).
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.69",
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",