@mega-yfue/eufy-sdk 0.3.0-beta.0 → 0.3.0-beta.10

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.
@@ -90,6 +90,11 @@ export type BatteryActions = Surface<typeof BATTERY_MEMBERS>;
90
90
  * The richer raw `APP_CMD_SET_POWER_SOURCE` blob is a separate param, surfaced as `powerSourceInfo`.
91
91
  */
92
92
  declare function decodePowerSource(raw: string | number | boolean): number | string;
93
+ /**
94
+ * Power tier of a camera for its live-media budget and standalone P2P session: `"battery"` only when it
95
+ * resolved the `battery` capability AND is not a listed mains-only model, else `"wired"`.
96
+ */
97
+ export declare function cameraPowerTier(model: string | undefined, capabilities: ReadonlySet<string>): "wired" | "battery";
93
98
  /**
94
99
  * The params whose subject IS the physical cell — so every member reading one must carry
95
100
  * {@link notMainsCamera}.
@@ -91,8 +91,9 @@ export declare class SessionExpiredError extends Error {
91
91
  /**
92
92
  * Whether this rejection landed inside that bar — a token replaced recently and rejected again since.
93
93
  *
94
- * It says the session is being DISPLACED rather than expiring: something else is signing in on this
95
- * account, and replacing the token again only trades one login for another.
94
+ * That is the shape displacement by another client signed in on this account has, where replacing the
95
+ * token again only trades one login for another. It is not proof of one, since a cloud refusing to
96
+ * re-issue the session looks the same.
96
97
  */
97
98
  readonly contended: boolean;
