@mega-yfue/eufy-sdk 0.2.0-beta.9 → 0.2.0

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 (53) hide show
  1. package/dist/client/device-registry.d.ts +14 -28
  2. package/dist/client/eufy-mega.d.ts +11 -6
  3. package/dist/client/types.d.ts +6 -2
  4. package/dist/core/contracts.d.ts +23 -27
  5. package/dist/core/crypto.d.ts +10 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/solix-types.d.ts +121 -0
  8. package/dist/core/store.d.ts +38 -10
  9. package/dist/index.js +2690 -485
  10. package/dist/index.js.map +4 -4
  11. package/dist/model/capabilities/access.d.ts +22 -3
  12. package/dist/model/capabilities/arming.d.ts +4 -0
  13. package/dist/model/capabilities/battery.d.ts +32 -4
  14. package/dist/model/capabilities/contact.d.ts +4 -0
  15. package/dist/model/capabilities/doorbell.d.ts +24 -14
  16. package/dist/model/capabilities/index.d.ts +14 -3
  17. package/dist/model/capabilities/lock.d.ts +15 -12
  18. package/dist/model/capabilities/ptz.d.ts +6 -2
  19. package/dist/model/capabilities/solix.d.ts +173 -0
  20. package/dist/model/capabilities/types.d.ts +60 -10
  21. package/dist/model/capabilities/vacuum-clean.d.ts +59 -0
  22. package/dist/model/classify.d.ts +3 -1
  23. package/dist/model/device-family.d.ts +2 -1
  24. package/dist/model/device-types.d.ts +1 -0
  25. package/dist/model/device.d.ts +15 -0
  26. package/dist/model/index.d.ts +5 -0
  27. package/dist/model/solix-catalog.d.ts +25 -0
  28. package/dist/model/solix-device.d.ts +137 -0
  29. package/dist/model/solix-family.d.ts +31 -0
  30. package/dist/model/solix-site.d.ts +70 -0
  31. package/dist/transport/ff09.d.ts +7 -0
  32. package/dist/transport/http/decodeImageV2.d.ts +8 -14
  33. package/dist/transport/http/index.d.ts +1 -0
  34. package/dist/transport/http/jpeg-scan.d.ts +59 -0
  35. package/dist/transport/http/media-download.d.ts +3 -0
  36. package/dist/transport/http/mega-client.d.ts +89 -9
  37. package/dist/transport/http/solix-client.d.ts +270 -0
  38. package/dist/transport/http/solix-constants.d.ts +56 -0
  39. package/dist/transport/media-failure.d.ts +48 -0
  40. package/dist/transport/mqtt/index.d.ts +3 -0
  41. package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
  42. package/dist/transport/mqtt/solix-mqtt.d.ts +360 -0
  43. package/dist/transport/mqtt/topics.d.ts +30 -0
  44. package/dist/transport/p2p/command-router.d.ts +131 -19
  45. package/dist/transport/p2p/live-stream.d.ts +5 -4
  46. package/dist/transport/p2p/live-trace.d.ts +32 -5
  47. package/dist/transport/p2p/media.d.ts +2 -2
  48. package/dist/transport/p2p/p2p-session.d.ts +9 -0
  49. package/dist/transport/p2p/session-manager.d.ts +57 -23
  50. package/dist/transport/p2p/shared-live-source.d.ts +10 -1
  51. package/dist/transport/p2p/station-channels.d.ts +54 -0
  52. package/dist/transport/stored-image-cache.d.ts +7 -1
  53. package/package.json +5 -5
@@ -1,13 +1,3 @@
1
- /**
2
- * The station a device's traffic belongs to, from its cloud record and its own serial.
3
- *
4
- * `parent_sn` carries the parent on a HomeBase-attached device. `station_sn` is frequently absent there —
5
- * empty on every attached sensor of a T8010 — and serves only as a fallback. An empty string states no
6
- * station.
7
- *
8
- * A device naming no parent answers its own serial, so every device has a station.
9
- */
10
- export declare function resolvedStationSn(raw: Record<string, unknown>, sn: string): string;
11
1
  /**
12
2
  * DeviceRegistry — the device list/record/capability-resolution collaborator behind {@link EufyMega}.
13
3
  *
@@ -85,6 +75,9 @@ export declare class DeviceRegistry {
85
75
  private devices;
86
76
  /** Per-(station, channel) capability cache for {@link capabilitiesForFrame}; `null` = negative hit. */
