@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.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.
- package/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eufy "mega" (Anker AIoT) cloud crypto — algo_ecdh.
|
|
3
|
+
*
|
|
4
|
+
* The full request scheme, reverse-engineered + verified against live traffic
|
|
5
|
+
* (2026-06). Three layers:
|
|
6
|
+
*
|
|
7
|
+
* A. Key exchange (bootstrap, per region):
|
|
8
|
+
* client_public_key = base64( IV(16) || AES-128-CBC(pubkeyHex, localKey) )
|
|
9
|
+
* signed with HMAC-SHA256(localKey-hex-utf8, `${ts}+${once}+${encPubKey}`),
|
|
10
|
+
* sending a client-generated X-Key-Ident. The server replies with its
|
|
11
|
+
* public key, AES-CBC-encrypted the same way; ECDH(P-256) → shareKey
|
|
12
|
+
* (first 32 hex chars of the shared secret).
|
|
13
|
+
*
|
|
14
|
+
* B. Per-request body: base64( IV(16) || AES-128-CBC-PKCS7(plaintext) ),
|
|
15
|
+
* key = shareKey[:16 bytes]. Response `data` decrypts the same way.
|
|
16
|
+
*
|
|
17
|
+
* C. x-signature = HMAC-SHA256( shareKey-hex-utf8,
|
|
18
|
+
* [ts, once, encBody?].join("+") ) (hex).
|
|
19
|
+
*
|
|
20
|
+
* Layers B and C are verified byte-exact against a captured request
|
|
21
|
+
* (src/__tests__/crypto.spec.ts).
|
|
22
|
+
*/
|
|
23
|
+
import { type ECDH } from "node:crypto";
|
|
24
|
+
export declare const P256 = "prime256v1";
|
|
25
|
+
/**
|
|
26
|
+
* The eufy_mega app localKey — a single AES-128 key (hex) used to bootstrap the
|
|
27
|
+
* ECDH key exchange across the whole mega host stack (openapi + passport + app-*).
|
|
28
|
+
* Extracted from the iOS Mega app bundle.
|
|
29
|
+
*/
|
|
30
|
+
export declare const EUFY_MEGA_LOCAL_KEY_HEX = "2500a7d5617812f9d52515b2c8f20a3d";
|
|
31
|
+
/**
|
|
32
|
+
* The **eufylife** data-host localKey — a SEPARATE AES-128 bootstrap key for the
|
|
33
|
+
* `security-app-{shard}.eufylife.com` gateway (faces, get_ciphers, commerce, geofence).
|
|
34
|
+
* That host runs its own ECDH key exchange at `/v3/openapi/oauth/key/exchange` and rejects
|
|
35
|
+
* the mega localKey. Identified by HMAC-matching a captured eufylife key-exchange signature.
|
|
36
|
+
*/
|
|
37
|
+
export declare const EUFYLIFE_LOCAL_KEY_HEX = "118c12c81e211149304bd70a0c071d01";
|
|
38
|
+
/**
|
|
39
|
+
* Hardcoded server P-256 public key (uncompressed 0x04||X||Y) used to encrypt
|
|
40
|
+
* the LOGIN password via a one-shot ECDH (separate from the per-session key).
|
|
41
|
+
*/
|
|
42
|
+
export declare const SERVER_STATIC_PUBLIC_KEY_HEX = "04c5c00c4f8d1197cc7c3167c52bf7acb054d722f0ef08dcd7e0883236e0d72a3868d9750cb47fa4619248f3d83f0f662671dadc6e2d31c2f41db0161651c7c076";
|
|
43
|
+
/** 32-hex random id (uuid-without-dashes), for X-Key-Ident / X-Request-Once. */
|
|
44
|
+
export declare function genId(): string;
|
|
45
|
+
/** Unix seconds as a string (X-Request-Ts). */
|
|
46
|
+
export declare function nowSec(): string;
|
|
47
|
+
/** gtoken header = md5(user_id) hex. */
|
|
48
|
+
export declare function gtoken(userId: string): string;
|
|
49
|
+
/** 16-byte AES key = first half of the shared secret hex (shareKey[:16 bytes]). */
|
|
50
|
+
export declare function aesKey(shareKeyHex: string): Buffer;
|
|
51
|
+
/** HMAC sign key = the shareKey hex string itself, as UTF-8 bytes. */
|
|
52
|
+
export declare function signKey(shareKeyHex: string): Buffer;
|
|
53
|
+
/** Encrypt a body: base64( IV(16) || AES-128-CBC-PKCS7(plaintext) ). */
|
|
54
|
+
export declare function encryptBody(plaintext: string | Buffer, shareKeyHex: string): string;
|
|
55
|
+
/** Decrypt a base64( IV(16) || AES-128-CBC-PKCS7 ) body. */
|
|
56
|
+
export declare function decryptBody(b64: string, shareKeyHex: string): Buffer;
|
|
57
|
+
/**
|
|
58
|
+
* x-signature = HMAC-SHA256(signKey(shareKey), [ts, once, encBody?].join("+")).
|
|
59
|
+
* `encBody` is the ENCRYPTED body actually sent; omit for empty-body requests.
|
|
60
|
+
*/
|
|
61
|
+
export declare function signRequest(shareKeyHex: string, ts: string, once: string, encBodyB64?: string): string;
|
|
62
|
+
/** A negotiated session: shareKey + the X-Key-Ident the client minted for it. */
|
|
63
|
+
export interface SessionEntry {
|
|
64
|
+
keyIdent: string;
|
|
65
|
+
shareKey: string;
|
|
66
|
+
clientPublicKeyHex: string;
|
|
67
|
+
clientPrivateKeyHex: string;
|
|
68
|
+
createdAt: number;
|
|
69
|
+
}
|
|
70
|
+
export interface KeyExchangePrep {
|
|
71
|
+
ecdh: ECDH;
|
|
72
|
+
localKey: Buffer;
|
|
73
|
+
keyIdent: string;
|
|
74
|
+
/** base64 body to POST as { client_public_key }. */
|
|
75
|
+
encryptedClientPublicKey: string;
|
|
76
|
+
/** algo_ecdh headers for the key-exchange POST. */
|
|
77
|
+
headers: Record<string, string>;
|
|
78
|
+
clientPublicKeyHex: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a key-exchange request: an ephemeral P-256 keypair whose public hex is
|
|
82
|
+
* AES-128-CBC(localKey)-encrypted, signed with the localKey-hex as UTF-8.
|
|
83
|
+
*/
|
|
84
|
+
export declare function prepareKeyExchange(localKeyHex?: string): KeyExchangePrep;
|
|
85
|
+
/**
|
|
86
|
+
* Finish the exchange: decrypt the server's AES-CBC-wrapped public key with the
|
|
87
|
+
* localKey, ECDH-derive, and take the first 32 hex chars as the shareKey.
|
|
88
|
+
*/
|
|
89
|
+
export declare function finishKeyExchange(prep: KeyExchangePrep, serverPublicKeyB64: string): SessionEntry;
|
|
90
|
+
/**
|
|
91
|
+
* Encrypt the login password against the hardcoded server static public key.
|
|
92
|
+
* AES-256-CBC, key = full 32-byte ECDH secret, IV = its first 16 bytes.
|
|
93
|
+
* Returns the client pubkey hex (for client_secret_info.public_key) + b64 ct.
|
|
94
|
+
*/
|
|
95
|
+
export declare function encryptLoginPassword(password: string): {
|
|
96
|
+
clientPublicKeyHex: string;
|
|
97
|
+
encryptedPassword: string;
|
|
98
|
+
};
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
export
|
|
1
|
+
export * from "./crypto.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./contracts.js";
|
|
4
|
+
export * from "./raw-dp-writer.js";
|
|
5
|
+
export * from "./raw-dp-hex.js";
|
|
6
|
+
export * from "./logger.js";
|
|
7
|
+
export * from "./store.js";
|
|
8
|
+
export * from "./util.js";
|
|
9
|
+
export * from "./lz4-block.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable diagnostics logging.
|
|
3
|
+
*
|
|
4
|
+
* The SDK emits internal diagnostics through a {@link Logger} a host supplies — so its logs flow
|
|
5
|
+
* into the host's own pipeline (tslog, winston, pino, …) with real log levels, instead of a hard-wired
|
|
6
|
+
* `console`. No logger supplied → {@link noopLogger} swallows everything (silent by default).
|
|
7
|
+
*
|
|
8
|
+
* The interface is a STRUCTURAL 4-level sink (`debug`/`info`/`warn`/`error`), shaped to match
|
|
9
|
+
* `console`, tslog, and winston natively; pino (object-first) plugs in via a one-line adapter:
|
|
10
|
+
* `{ logger: { debug: (m, ...a) => pino.debug(a[0] ?? {}, m), … } }`.
|
|
11
|
+
*/
|
|
12
|
+
/** Severity levels, low → high. */
|
|
13
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
14
|
+
/**
|
|
15
|
+
* Host-pluggable diagnostics sink. Pass one as `logger` when constructing `EufyMega`; the
|
|
16
|
+
* SDK calls the matching level method with a `[subsystem]`-prefixed message and optional args.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // A custom sink (or pass a tslog / winston instance directly — they already match this shape):
|
|
21
|
+
* const eufy = new EufyMega({
|
|
22
|
+
* logger: { debug: (m, ...a) => log.debug(m, ...a), info: () => {}, warn: console.warn, error: console.error },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export interface Logger {
|
|
27
|
+
debug(message: string, ...args: unknown[]): void;
|
|
28
|
+
info(message: string, ...args: unknown[]): void;
|
|
29
|
+
warn(message: string, ...args: unknown[]): void;
|
|
30
|
+
error(message: string, ...args: unknown[]): void;
|
|
31
|
+
}
|
|
32
|
+
/** Discards everything. The default when no `logger` is supplied — the SDK is silent. */
|
|
33
|
+
export declare const noopLogger: Logger;
|
|
34
|
+
/**
|
|
35
|
+
* Built-in {@link Logger} that writes to `console`, gated by a minimum level. `new ConsoleLogger()`
|
|
36
|
+
* turns on all diagnostics; `new ConsoleLogger("warn")` shows only warnings and errors. Each level maps
|
|
37
|
+
* to the matching `console` method.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const eufy = new EufyMega({ logger: new ConsoleLogger() }); // verbose
|
|
42
|
+
* const quiet = new EufyMega({ logger: new ConsoleLogger("warn") }); // warn + error only
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare class ConsoleLogger implements Logger {
|
|
46
|
+
private readonly minLevel;
|
|
47
|
+
constructor(minLevel?: LogLevel);
|
|
48
|
+
private enabled;
|
|
49
|
+
debug(message: string, ...args: unknown[]): void;
|
|
50
|
+
info(message: string, ...args: unknown[]): void;
|
|
51
|
+
warn(message: string, ...args: unknown[]): void;
|
|
52
|
+
error(message: string, ...args: unknown[]): void;
|
|
53
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decompressor for a raw LZ4 *block* — the form the map stream's pixel planes arrive in.
|
|
3
|
+
*
|
|
4
|
+
* The robot publishes its map over a second MQTT topic, and the pixel planes inside are compressed
|
|
5
|
+
* with LZ4 whenever that saves anything. There is no flag saying so: the message carries both the
|
|
6
|
+
* `pixels` bytes and a `pixel_size`, and **the two disagreeing IS the flag** — a plane that compressed
|
|
7
|
+
* to nothing useful is sent verbatim, with the two equal. So the caller decides, and passes the size
|
|
8
|
+
* it already has.
|
|
9
|
+
*
|
|
10
|
+
* **Why not an npm package.** What travels here is a bare LZ4 block: no magic number, no frame header,
|
|
11
|
+
* no checksum, no stored size. Every LZ4 package on npm speaks the *frame* format by default and
|
|
12
|
+
* exposes the block API awkwardly if at all, and the block format itself is one loop over four
|
|
13
|
+
* quantities. A dependency would be more code to audit than the sixty lines here, so this is
|
|
14
|
+
* hand-written and tested against blocks produced by the reference liblz4 implementation.
|
|
15
|
+
*
|
|
16
|
+
* @module core/lz4-block
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Decompress an LZ4 block into exactly `size` bytes, or `undefined` if the block is malformed.
|
|
20
|
+
*
|
|
21
|
+
* **Every failure is `undefined`, and the length check is what makes that trustworthy.** A truncated or
|
|
22
|
+
* corrupt block does not fail loudly on its own — LZ4 has no checksum and no terminator, so a mangled
|
|
23
|
+
* block usually just stops producing output early, and a decompressor that returned what it had would
|
|
24
|
+
* hand back a plausible, short, wrong map. Requiring the output to reach `size` exactly turns that into
|
|
25
|
+
* a rejection, and it costs nothing: the caller already knows the size, which is how it knew to call.
|
|
26
|
+
*
|
|
27
|
+
* The other rejections are the reads that would otherwise wander: a length that runs off the end of the
|
|
28
|
+
* block, a back-reference pointing before the start of the output or nowhere at all, and a match that
|
|
29
|
+
* would overrun `size`.
|
|
30
|
+
*
|
|
31
|
+
* Matches may overlap their own output — an offset of 1 with a length of 200 is how a run of one
|
|
32
|
+
* repeated byte is encoded, and it is the common case in a mostly-empty map. That is why the copy below
|
|
33
|
+
* is a byte-at-a-time loop and not a block copy: the bytes it reads are ones it just wrote.
|
|
34
|
+
*/
|
|
35
|
+
export declare function lz4BlockDecompress(block: Uint8Array, size: number): Buffer | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Raw-DP frame, arriving hex-encoded instead of base64.
|
|
3
|
+
*
|
|
4
|
+
* A robot publishes its map to a second MQTT topic — `biz/eufy_home/<model>/<sn>/res` — and the
|
|
5
|
+
* payloads there are the SAME `varint(len) ++ protobufMessage` frame every Raw DP carries, wrapped in
|
|
6
|
+
* a JSON envelope and written as hex rather than base64. So the whole reader already exists: only the
|
|
7
|
+
* outer encoding differs, and this turns one into the other.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately not a second codec. `RawDpCodec` owns the framing, the length-prefix validity check and
|
|
10
|
+
* the wire-type walk; re-implementing any of that for a different string encoding would be two readers
|
|
11
|
+
* to keep in agreement, which is how they stop agreeing.
|
|
12
|
+
*
|
|
13
|
+
* @module core/raw-dp-hex
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Hex the biz stream carries, re-encoded as the base64 a {@link RawDpCodec} reads — or `undefined`
|
|
17
|
+
* when the input is not clean hex.
|
|
18
|
+
*
|
|
19
|
+
* **The validation is the point, and it is not paranoia.** `Buffer.from(s, "hex")` never throws: it
|
|
20
|
+
* decodes until it meets a character that is not a hex digit and silently returns what it got, and it
|
|
21
|
+
* drops a trailing half-byte from an odd-length string. For base64 that silent truncation is harmless,
|
|
22
|
+
* because the codec's length prefix will not match the shortened body and the payload is rejected —
|
|
23
|
+
* the reader's own doc says so. Hex breaks that guarantee: truncation removes bytes from the END,
|
|
24
|
+
* so `"02" + "aabb" + <garbage>` decodes to a prefix of 2 followed by exactly two bytes, passes the
|
|
25
|
+
* length check, and hands back a message with the garbage quietly discarded. A shorter,
|
|
26
|
+
* self-consistent, WRONG payload is the one failure the codec cannot catch for us, so it is caught
|
|
27
|
+
* here instead.
|
|
28
|
+
*
|
|
29
|
+
* An empty string is `undefined` rather than an empty frame: the envelope omits the field entirely
|
|
30
|
+
* when there is nothing to send, so "" means "no payload", not "a payload of no bytes".
|
|
31
|
+
*/
|
|
32
|
+
export declare function hexToRawDp(hex: string): string | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writer for the structured, base64-encoded values some device data points carry in place of a plain
|
|
3
|
+
* scalar — the mirror of the `RawDpCodec` reader in `./contracts.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Hand-rolling a write's bytes is where silent wire bugs come from, and the nesting gets deep fast: a
|
|
6
|
+
* dock command wraps a `oneof` two levels deep, a clean-parameter write nests three, and a room-select
|
|
7
|
+
* carries a repeated sub-message. On a fire-and-forget write, a bad frame is indistinguishable from
|
|
8
|
+
* success.
|
|
9
|
+
*
|
|
10
|
+
* **What this is not.** It is not a protobuf library and does not want to be. There is no schema, no
|
|
11
|
+
* field-name lookup, no wire-type inference from a declared type: the caller states the field number and
|
|
12
|
+
* the shape, exactly as the reader states the field number it wants back. That keeps the model layer
|
|
13
|
+
* free of `protobufjs` (an architecture rule, not a preference — the model layer may not take transport
|
|
14
|
+
* dependencies) and keeps the whole encoder small enough to read in one sitting.
|
|
15
|
+
*
|
|
16
|
+
* **Framing.** {@link RawDpWriter.finish} emits `varint(bodyLength) ++ body`, base64-encoded — the same
|
|
17
|
+
* envelope `RawDpCodec.decode` expects, which is what makes a round-trip through the two a real test.
|
|
18
|
+
*
|
|
19
|
+
* @module core/raw-dp-writer
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Encode an unsigned integer as a base-128 varint, low group first with the continuation bit set on
|
|
23
|
+
* every group but the last.
|
|
24
|
+
*
|
|
25
|
+
* Takes a `number` and rejects anything that is not a non-negative safe integer, rather than silently
|
|
26
|
+
* truncating: JavaScript's bitwise operators are 32-bit, so a naive shift-based loop wraps above 2³¹
|
|
27
|
+
* and would emit a plausible-looking frame for the wrong value. Division keeps the full safe-integer
|
|
28
|
+
* range available and the guard makes the boundary explicit instead of surprising.
|
|
29
|
+
*/
|
|
30
|
+
export declare function encodeVarint(value: number): number[];
|
|
31
|
+
/**
|
|
32
|
+
* ZigZag-encode a signed integer, mapping small negatives onto small unsigned values so a `sint32`
|
|
33
|
+
* costs one byte rather than ten.
|
|
34
|
+
*
|
|
35
|
+
* The clean line needs this for map coordinates, which are signed centimetres and routinely negative —
|
|
36
|
+
* a robot sent to a point encoded as a plain varint goes somewhere real and wrong. Kept separate from
|
|
37
|
+
* {@link encodeVarint} because the choice belongs to the field's declared type, which only the caller
|
|
38
|
+
* knows: `int32` and `sint32` are the same wire type and encode the same number differently.
|
|
39
|
+
*/
|
|
40
|
+
export declare function zigzag(value: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Builds one protobuf message body, field by field.
|
|
43
|
+
*
|
|
44
|
+
* A field's number and shape are given at the call site, which is the same contract the reader offers in
|
|
45
|
+
* reverse. Nothing here validates that a field number suits the message being built — this layer does
|
|
46
|
+
* not know what message that is, and the capability that does states it in its own field-number table.
|
|
47
|
+
*
|
|
48
|
+
* **Zero-valued fields are the caller's decision, not this writer's.** proto3 omits them by default and
|
|
49
|
+
* a device reads an absent field as its zero, so emitting `0` explicitly and omitting it usually mean
|
|
50
|
+
* the same thing — but not always, and only the capability knows which. {@link int} therefore writes
|
|
51
|
+
* exactly what it is given, and omitting the call is what leaves the field absent.
|
|
52
|
+
*/
|
|
53
|
+
export declare class RawDpWriter {
|
|
54
|
+
private readonly body;
|
|
55
|
+
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
56
|
+
int(field: number, value: number): this;
|
|
57
|
+
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
58
|
+
sint(field: number, value: number): this;
|
|
59
|
+
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
60
|
+
bool(field: number, value: boolean): this;
|
|
61
|
+
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
62
|
+
bytes(field: number, value: Uint8Array | readonly number[]): this;
|
|
63
|
+
/**
|
|
64
|
+
* A nested sub-message, built by `build` into a writer of its own.
|
|
65
|
+
*
|
|
66
|
+
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
67
|
+
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
68
|
+
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
69
|
+
* distinction. Omitting the call is what says nothing about it.
|
|
70
|
+
*/
|
|
71
|
+
sub(field: number, build: (w: RawDpWriter) => void): this;
|
|
72
|
+
/** The message body on its own, unframed — for nesting into another writer. */
|
|
73
|
+
toBytes(): readonly number[];
|
|
74
|
+
/**
|
|
75
|
+
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
76
|
+
*
|
|
77
|
+
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
78
|
+
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
79
|
+
*/
|
|
80
|
+
finish(): string;
|
|
81
|
+
}
|
|
82
|
+
/** Build a DP value in one expression: `rawDp((w) => w.int(1, 6).int(2, seq))`. */
|
|
83
|
+
export declare function rawDp(build: (w: RawDpWriter) => void): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RegionShard } from "../transport/http/mega-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* The persisted session record. Internal shape — a host supplies a `SessionStore`, never builds this.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export interface PersistedSession {
|
|
7
|
+
userId: string;
|
|
8
|
+
authToken: string;
|
|
9
|
+
geoKey?: string;
|
|
10
|
+
region: RegionShard;
|
|
11
|
+
openudid: string;
|
|
12
|
+
/** This install's reported device model + media user-agent, generated once and reused. */
|
|
13
|
+
phoneModel?: string;
|
|
14
|
+
mediaUserAgent?: string;
|
|
15
|
+
/** ECDH session: shareKey hex (32 chars) + the bound key-ident. */
|
|
16
|
+
shareKey: string;
|
|
17
|
+
keyIdent: string;
|
|
18
|
+
/** Unix seconds when the auth token expires (0 = unknown). */
|
|
19
|
+
tokenExpiresAt: number;
|
|
20
|
+
savedAt: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SessionStore {
|
|
23
|
+
load(): PersistedSession | null;
|
|
24
|
+
save(s: PersistedSession): void;
|
|
25
|
+
clear(): void;
|
|
26
|
+
}
|
|
27
|
+
/** In-memory store (no persistence) — the default. */
|
|
28
|
+
export declare class MemorySessionStore implements SessionStore {
|
|
29
|
+
private s;
|
|
30
|
+
load(): PersistedSession | null;
|
|
31
|
+
save(s: PersistedSession): void;
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
/** JSON-file store, e.g. new FileSessionStore("./.eufy-session.json"). */
|
|
35
|
+
export declare class FileSessionStore implements SessionStore {
|
|
36
|
+
private readonly path;
|
|
37
|
+
constructor(path: string);
|
|
38
|
+
load(): PersistedSession | null;
|
|
39
|
+
save(s: PersistedSession): void;
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
42
|
+
/** A persisted session is usable if it has a token that isn't (near-)expired. */
|
|
43
|
+
export declare function isSessionValid(s: PersistedSession | null, skewSec?: number): boolean;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified device + transport model.
|
|
3
|
+
*
|
|
4
|
+
* One package, every eufy device class. Two things vary per device and are
|
|
5
|
+
* abstracted here so the rest of the code never branches on device kind:
|
|
6
|
+
*
|
|
7
|
+
* API backend — how we talk to the cloud:
|
|
8
|
+
* - 'mega' : the v6 Anker AIoT API (algo_ecdh signed bodies). Used for EVERY device —
|
|
9
|
+
* appliances/lights/vacuums AND cameras/HomeBases/sensors (device list, dsk,
|
|
10
|
+
* ciphers, faces, events). This is the only backend the SDK uses.
|
|
11
|
+
* - 'legacy' : the classic pre-v6 eufy security API. NOT used by this
|
|
12
|
+
* SDK (v6 migrated everything to mega); kept for reference/old apps.
|
|
13
|
+
*
|
|
14
|
+
* Realtime transport — how live state/commands flow:
|
|
15
|
+
* - 'smqtt' : secure Anker MQTT broker (mTLS, aiot-mqtt-*.anker.com:8883).
|
|
16
|
+
* - 'p2p' : peer-to-peer ThroughTek PPCS (LAN-direct/relay) — cameras. (eufy's own leo_rtc
|
|
17
|
+
* WebRTC is a separate stack used only by newer NVR-class devices.)
|
|
18
|
+
*
|
|
19
|
+
* A device declares which it uses; the facade routes accordingly.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Names the wire backends a device is reachable on — transport vocabulary, not a host concern.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export type ApiBackend = "mega" | "legacy" | "ankermake";
|
|
26
|
+
export type RealtimeKind = "smqtt" | "p2p" | "ankermake-mqtt";
|
|
27
|
+
/**
|
|
28
|
+
* The device `category` strings that mark a 3D printer (ankermake plane). Defined once here — the model
|
|
29
|
+
* layer's codec classifier reads THIS rather than restating the literal, so widening it can't leave the
|
|
30
|
+
* two deciders disagreeing (the dependency rule only allows sharing in this direction).
|
|
31
|
+
*/
|
|
32
|
+
export declare const PRINTER_CATEGORY_RE: RegExp;
|
|
33
|
+
/**
|
|
34
|
+
* The coarse device grouping. Every value is one the SDK can actually DERIVE from a
|
|
35
|
+
* resolved codec — anything the codec space doesn't confidently name lands in `"other"` rather than a
|
|
36
|
+
* guess. Intentionally coarse: the precise kind is named by the capabilities.
|
|
37
|
+
*/
|
|
38
|
+
export type DeviceClass = "camera" | "homebase" | "vacuum" | "mower" | "sensor" | "light" | "printer" | "other";
|
|
39
|
+
/** Normalised device record, regardless of which API produced it. */
|
|
40
|
+
export interface EufyDevice {
|
|
41
|
+
sn: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
model: string;
|
|
44
|
+
/** Anker category string: eufy_security | eufy_mega | eufy_home … */
|
|
45
|
+
category: string;
|
|
46
|
+
deviceClass: DeviceClass;
|
|
47
|
+
/** Which cloud API owns this device. */
|
|
48
|
+
api: ApiBackend;
|
|
49
|
+
/** Which realtime transport carries its live state/commands. */
|
|
50
|
+
realtime: RealtimeKind;
|
|
51
|
+
/**
|
|
52
|
+
* The station this device's traffic belongs to: its parent HomeBase, or its own serial when it has none.
|
|
53
|
+
*
|
|
54
|
+
* `parent_sn` carries the parent on an attached device. `station_sn` is frequently absent there — empty on
|
|
55
|
+
* every attached sensor of a T8010 — and serves only as a fallback for a device naming no parent.
|
|
56
|
+
*/
|
|
57
|
+
stationSn?: string;
|
|
58
|
+
/** Present (and non-empty) for P2P devices. */
|
|
59
|
+
p2pDid?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Device state data points (param_type → param_value), exactly as the cloud record carries them.
|
|
62
|
+
* Raw and unnamed: the capability surface resolves these into named properties. `Device.inspect`
|
|
63
|
+
* labels them for troubleshooting.
|
|
64
|
+
*
|
|
65
|
+
* **Not a realtime source.** These are refreshed server-side on the device's own cloud heartbeat,
|
|
66
|
+
* which is slow: measured across a live fleet, the freshest param on an active device was ~12 minutes
|
|
67
|
+
* old, and the per-device `get_device_param_list` call returns the same staleness as the bulk list
|
|
68
|
+
* (so fetching harder does not make them fresher). They are a coarse state snapshot plus liveness
|
|
69
|
+
* evidence ({@link paramUpdatedAt}); realtime state arrives over push / P2P / MQTT.
|
|
70
|
+
*/
|
|
71
|
+
params?: Record<number, string>;
|
|
72
|
+
/**
|
|
73
|
+
* When each param in {@link params} was last written server-side (`param_type` → **unix SECONDS**,
|
|
74
|
+
* the wire's own `update_time` unit — not milliseconds). The device heartbeats its params up to the
|
|
75
|
+
* cloud, so these are the freshest liveness evidence available without opening a transport.
|
|
76
|
+
*
|
|
77
|
+
* Sparse by construction: a param the record delivered without an `update_time` has no entry here.
|
|
78
|
+
*/
|
|
79
|
+
paramUpdatedAt?: Record<number, number>;
|
|
80
|
+
/**
|
|
81
|
+
* The most recent {@link paramUpdatedAt} across all of this device's params, in **milliseconds**
|
|
82
|
+
* (comparable to `Date.now()` directly). `undefined` when the record carried no timestamp at all.
|
|
83
|
+
*
|
|
84
|
+
* This is an observation — "the device last reported at T" — deliberately **not** a reachability
|
|
85
|
+
* verdict. How long a silence means "unreachable" depends on the device (a mains camera heartbeats
|
|
86
|
+
* constantly; a battery sensor may be quiet for days by design), so the threshold is the caller's to
|
|
87
|
+
* choose; the SDK reports the fact.
|
|
88
|
+
*/
|
|
89
|
+
lastSeenMs?: number;
|
|
90
|
+
/** Raw record from the source API, for fields not yet normalised. */
|
|
91
|
+
raw?: unknown;
|
|
92
|
+
}
|
|
93
|
+
/** A realtime message normalised across transports. */
|
|
94
|
+
export interface RealtimeMessage {
|
|
95
|
+
deviceSn?: string;
|
|
96
|
+
topic?: string;
|
|
97
|
+
raw: unknown;
|
|
98
|
+
}
|
|
99
|
+
/** A vendor-authored availability state, distinct from inferred reachability or transport lifecycle. */
|
|
100
|
+
export type AvailabilityState = "available" | "unavailable";
|
|
101
|
+
/**
|
|
102
|
+
* Provenance of an authoritative availability observation. The current contract contains only the
|
|
103
|
+
* secure-MQTT signal whose polarity and device attribution are established by the current vendor app.
|
|
104
|
+
*/
|
|
105
|
+
export type AvailabilitySource = {
|
|
106
|
+
readonly transport: "smqtt";
|
|
107
|
+
readonly signal: "state-info";
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* An explicit availability observation whose entity, polarity and scope are verified on the current
|
|
111
|
+
* vendor wire. The SDK emits this only for authoritative signals; silence, stale cloud facts, failed
|
|
112
|
+
* operations and idle transports do not create or clear one.
|
|
113
|
+
*/
|
|
114
|
+
export interface AvailabilityObservation {
|
|
115
|
+
readonly entity: {
|
|
116
|
+
readonly kind: "device";
|
|
117
|
+
readonly sn: string;
|
|
118
|
+
};
|
|
119
|
+
readonly availability: AvailabilityState;
|
|
120
|
+
readonly source: AvailabilitySource;
|
|
121
|
+
readonly scope: "device";
|
|
122
|
+
/** Vendor-supplied observation time in milliseconds, when the envelope carries one. */
|
|
123
|
+
readonly observedAt?: number;
|
|
124
|
+
/** Vendor-supplied message ordering value, when the envelope carries one. */
|
|
125
|
+
readonly sequence?: number;
|
|
126
|
+
/** When the SDK received this explicit signal, in milliseconds. */
|
|
127
|
+
readonly receivedAt: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Common surface for every realtime transport (smqtt / p2p).
|
|
131
|
+
* Implementations live under src/mqtt and src/p2p; the facade owns selection.
|
|
132
|
+
*/
|
|
133
|
+
export interface RealtimeTransport {
|
|
134
|
+
readonly kind: RealtimeKind;
|
|
135
|
+
connect(): Promise<void>;
|
|
136
|
+
disconnect(): Promise<void>;
|
|
137
|
+
subscribeDevice(device: EufyDevice): Promise<void>;
|
|
138
|
+
/** Publish a command body to a device topic (the `/req` direction). */
|
|
139
|
+
publish(topic: string, body: string | Buffer, opts?: {
|
|
140
|
+
qos?: 0 | 1 | 2;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
on(event: "connect", listener: () => void): this;
|
|
143
|
+
on(event: "disconnect", listener: (reason?: unknown) => void): this;
|
|
144
|
+
on(event: "message", listener: (msg: RealtimeMessage) => void): this;
|
|
145
|
+
on(event: "error", listener: (err: Error) => void): this;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Map a device's category to its API backend + realtime transport.
|
|
149
|
+
*
|
|
150
|
+
* Deliberately does NOT classify the device itself. Which *kind* of thing a record is depends on the
|
|
151
|
+
* eufy `DeviceType` space, which grows with every product launch — that knowledge lives in `model/`
|
|
152
|
+
* (`codecForType`, where camera is the residual bucket so a new SKU needs no edit), and duplicating a
|
|
153
|
+
* list of ids here would mean editing the leaf layer on every release. The client derives
|
|
154
|
+
* {@link EufyDevice.deviceClass} from the resolved codec instead.
|
|
155
|
+
*
|
|
156
|
+
* The security ecosystem (cameras / HomeBases / sensors) is `p2p`: v6 serves its cloud side through
|
|
157
|
+
* the SAME mega (`algo_ecdh`) API as everything else — device list, dsk, ciphers, faces, events — but
|
|
158
|
+
* its realtime is P2P plus push, never MQTT. A `p2p_did` alone is sufficient evidence, since only that
|
|
159
|
+
* ecosystem is issued one. Everything else is a mega/home appliance on Anker secure MQTT.
|
|
160
|
+
*
|
|
161
|
+
* Internal classification helper; `Device` carries the resolved result.
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
export declare function classifyDevice(raw: {
|
|
165
|
+
category?: string;
|
|
166
|
+
device_model?: string;
|
|
167
|
+
p2p_did?: string;
|
|
168
|
+
device_type?: number;
|
|
169
|
+
} & Record<string, unknown>): Pick<EufyDevice, "api" | "realtime" | "category">;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared utilities used across the SDK.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A restartable single-shot timer — the arm / cancel / re-arm pattern the P2P lifecycle repeats in
|
|
6
|
+
* several places (session idle-detach, stream linger, battery budget + its grace, the warm-up
|
|
7
|
+
* deadline). {@link arm} (re)schedules, replacing any pending fire; {@link cancel} clears it; the
|
|
8
|
+
* handle clears itself right before firing so {@link pending} reads `false` inside the callback. The
|
|
9
|
+
* timer is `unref`'d so a pending fire never keeps the process alive.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Timer {
|
|
12
|
+
private handle?;
|
|
13
|
+
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
14
|
+
arm(ms: number, fn: () => void): void;
|
|
15
|
+
/** Cancel a pending fire. No-op if not armed. */
|
|
16
|
+
cancel(): void;
|
|
17
|
+
/** Whether a fire is currently scheduled. */
|
|
18
|
+
get pending(): boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Exhaustiveness guard for discriminated unions. Placed in a `switch` `default` (or the else of an
|
|
22
|
+
* `if`-chain) over every variant: if a new variant is added and left unhandled, the call fails to
|
|
23
|
+
* typecheck (its type is no longer `never`), and at runtime it throws instead of silently falling
|
|
24
|
+
* through.
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertNever(value: never): never;
|
|
27
|
+
/**
|
|
28
|
+
* Coerce a property value to a boolean WITHOUT the `Boolean("false") === true` footgun (every
|
|
29
|
+
* non-empty string — including `"false"` and `"0"` — is truthy under `Boolean()`). Only an explicit
|
|
30
|
+
* `true` / `1` / `"1"` / `"true"` (case-insensitive) enables; everything else is `false`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function asBool(value: unknown): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Round to an integer and clamp into `[min, max]`. For scalar device params that take a bounded int
|
|
35
|
+
* (volume/brightness/color-temp 0..100, etc.), dispatched fire-and-forget — a non-numeric or
|
|
36
|
+
* out-of-range input must never reach the wire as-is. NaN input (e.g. `Number("x")`) fails safe to
|
|
37
|
+
* `min`: `Math.max`/`Math.min` propagate NaN rather than ignoring it, so that has to be checked
|
|
38
|
+
* explicitly, not relied on implicitly.
|
|
39
|
+
*/
|
|
40
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Order-insensitive deep structural equality. Used for change detection over decoded param values
|
|
43
|
+
* (`boolean | number | string | object | array`): a re-serialized param whose object keys come back
|
|
44
|
+
* in a different order must NOT read as "changed" (a `JSON.stringify` compare is key-order sensitive
|
|
45
|
+
* and flaps). Primitives compare with `Object.is` (so `NaN === NaN`, `+0 !== -0`); arrays compare
|
|
46
|
+
* length then elementwise; plain objects compare the same key set recursively; anything else
|
|
47
|
+
* (functions, class instances, Buffers, …) falls back to `Object.is`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function structuralEqual(a: unknown, b: unknown): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Coerce `value` to one of the numeric values of a fixed enum-object (e.g. `Watermark`,
|
|
52
|
+
* `HubAlarmTone`, `DoorbellRingtone`), or `undefined` if it isn't one. Unlike `clamp` (for a
|
|
53
|
+
* continuous range), an enum has a small fixed set of real options — silently rounding/clamping a bad
|
|
54
|
+
* index to the nearest valid one would send a WRONG-but-plausible value on a fire-and-forget write,
|
|
55
|
+
* not a safe default. Anything outside the set is rejected instead.
|
|
56
|
+
*/
|
|
57
|
+
export declare function coerceEnumValue(enumObj: Record<string, number>, value: unknown): number | undefined;
|
|
58
|
+
/** The valid values of an enum-object, `/`-joined — for the "not a valid option (valid: 0/1/2)" errors. */
|
|
59
|
+
export declare function enumValues(enumObj: Record<string, number>): string;
|
|
60
|
+
/**
|
|
61
|
+
* Invert an enum-object (`name → wire value`) into the `raw → label` form a published option set
|
|
62
|
+
* takes, so a named set is declared ONCE — as the enum callers write against — and the published
|
|
63
|
+
* domain cannot drift from it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function enumLabels(enumObj: Readonly<Record<string, number>>): Record<number, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Parse `text` as a JSON object, or `undefined` on any failure — never throws. Shared by the MQTT
|
|
68
|
+
* and Tuya transport layers; sits here so each module doesn't define its own copy.
|
|
69
|
+
*/
|
|
70
|
+
export declare function jsonObject(text: unknown): Record<string, unknown> | undefined;
|
|
71
|
+
/** 16-bit unsigned, little-endian. */
|
|
72
|
+
export declare function u16le(n: number): Buffer;
|
|
73
|
+
/** 16-bit unsigned, big-endian. */
|
|
74
|
+
export declare function u16be(n: number): Buffer;
|
|
75
|
+
/** 32-bit unsigned, little-endian. */
|
|
76
|
+
export declare function u32le(n: number): Buffer;
|
|
77
|
+
/** 32-bit unsigned, big-endian. */
|
|
78
|
+
export declare function u32be(n: number): Buffer;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* eufy-sdk — one typed client for every Anker eufy device.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a contract in `core/`. `client/` is the facade a caller actually holds.
|
|
7
|
-
*
|
|
8
|
-
* The layers are empty in this scaffold commit; the implementation lands with the source move.
|
|
4
|
+
* Log in, model devices as capability-driven objects with a fluent typed API,
|
|
5
|
+
* and subscribe to typed semantic events. Transport is internal.
|
|
9
6
|
*/
|
|
7
|
+
export * from "./client/index.js";
|
|
10
8
|
export * from "./core/index.js";
|
|
11
9
|
export * from "./transport/index.js";
|
|
12
10
|
export * from "./model/index.js";
|
|
13
|
-
export * from "./client/index.js";
|