98
99
  constructor(message: string, opts?: {
@@ -212,6 +213,8 @@ export declare class MegaHttpClient {
212
213
  private sessionKey?;
213
214
  /** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
214
215
  private readonly sessionKeys;
216
+ /** The in-flight key exchanges, by host and the token they carry — see {@link ensureSessionKey}. */
217
+ private readonly keyExchanges;
215
218
  /**
216
219
  * The held credential. `userId` is the login reply's `ap_cloud_user_id` where it has one — the Anker
217
220
  * Passport cloud's id — while `accountUserId` is the eufy account's own `user_id`.
@@ -318,6 +321,8 @@ export declare class MegaHttpClient {
318
321
  * so it MUST be reused (don't re-exchange after login).
319
322
  */
320
323
  ensureSessionKey(targetHost?: string): Promise<SessionEntry>;
324
+ /** One key exchange against `host`, installed as that host's key. */
325
+ private exchangeSessionKey;
321
326
  /**
322
327
  * Signed + encrypted POST. Content-type auto-falls-back (text/plain ↔ json).
323
328
  *
@@ -523,8 +528,8 @@ export declare class MegaHttpClient {
523
528
  * like the same device — and the cloud keeps one session per device. Each finds its token rejected, replaces
524
529
  * it, and evicts the other: an unbounded login war, silent, and repeated logins are exactly what makes an
525
530
  * account start demanding captchas. The first replacement is immediate, because a token displaced once is
526
- * the ordinary case; a second one soon after is evidence of contention rather than expiry, so the wait grows
527
- * and a caller is told the honest reason instead of being served a fight.
531
+ * the ordinary case; a second one soon after has the shape of contention rather than expiry, so the wait grows
532
+ * and a caller is told what was seen instead of being served a fight.
528
533
  */
529
534
  private recoveryDue;
530
535
  /**
@@ -79,15 +79,24 @@ export interface SecureMqttOptions {
79
79
  export declare class SecureMqtt extends EventEmitter implements RealtimeTransport {
80
80
  readonly kind: "smqtt";
81
81
  private client?;
82
+ /**
83
+ * The in-flight or established connect. A client ID is exclusive at the broker, so a second mqtt.js
84
+ * client under the same ID evicts the first, which reconnects and evicts it back, forever; `connect()`
85
+ * therefore hands a later caller this same attempt instead of opening a rival. Cleared when the
86
+ * attempt fails and on `disconnect()`, so a caller can still retry or deliberately reconnect.
87
+ */
88
+ private connecting?;
82
89
  private readonly o;
83
90
  private readonly logger;
84
91
  constructor(opts: SecureMqttOptions);
85
92
  get id(): string;
93
+ /** Connect, joining the attempt in `connecting` when one is already opening or open. */
94
+ connect(): Promise<void>;
86
95
  /**
87
96
  * Open the broker connection, resolving once it is established. Pinned to a broker instance's IP, or
88
97
  * to the plain hostname; only the former needs its own TLS shape, see `./bare-ip-tls.ts`.
89
98
  */
90
- connect(): Promise<void>;
99
+ private open;
91
100
  /**
92
101
  * Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
93
102
  * four topics for `eufy_life`).
@@ -128,13 +128,18 @@ export declare class P2PSession extends EventEmitter {
128
128
  private lookupTimer?;
129
129
  private heartbeatTimer?;
130
130
  private connectTimer?;
131
- /** Our own bound host:port, self-reported inside LOOKUP_WITH_KEY requests (see sendLookups). */
132
- private selfAddress?;
131
+ /** Extra sockets registered alongside {@link socket} while connecting; see {@link PUNCH_PROBE_SOCKETS}. */
132
+ private probeSockets;
133
+ /** Cloud lookup inputs resolved once for a connection. */
134
+ private cloudLookup?;
135
+ /** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
136
+ private selfHost?;
133
137
  /** In-flight multi-datagram frame per data channel (see onData). */
134
138
  private readonly pendingByDataType;
135
- /** Last datagram sequence number seen per dataType — used to detect a lost/reordered datagram
136
- * mid-frame and drop the (now unrecoverable) partial frame instead of splicing wrong bytes. */
139
+ /** Last delivered datagram sequence number per data type. */
137
140
  private readonly lastSeqByType;
141
+ /** Held datagrams and their active gap timer, grouped by data type. */
142
+ private readonly reorderByType;
138
143
  private tracedDatagramGaps;
139
144
  private readonly level1Key;
140
145
  /** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
@@ -296,6 +301,8 @@ export declare class P2PSession extends EventEmitter {
296
301
  */
297
302
  private static detectLocalIp;
298
303
  private sendLookups;
304
+ /** Register a bound socket's own port with each configured cloud lookup address. */
305
+ private sendCloudLookup;
299
306
  /**
300
307
  * Route one inbound UDP datagram by its message type, tracing every non-DATA one and any type this session
301
308
  * does not model.
@@ -309,6 +316,10 @@ export declare class P2PSession extends EventEmitter {
309
316
  */
310
317
  private onMessage;
311
318
  private beginCheckCam;
319
+ /** Bind additional UDP source ports before the first cloud lookup. */
320
+ private startPunchProbes;
321
+ /** Close all lookup sockets except the one that completed the handshake. */
322
+ private stopPunchProbes;
312
323
  private onConnected;
313
324
  /**
314
325
  * Start the realtime media stream for a camera `channel` (the device's `device_channel`; defaults to
@@ -601,8 +612,7 @@ export declare class P2PSession extends EventEmitter {
601
612
  * Acknowledge and reassemble one DATA datagram, sequenced independently per data type.
602
613
  *
603
614
  * The device numbers each data type's datagrams in its own 16-bit space and repeats what it thinks was
604
- * lost, so a datagram that does not advance the sequence — a duplicate, or one already superseded — is a
605
- * retransmission of something already reassembled: it is acknowledged, then ignored. Distance is measured
615
+ * lost. A repeat of something already delivered is acknowledged, then ignored. Distance is measured
606
616
  * modulo the sequence space and read as backwards beyond {@link SEQUENCE_LOOKBACK}, which is what lets the
607
617
  * numbering wrap without the next datagram looking like a jump of nearly a full space.
608
618
  *
@@ -612,11 +622,29 @@ export declare class P2PSession extends EventEmitter {
612
622
  * numbering and the half-assembled frame goes — because ignoring it would freeze the mark, and every
613
623
  * datagram of the new numbering would then be read as behind it too, for as long as it took to climb back.
614
624
  *
615
- * Only a forward gap means a datagram is genuinely missing. A logical frame's payload spans datagrams that
616
- * carry no header of their own, so the bytes cannot be reassembled around the hole: whatever was pending
617
- * for that data type is discarded, and the frame is rebuilt from the next header.
625
+ * A forward gap means a datagram has not arrived YET, which is not the same as lost. The device repeats
626
+ * unacknowledged datagrams, so successors are held for a bounded wait and delivered strictly in sequence
627
+ * if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
628
+ * own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
629
+ *
630
+ * Only once {@link REORDER_WAIT_MS} passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
631
+ * treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
618
632
  */
619
633
  private onData;
634
+ /** Hold a datagram that arrived past a hole, and arm the wait for the missing one to be repeated. */
635
+ private holdForRetransmit;
636
+ /** Start a full wait for the current hole, leaving an existing wait undisturbed. */
637
+ private armReorderTimer;
638
+ /** Deliver held datagrams that now follow directly on from the last one delivered. */
639
+ private drainReorder;
640
+ /**
641
+ * The missing datagram was not repeated in time: report the gap, drop the frame it belonged to, and
642
+ * resume from the earliest datagram still held so the stream keeps moving.
643
+ */
644
+ private abandonHole;
645
+ private clearReorderTimer;
646
+ /** Reassemble one in-sequence datagram body into logical frames. */
647
+ private reassemble;
620
648
  /**
621
649
  * Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
622
650
  *
@@ -10,6 +10,10 @@ import type { FcmCredentials, PushEvent, RawPushMessage } from "./types.js";
10
10
  import { type Logger } from "../../core/logger.js";
11
11
  /**
12
12
  * Normalises a decoded eufy envelope without consulting device semantics; semantic event names remain unset.
13
+ *
14
+ * The detail (`event_type`, `pic_url`, `cipher`) is read from the deepest level of
15
+ * `payloadLevels`; identity (`device_sn`, `station_sn`) is gathered from every level, deepest
16
+ * first, so a serial is found whichever level the push carries it on.
13
17
  * @internal
14
18
  */
15
19
  export declare function normalizePushEvent(raw: RawPushMessage): PushEvent;
@@ -55,6 +59,13 @@ export declare class PushClient extends EventEmitter {
55
59
  * {@link MAX_LOGIN_FAILURES} consecutive attempts (creds genuinely stale) is it emitted as `error`.
56
60
  */
57
61
  private onLoginError;
62
+ /**
63
+ * Decode one MCS `DataMessageStanza` into a {@link RawPushMessage} and the normalised event.
64
+ *
65
+ * `payload` carries the whole app_data envelope, with its `payload` entry (base64 of NUL-terminated
66
+ * JSON) parsed in place: the envelope's own keys, `device_sn` and `station_sn` among them, sit beside
67
+ * that entry.
68
+ */
58
69
  private handleDataMessage;
59
70
  private startHeartbeat;
60
71
  private stopHeartbeat;
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * eufy FCM push payload types.
3
3
  *
4
- * A push arrives as an MCS DataMessageStanza whose `app_data` has a `payload`
5
- * entry = base64( NUL-terminated JSON ). That JSON is the EufyPushMessage; its
6
- * nested `payload` is device-type specific. The v6 app enriches these with AI
7
- * detection fields (person/vehicle/pet/package/faces/crops/short video) — see
8
- * PushEnrichment.
4
+ * A push arrives as an MCS DataMessageStanza whose `app_data` entries make up the
5
+ * EufyPushMessage envelope. Its `payload` entry is base64( NUL-terminated JSON ), the
6
+ * device-type specific detail, which may itself nest a further `payload`. The v6 app
7
+ * enriches these with AI detection fields (person/vehicle/pet/package/faces/crops/short
8
+ * video) — see PushEnrichment.
9
9
  */
10
10
  /**
11
11
  * Raw MCS frame: a tag + the decoded protobuf object.
@@ -31,10 +31,10 @@ export interface RawPushMessage {
31
31
  persistentId?: string;
32
32
  ttl?: number;
33
33
  sent?: string;
34
- /** The decoded eufy payload (the `payload` app_data entry, JSON-parsed). */
34
+ /** The eufy envelope, as `EufyPushMessage` describes it. */
35
35
  payload: EufyPushMessage;
36
36
  }
37
- /** The eufy JSON envelope inside the push. */
37
+ /** The eufy envelope: the push's `app_data` entries, with the `payload` entry JSON-parsed in place. */
38
38
  export interface EufyPushMessage {
39
39
  type?: string | number;
40
40
  title?: string;
@@ -45,7 +45,7 @@ export interface EufyPushMessage {
45
45
  push_time?: string;
46
46
  doorbell?: string;
47
47
  "google.c.sender.id"?: string;
48
- /** Device-type specific body (often a JSON string that we further parse). */
48
+ /** The decoded `payload` entry: device-type specific detail, which may nest a further `payload`. */
49
49
  payload?: PushPayload;
50
50
  [k: string]: unknown;
51
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.3.0-beta.0",
3
+ "version": "0.3.0-beta.10",
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",