87
77
  private readonly frameCapsCache;
78
+ /** {@link stationChannels} of {@link channelMapFor}, the roster it was computed over. */
79
+ private channelMap;
80
+ private channelMapFor?;
88
81
  /**
89
82
  * Serials whose per-device param overlay has been refused. The call is owner-gated, so on a shared or
90
83
  * member account it fails for the whole life of the client — retrying it every refresh spends a request
@@ -291,33 +284,26 @@ export declare class DeviceRegistry {
291
284
  * hub would emit indistinguishable events. The facade enriches the payload with this.
292
285
  */
293
286
  serialForFrame(stationSn: string, channel: number): string | undefined;
294
- /**
295
- * The parent station a device's frames arrive under.
296
- *
297
- * `parent_sn` on the cloud record is the field that is actually populated for a HomeBase-attached
298
- * device — `stationSn` is frequently absent (observed empty on every attached sensor of a T8010),
299
- * so keying on it alone silently resolves an attached device to ITSELF and no frame ever matches.
300
- * Mirrors the router's own session-keying precedence, which is the source of truth for which
301
- * station a device's traffic belongs to. Answering the device's OWN serial is what "stands alone"
302
- * means, so this is also the topology signal `record()`/`capsOf` hand the resolver.
303
- */
304
- private stationOf;
305
287
  /**
306
288
  * The device a `(station, channel)` pair refers to — a station fans out to attached devices by
307
289
  * `device_channel`, while a standalone device is its own station at channel 0.
308
290
  *
309
- * A device claims a channel only when its record actually STATES one. Treating a missing
310
- * `device_channel` as 0 turns every such device into a rival claimant for channel 0, where a
311
- * station legitimately has an attached device already, and the winner is then decided by cloud list
312
- * order — so the same frame resolves to different devices across refreshes. The resolved serial now
313
- * decides where realtime state is written, not just which decoders may run, so an ambiguous answer
291
+ * A device claims a channel only when its record actually STATES one that no other device attached to
292
+ * the same station also states ({@link stationChannels}). Treating a missing `device_channel` as 0, or
293
+ * letting two claimants of one channel both hold it, leaves the winner to cloud list order — which a
294
+ * partial refresh reorders — so the same frame resolves to different devices across refreshes. The resolved
295
+ * serial decides where realtime state is written, not just which decoders may run, so an ambiguous answer
314
296
  * writes one device's params onto another.
315
297
  *
316
298
  * An attached device that names the channel wins over the station itself, which is what a station
317
- * fanning traffic out by channel means; the station answers for channel 0 only when nothing is
318
- * attached there, which is also the standalone case (a device is its own station).
299
+ * fanning traffic out by channel means. A channel two attached devices both state belongs to one of them,
300
+ * which cannot be told apart, so it answers nothing rather than the station. The station answers for
301
+ * channel 0 only when nothing attached states it, which is also the standalone case (a device is its own
302
+ * station).
319
303
  */
320
304
  private deviceForFrame;
