@mega-yfue/eufy-sdk 0.2.0-beta.0 → 0.2.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.
- package/README.md +17 -41
- package/dist/client/eufy-mega.d.ts +33 -6
- package/dist/core/contracts.d.ts +58 -3
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/logger.d.ts +5 -3
- package/dist/core/solix-types.d.ts +36 -0
- package/dist/core/store.d.ts +20 -9
- package/dist/index.js +1229 -102
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +58 -28
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/index.d.ts +11 -5
- package/dist/model/capabilities/solix.d.ts +75 -0
- package/dist/model/capabilities/types.d.ts +16 -4
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- package/dist/model/index.d.ts +3 -0
- package/dist/model/param-dictionary.d.ts +24 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/solix-catalog.d.ts +20 -0
- package/dist/model/solix-device.d.ts +102 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/mega-client.d.ts +10 -2
- package/dist/transport/http/solix-client.d.ts +158 -0
- package/dist/transport/http/solix-constants.d.ts +29 -0
- package/dist/transport/mqtt/app-client-id.d.ts +9 -3
- package/dist/transport/mqtt/command-router.d.ts +0 -3
- package/dist/transport/mqtt/index.d.ts +2 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +24 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +214 -0
- package/dist/transport/mqtt/topics.d.ts +20 -0
- package/dist/transport/p2p/command-router.d.ts +23 -0
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-trace.d.ts +100 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -23,11 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> [!IMPORTANT]
|
|
27
|
-
> **Not usable yet.** This repository is being set up: the scaffold, the CI gate and the docs pipeline
|
|
28
|
-
> are in place, the library source lands next. `0.0.1` exists on npm only to prove the release
|
|
29
|
-
> pipeline works — it is an empty package. Wait for `0.1.0`.
|
|
30
|
-
|
|
31
26
|
## What it is
|
|
32
27
|
|
|
33
28
|
A TypeScript SDK for the Anker eufy cloud that the current eufy app speaks. It logs in (captcha and 2FA
|
|
@@ -37,14 +32,18 @@ you drive through a **typed, fluent API**:
|
|
|
37
32
|
```ts
|
|
38
33
|
const dev = await eufy.getDevice(sn);
|
|
39
34
|
|
|
40
|
-
const stored = await dev.camera()?.snapshotStored?.(); // latest retained push JPEG
|
|
41
|
-
const fresh = await dev.camera()?.snapshotLive(); // explicit fresh live capture
|
|
42
|
-
await dev.
|
|
43
|
-
await dev.light()?.setBrightness(60);
|
|
35
|
+
const stored = await dev.camera?.()?.snapshotStored?.(); // latest retained push JPEG
|
|
36
|
+
const fresh = await dev.camera?.()?.snapshotLive?.(); // explicit fresh live capture
|
|
37
|
+
await dev.ptz?.()?.rotate(PtzDirection.left);
|
|
38
|
+
await dev.light?.()?.setBrightness(60);
|
|
44
39
|
|
|
45
|
-
eufy.on("motion", (e) => console.log(e.
|
|
40
|
+
eufy.on("motion", (e) => console.log(e.deviceSn, "saw something"));
|
|
46
41
|
```
|
|
47
42
|
|
|
43
|
+
Accessors and methods are optional because both are **evidence-gated**: a device exposes exactly the
|
|
44
|
+
features it reported, so the optionality states that one may be absent. An **unverified write path
|
|
45
|
+
throws** rather than send a frame it cannot stand behind.
|
|
46
|
+
|
|
48
47
|
Realtime arrives over **P2P** (cameras and HomeBases), **secure MQTT** (appliances) and **push**
|
|
49
48
|
(events), all surfaced as typed semantic events. Live **video streaming** works, with one shared pull
|
|
50
49
|
fanned out to every consumer.
|
|
@@ -56,12 +55,10 @@ code path and an unlisted or future device resolves the same way as a known one.
|
|
|
56
55
|
## Install
|
|
57
56
|
|
|
58
57
|
```bash
|
|
59
|
-
npm install @mega-yfue/eufy-sdk
|
|
58
|
+
npm install @mega-yfue/eufy-sdk # latest stable
|
|
59
|
+
npm install @mega-yfue/eufy-sdk@beta # the prerelease of the version in review
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Releases go to **npmjs**, published from CI with provenance. Prereleases ship on the `beta` channel
|
|
63
|
-
(`npm install @mega-yfue/eufy-sdk@beta`) while a version is still under review.
|
|
64
|
-
|
|
65
62
|
**Node.js ≥ 24.5.0** is required, not just recommended (see [`.nvmrc`](./.nvmrc)). `ffmpeg` is
|
|
66
63
|
optional — only the live JPEG snapshot and one-shot mp4 record paths use it,
|
|
67
64
|
and a host that ships its own build names it with `new EufyMega({ ffmpegPath })` rather than needing
|
|
@@ -69,34 +66,13 @@ one on `PATH`.
|
|
|
69
66
|
|
|
70
67
|
## Documentation
|
|
71
68
|
|
|
72
|
-
The guides at **<https://mega-yfue.github.io/>**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
in [`examples/`](./examples/).
|
|
76
|
-
|
|
77
|
-
## Design
|
|
78
|
-
|
|
79
|
-
Four layers, one dependency direction — `core` → `transport` → `model` → `client`:
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
src/
|
|
83
|
-
core/ shared floor: crypto, cross-layer contracts, value types, session store
|
|
84
|
-
transport/ every byte-on-a-wire module: http, mqtt, p2p, push, tuya
|
|
85
|
-
model/ Device + one self-contained module per capability
|
|
86
|
-
client/ the facade: login, device registry, event fan-out
|
|
87
|
-
index.ts public surface — one `export *` per layer barrel
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Capability ↔ transport decorrelation is a hard, CI-enforced rule:** `model/` never imports
|
|
91
|
-
`transport/` and vice versa. A capability describes what a value MEANS; a transport moves bytes and
|
|
92
|
-
never names a feature. Anything genuinely shared is a contract in `core/`. That rule and the rest of
|
|
93
|
-
the code practice are in [AGENTS.md](./AGENTS.md).
|
|
94
|
-
|
|
95
|
-
Three runtime dependencies — `mqtt`, `protobufjs`, `jpeg-js` — and that is deliberate. HTTP is native
|
|
96
|
-
`fetch`, hashing and ciphers are `node:crypto`, 64-bit integers are `BigInt`.
|
|
69
|
+
The guides at **<https://mega-yfue.github.io/>** cover installing and logging in, devices and
|
|
70
|
+
capabilities, events and realtime transports, consuming live media, and the generated API reference.
|
|
71
|
+
Runnable, typechecked samples live in [`examples/`](./examples/).
|
|
97
72
|
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
The architecture — four layers with one dependency direction, and the CI-enforced rule that keeps
|
|
74
|
+
capabilities and transports from importing each other — is in [AGENTS.md](./AGENTS.md), with the rest of
|
|
75
|
+
the code practice.
|
|
100
76
|
|
|
101
77
|
## Develop
|
|
102
78
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Cloud APIs: the eufy v6 cloud (+ legacy, planned)
|
|
5
5
|
* Realtime: secure MQTT (appliances) + P2P (cameras/HomeBases)
|
|
6
6
|
*
|
|
7
|
-
* const eufy = new EufyMega({ email, password, region: "eu" });
|
|
7
|
+
* const eufy = new EufyMega({ email, password, region: "eu-pr" });
|
|
8
8
|
* await eufy.login(); // → LoginResult; on success the SDK auto-starts realtime (push/MQTT/wired P2P)
|
|
9
9
|
* eufy.on("motion", (e) => console.log(e.deviceSn)); // typed semantic events — flowing already
|
|
10
10
|
* const dev = await eufy.getDevice((await eufy.getDevices())[0].sn);
|
|
11
|
-
* await dev.camera()?.snapshotStored();
|
|
11
|
+
* await dev.camera?.()?.snapshotStored?.();
|
|
12
12
|
*
|
|
13
13
|
* Connectivity is SDK-managed: the host calls no `connect*`. P2P to a battery camera is opened only
|
|
14
14
|
* when a command/stream/doorbell-ring needs it and closed when idle, so the camera can sleep.
|
|
@@ -346,8 +346,8 @@ export declare class EufyMega extends EventEmitter {
|
|
|
346
346
|
* @example
|
|
347
347
|
* ```ts
|
|
348
348
|
* const res = await eufy.login();
|
|
349
|
-
* if (res.status === "captcha") await eufy.solveCaptcha(await
|
|
350
|
-
* else if (res.status === "2fa") await eufy.submitVerifyCode(await
|
|
349
|
+
* if (res.status === "captcha") await eufy.solveCaptcha(await promptUser(res.image));
|
|
350
|
+
* else if (res.status === "2fa") await eufy.submitVerifyCode(await promptUser());
|
|
351
351
|
* ```
|
|
352
352
|
*/
|
|
353
353
|
login(opts?: {
|
|
@@ -493,7 +493,7 @@ export declare class EufyMega extends EventEmitter {
|
|
|
493
493
|
* @example
|
|
494
494
|
* ```ts
|
|
495
495
|
* const dev = await eufy.getDevice(sn);
|
|
496
|
-
* if (dev.has("camera")) await dev.camera()?.snapshotStored();
|
|
496
|
+
* if (dev.has("camera")) await dev.camera?.()?.snapshotStored?.();
|
|
497
497
|
* console.log(dev.getProperty("battery"));
|
|
498
498
|
* ```
|
|
499
499
|
*/
|
|
@@ -707,11 +707,38 @@ export declare class EufyMega extends EventEmitter {
|
|
|
707
707
|
* {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
|
|
708
708
|
* lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
|
|
709
709
|
*
|
|
710
|
+
* Identified by a client id built from this client's `openudid`, not by the certificate's name:
|
|
711
|
+
* that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
|
|
712
|
+
* duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
|
|
713
|
+
* the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
|
|
714
|
+
* the broker grants on the `eufy_security` credential.
|
|
715
|
+
*
|
|
716
|
+
* It separates two clients exactly as far as their `openudid` does: a caller that supplies none
|
|
717
|
+
* gets the value derived from the account, which every such client shares — the same condition
|
|
718
|
+
* under which their logins already displace each other (`MegaClientConfig.openudid`).
|
|
719
|
+
*
|
|
720
|
+
* A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
|
|
721
|
+
* none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
|
|
722
|
+
* that fails for any other reason rejects the bring-up, which clears its memo in
|
|
723
|
+
* {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
|
|
724
|
+
* socket must not be what moves a process onto the shared name for good.
|
|
725
|
+
*/
|
|
726
|
+
private startMqtt;
|
|
727
|
+
/**
|
|
728
|
+
* Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
|
|
729
|
+
* omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
|
|
730
|
+
*
|
|
731
|
+
* The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
|
|
732
|
+
* broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
|
|
733
|
+
* the connect it just completed is announced here instead. Errors raised while connecting are held
|
|
734
|
+
* only to keep an emitter without an `error` listener from throwing, and are reported once the
|
|
735
|
+
* transport is one a consumer owns.
|
|
736
|
+
*
|
|
710
737
|
* The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
|
|
711
738
|
* runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
|
|
712
739
|
* transport — so a capability reads tags without owning any framing.
|
|
713
740
|
*/
|
|
714
|
-
private
|
|
741
|
+
private connectMqtt;
|
|
715
742
|
/**
|
|
716
743
|
* Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
|
|
717
744
|
* unreachable device must not stop the rest of the roster from coming up).
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -74,6 +74,61 @@ export declare class CameraDisabledError extends Error {
|
|
|
74
74
|
cause?: unknown;
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Work on a station was refused: the station did not provide the session key that work requires.
|
|
79
|
+
*
|
|
80
|
+
* A station reached over its HomeBase encrypts what it is sent under a key negotiated once per connection, and
|
|
81
|
+
* a media start for an attached camera has no unencrypted form at all — so without that key there is nothing
|
|
82
|
+
* to send, however reachable the station is. Naming this apart from a source that failed is what separates an
|
|
83
|
+
* account whose cipher material could not be resolved from a camera that is off, a station that is busy, or a
|
|
84
|
+
* stream that produced nothing: they share no next step.
|
|
85
|
+
*
|
|
86
|
+
* The `level2-unavailable` trace states WHY the key is not coming. This states only that it is not, because
|
|
87
|
+
* that is what the refusal itself knows.
|
|
88
|
+
*
|
|
89
|
+
* `stationSn` is the station that owed the key, which is the parent for an attached camera and therefore not
|
|
90
|
+
* the serial the refused call was made about: several cameras refused at once are one station's outcome, and
|
|
91
|
+
* nothing else in the refusal says so.
|
|
92
|
+
*/
|
|
93
|
+
export declare class StationKeyUnavailableError extends Error {
|
|
94
|
+
/** The station whose session key did not arrive. */
|
|
95
|
+
readonly stationSn: string;
|
|
96
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
97
|
+
readonly retryable = true;
|
|
98
|
+
constructor(
|
|
99
|
+
/** The station whose session key did not arrive. */
|
|
100
|
+
stationSn: string, options?: {
|
|
101
|
+
cause?: unknown;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Work on a station was refused: its session did not connect within the wait it was given.
|
|
106
|
+
*
|
|
107
|
+
* A station is reached over its own session, and nothing addressed to it — a media start, a property read, a
|
|
108
|
+
* still — can be attempted before that session is up. Naming this apart from every other failure is what tells
|
|
109
|
+
* a station that could not be reached at all from one that answered and then refused, or one that served media
|
|
110
|
+
* a caller could not use: those call for opposite next steps, and a caller cannot infer which it had from a
|
|
111
|
+
* message.
|
|
112
|
+
*
|
|
113
|
+
* `waitedMs` is how long was actually waited, which a caller compares against its own deadline to know whether
|
|
114
|
+
* this SDK concluded or its own bound expired first. `stationSn` is the station that could not be reached —
|
|
115
|
+
* the parent for an attached camera, so it is not derivable from the serial the call was made about.
|
|
116
|
+
*/
|
|
117
|
+
export declare class StationUnreachableError extends Error {
|
|
118
|
+
/** The station whose session did not connect. */
|
|
119
|
+
readonly stationSn: string;
|
|
120
|
+
/** How long the session was waited on before this was raised. */
|
|
121
|
+
readonly waitedMs: number;
|
|
122
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
123
|
+
readonly retryable = true;
|
|
124
|
+
constructor(
|
|
125
|
+
/** The station whose session did not connect. */
|
|
126
|
+
stationSn: string,
|
|
127
|
+
/** How long the session was waited on before this was raised. */
|
|
128
|
+
waitedMs: number, options?: {
|
|
129
|
+
cause?: unknown;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
77
132
|
/**
|
|
78
133
|
* A live stream was refused: the station is already serving another of its cameras to a viewer.
|
|
79
134
|
*
|
|
@@ -703,9 +758,9 @@ export interface MediaProvider {
|
|
|
703
758
|
*
|
|
704
759
|
* @example
|
|
705
760
|
* ```ts
|
|
706
|
-
* const stream = await cam.live();
|
|
707
|
-
* stream
|
|
708
|
-
* stream
|
|
761
|
+
* const stream = await cam.live?.();
|
|
762
|
+
* stream?.on("video", (frame) => sink.write(frame.data)); // Annex-B
|
|
763
|
+
* stream?.stop(); // detach this consumer
|
|
709
764
|
* ```
|
|
710
765
|
*/
|
|
711
766
|
live(opts?: SharedSourceHints & AbortableCall & Record<string, unknown>): Promise<LiveStreamConsumer>;
|
package/dist/core/crypto.d.ts
CHANGED
|
@@ -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]). */
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/logger.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
|
19
19
|
* ```ts
|
|
20
20
|
* // A custom sink (or pass a tslog / winston instance directly — they already match this shape):
|
|
21
21
|
* const eufy = new EufyMega({
|
|
22
|
-
*
|
|
22
|
+
* email,
|
|
23
|
+
* password,
|
|
24
|
+
* logger: { debug: (m, ...a) => myLog.debug(m, ...a), info: () => {}, warn: console.warn, error: console.error },
|
|
23
25
|
* });
|
|
24
26
|
* ```
|
|
25
27
|
*/
|
|
@@ -38,8 +40,8 @@ export declare const noopLogger: Logger;
|
|
|
38
40
|
*
|
|
39
41
|
* @example
|
|
40
42
|
* ```ts
|
|
41
|
-
* const eufy = new EufyMega({ logger: new ConsoleLogger() }); // verbose
|
|
42
|
-
* const quiet = new EufyMega({ logger: new ConsoleLogger("warn") }); // warn + error only
|
|
43
|
+
* const eufy = new EufyMega({ email, password, logger: new ConsoleLogger() }); // verbose
|
|
44
|
+
* const quiet = new EufyMega({ email, password, logger: new ConsoleLogger("warn") }); // warn + error only
|
|
43
45
|
* ```
|
|
44
46
|
*/
|
|
45
47
|
export declare class ConsoleLogger implements Logger {
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
/** One product in the pairable-product catalog. Extra vendor fields (images, guides) are preserved. */
|
|
22
|
+
export interface SolixProduct {
|
|
23
|
+
/** SKU / model code, e.g. `A1782`. */
|
|
24
|
+
product_code: string;
|
|
25
|
+
/** Marketing name, e.g. `SOLIX F3000`. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Variant/sub-model codes under this product, when present. */
|
|
28
|
+
p_codes?: unknown[];
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
/** A catalog category (e.g. "Portable Power Station") and its products. */
|
|
32
|
+
export interface SolixProductCategory {
|
|
33
|
+
name: string;
|
|
34
|
+
products: SolixProduct[];
|
|
35
|
+
[k: string]: unknown;
|
|
36
|
+
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -19,25 +19,36 @@ export interface PersistedSession {
|
|
|
19
19
|
tokenExpiresAt: number;
|
|
20
20
|
savedAt: number;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A place to persist a session record across runs. Parameterised on the record shape so other Anker
|
|
24
|
+
* lines (e.g. Solix, whose record is not a `PersistedSession`) can reuse the same file/memory
|
|
25
|
+
* stores rather than re-implementing them. Defaults to `PersistedSession` for the eufy path.
|
|
26
|
+
*/
|
|
27
|
+
export interface SessionStore<T = PersistedSession> {
|
|
28
|
+
load(): T | null;
|
|
29
|
+
save(s: T): void;
|
|
25
30
|
clear(): void;
|
|
26
31
|
}
|
|
27
32
|
/** In-memory store (no persistence) — the default. */
|
|
28
|
-
export declare class MemorySessionStore implements SessionStore {
|
|
33
|
+
export declare class MemorySessionStore<T = PersistedSession> implements SessionStore<T> {
|
|
29
34
|
private s;
|
|
30
|
-
load():
|
|
31
|
-
save(s:
|
|
35
|
+
load(): T | null;
|
|
36
|
+
save(s: T): void;
|
|
32
37
|
clear(): void;
|
|
33
38
|
}
|
|
34
39
|
/** JSON-file store, e.g. new FileSessionStore("./.eufy-session.json"). */
|
|
35
|
-
export declare class FileSessionStore implements SessionStore {
|
|
40
|
+
export declare class FileSessionStore<T = PersistedSession> implements SessionStore<T> {
|
|
36
41
|
private readonly path;
|
|
37
42
|
constructor(path: string);
|
|
38
|
-
load():
|
|
39
|
-
save(s:
|
|
43
|
+
load(): T | null;
|
|
44
|
+
save(s: T): void;
|
|
40
45
|
clear(): void;
|
|
41
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A token is still usable if it has no known expiry, or expires more than `skewSec` from now. The one
|
|
49
|
+
* place the expiry/skew rule lives — reused by {@link isSessionValid} and by other lines' session checks
|
|
50
|
+
* (e.g. Solix) whose session shape differs but whose freshness rule is identical.
|
|
51
|
+
*/
|
|
52
|
+
export declare function tokenNotExpired(tokenExpiresAt: number | undefined, skewSec?: number): boolean;
|
|
42
53
|
/** A persisted session is usable if it has a token that isn't (near-)expired. */
|
|
43
54
|
export declare function isSessionValid(s: PersistedSession | null, skewSec?: number): boolean;
|