@mega-yfue/eufy-sdk 0.6.0-beta.4 → 0.6.0-beta.41

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.
Files changed (43) hide show
  1. package/dist/client/device-registry.d.ts +6 -0
  2. package/dist/client/eufy-mega.d.ts +103 -14
  3. package/dist/client/push-repeats.d.ts +10 -0
  4. package/dist/client/types.d.ts +12 -9
  5. package/dist/core/contracts.d.ts +28 -14
  6. package/dist/core/store.d.ts +12 -1
  7. package/dist/core/types.d.ts +4 -2
  8. package/dist/index.js +3362 -2446
  9. package/dist/index.js.map +4 -4
  10. package/dist/model/capabilities/access.d.ts +3 -2
  11. package/dist/model/capabilities/arming.d.ts +2 -1
  12. package/dist/model/capabilities/battery.d.ts +8 -7
  13. package/dist/model/capabilities/camera.d.ts +13 -2
  14. package/dist/model/capabilities/doorbell.d.ts +15 -3
  15. package/dist/model/capabilities/index.d.ts +1 -1
  16. package/dist/model/capabilities/members.d.ts +3 -1
  17. package/dist/model/capabilities/motion.d.ts +16 -2
  18. package/dist/model/capabilities/mower.d.ts +3 -2
  19. package/dist/model/capabilities/ptz.d.ts +11 -5
  20. package/dist/model/capabilities/vacuum-clean.d.ts +23 -0
  21. package/dist/model/device-family.d.ts +3 -1
  22. package/dist/model/device-types.d.ts +1 -0
  23. package/dist/transport/http/mega-client.d.ts +18 -5
  24. package/dist/transport/mqtt/biz-stream.d.ts +22 -25
  25. package/dist/transport/p2p/audio-frame.d.ts +13 -0
  26. package/dist/transport/p2p/codec.d.ts +8 -2
  27. package/dist/transport/p2p/command-router.d.ts +48 -13
  28. package/dist/transport/p2p/database-wire.d.ts +30 -0
  29. package/dist/transport/p2p/level2-settle.d.ts +9 -0
  30. package/dist/transport/p2p/live-stream.d.ts +34 -7
  31. package/dist/transport/p2p/live-trace.d.ts +7 -5
  32. package/dist/transport/p2p/p2p-session.d.ts +112 -16
  33. package/dist/transport/p2p/readable-egress.d.ts +3 -1
  34. package/dist/transport/p2p/recording-download.d.ts +109 -13
  35. package/dist/transport/p2p/shared-live-source.d.ts +9 -3
  36. package/dist/transport/p2p/video.d.ts +12 -6
  37. package/dist/transport/push/store.d.ts +7 -6
  38. package/dist/transport/rtc/command-router.d.ts +86 -9
  39. package/dist/transport/rtc/live.d.ts +4 -1
  40. package/dist/transport/rtc/portal-packet.d.ts +3 -13
  41. package/dist/transport/rtc/session.d.ts +19 -2
  42. package/dist/transport/stored-image-cache.d.ts +14 -0
  43. package/package.json +1 -1