305
+ /** {@link stationChannels} over the current roster, recomputed only when the roster itself is replaced. */
306
+ private stationChannelMap;
321
307
  /**
322
308
  * Resolve the capability set of the device a P2P frame belongs to — the `(station, channel)` pair
323
309
  * (a station fans out to attached devices by `device_channel`; a standalone device is its own
@@ -393,9 +393,9 @@ export declare class EufyMega extends EventEmitter {
393
393
  /**
394
394
  * Combine explicit P2P media with the optional passive push-thumbnail provider.
395
395
  *
396
- * The retained still also becomes the answer for a live still that could not be captured. A station
397
- * serves one camera at a time and a live view outranks a tile, so a still asked for while a sibling is
398
- * being watched is refused at the transport. Answering the retained bytes answers the read rather than
396
+ * The retained still also becomes the answer for a live still that could not be captured. One session
397
+ * serves one camera at a time and a live view outranks a tile — a still does not open a connection of its
398
+ * own — so a still asked for while a sibling is being watched is refused at the transport. Answering the retained bytes answers the read rather than
399
399
  * failing it, marked {@link MediaProvider.snapshotLive} `retained` so the caller knows they are not
400
400
  * current. With nothing retained the refusal stands.
401
401
  */
@@ -752,10 +752,15 @@ export declare class EufyMega extends EventEmitter {
752
752
  */
753
753
  private sendRealtimeInit;
754
754
  /**
755
- * Stations with a live P2P session. P2P is auto-managed: wired stations are warmed at login, battery
755
+ * The open P2P sessions, by key. P2P is auto-managed: wired stations are warmed at login, battery
756
756
  * stations open on demand (command / stream, or an opted-in event pre-warm) and idle-detach — so this
757
- * map grows and shrinks over time. `p2pConnect(stationSn)` / `p2pClose(stationSn)` events track the
758
- * changes.
757
+ * map grows and shrinks over time.
758
+ *
759
+ * A station's own session is keyed by its serial, and `p2pConnect(stationSn)` / `p2pClose(stationSn)`
760
+ * track those. A station serving more than one camera at once also holds a session per extra camera,
761
+ * keyed `<stationSn>#live:<channel>` — these carry media alone and raise no connection events, because
762
+ * a station announces its state to every client that connects and reporting each copy would double
763
+ * every event the station's own session already delivers.
759
764
  */
760
765
  getP2pSessions(): Map<string, P2PSession>;
761
766
  /**
@@ -5,7 +5,7 @@
5
5
  * `interface EufyMega` (the typed on/once/off/emit overloads) stays in `eufy-mega.ts` next to the
6
6
  * class — TS declaration merging requires both in the same module.
7
7
  */
8
- import type { MegaClientConfig } from "../transport/http/mega-client.js";
8
+ import type { MegaClientConfig, SessionExpiredError } from "../transport/http/mega-client.js";
9
9
  import type { FcmStore } from "../transport/push/store.js";
10
10
  import type { FfmpegLevel } from "../transport/ffmpeg.js";
11
11
  import type { DeviceEventMap } from "../model/capabilities/index.js";
@@ -375,8 +375,12 @@ export type EufyMegaEventMap = {
375
375
  * token expired. The SDK has already cleared the persisted session, so recovery is a fresh `login()`
376
376
  * (which usually needs 2FA). Distinct from `error`: a session error is emitted ONLY here, not also
377
377
  * on `error`.
378
+ *
379
+ * The error carries the rate: `err.retryAfterMs` is how long the next session replacement is barred
380
+ * for, and `err.contended` says this session is being displaced by another client rather than expiring
381
+ * — which a re-login does not answer. A login made before that wait elapses extends it.
378
382
  */
379
- sessionExpired: [err: Error];
383
+ sessionExpired: [err: SessionExpiredError];
380
384
  error: [err: Error];
381
385
  };
382
386
  /** Event names {@link EufyMega} can emit. */
@@ -130,28 +130,23 @@ export declare class StationUnreachableError extends Error {
130
130
  });
131
131
  }
