@mega-yfue/eufy-sdk 0.6.0-beta.13 → 0.6.0-beta.15

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.
@@ -1,6 +1,7 @@
1
1
  import { type SessionEntry } from "../../core/crypto.js";
2
2
  import { type SessionStore } from "../../core/store.js";
3
3
  import { type Logger } from "../../core/logger.js";
4
+ import type { EufyDevice } from "../../core/types.js";
4
5
  import type { SecureMqttCredentials } from "../mqtt/secure-mqtt.js";
5
6
  export type RegionShard = "eu-pr" | "us-pr";
6
7
  /**
@@ -354,11 +355,11 @@ export declare class MegaHttpClient {
354
355
  /** Authed call to a mega service host: app-{service}-{region}.eufy.com. */
355
356
  post<T = unknown>(service: string, path: string, body?: unknown, headerOverrides?: Record<string, string>): Promise<T>;
356
357
  /**
357
- * Fetch the per-station DSK key used in the P2P cloud-lookup payload. Mirrors
358
+ * Fetch the per-station DSK key using each station's reported cloud category. Mirrors
358
359
  * the legacy `get_dsk_keys` body shape on the mega devicerelation service.
359
360
  * The P2P *local* lookup path does not need this; only the cloud path does.
360
361
  */
361
- getDskKeys(stationSns: string[], priorDsks?: Record<string, string>): Promise<Record<string, {
362
+ getDskKeys(stations: ReadonlyArray<Pick<EufyDevice, "sn" | "category">>, priorDsks?: Record<string, string>): Promise<Record<string, {
362
363
  dskKey: string;
363
364
  expiration: number;
364
365
  }>>;
@@ -224,8 +224,8 @@ export declare function buildRawCommandPayload(data: Buffer, channel?: number, s
224
224
  export declare function buildAckPayload(dataTypeHeader: Buffer, seqNo: number): Buffer;
225
225
  /** Parse the 16-byte data-frame header that follows the "XZYH" magic. */
226
226
  export declare function parseDataFrameHeader(frame: Buffer): P2PDataFrameHeader;
227
- /** Parse a LOOKUP_ADDR or RLY_TO message's leading address record. */
228
- export declare function parseLookupAddr(msg: Buffer): Address;
227
+ /** A LOOKUP_ADDR message's leading address record, or `undefined` for a message too short to carry it. */
228
+ export declare function parseLookupAddr(msg: Buffer): Address | undefined;
229
229
  /** The relay servers a LIST_REQ_ACK names, in the order listed, without the unspecified address; records past the message's end are dropped. */
230
230
  export declare function parseRelayList(msg: Buffer): Address[];
231
231
  /** A RLY_PORT_ACK's mark and relay port, or `undefined` for a message too short to carry them. */
@@ -102,6 +102,18 @@ export declare class P2PCommandRouter {
102
102
  * {@link liveSources} is a claim in flight.
103
103
  */
104
104
  private readonly liveSessionKeys;
105
+ /**
106
+ * The live source build in flight per `${parentSn}:${channel}`, settled once its connection is opened
107
+ * or refused.
108
+ *
109
+ * A build awaits its media session before it can file the source in {@link liveSources}, so a second
110
+ * request for the same camera in that window finds nothing cached. Building a second source there
111
+ * overwrites the first, and the orphan's teardown then closes the connection the cached one streams
112
+ * on. A request that finds a build here waits for it, until its own signal aborts, then resolves its
113
+ * session again and reuses what the build filed, or builds its own when that build failed. The entry
114
+ * is removed when the build settles and its promise never rejects, so each wait ends on a later state.
115
+ */
116
+ private readonly liveSourceBuilds;
105
117
  /**
106
118
  * The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
107
119
  * time and the session carries one audio sequence, so this path is exclusive where a live pull is
@@ -220,6 +232,11 @@ export declare class P2PCommandRouter {
220
232
  * so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
221
233
  * shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
222
234
  * carry the start is not a connected session, so this refuses rather than returning one.
235
+ *
236
+ * A session refused its key within the grace, or never connected, is closed and dropped from the manager at
237
+ * once. Nothing else releases it: no source holds it, a wired station's session never idles out, and the
238
+ * manager hands the same session to every later open of the camera, so the next open dials a fresh
239
+ * connection instead. A session another open has since replaced is left to its own close.
223
240
  */
224
241
  private openMediaSession;
225
242
  /**
@@ -394,8 +411,9 @@ export declare class P2PCommandRouter {
394
411
  * {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
395
412
  * reuse branch, so a reuse frees the station as a cold start does.
396
413
  *
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
414
+ * `continuous` decides whether a camera that finds the station's own session claimed may open a
415
+ * connection of its own for it; on an {@link OWN_LIVE_SESSION_MODELS} station it always gets one, and an
416
+ * 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
417
  * frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
400
418
  * so a still asked for while a sibling is being watched contends as it always did, and the caller's
401
419
  * retained image answers it. A live view still outranks a tile; what changed is that two live views no
@@ -404,7 +422,7 @@ export declare class P2PCommandRouter {
404
422
  * The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
405
423
  * place.
406
424
  */
407
- sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, mayOpenOwnSession?: boolean): Promise<SharedLiveSource>;
425
+ sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, continuous?: boolean): Promise<SharedLiveSource>;
408
426
  /**
409
427
  * Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
410
428
  *
@@ -559,12 +577,17 @@ export declare class P2PCommandRouter {
559
577
  * yield CMAF fragments (init segment first, then a `moof`+`mdat` per keyframe boundary) muxed by the
560
578
  * dependency-free internal fMP4 muxer. The returned recording handle exposes battery-budget notices
561
579
  * and detaches its consumer on `stop`, iterator return, or iterator throw. No ffmpeg.
580
+ *
581
+ * It is a continuous pull only on an {@link OWN_LIVE_SESSION_MODELS} station; elsewhere it never opens a
582
+ * connection of its own.
562
583
  */
563
584
  recordFragments(sn: string, opts?: {
564
585
  fragmentSeconds?: number;
565
586
  eccPrivateKey?: Buffer;
566
587
  keepAliveMs?: number;
567
588
  } & SharedSourceHints): FragmentRecording;
589
+ /** Whether `stationSn` is an {@link OWN_LIVE_SESSION_MODELS} station. */
590
+ private ownsLiveSession;
568
591
  /**
569
592
  * **Generic P2P request/reply query.** Sends a `SET_PAYLOAD` (1350) wrapper carrying `subCmd` on the
570
593
  * device channel, then resolves with the reply frame's `payload` — the `NOTIFY_PAYLOAD` (1351)
@@ -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>;
@@ -137,10 +137,10 @@ export type LiveTrace =
137
137
  *
138
138
  * Every ending of a level-2 wait carries one of these reasons, so a start refused for want of a key is
139
139
  * 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.
140
+ * the rest are answered without waiting, because a concluded negotiation stays concluded until a
141
+ * re-prompt for `CMD_GATEWAYINFO` re-opens it. `no-cipher-key` and `derivation-failed` are about this
142
+ * account's cipher material, `not-negotiating` and `session-closed` about the station or its connection —
143
+ * and only a reason reached under a negotiation has a cipher to name.
144
144
  *
145
145
  * A `grace-elapsed` start proceeds at level 1 where it has such a form, and not at all where it does not.
146
146
  */
@@ -180,11 +180,14 @@ export declare class P2PSession extends EventEmitter {
180
180
  private level2Seq;
181
181
  private rsaPrivateKey?;
182
182
  private rsaModulusHex?;
183
- /** Guards the one-shot level-2 key negotiation kicked off by the GATEWAYINFO reply. */
183
+ /**
184
+ * Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
185
+ * settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
186
+ */
184
187
  private level2Negotiating;
185
188
  /**
186
189
  * 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.
190
+ * once a negotiation concluded without one, and from the start when nothing can negotiate one.
188
191
  */
189
192
  private level2Pending;
190
193
  /** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
@@ -260,7 +263,8 @@ export declare class P2PSession extends EventEmitter {
260
263
  * The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
261
264
  * beginning later does not restart it: best-effort media can proceed without the key, and restarting
262
265
  * 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.
266
+ * settled answers without waiting at all: the answer stands until {@link repromptLevel2Key} re-opens the
267
+ * wait, since the station is prompted for `CMD_GATEWAYINFO` only on connect and on that re-prompt.
264
268
  *
265
269
  * An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
266
270
  * on one account: own-session cameras of two device types negotiated a key, three others never did, and
@@ -276,12 +280,13 @@ export declare class P2PSession extends EventEmitter {
276
280
  /**
277
281
  * Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
278
282
  *
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
283
+ * Without this the station is prompted once on connect, and a negotiation that settles without a key
284
+ * leaves {@link awaitLevel2Key} answering `false` at once for the rest of that connection. That is the
281
285
  * right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
282
286
  * level-2 is then refused for the whole life of that connection, while a fresh session over the same
283
287
  * 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.
288
+ * until it was rebuilt, at which point the station answered with a cipher id straight away. The reply to
289
+ * this ask runs the negotiation again and settles the re-opened wait either way.
285
290
  *
286
291
  * Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
287
292
  * flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
@@ -296,13 +301,14 @@ export declare class P2PSession extends EventEmitter {
296
301
  * `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
297
302
  * `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
298
303
  * 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
304
+ * key. Emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
300
305
  * `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` when the ECC unwrap
301
306
  * fails; an RSA unwrap failure is only traced (non-fatal either way — level-1 traffic keeps working).
302
307
  *
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.
308
+ * Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
309
+ * left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
310
+ * GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
311
+ * generation ended settles nothing.
306
312
  */
307
313
  private negotiateLevel2Key;
308
314
  /**
@@ -682,11 +688,6 @@ export declare class P2PSession extends EventEmitter {
682
688
  * and holds no level-2 key.
683
689
  */
684
690
  private databasePayload;
685
- /**
686
- * Standard "give me the whole table" query params the eufy app uses for a direct
687
- * `CMD_DATABASE` read (inner `cmd 10000`). `count` bounds the row count.
688
- */
689
- private fullTableQuery;
690
691
  /**
691
692
  * Request the on-device edge-AI **face roster** over P2P — the phone-free path.
692
693
  *
@@ -797,6 +798,8 @@ export declare class P2PSession extends EventEmitter {
797
798
  *
798
799
  * Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
799
800
  * treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
801
+ *
802
+ * A datagram shorter than the 8-byte DATA header carries no sequence number and is dropped unacknowledged.
800
803
  */
801
804
  private onData;
802
805
  /** Hold a datagram that arrived past a hole, and arm the wait for the missing one to be repeated. */
@@ -830,6 +833,13 @@ export declare class P2PSession extends EventEmitter {
830
833
  */
831
834
  private resetInboundSequencing;
832
835
  private ackTypeHeader;
836
+ /**
837
+ * Decrypt and dispatch one inbound data frame.
838
+ *
839
+ * A `CMD_GATEWAYINFO` (1100) reply carries the ECIES envelope for the level-2 session key. When a
840
+ * cipher-key resolver is configured, no key that seals a set-payload is held and no negotiation is in
841
+ * flight, the reply starts one (async, fire-and-forget); a reply landing while one is in flight is ignored.
842
+ */
833
843
  private handleFrame;
834
844
  private send;
835
845
  /** 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;
@@ -256,7 +256,8 @@ export declare class SharedLiveSource {
256
256
  *
257
257
  * The previous stream is dropped rather than stopped through the state machine: it speaks to a session that
258
258
  * is gone, and its `stop` would be read as an upstream end. Only a warm-up that asked for a replacement
259
- * rewarms, so this is inert on a source that is streaming or has already failed.
259
+ * rewarms, so this is inert on a source that is streaming or has already failed. Each call replaces the
260
+ * re-issue ticker rather than adding one, so the start goes out once per interval however often it runs.
260
261
  */
261
262
  rewarm(): void;
262
263
  /**
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Persistence for FCM credentials + seen persistent-ids, so we register the FCM
3
+ * token ONCE and just reconnect (and don't re-receive old pushes) across runs.
4
+ */
5
+ import { FileSessionStore } from "../../core/store.js";
1
6
  import type { FcmCredentials } from "./types.js";
2
7
  export interface PersistedPush {
3
8
  creds: FcmCredentials;
@@ -14,10 +19,6 @@ export declare class MemoryFcmStore implements FcmStore {
14
19
  save(p: PersistedPush): void;
15
20
  clear(): void;
16
21
  }
17
- export declare class FileFcmStore implements FcmStore {
18
- private readonly path;
19
- constructor(path: string);
20
- load(): PersistedPush | null;
21
- save(p: PersistedPush): void;
22
- clear(): void;
22
+ /** JSON-file FCM store: the {@link FileSessionStore} file format and write guarantees, on the push record. */
23
+ export declare class FileFcmStore extends FileSessionStore<PersistedPush> implements FcmStore {
23
24
  }
@@ -14,6 +14,11 @@
14
14
  * a live pull holds it open. Sends on a session are serialised; an ACK is correlated by its outer command
15
15
  * and segment, since it carries no inner command. An ACK is not an observation of the resulting state.
16
16
  *
17
+ * The station's own database is read over the same channel: a `set-payload` with inner `1306`
18
+ * (CMD_DATABASE) answers a table and inner `1308` (CMD_DATABASE_IMAGE) a stored image, each as one
19
+ * notify frame carrying the whole document, zstd-compressed when large. The notify link carries
20
+ * unrelated station reports between them, so a reply is matched by its content, never by arrival order.
21
+ *
17
22
  * Live view of an attached camera rides the same session (the hub accepts one RTC session per account),
18
23
  * through one shared live source per station: the station serves one camera at a time, so a second
19
24
  * camera is refused until the first one's view ends.
@@ -72,8 +77,48 @@ export declare class RtcCommandRouter {
72
77
  private readonly lives;
73
78
  private readonly warm;
74
79
  private readonly callSources;
80
+ private lastTransaction;
75
81
  constructor(deps: RtcCommandRouterDeps);
76
82
  dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
83
+ /**
84
+ * Query one on-station table over the station's control channel and answer its rows.
85
+ *
86
+ * Sends the `1350` envelope with inner `{cmd: 1306, payload: {cmd: 10000, table, payload, transaction}}`,
87
+ * the whole-table query, on the station channel. The hub ACKs on the command link and answers with one notify frame whose body
88
+ * carries `{cmd, table, data, mIntRet, msg, transaction, …}`, echoing the read's `transaction`; that echo
89
+ * is the only thing a reply is attributed by, and every read sends a `transaction` no other read on this
90
+ * router has sent. Resolves with the reply's `data` rows. Rejects with the hub's `msg` on a non-zero
91
+ * `mIntRet`, on an ACK refusing the envelope, at once when `signal` aborts (queued or sent), when the
92
+ * session closes, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
93
+ *
94
+ * One read or command at a time per station: the read holds the station's send queue until it settles.
95
+ */
96
+ readTable(route: RtcRoute, table: string, opts?: {
97
+ timeoutMs?: number;
98
+ signal?: AbortSignal;
99
+ }): Promise<unknown[]>;
100
+ /**
101
+ * Fetch one stored image by its on-station path over the station's control channel.
102
+ *
103
+ * Sends the `1350` envelope with inner `{cmd: 1308, payload: [{file}]}` on the station channel. The hub
104
+ * ACKs on the command link and answers with one notify frame whose body is `{file, content}`, `content`
105
+ * the image in base64, and no `transaction`; the reply is the one whose `file` equals `filePath`. A reply
106
+ * arriving after an abandoned read of the same path carries that path's bytes, which are the bytes a
107
+ * later read of it asks for. Resolves with the decoded bytes. Rejects on an ACK refusing the envelope,
108
+ * at once when `signal` aborts (queued or sent), when the session closes, and when `timeoutMs` (default
109
+ * {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
110
+ *
111
+ * One read or command at a time per station: the read holds the station's send queue until it settles.
112
+ */
113
+ readImage(route: RtcRoute, filePath: string, opts?: {
114
+ timeoutMs?: number;
115
+ signal?: AbortSignal;
116
+ }): Promise<Buffer>;
117
+ /**
118
+ * A `transaction` stamp in the vendor app's shape, a decimal epoch-in-milliseconds string, strictly
119
+ * greater than the last one this router issued, so two reads in the same millisecond never share one.
120
+ */
121
+ private nextTransaction;
77
122
  /**
78
123
  * The media of a camera on a T9000: live video, a readable feed and a live still over the station's
79
124
  * session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
@@ -108,9 +153,24 @@ export declare class RtcCommandRouter {
108
153
  private refuseIfWatched;
109
154
  private newLiveSource;
110
155
  /**
111
- * Completes on the command ACK or a correlated control-result notification. The notification must
112
- * match the request channel, parameter and segment and contain a structured payload. Receipt does
113
- * not establish physical actuation. The command is sent once, bounded by {@link ACK_TIMEOUT_MS}.
156
+ * Send one envelope on the station's session, queued behind its other sends, and settle as
157
+ * {@link sendAwaitAck} does. `body` is the inner payload; the station session, the `account_id` it is
158
+ * stamped with and the packet are resolved here, so a command and a read reach the wire through the same
159
+ * steps. With `reply.signal`, the caller's wait rejects the moment it aborts, also while the send is still
160
+ * queued; a send not yet made by then is skipped.
161
+ */
162
+ private sendQueued;
163
+ /**
164
+ * Without `reply`, completes on the command ACK or a correlated control-result notification. The
165
+ * notification must match the request channel, parameter and segment and contain a structured payload.
166
+ * Receipt does not establish physical actuation. Bounded by {@link ACK_TIMEOUT_MS}.
167
+ *
168
+ * With `reply`, a zero-result ACK is not the answer: the first notify body (an object) for which
169
+ * `reply.pick` answers a value resolves with it, one `pick` throws on rejects with that error, and every
170
+ * other notify frame is left alone. Bounded by `reply.timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}),
171
+ * and ended by `reply.signal`; one already aborted is not sent.
172
+ *
173
+ * Either way an ACK with a non-zero result code rejects, and the command is sent once.
114
174
  */
115
175
  private sendAwaitAck;
116
176
  private newSession;
@@ -1,14 +1,3 @@
1
- /**
2
- * The command framing a HomeBase S1 Pro (T9000) speaks over its WebRTC data channel — the wire the
3
- * security.eufy.com web client builds, reproduced byte-exact.
4
- *
5
- * A packet is a 16-byte header behind the ASCII magic `XZYH`, then a JSON body, sent in the clear (the
6
- * DTLS channel is the encryption). Replies come back with `isResponse = 1`, the same `segment` the
7
- * request carried, and a body whose first four bytes are a little-endian result code.
8
- *
9
- * Every offset is the portal bundle's header builder and parser (`Gr()` / `Qr()`) and its
10
- * `worker_sctp_send`.
11
- */
12
1
  export declare const PORTAL_HEADER_LENGTH = 16;
13
2
  /** The SET_PAYLOAD envelope. */
14
3
  export declare const PORTAL_CMD_SET_PAYLOAD = 1350;
@@ -61,8 +50,9 @@ export declare function isPortalPacket(buf: Buffer): boolean;
61
50
  /** A complete request packet: header + body. */
62
51
  export declare function buildPortalPacket(req: PortalRequest): Buffer;
63
52
  /**
64
- * Parse an inbound packet. On the command channel the body opens with the int32 result code and may
65
- * carry JSON after it; on the notify channel the whole body is JSON with a nested `cmd`.
53
+ * Parse an inbound packet, opening a zstd-compressed one first. On the command channel the body opens
54
+ * with the int32 result code and may carry JSON after it; on the notify channel the whole body is JSON
55
+ * with a nested `cmd`. `undefined` for a frame that is neither a packet nor a stream holding one.
66
56
  */
67
57
  export declare function parsePortalPacket(buf: Buffer, linkType?: number): PortalResponse | undefined;
68
58
  /** A 1..255 segment counter that never yields 0 (the portal reserves it). */
@@ -9,6 +9,12 @@ export declare class StoredImageCache {
9
9
  private readonly activeDevices;
10
10
  private activeDownloads;
11
11
  private generation;
12
+ /**
13
+ * Bumped by {@link clearLifecycleErrors}. A lifecycle failure from a download started under an older
14
+ * count is not recorded; the candidate is queued again, unless a newer one is already queued, so the
15
+ * current session fetches it.
16
+ */
17
+ private lifecycle;
12
18
  constructor(downloader: (url: string, deviceKey: string) => Promise<Buffer>, logger: Logger, clock?: () => number, isLifecycleError?: (error: unknown) => boolean);
13
19
  /**
14
20
  * Observe a normalized thumbnail URL and start acquisition eagerly.
@@ -20,6 +26,14 @@ export declare class StoredImageCache {
20
26
  observe(deviceKey: string, url: string): void;
21
27
  /** Return retained bytes without starting or awaiting network work. */
22
28
  snapshotStored(deviceKey: string): Promise<Buffer>;
29
+ /**
30
+ * Forget every device's recorded lifecycle failure, keeping retained bytes and candidate state.
31
+ *
32
+ * A lifecycle failure describes the session that was current when the download ran, so it no longer
33
+ * holds once a login succeeds again; {@link snapshotStored} then answers from the retained image, or
34
+ * with the reason a stored image is absent. A download in flight across the call cannot record one.
35
+ */
36
+ clearLifecycleErrors(): void;
23
37
  /** Invalidate all retained and candidate state. */
24
38
  clear(): void;
25
39
  private pump;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.6.0-beta.13",
3
+ "version": "0.6.0-beta.15",
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",