@@ -62,6 +62,8 @@ export interface P2PRouterDeps {
62
62
  onClose: (stationSn: string) => void;
63
63
  onError: (err: Error) => void;
64
64
  onLevel2Ready: (stationSn: string, cipherId: number) => void;
65
+ /** The station answered on its own session at `atMs` — a PONG or an ACK from the connected peer. */
66
+ onPeerAnswer?: (stationSn: string, atMs: number) => void;
65
67
  /** A raw decoded frame — the client emits the low-level `p2p` event + runs the semantic decode. */
66
68
  onFrame: (stationSn: string, frame: P2PFrame) => void;
67
69
  /**
@@ -102,6 +104,18 @@ export declare class P2PCommandRouter {
102
104
  * {@link liveSources} is a claim in flight.
103
105
  */
104
106
  private readonly liveSessionKeys;
107
+ /**
108
+ * The live source build in flight per `${parentSn}:${channel}`, settled once its connection is opened
109
+ * or refused.
110
+ *
111
+ * A build awaits its media session before it can file the source in {@link liveSources}, so a second
112
+ * request for the same camera in that window finds nothing cached. Building a second source there
113
+ * overwrites the first, and the orphan's teardown then closes the connection the cached one streams
114
+ * on. A request that finds a build here waits for it, until its own signal aborts, then resolves its
115
+ * session again and reuses what the build filed, or builds its own when that build failed. The entry
116
+ * is removed when the build settles and its promise never rejects, so each wait ends on a later state.
117
+ */
118
+ private readonly liveSourceBuilds;
105
119
  /**
106
120
  * The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
107
121
  * time and the session carries one audio sequence, so this path is exclusive where a live pull is
@@ -220,6 +234,11 @@ export declare class P2PCommandRouter {
220
234
  * so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
221
235
  * shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
222
236
  * carry the start is not a connected session, so this refuses rather than returning one.
237
+ *
238
+ * A session refused its key within the grace, or never connected, is closed and dropped from the manager at
239
+ * once. Nothing else releases it: no source holds it, a wired station's session never idles out, and the
240
+ * manager hands the same session to every later open of the camera, so the next open dials a fresh
241
+ * connection instead. A session another open has since replaced is left to its own close.
223
242
  */
224
243
  private openMediaSession;
225
244
  /**
@@ -295,24 +314,28 @@ export declare class P2PCommandRouter {
295
314
  */
296
315
  mediaProviderFor(sn: string): MediaProvider;
297
316
  /**
298
- * Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
299
- * and frame formats are confirmed.
317
+ * The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
318
+ * station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
319
+ * station, answers `undefined`.
300
320
  */
301
- private downloadsRecordings;
321
+ private recordingStationFor;
302
322
  /**
303
- * Download one recording a HomeBase 2 holds for an attached camera, then decode it
304
- * ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
305
- * on the camera's channel, so a download starts only once the previous one has drained
306
- * ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
307
- * while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
308
- * turn is held until the transfer drains.
323
+ * Download one recording `station` holds for the attached camera `sn`, then decode it
324
+ * ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
325
+ * station: the station streams one recording at a time on the camera's channel, so a download starts only
326
+ * once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
327
+ * has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
328
+ * once and the station's turn is held until the transfer drains.
309
329
  */
310
330
  private downloadRecording;
311
331
  /** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
312
332
  private waitTurn;
313
333
  /**
314
334
  * Start one recording download, once the station's previous download has drained: the decoded recording,
315
- * and when the station is done sending it.
335
+ * and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
336
+ * it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
337
+ * is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
338
+ * reads no cipher, and `opts.cipherId` is not read.
316
339
  */
317
340
  private startRecordingDownload;
318
341
  /**
@@ -394,8 +417,9 @@ export declare class P2PCommandRouter {
394
417
  * {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
395
418
  * reuse branch, so a reuse frees the station as a cold start does.
396
419
  *
397
- * `mayOpenOwnSession` decides whether a camera that finds the station's own session claimed may open a
398
- * connection of its own for it. Only the continuous-pull egresses pass it. A still must not: it wants one
420
+ * `continuous` decides whether a camera that finds the station's own session claimed may open a
421
+ * connection of its own for it; on an {@link OWN_LIVE_SESSION_MODELS} station it always gets one, and an
422
+ * unwatched source left on the station's session is rebuilt there. Only the continuous-pull egresses pass it. A still must not: it wants one
399
423
  * frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
400
424
  * so a still asked for while a sibling is being watched contends as it always did, and the caller's
401
425
  * retained image answers it. A live view still outranks a tile; what changed is that two live views no
@@ -404,7 +428,7 @@ export declare class P2PCommandRouter {
404
428
  * The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
405
429
  * place.
406
430
  */
407
- sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, mayOpenOwnSession?: boolean): Promise<SharedLiveSource>;
431
+ sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, continuous?: boolean): Promise<SharedLiveSource>;
408
432
  /**
409
433
  * Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
410
434
  *
@@ -559,12 +583,17 @@ export declare class P2PCommandRouter {
559
583
  * yield CMAF fragments (init segment first, then a `moof`+`mdat` per keyframe boundary) muxed by the
560
584
  * dependency-free internal fMP4 muxer. The returned recording handle exposes battery-budget notices
561
585
  * and detaches its consumer on `stop`, iterator return, or iterator throw. No ffmpeg.
586
+ *
587
+ * It is a continuous pull only on an {@link OWN_LIVE_SESSION_MODELS} station; elsewhere it never opens a
588
+ * connection of its own.
562
589
  */
563
590
  recordFragments(sn: string, opts?: {
564
591
  fragmentSeconds?: number;
565
592
  eccPrivateKey?: Buffer;
566
593
  keepAliveMs?: number;
567
594
  } & SharedSourceHints): FragmentRecording;