132
132
  /**
133
- * A live stream was refused: the station is already serving another of its cameras to a viewer.
133
+ * Work on a device was refused: its channel within its station cannot be established from the device records.
134
134
  *
135
- * A station fans several cameras out over one session and serves ONE of them at a time. Accepting a second
136
- * live pull does not make it serve two: measured on a base carrying three attached cameras, each opened
137
- * stream took the station from the others in turn and all three received their media in bursts. So a second
138
- * viewer is refused rather than admitted and degraded, which is the difference between a caller being told
139
- * the constraint and a caller watching every picture stutter.
140
- *
141
- * Which camera deserves the station is the caller's decision, not the SDK's, so nothing is queued or
142
- * pre-empted here.
143
- *
144
- * A still is not refused: it yields the station instead, and answers with the retained image where one is
145
- * held. Only pulls that deliver continuous media contend for a viewer's place.
135
+ * A device attached to a HomeBase is addressed by a channel within that station: a media start and every
136
+ * per-channel command name it. When its record states no channel, or another device attached to the same station
137
+ * states the same one, any channel chosen would address whichever device actually holds it (streaming another
138
+ * camera's video under this serial), so nothing is sent. The `station-channel-unresolved` trace says which.
146
139
  */
147
- export declare class StationBusyError extends Error {
148
- /** The channel the station is already serving. */
149
- readonly servingChannel: number;
150
- /** Always true: the station is busy now, and stops being busy when the other stream is released. */
151
- readonly retryable = true;
140
+ export declare class DeviceChannelUnresolvedError extends Error {
141
+ /** The device that could not be addressed. */
142
+ readonly sn: string;
143
+ /** The station it is attached to. */
144
+ readonly stationSn: string;
152
145
  constructor(
153
- /** The channel the station is already serving. */
154
- servingChannel: number, options?: {
146
+ /** The device that could not be addressed. */
147
+ sn: string,
148
+ /** The station it is attached to. */
149
+ stationSn: string, options?: {
155
150
  cause?: unknown;
156
151
  });
157
152
  }
@@ -739,8 +734,9 @@ export interface MediaProvider {
739
734
  /**
740
735
  * Present and `true` only when these bytes are the RETAINED still rather than a fresh capture.
741
736
  *
742
- * A live still is refused while a sibling camera on the same station is being watched, because a
743
- * station serves one camera at a time and the live view is the picture someone is looking at. Answering
737
+ * A live still is refused while a sibling camera on the same station is being watched, because one
738
+ * session serves one camera at a time, a still does not open a connection of its own, and the live view
739
+ * is the picture someone is looking at. Answering
744
740
  * the retained still there answers the call instead of failing it, and this says the bytes are not
745
741
  * current. Absent means freshly captured.
746
742
  */
@@ -749,12 +745,12 @@ export interface MediaProvider {
749
745
  /**
750
746
  * Open a managed live stream.
751
747
  *
752
- * Several cameras behind one station may stream at the same time only where the station serves them at
753
- * the same time. Where it serves one camera at a time, a second viewer is refused with
754
- * {@link StationBusyError} rather than admitted and degraded: accepting it does not make the station
755
- * serve two, it makes both stutter. Which camera deserves the station is the caller's decision, so
756
- * nothing is queued or pre-empted. Each handle receives only the frames the station tagged for ITS
757
- * camera.
748
+ * Several cameras behind one station stream at the same time, each over its own connection to it. One
749
+ * connection serves one camera — a station answers the most recent start on a session, so two cameras
750
+ * sharing one take it from each other in turn — so a camera asked for while its station is already
751
+ * serving another gets a connection of its own. Measured on a base carrying two attached cameras, one
752
+ * at 3840x2160: both held full frame rate at once. Each handle receives only the frames the station
753
+ * tagged for ITS camera.
758
754
  *
759
755
  * @example
760
756
  * ```ts
@@ -35,6 +35,14 @@ export declare const EUFY_MEGA_LOCAL_KEY_HEX = "2500a7d5617812f9d52515b2c8f20a3d
35
35
  * the mega localKey. Identified by HMAC-matching a captured eufylife key-exchange signature.
36
36
  */
37
37
  export declare const EUFYLIFE_LOCAL_KEY_HEX = "118c12c81e211149304bd70a0c071d01";
38
+ /**
39
+ * The **Anker Solix** passport localKey — the AES-128 bootstrap key for the `anker_power` app-line
40
+ * (power stations / smart meter). Solix runs the SAME `algo_ecdh` passport as the eufy_mega stack,
41
+ * re-skinned under a different `app-name` + API host, so the login key-exchange wraps the ephemeral
42
+ * client public key with this key; distinct from {@link EUFY_MEGA_LOCAL_KEY_HEX}. Authenticated Solix
43
+ * reads carry only the token + `gtoken` (no per-request encryption).
44
+ */
45
+ export declare const SOLIX_LOCAL_KEY_HEX = "e8ad18f61bbd3fbd52d5ed12d14d3b9c";
38
46
  /**
39
47
  * Hardcoded server P-256 public key (uncompressed 0x04||X||Y) used to encrypt
40
48
  * the LOGIN password via a one-shot ECDH (separate from the per-session key).
@@ -44,6 +52,8 @@ export declare const SERVER_STATIC_PUBLIC_KEY_HEX = "04c5c00c4f8d1197cc7c3167c52
44
52
  export declare function genId(): string;
45
53
  /** Unix seconds as a string (X-Request-Ts). */
46
54
  export declare function nowSec(): string;
55
+ /** md5 hex digest — the one place this derivation lives (gtoken, openudid seeds, …). */
56
+ export declare function md5Hex(input: string): string;
47
57
  /** gtoken header = md5(user_id) hex. */
48
58
  export declare function gtoken(userId: string): string;
49
59
  /** 16-byte AES key = first half of the shared secret hex (shareKey[:16 bytes]). */
@@ -5,5 +5,6 @@ export * from "./raw-dp-writer.js";
5
5
  export * from "./raw-dp-hex.js";
6
6
  export * from "./logger.js";
7
7
  export * from "./store.js";
8
+ export * from "./solix-types.js";
8
9
  export * from "./util.js";
9
10
  export * from "./lz4-block.js";
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Anker Solix vendor-JSON record shapes — the cross-layer contract between the transport client (which
3
+ * RETURNS them off the wire) and the model layer (which resolves them into `SolixDevice`). They live in
4
+ * `core` for the same reason the command/media boundary does: the hard `transport ⊥ model` rule forbids
5
+ * either layer importing the other, so a type both need is neither's to own.
6
+ *
7
+ * @module core/solix-types
8
+ */
9
+ /** A discovered Solix device record, as returned by `SolixClient.getDevices()`. */
10
+ export interface SolixDeviceRecord {
11
+ device_sn: string;
12
+ product_code: string;
13
+ device_name?: string;
14
+ alias_name?: string;
15
+ device_sw_version?: string;
16
+ wifi_online?: boolean;
17
+ wifi_name?: string;
18
+ rssi?: string | number;
19
+ [k: string]: unknown;
20
+ }
21
+ /**
22
+ * One battery discharge-cutoff (minimum-SOC) preset, as returned by
23
+ * `SolixClient.getPowerCutoff()`. `output_cutoff_data` is the minimum state-of-charge percent the
24
+ * option enforces; `id` is what `setPowerCutoff()` takes; `is_selected` (1) marks the current one.
25
+ */
26
+ export interface SolixPowerCutoffOption {
27
+ id: number;
28
+ output_cutoff_data: number;
29
+ is_selected?: number;
30
+ [k: string]: unknown;
31
+ }
32
+ /**
33
+ * The Solarbank's battery SOC-limit settings — the `param_data` block read from / written to
34
+ * `site/get_site_device_param` under `param_type "27"` (verified live on an AE103). All values are
35
+ * whole-percent integers. `chargeUpperLimit` caps charging (the app's "charge limit" / max SOC) and
36
+ * `dischargeLowerLimit` floors discharging (the "discharge limit" / minimum SOC — the same quantity the
37
+ * realtime `b5` telemetry blob reports). `backupReserve` (+ its switch) is the reserved-for-outage SOC;
38
+ * `socCalibrationEnable` is the periodic full-cycle calibration toggle. The wire keys are the
39
+ * snake_case form (`charge_upper_limit`, `discharge_lower_limit`, `backup_reserve`,
40
+ * `backup_reserve_switch`, `soc_calibration_enable`).
41
+ */
42
+ export interface SolixSocParams {
43
+ chargeUpperLimit: number;
44
+ dischargeLowerLimit: number;
45
+ backupReserve: number;
46
+ backupReserveSwitch: number;
47
+ socCalibrationEnable: number;
48
+ }
49
+ /** One product in the pairable-product catalog. Extra vendor fields (images, guides) are preserved. */
50
+ export interface SolixProduct {
51
+ /** SKU / model code, e.g. `A1782`. */
52
+ product_code: string;
53
+ /** Marketing name, e.g. `SOLIX F3000`. */
54
+ name: string;
55
+ /** Variant/sub-model codes under this product, when present. */
56
+ p_codes?: unknown[];
57
+ [k: string]: unknown;
58
+ }
59
+ /** A catalog category (e.g. "Portable Power Station") and its products. */
60
+ export interface SolixProductCategory {
61
+ name: string;
62
+ products: SolixProduct[];
63
+ [k: string]: unknown;
64
+ }
65
+ /** One device's membership entry within a site, as carried by `get_site_list`'s `site_device_list`. */
66
+ export interface SolixSiteDeviceEntry {
67
+ device_sn: string;
68
+ /** The device's product/model code (the site list names this field `device_model`). */
69
+ device_model: string;
70
+ device_name?: string;
71
+ /** Anker device-type discriminator (e.g. 3 = Solarbank/battery, 6 = smart meter). */
72
+ device_type?: number;
73
+ [k: string]: unknown;
74
+ }
75
+ /**
76
+ * A site ("system") record, as returned by `SolixClient.getSites()`. A site is the account's home
77
+ * energy system — the "My Home" the app shows — grouping the member devices ({@link site_device_list})
78
+ * that a {@link SolixSiteReader} resolves into a `SolixSite`. Extra vendor fields are preserved.
79
+ */
80
+ export interface SolixSiteRecord {
81
+ site_id: string;
82
+ site_name?: string;
83
+ /** Anker's site-type discriminator (e.g. 20 for a Solarbank-anchored home system). */
84
+ power_site_type?: number;
85
+ site_device_list?: SolixSiteDeviceEntry[];
86
+ [k: string]: unknown;
87
+ }
88
+ /**
89
+ * One Solarbank/battery entry inside a site "scene" snapshot ({@link SolixSiteScene}). The scene mirrors
90
+ * the app's dashboard read: values arrive as STRINGS. Only the fields this SDK actually consumes are
91
+ * typed (chiefly `bat_temperature`, the realtime `ff09` push doesn't reliably carry); the index signature
92
+ * preserves the rest (`bat_charge_power`, `charging_status`, `load_port_*`, `function_switch`, …) verbatim.
93
+ */
94
+ export interface SolixSceneSolarbank {
95
+ device_sn?: string;
96
+ device_pn?: string;
97
+ /** Battery pack temperature in °C (string on the wire). The scene is the reliable source for this. */
98
+ bat_temperature?: string | number;
99
+ /** State of charge, % (string on the wire). Cross-checks the `ff09` `0xa3` SOC. */
100
+ bat_soc?: string | number;
101
+ /**
102
+ * Number of ATTACHED expansion battery packs — the built-in battery is the host, not a pack. Read as
103
+ * `expansionPacks`. Observed live as `0` on a standalone AE103 main unit; a populated value (and the
104
+ * matching per-pack `bms_list` detail) has not yet been captured with a pack attached.
105
+ */
106
+ sub_package_num?: string | number;
107
+ [k: string]: unknown;
108
+ }
109
+ /**
110
+ * A site "scene" snapshot from {@link SolixClient.getSiteScene} — the app's dashboard read. The
111
+ * battery detail lives under `solarbank_info.solarbank_list` (NOT a top-level list). Only the shape this
112
+ * SDK reads is typed; everything else (grid_info, home_load_power, function flags, …) is preserved by the
113
+ * index signature. This is a low-rate backstop, never the realtime telemetry source.
114
+ */
115
+ export interface SolixSiteScene {
116
+ solarbank_info?: {
117
+ solarbank_list?: SolixSceneSolarbank[];
118
+ [k: string]: unknown;
119
+ };
120
+ [k: string]: unknown;
121
+ }
@@ -5,6 +5,14 @@ import type { RegionShard } from "../transport/http/mega-client.js";
5
5
  */
6
6
  export interface PersistedSession {
7
7
  userId: string;
8
+ /**
9
+ * The eufy account's own `user_id` — the id the `gtoken` header is hashed from, which is not always the
10
+ * `ap_cloud_user_id` that `userId` prefers.
11
+ *
12
+ * Always written, equal to `userId` when the login reply carried only the one id. Absent therefore means a
13
+ * record from before this was tracked, which {@link isSessionValid} refuses rather than restore.
14
+ */
15
+ accountUserId?: string;
8
16
  authToken: string;
9
17
  geoKey?: string;
10
18
  region: RegionShard;
@@ -19,25 +27,45 @@ export interface PersistedSession {
19
27
  tokenExpiresAt: number;
20
28
  savedAt: number;
21
29
  }
22
- export interface SessionStore {
23
- load(): PersistedSession | null;
24
- save(s: PersistedSession): void;
30
+ /**
31
+ * A place to persist a session record across runs. Parameterised on the record shape so other Anker
32
+ * lines (e.g. Solix, whose record is not a `PersistedSession`) can reuse the same file/memory
33
+ * stores rather than re-implementing them. Defaults to `PersistedSession` for the eufy path.
34
+ */
35
+ export interface SessionStore<T = PersistedSession> {
36
+ load(): T | null;
37
+ save(s: T): void;
25
38
  clear(): void;
26
39
  }
27
40
  /** In-memory store (no persistence) — the default. */
28
- export declare class MemorySessionStore implements SessionStore {
41
+ export declare class MemorySessionStore<T = PersistedSession> implements SessionStore<T> {
29
42
  private s;
30
- load(): PersistedSession | null;
31
- save(s: PersistedSession): void;
43
+ load(): T | null;
44
+ save(s: T): void;
32
45
  clear(): void;
33
46
  }
34
47
  /** JSON-file store, e.g. new FileSessionStore("./.eufy-session.json"). */
35
- export declare class FileSessionStore implements SessionStore {
48
+ export declare class FileSessionStore<T = PersistedSession> implements SessionStore<T> {
36
49
  private readonly path;
37
50
  constructor(path: string);
38
- load(): PersistedSession | null;
39
- save(s: PersistedSession): void;
51
+ load(): T | null;
52
+ save(s: T): void;
40
53
  clear(): void;
41
54
  }
42
- /** A persisted session is usable if it has a token that isn't (near-)expired. */
55
+ /**
56
+ * A token is still usable if it has no known expiry, or expires more than `skewSec` from now. The one
57
+ * place the expiry/skew rule lives — reused by {@link isSessionValid} and by other lines' session checks
58
+ * (e.g. Solix) whose session shape differs but whose freshness rule is identical.
59
+ */
60
+ export declare function tokenNotExpired(tokenExpiresAt: number | undefined, skewSec?: number): boolean;
61
+ /**
62
+ * A persisted session is usable if it carries a complete credential — token, bound ECDH key, and the account
63
+ * id the `gtoken` header is hashed from — whose token isn't (near-)expired.
64
+ *
65
+ * `accountUserId` is part of the credential, not an optional extra: a record without it was written before
66
+ * that id was tracked, so it can only be restored as the other id, which is what the gateway rejects the
67
+ * header on. Nothing in a restored session can recover it either — the id arrives with a login reply. So such
68
+ * a record is refused and one login re-establishes it, rather than reinstating a session whose every
69
+ * authenticated call fails identically.
70
+ */
43
71
  export declare function isSessionValid(s: PersistedSession | null, skewSec?: number): boolean;