595
+ /** Whether `stationSn` is an {@link OWN_LIVE_SESSION_MODELS} station. */
596
+ private ownsLiveSession;
568
597
  /**
569
598
  * **Generic P2P request/reply query.** Sends a `SET_PAYLOAD` (1350) wrapper carrying `subCmd` on the
570
599
  * device channel, then resolves with the reply frame's `payload` — the `NOTIFY_PAYLOAD` (1351)
@@ -668,6 +697,9 @@ export declare class P2PCommandRouter {
668
697
  * sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
669
698
  * Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
670
699
  * than closed again.
700
+ *
701
+ * `clearFallback` hands over a session whose station {@link P2PSession.refusedLevel2} instead of throwing, for
702
+ * a standalone device of a model in {@link CLEAR_FF09_MODELS}.
671
703
  */
672
704
  private resolveSession;
673
705
  /**
@@ -693,6 +725,9 @@ export declare class P2PCommandRouter {
693
725
  * **Station-scoped scalar** (`p2p-station-scalar` intent): the 132-byte channel-less body
694
726
  * ({@link buildDirectBinaryBody} with no `channel`) on an EXPLICIT channel, signCode 8. The
695
727
  * HomeBase's own controls ride the station broadcast channel 255 (alarm/speaker volume 1235).
728
+ *
729
+ * `form` `"auto"` seals it at the session's level, as {@link sendBySessionLevel} defines it: level-2 on a
730
+ * keyed session, the same body sealed level-1 on a keyless one. Absent, the send waits for the level-2 key.
696
731
  */
697
732
  private sendStationScalar;
698
733
  /** Send a level-1 int-plus-string frame with authenticated account identity injected by the transport. */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Database wire — the ids and the whole-table query a station's on-station table and stored-image
3
+ * reads are built from.
4
+ *
5
+ * A HomeBase answers them over its P2P session and a T9000 over its portal control channel, with the
6
+ * same inner payloads, so both transports build their reads from this one module. The barrel does not
7
+ * re-export it: these are wire ids, not part of the package surface.
8
+ *
9
+ * @module p2p/database-wire
10
+ */
11
+ /** CMD_DATABASE_IMAGE (1308) — fetch one stored image by its on-station path. */
12
+ export declare const CMD_DATABASE_IMAGE = 1308;
13
+ /** CMD_DATABASE (1306) — query one on-station table; reply carries `{cmd:10000,count,data:[…]}`. */
14
+ export declare const CMD_DATABASE = 1306;
15
+ /**
16
+ * Inner query cmds carried in a {@link CMD_DATABASE} payload. `FULL_TABLE` reads one table whole;
17
+ * `COMBINATION_WITH_AI` is the AI event-history read that bundles the face roster.
18
+ */
19
+ export declare const DB_QUERY: {
20
+ readonly FULL_TABLE: 10000;
21
+ readonly COMBINATION_WITH_AI: 10011;
22
+ readonly QUERY_LOCAL: 10017;
23
+ };
24
+ /** How long a station's database reply has to arrive whole, when the caller states no bound. */
25
+ export declare const DB_TABLE_TIMEOUT_MS = 15000;
26
+ /**
27
+ * The query params the vendor app sends with a whole-table `CMD_DATABASE` read (inner `cmd 10000`).
28
+ * `count` bounds the row count.
29
+ */
30
+ export declare function fullTableQuery(count?: number): Record<string, unknown>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The bound a caller waits for the level-2 negotiation to SETTLE, measured from connect.
3
+ *
4
+ * For a caller that picks its seal once from `P2PSession.hasLevel2Key` and has no second chance. A media
5
+ * start reads the key on every send and is re-issued by the warm-up, so it needs no wait; a property write
6
+ * framed level-1 to a family that only accepts level-2 is ignored, and nothing re-frames it. Session-scoped,
7
+ * so a station that offers no key does not charge this to every later command.
8
+ */
9
+ export declare const LEVEL2_SETTLE_MS = 8000;
@@ -7,9 +7,11 @@
7
7
  * - **Plaintext** (`signCode 0`, e.g. HomeBase-attached cameras): a fixed 22-byte frame header
8
8
  * (`parseVideoFrameHeader`) followed directly by Annex-B NAL units — verified live (960×540,
9
9
  * keyframe = header flag bit0). We strip the 22-byte header.
10
- * - **Encrypted** (E2E cameras): the body is AES-256-GCM under a per-stream media key wrapped in a
11
- * keyframe ECIES envelope. If an `eccPrivateKey` is supplied we run `VideoFrameDecoder`; otherwise
12
- * those frames are skipped (no key → no video).
10
+ * - **Encrypted keyframes**: either the AES-256-GCM body under a media key wrapped in an ECIES envelope
11
+ * for an offered P-256 key ({@link VideoFrameDecoder}), or an AES-ECB head under a media key wrapped for
12
+ * the session's RSA key ({@link P2PSession.decodeVideoFrame}). An attached camera's start on a station
13
+ * with a {@link P2PSession.mediaEccPrivateKey} offers that P-256 key first, so its stream decodes with
14
+ * that pair unless `eccPrivateKey` names another.
13
15
  *
14
16
  * Emits: `video` ({@link LiveVideoFrame} — Annex-B), `audio` ({@link LiveAudioFrame}), `start`, `stop`,
15
17
  * `error`.
@@ -54,7 +56,10 @@ export interface LiveStreamOptions {
54
56
  * however long the station keeps serving that sibling instead of this one.
55
57
  */
56
58
  channel?: number;
57
- /** Camera ECC private key (32B) for E2E/encrypted cameras; omit for plaintext cameras. */
59
+ /**
60
+ * P-256 private key (32B) whose public point the camera wraps each keyframe's media key for. Defaults, on an
61
+ * attached camera, to the session's `P2PSession.mediaEccPrivateKey`, the pair its start offers.
62
+ */
58
63
  eccPrivateKey?: Buffer;
59
64
  /** Admin account id — required for the level-2 (`signCode 8`) media-start payload selecting a camera. */
60
65
  accountId?: string;
@@ -112,6 +117,8 @@ export declare class LiveStream extends EventEmitter {
112
117
  private tracedFirstVideoUnit;
113
118
  private tracedFirstKeyframe;
114
119
  private tracedDecodeFailures;
120
+ /** How the frame that opened the latest access unit was read; see {@link settleStartKey}. */
121
+ private unitOpenedBy;
115
122
  private tracedFirstForeignFrame;
116
123
  private stallTimer?;
117
124
  /**
@@ -173,6 +180,7 @@ export declare class LiveStream extends EventEmitter {
173
180
  stop(): void;
174
181
  /** Extract the Annex-B payload from a plaintext 1300 frame (fixed 22-byte header), or undefined. */
175
182
  private plaintextAnnexB;
183
+ /** Emit one accepted media frame; an audio frame with no bytes past its header emits nothing. */
176
184
  private onFrame;
177
185
  /**
178
186
  * Whether this stream may take `frame` — the demultiplexer for a station that serves several cameras.
@@ -198,11 +206,30 @@ export declare class LiveStream extends EventEmitter {
198
206
  */
199
207
  private acceptsMedia;
200
208
  /**
201
- * The Annex-B payload of a frame that starts an access unit. The session decodes it when it can (it
202
- * holds the RSA private key, and handles the plaintext form); the legacy header-strip and the ECIES
203
- * {@link VideoFrameDecoder} are the fallbacks for an E2E camera.
209
+ * The Annex-B payload of a frame that starts an access unit. The authenticated ECIES/GCM
210
+ * {@link VideoFrameDecoder} goes first: its HMAC and GCM checks fail closed on a frame not sealed for its
211
+ * key. The session's RSA path cannot: on Node ≥ 24.5, PKCS#1 v1.5 decryption of a key not wrapped for it
212
+ * returns pseudo-random bytes instead of throwing (implicit rejection), which would decode an ECC
213
+ * keyframe into noise. The session's decode, which also passes plaintext frames, and the header strip
214
+ * follow. A payload that opens an access unit records how it was read: by the ECIES path, or otherwise by
215
+ * its sign code, sealed (`"rsa"`) or in the clear (`"plain"`).
204
216
  */
205
217
  private annexbOf;
218
+ /**
219
+ * Tell the session how this keyframe was opened, which answers the attached starts on this channel
220
+ * ({@link P2PSession.settleAttachedKey}), and re-issue the start when the session asks for one: a plaintext
221
+ * keyframe answering the session's P-256 offer, from a camera that did not use the point, which the next
222
+ * start asks with RSA. Only a delivered keyframe answers: a frame no key opens, or a continuation without the
223
+ * frame that opened its unit, delivers nothing.
224
+ */
225
+ private settleStartKey;
226
+ /**
227
+ * The payload of an audio frame. On a stream with an ECIES decoder, a sealed frame ({@link isSealedAudio}) is
228
+ * opened under the media key of the latest keyframe, and answers `undefined` when it does not open or arrives
229
+ * before any keyframe gave a media key, since its bytes are ciphertext. Every other frame is the bytes after
230
+ * its header, empty for a frame that carries only its header.
231
+ */
232
+ private audioPayload;
206
233
  /**
207
234
  * Report an access unit the transport could not complete.
208
235
  *
@@ -23,6 +23,8 @@ export type LiveTrace =
23
23
  topology: "attached" | "own";
24
24
  action: "start" | "keepalive";
25
25
  level2: boolean;
26
+ /** The media-key type an attached start offered. */
27
+ key?: "ecc" | "rsa";
26
28
  }
27
29
  /** The device acknowledged a retained start, or that start was repeated / abandoned unacknowledged. */
28
30
  | {
@@ -137,16 +139,16 @@ export type LiveTrace =
137
139
  *
138
140
  * Every ending of a level-2 wait carries one of these reasons, so a start refused for want of a key is
139
141
  * accounted for however it ended. `grace-elapsed` is a wait that ran out and states how long was waited;
140
- * the rest are answered without waiting, because the negotiation is one-shot per connection and a
141
- * concluded one is final. `no-cipher-key` and `derivation-failed` are about this account's cipher
142
- * material, `not-negotiating` and `session-closed` about the station or its connection — and only a
143
- * reason reached under a negotiation has a cipher to name.
142
+ * the rest are answered without waiting, because a concluded negotiation stays concluded until a
143
+ * re-prompt for `CMD_GATEWAYINFO` re-opens it. `no-cipher-key` and `derivation-failed` are about this
144
+ * account's cipher material, `not-negotiating` and `session-closed` about the station or its connection —
145
+ * and only a reason reached under a negotiation has a cipher to name.
144
146
  *
145
147
  * A `grace-elapsed` start proceeds at level 1 where it has such a form, and not at all where it does not.
146
148
  */
147
149
  | {
148
150
  phase: "level2-unavailable";
149
- reason: "no-cipher-key" | "derivation-failed" | "not-negotiating" | "session-closed" | "grace-elapsed";
151
+ reason: "no-cipher-key" | "derivation-failed" | "not-negotiating" | "session-closed" | "grace-elapsed" | "gateway-refused";
150
152
  cipherId?: number;
151
153
  waitedMs?: number;
152
154
  }
@@ -95,6 +95,12 @@ export interface P2PFrame extends P2PDataFrameHeader {
95
95
  * the values as device state without any per-capability decoder.
96
96
  */
97
97
  params?: Record<number, string>;
98
+ /**
99
+ * A station's camera-info table, one `param_type → param_value` map per channel it names (255 is the
100
+ * station). Set instead of {@link params} when the station answered on {@link STATION_CHANNEL}, so each
101
+ * channel's state can land on the device attached there.
102
+ */
103
+ channelParams?: Record<number, Record<number, string>>;
98
104
  }
99
105
  /**
100
106
  * A live PPCS session. Internal transport; a host drives cameras through the capability surface.
@@ -134,7 +140,12 @@ export declare class P2PSession extends EventEmitter {
134
140
  private audioStalled;
135
141
  private audioRetransmitTimer?;
136
142
  private lastPongData?;
137
- /** When the selected peer last answered outbound P2P traffic on this connection. */
143
+ /**
144
+ * When the selected peer last answered outbound P2P traffic on this connection.
145
+ *
146
+ * Each answer also emits `peerAnswer` with this time: a PONG or an ACK from the connected address is the
147
+ * station replying on this connection now, which a heartbeat draws every {@link HEARTBEAT_MS}.
148
+ */
138
149
  private lastPeerAt?;
139
150
  /** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
140
151
  private pathStaleTraced;
@@ -180,11 +191,21 @@ export declare class P2PSession extends EventEmitter {
180
191
  private level2Seq;
181
192
  private rsaPrivateKey?;
182
193
  private rsaModulusHex?;
183
- /** Guards the one-shot level-2 key negotiation kicked off by the GATEWAYINFO reply. */
194
+ /** The P-256 pair an attached camera's media start offers; see {@link mediaEccPrivateKey}. */
195
+ private mediaEcc?;
196
+ /**
197
+ * Per attached channel, the media-key type its latest start offered on this connection, or that last answered,
198
+ * and whether a keyframe has answered it. See {@link attachedStartKey} and {@link settleAttachedKey}.
199
+ */
200
+ private readonly attachedStartKeys;
201
+ /**
202
+ * Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
203
+ * settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
204
+ */
184
205
  private level2Negotiating;
185
206
  /**
186
207
  * Whether waiting for a level-2 key can still change the answer: `false` once one has been negotiated,
187
- * once the one-shot negotiation concluded without one, and from the start when nothing can negotiate one.
208
+ * once a negotiation concluded without one, and from the start when nothing can negotiate one.
188
209
  */
189
210
  private level2Pending;
190
211
  /** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
@@ -203,6 +224,8 @@ export declare class P2PSession extends EventEmitter {
203
224
  private readonly liveStartedChannels;
204
225
  /** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
205
226
  private gatewayCipherId?;
227
+ /** Whether this connection's latest `CMD_GATEWAYINFO` reply was a refusal — see {@link gatewayInfoRefused}. */
228
+ private gatewayRefused;
206
229
  /** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
207
230
  private clearCommandCount;
208
231
  private readonly unackedLiveStarts;
@@ -260,7 +283,8 @@ export declare class P2PSession extends EventEmitter {
260
283
  * The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
261
284
  * beginning later does not restart it: best-effort media can proceed without the key, and restarting
262
285
  * would charge another grace to a source that may already be streaming. A session whose negotiation has
263
- * settled answers without waiting at all, since being one-shot is what makes that answer final.
286
+ * settled answers without waiting at all: the answer stands until {@link repromptLevel2Key} re-opens the
287
+ * wait, since the station is prompted for `CMD_GATEWAYINFO` only on connect and on that re-prompt.
264
288
  *
265
289
  * An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
266
290
  * on one account: own-session cameras of two device types negotiated a key, three others never did, and
@@ -276,12 +300,13 @@ export declare class P2PSession extends EventEmitter {
276
300
  /**
277
301
  * Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
278
302
  *
279
- * The negotiation is one-shot per connection: the station is prompted once on connect, and a reply that
280
- * never lands settles the wait so {@link awaitLevel2Key} answers `false` at once forever after. That is the
303
+ * Without this the station is prompted once on connect, and a negotiation that settles without a key
304
+ * leaves {@link awaitLevel2Key} answering `false` at once for the rest of that connection. That is the
281
305
  * right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
282
306
  * level-2 is then refused for the whole life of that connection, while a fresh session over the same
283
307
  * device negotiates a key normally. Measured: a session that had settled refused every such operation
284
- * until it was rebuilt, at which point the station answered with a cipher id straight away.
308
+ * until it was rebuilt, at which point the station answered with a cipher id straight away. The reply to
309
+ * this ask runs the negotiation again and settles the re-opened wait either way.
285
310
  *
286
311
  * Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
287
312
  * flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
@@ -296,13 +321,15 @@ export declare class P2PSession extends EventEmitter {
296
321
  * `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
297
322
  * `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
298
323
  * ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
299
- * key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
324
+ * key. Emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
300
325
  * `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` when the ECC unwrap
301
326
  * fails; an RSA unwrap failure is only traced (non-fatal either way — level-1 traffic keeps working).
302
327
  *
303
- * Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
304
- * failure final for this connection generation: nothing will retry it there, so a later command must be
305
- * told at once rather than left to time out against a key that is not coming.
328
+ * Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
329
+ * left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
330
+ * GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
331
+ * generation ended settles nothing. A {@link gatewayInfoRefused} reply names no cipher, so it settles at once
332
+ * with no cloud lookup, and a later reply naming a cipher withdraws the refusal.
306
333
  */
307
334
  private negotiateLevel2Key;
308
335
  /**
@@ -327,7 +354,15 @@ export declare class P2PSession extends EventEmitter {
327
354
  * inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
328
355
  */
329
356
  private requestReceiveBuffer;
330
- /** Open the socket and start the lookup → hole-punch handshake. */
357
+ /**
358
+ * Open the socket and start the lookup → hole-punch handshake.
359
+ *
360
+ * A socket closed or failing while binding never invokes its bind callback, so the bind wait also
361
+ * settles on the socket's `close` and `error`. On `close`, a connect whose session was closed meanwhile
362
+ * returns without starting the handshake, so it never sends a lookup or arms a timer on a closed
363
+ * session. On `error`, connect closes the session and rejects with {@link StationUnreachableError}
364
+ * carrying the bind error as its `cause`.
365
+ */
331
366
  connect(): Promise<void>;
332
367
  /** Cached across every `P2PSession` in this process — the local outbound IPv4 doesn't vary by
333
368
  * station, so there's no reason to re-probe it per session (e.g. once per station in a fleet). Only
@@ -494,6 +529,11 @@ export declare class P2PSession extends EventEmitter {
494
529
  sendIntStringCommand(commandType: number, value: number, valueSub: number, strValue: string, channel?: number): void;
495
530
  /** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
496
531
  sendStringPairCommand(commandType: number, strValue: string, strValueSub: string, channel: number): void;
532
+ /**
533
+ * Whether this connection's station answered `CMD_GATEWAYINFO` with a refusal ({@link gatewayInfoRefused}) and
534
+ * no key that frames a set-payload is held.
535
+ */
536
+ get refusedLevel2(): boolean;
497
537
  /**
498
538
  * Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
499
539
  * target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
@@ -504,8 +544,12 @@ export declare class P2PSession extends EventEmitter {
504
544
  * On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
505
545
  * layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
506
546
  * byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
547
+ *
548
+ * With `clearOnRefusal` on a session that {@link refusedLevel2}, the frame goes in the clear in the vendor
549
+ * app's form for such a device: header bytes `[01 00]`, signCode 0, and a body of `{cmd, payload}` alone.
550
+ * Without it, a refused session answers `false` and sends nothing.
507
551
  */
508
- sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
552
+ sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number, clearOnRefusal?: boolean): boolean;
509
553
  /**
510
554
  * Send a **level-2 (GCM, signCode 8) frame whose plaintext is exactly `json`** — no
511
555
  * `{account_id,cmd,mChannel,…}` envelope. The target device is selected by the frame-header
@@ -612,6 +656,10 @@ export declare class P2PSession extends EventEmitter {
612
656
  * Send a CMD_SET_PAYLOAD(1350) wrapping `{account_id, cmd:<subCmd>, mChannel, mValue3:<subCmd>,
613
657
  * payload}`, encrypted level-2 (AES-256-GCM, signCode 8) — the app's media-control path. The
614
658
  * camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
659
+ * A start (HomeBase-controlled camera path, the V6 app's media-start envelope) also carries an
660
+ * `accountId` inside its payload and the media key `key` offers (see {@link attachedStartKey}). A start
661
+ * offering the P-256 point labels itself `ClientOS: "IOS"`, as every simple attached start in the app
662
+ * captures that carry that key form does; the RSA start keeps `"Android"`.
615
663
  */
616
664
  private sendMediaPayloadLevel2;
617
665
  /**
@@ -632,6 +680,41 @@ export declare class P2PSession extends EventEmitter {
632
680
  * (T8171 2560×1440, T8210 640×480). Returns `undefined` on failure (frame is skipped).
633
681
  */
634
682
  private rsaUnwrapKey;
683
+ /**
684
+ * The media-key type the next attached start sent on `channel` offers, recorded as offered: ECC, the current
685
+ * app's form, for the first start this connection sends there; RSA for every start after one that no keyframe
686
+ * answered before it was re-issued; and, once a keyframe has answered, the type that opened it, until
687
+ * {@link settleAttachedKey} records another. Only an opened keyframe answers: frames no key opens, and deltas,
688
+ * do not. A re-issue is a full restart on an attached camera, so a camera that cannot read the point is asked
689
+ * with RSA, the start every station answers, and is not asked with the point again while that goes unanswered.
690
+ * A stream decoding with the session's pair reads either answer. A station without {@link mediaEccPrivateKey}
691
+ * is offered RSA only. A start that cannot be sent offers nothing and records nothing.
692
+ */
693
+ private attachedStartKey;
694
+ /**
695
+ * Record how a keyframe of `channel` was opened, which answers the starts there. A keyframe opened under a
696
+ * key type is an answer of that type, whichever type the latest start offered: a station can serve the
697
+ * P-256 start after the RSA re-issue went out. A plaintext keyframe
698
+ * answering an unanswered P-256 offer is a camera that did not use the point, and the next start there offers
699
+ * RSA; once a P-256 answer has been opened it changes nothing, and answering RSA, it settles RSA. Answers
700
+ * whether a new start is needed. A channel no attached start has offered a key on is left as it is.
701
+ */
702
+ settleAttachedKey(channel: number, opened: "ecc" | "rsa" | "plain"): boolean;
703
+ /**
704
+ * The 32-byte P-256 private scalar paired with the point an attached camera's media start offers as `key`.
705
+ * A camera answering that start wraps each keyframe's AES-256-GCM media key in an ECIES envelope for this
706
+ * key ({@link VideoFrameDecoder}). Generated once per session, like the RSA pair.
707
+ *
708
+ * `undefined` unless the station is a HomeBase 3 (`T8030`): the app's P-256 start and the device runs
709
+ * that answered it were both observed on that station only.
710
+ */
711
+ get mediaEccPrivateKey(): Buffer | undefined;
712
+ /**
713
+ * The session's P-256 pair: the private scalar as 32 bytes, and the public point as 128 hex characters,
714
+ * `x ‖ y` without the uncompressed-point prefix, the form the current app's attached-camera start carries in
715
+ * `key`. `ECDH.getPrivateKey()` drops a scalar's leading zero bytes, so the scalar is padded back to 32.
716
+ */
717
+ private mediaEccPair;
635
718
  /**
636
719
  * The RSA-1024 public-key modulus (128-byte hex) the station uses to wrap the per-stream media
637
720
  * key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
@@ -677,10 +760,11 @@ export declare class P2PSession extends EventEmitter {
677
760
  innerCmd?: number;
678
761
  }): void;
679
762
  /**
680
- * Standard "give me the whole table" query params the eufy app uses for a direct
681
- * `CMD_DATABASE` read (inner `cmd 10000`). `count` bounds the row count.
763
+ * The `CMD_SET_PAYLOAD` payload of a database read or stored-image request: the JSON level-1 encrypted, or
764
+ * in the clear with header bytes from {@link nextClearCommandBytes} on a session that {@link sendsInTheClear}
765
+ * and holds no level-2 key.
682
766
  */
683
- private fullTableQuery;
767
+ private databasePayload;
684
768
  /**
685
769
  * Request the on-device edge-AI **face roster** over P2P — the phone-free path.
686
770
  *
@@ -719,6 +803,9 @@ export declare class P2PSession extends EventEmitter {
719
803
  *
720
804
  * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
721
805
  * session throws, so a second reply cannot land in this buffer.
806
+ *
807
+ * The query's framing follows the cipher the gateway-info reply names, so on a session still negotiating
808
+ * its level-2 key the query waits for that to settle, up to {@link LEVEL2_SETTLE_MS} from connect.
722
809
  */
723
810
  readDatabase(table: string, opts?: {
724
811
  accountId?: string;
@@ -788,6 +875,8 @@ export declare class P2PSession extends EventEmitter {
788
875
  *
789
876
  * Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
790
877
  * treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
878
+ *
879
+ * A datagram shorter than the 8-byte DATA header carries no sequence number and is dropped unacknowledged.
791
880
  */
792
881
  private onData;
793
882
  /** Hold a datagram that arrived past a hole, and arm the wait for the missing one to be repeated. */
@@ -821,6 +910,13 @@ export declare class P2PSession extends EventEmitter {
821
910
  */
822
911
  private resetInboundSequencing;
823
912
  private ackTypeHeader;
913
+ /**
914
+ * Decrypt and dispatch one inbound data frame.
915
+ *
916
+ * A `CMD_GATEWAYINFO` (1100) reply carries the ECIES envelope for the level-2 session key. When a
917
+ * cipher-key resolver is configured, no key that seals a set-payload is held and no negotiation is in
918
+ * flight, the reply starts one (async, fire-and-forget); a reply landing while one is in flight is ignored.
919
+ */
824
920
  private handleFrame;
825
921
  private send;
826
922
  /** Finish the END send on the current socket before closing it; send completion is not a peer ACK. */
@@ -25,6 +25,8 @@ export interface ReadableEgressOptions {
25
25
  /**
26
26
  * Wrap a shared-source {@link Consumer} in a fresh Readable. The consumer is detached when the
27
27
  * Readable is destroyed/ended, so callers own the lifetime by owning the stream.
28
- * Frames arrive through onMedia, preserving their source-arrival metadata.
28
+ * Frames arrive through onMedia, preserving their source-arrival metadata. The consumer's battery
29
+ * `budget` notice is re-emitted unchanged as a `budget` event carrying a {@link StreamBudgetNotice};
30
+ * without an `extend()` within its grace the shared source ends the consumer and the Readable reaches EOF.
29
31
  */
30
32
  export declare function openReadableFromConsumer(consumer: Consumer, opts?: ReadableEgressOptions): Readable;