@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,14 @@
|
|
|
1
|
+
type ResolveHost = (hostname: string) => Promise<readonly {
|
|
2
|
+
address: string;
|
|
3
|
+
family: number;
|
|
4
|
+
}[]>;
|
|
5
|
+
/** Signals rejection of the active Eufy session without exposing response content. @internal */
|
|
6
|
+
export declare class MediaDownloadAuthenticationError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Download one bounded push-media resource, following at most one allowlisted object-store redirect.
|
|
10
|
+
* Authentication headers are sent only to the original Eufy media host.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function downloadMediaResource(url: string, authenticatedHeaders: RequestInit["headers"], fetchImpl?: typeof fetch, resolver?: ResolveHost): Promise<Buffer>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { type SessionEntry } from "../../core/crypto.js";
|
|
2
|
+
import { type SessionStore } from "../../core/store.js";
|
|
3
|
+
import { type Logger } from "../../core/logger.js";
|
|
4
|
+
import type { SecureMqttCredentials } from "../mqtt/secure-mqtt.js";
|
|
5
|
+
export type RegionShard = "eu-pr" | "us-pr";
|
|
6
|
+
/**
|
|
7
|
+
* Construction options for the internal HTTP client.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export interface MegaClientConfig {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
/** Two-letter account country code (e.g. "GB", "US", "DE"). Routes the region. */
|
|
14
|
+
countryCode?: string;
|
|
15
|
+
/** Force a region shard, skipping estimate_domain. */
|
|
16
|
+
region?: RegionShard;
|
|
17
|
+
appName?: string;
|
|
18
|
+
appVersion?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Phone model reported to the cloud as this install's device. Defaults to a realistic, RANDOM model
|
|
21
|
+
* (see {@link randomPhoneModel}) seeded by `openudid` so it is stable across runs — this keeps many
|
|
22
|
+
* SDK installs from all reporting one identical model. An explicit value pins a fixed identity.
|
|
23
|
+
*/
|
|
24
|
+
phoneModel?: string;
|
|
25
|
+
/** OS version string reported in headers. */
|
|
26
|
+
osVersion?: string;
|
|
27
|
+
/** Stable per-install device id (the auth token binds to it). Derived from email if absent. */
|
|
28
|
+
openudid?: string;
|
|
29
|
+
/**
|
|
30
|
+
* `user-agent` sent on the push-media download path (`downloadMedia`/`downloadImage`). Defaults to a
|
|
31
|
+
* realistic Android string consistent with `phoneModel` and seeded by `openudid` (stable across runs);
|
|
32
|
+
* an explicit value pins a fixed one. Not the account identity — that's `phoneModel`.
|
|
33
|
+
*/
|
|
34
|
+
mediaUserAgent?: string;
|
|
35
|
+
/** Persist + reuse the session (token + session key) across runs. Default: in-memory. */
|
|
36
|
+
store?: SessionStore;
|
|
37
|
+
/** Diagnostics sink. Omit for silence; pass a `Logger` (or `new ConsoleLogger()`) to see logs. */
|
|
38
|
+
logger?: Logger;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A mega API call the server answered with a non-zero envelope code, carrying that code rather than only a
|
|
42
|
+
* message.
|
|
43
|
+
*
|
|
44
|
+
* The retry/auth logic here already decides what to do by NUMBER (4416, 10000, 4404, 26084), so the number is
|
|
45
|
+
* the authoritative fact about which condition was hit. Formatting it into a message and throwing a bare
|
|
46
|
+
* `Error` left every caller that needs to tell one condition from another parsing this module's message
|
|
47
|
+
* format back apart — a decision that belongs to the layer that owns the wire, not to whoever reads it.
|
|
48
|
+
*/
|
|
49
|
+
export declare class MegaApiError extends Error {
|
|
50
|
+
/** The envelope's `code`, or undefined when the failure produced no envelope. */
|
|
51
|
+
readonly code: number | undefined;
|
|
52
|
+
/** The HTTP status the envelope arrived with. */
|
|
53
|
+
readonly status: number | undefined;
|
|
54
|
+
constructor(message: string,
|
|
55
|
+
/** The envelope's `code`, or undefined when the failure produced no envelope. */
|
|
56
|
+
code: number | undefined,
|
|
57
|
+
/** The HTTP status the envelope arrived with. */
|
|
58
|
+
status: number | undefined);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `get_device_param_list` refuses a shared or member account: only the device's owner may read it. The
|
|
62
|
+
* refusal is permanent for the life of that account's session, so a retry cannot change it — the
|
|
63
|
+
* device-list params carry the same `{param_type, param_value, update_time}` and are not owner-gated.
|
|
64
|
+
*/
|
|
65
|
+
export declare const OWNER_ONLY_CODE = 20004;
|
|
66
|
+
/** Thrown when a persisted/expired session is rejected (401). Re-login to recover. */
|
|
67
|
+
export declare class SessionExpiredError extends Error {
|
|
68
|
+
constructor(message: string);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* eufy cloud gateway error codes — the numeric `code` carried in a response envelope alongside the
|
|
72
|
+
* HTTP status.
|
|
73
|
+
*
|
|
74
|
+
* PROVENANCE — backend-only. These numbers are NOT hardcoded anywhere in the app: verified absent from
|
|
75
|
+
* the v6 APK's Java sources, resources, every native `.so` (including the Flutter `libapp.so`) and the
|
|
76
|
+
* Hermes bundles. The app reacts to the *condition* via generic logout handling + user-facing strings
|
|
77
|
+
* (e.g. `account_login_log_out_notice` — "logged in on another device"), not by matching the code. So a
|
|
78
|
+
* code here is known only from an observed live response; add new ones the same way.
|
|
79
|
+
*
|
|
80
|
+
* Internal: these are gateway mechanics, not a surface a host acts on — the transport already turns the
|
|
81
|
+
* conditions that matter into a typed outcome (`SessionExpiredError`, `LoginStatus.Captcha`). Exported
|
|
82
|
+
* only so the transport's own specs can name a code instead of repeating the number.
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export declare const EufyCloudErrorCode: {
|
|
86
|
+
/**
|
|
87
|
+
* Session kicked out — the account logged in on another device (eufy enforces ~one active session
|
|
88
|
+
* per account). Arrives as HTTP 401 with this `code`; the human message wording varies across
|
|
89
|
+
* endpoints ("...kicked out", "token error", ...), which is why the classifier keys off this code
|
|
90
|
+
* and only falls back to message parsing.
|
|
91
|
+
*/
|
|
92
|
+
readonly SESSION_KICKED: 26084;
|
|
93
|
+
/**
|
|
94
|
+
* "get identity error" (usually HTTP 463) — the gateway no longer knows our `x-key-ident`; the ECDH
|
|
95
|
+
* session key was rotated/expired server-side (common after a restored session sits idle for days).
|
|
96
|
+
* The auth token may still be fine, so this triggers a one-shot key re-exchange before the request
|
|
97
|
+
* is treated as a dead session.
|
|
98
|
+
*/
|
|
99
|
+
readonly IDENTITY_KEY_STALE: 4404;
|
|
100
|
+
/** Signature invalid — the wrong per-host `content-type` was tried; expected during the content-type
|
|
101
|
+
* probe and retried on the alternate type (not a real failure). */
|
|
102
|
+
readonly SIGNATURE_INVALID: 4416;
|
|
103
|
+
/** Generic gateway "try again" seen during the same content-type probe; retried, not surfaced. */
|
|
104
|
+
readonly PROBE_RETRY: 10000;
|
|
105
|
+
/** Captcha required before login can proceed — fetch a challenge and solve it. */
|
|
106
|
+
readonly CAPTCHA_REQUIRED: 100032;
|
|
107
|
+
/** Captcha answer was wrong — fetch a fresh challenge and re-solve. */
|
|
108
|
+
readonly CAPTCHA_WRONG: 100033;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Any one of the gateway error codes above.
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export type EufyCloudErrorCode = (typeof EufyCloudErrorCode)[keyof typeof EufyCloudErrorCode];
|
|
115
|
+
/**
|
|
116
|
+
* The raw login reply shape — internal; a host reads the `LoginResult` union.
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
export interface MegaLoginResult {
|
|
120
|
+
userId: string;
|
|
121
|
+
authToken: string;
|
|
122
|
+
geoKey?: string;
|
|
123
|
+
raw: Record<string, unknown>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The status discriminant of a {@link LoginResult}. Compare `result.status` against these constants
|
|
127
|
+
* (e.g. `if (r.status === LoginStatus.Captcha)`).
|
|
128
|
+
*/
|
|
129
|
+
export declare const LoginStatus: {
|
|
130
|
+
/** Authenticated — `result.session` carries the token. */
|
|
131
|
+
readonly Ok: "ok";
|
|
132
|
+
/** Solve `result.image` and call `solveCaptcha(answer)`. */
|
|
133
|
+
readonly Captcha: "captcha";
|
|
134
|
+
/** A code was sent; call `submitVerifyCode(code)`. */
|
|
135
|
+
readonly TwoFactor: "2fa";
|
|
136
|
+
};
|
|
137
|
+
export type LoginStatus = (typeof LoginStatus)[keyof typeof LoginStatus];
|
|
138
|
+
/**
|
|
139
|
+
* Outcome of `login` / continuation steps — a discriminated union so the
|
|
140
|
+
* caller switches on `status` instead of catching thrown errors for the expected captcha/2FA flow.
|
|
141
|
+
* Compare `status` against {@link LoginStatus}:
|
|
142
|
+
* - `Ok` — authenticated; `session` carries the token.
|
|
143
|
+
* - `Captcha` — solve `image` (a `data:image/png;base64` 4-char challenge) and call
|
|
144
|
+
* `solveCaptcha(answer)`. `retry` is true when a prior answer was wrong.
|
|
145
|
+
* - `TwoFactor` — a code was sent (`method` says how); call `submitVerifyCode(code)`.
|
|
146
|
+
* The `captchaId` / pending-2FA token are held internally, so continuation methods take only the
|
|
147
|
+
* user-supplied answer/code.
|
|
148
|
+
*/
|
|
149
|
+
export type LoginResult = {
|
|
150
|
+
status: typeof LoginStatus.Ok;
|
|
151
|
+
session: MegaLoginResult;
|
|
152
|
+
} | {
|
|
153
|
+
status: typeof LoginStatus.Captcha;
|
|
154
|
+
image: string;
|
|
155
|
+
retry: boolean;
|
|
156
|
+
} | {
|
|
157
|
+
status: typeof LoginStatus.TwoFactor;
|
|
158
|
+
method: string;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* {@link MegaHttpClient.postSigned}'s bookkeeping for the two recoveries it performs on itself, so each is
|
|
162
|
+
* attempted once per call rather than once per rejection.
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
export interface SignedRetry {
|
|
166
|
+
/** The session key has already been re-exchanged for this call. */
|
|
167
|
+
identity?: boolean;
|
|
168
|
+
/** The token has already been replaced by a fresh login for this call. */
|
|
169
|
+
reauth?: boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The cloud HTTP client. Internal transport, reachable as an escape hatch via `EufyMega.api`.
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export declare class MegaHttpClient {
|
|
176
|
+
private readonly cfg;
|
|
177
|
+
private region;
|
|
178
|
+
private bootstrapDomain?;
|
|
179
|
+
private sessionKey?;
|
|
180
|
+
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
181
|
+
private readonly sessionKeys;
|
|
182
|
+
private auth_?;
|
|
183
|
+
/** captcha_id of an in-flight challenge, held between login() and solveCaptcha(). */
|
|
184
|
+
private pendingCaptchaId?;
|
|
185
|
+
/** True while a 2FA code is outstanding: `auth_` holds only the limited pre-verify token, so the
|
|
186
|
+
* restored-session short-circuit must NOT treat it as a usable session. Cleared on Ok/reset. */
|
|
187
|
+
private pending2fa;
|
|
188
|
+
private tokenExpiresAt;
|
|
189
|
+
/** Stable per-install device id — the auth token is bound to it. */
|
|
190
|
+
private openudid;
|
|
191
|
+
/** The device model reported to the cloud (explicit `phoneModel`, else a stable random one). */
|
|
192
|
+
private readonly phoneModel;
|
|
193
|
+
/** The `user-agent` for the media-download path (explicit `mediaUserAgent`, else derived from the model). */
|
|
194
|
+
private readonly mediaUserAgent;
|
|
195
|
+
private readonly store;
|
|
196
|
+
private readonly logger;
|
|
197
|
+
/** Remembered working Content-Type per host (the gateway is picky + inconsistent). */
|
|
198
|
+
private readonly contentTypeByHost;
|
|
199
|
+
/** True while a `/passport/login` round trip is in flight — see {@link canReauthenticate}. */
|
|
200
|
+
private loggingIn;
|
|
201
|
+
/** The one in-flight re-login every call rejected on the same dead token waits on. */
|
|
202
|
+
private reauthAttempt?;
|
|
203
|
+
/** Replacements since the held session last proved stable, and when the last one ran — see {@link recoveryDue}. */
|
|
204
|
+
private recoveries;
|
|
205
|
+
private lastRecoveryAt;
|
|
206
|
+
constructor(cfg: MegaClientConfig);
|
|
207
|
+
/**
|
|
208
|
+
* Install the session the store holds, if it holds a usable one: the token + its bound ECDH key, skipping
|
|
209
|
+
* estimate/key-exchange/login/2FA entirely. Answers the token adopted, or `undefined`.
|
|
210
|
+
*
|
|
211
|
+
* Read at construction, and again when a rejection is being recovered from — a store SHARED with another
|
|
212
|
+
* client may already hold the session that client obtained, which is cheaper to adopt than to compete with.
|
|
213
|
+
*
|
|
214
|
+
* Note: the device identity (openudid + phone model + media UA) is NOT restored here — it is resolved once
|
|
215
|
+
* in the constructor, where an explicit config wins over the stored value, and must not be re-derived from a
|
|
216
|
+
* session this may adopt from a shared store during recovery.
|
|
217
|
+
*/
|
|
218
|
+
private hydrateFromStore;
|
|
219
|
+
get auth(): {
|
|
220
|
+
userId: string;
|
|
221
|
+
authToken: string;
|
|
222
|
+
} | undefined;
|
|
223
|
+
/** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
|
|
224
|
+
get regionShard(): RegionShard;
|
|
225
|
+
/**
|
|
226
|
+
* The logged-in account's display name — the login email's local-part (e.g. `someone+tag` for
|
|
227
|
+
* `someone+tag@example.com`). This is the string the app writes into the ff09 command's acting
|
|
228
|
+
* "username" field (verified against a captured T8531 unlock frame). Falls back to the whole email
|
|
229
|
+
* if it has no `@`.
|
|
230
|
+
*/
|
|
231
|
+
get accountName(): string;
|
|
232
|
+
/**
|
|
233
|
+
* Headers the gateway expects on every call. Note the deliberate
|
|
234
|
+
* dash/underscore duplicates (os-type + os_type, app-version + app_version,
|
|
235
|
+
* …) — the app sends both forms and the gateway reads a mix. `openudid` is
|
|
236
|
+
* REQUIRED: the auth token is bound to it, so login + data calls must use the
|
|
237
|
+
* same value or the gateway 401s with "secret or user_id is empty".
|
|
238
|
+
*/
|
|
239
|
+
private baseHeaders;
|
|
240
|
+
/**
|
|
241
|
+
* The account-credential headers every authed call carries — `x-auth-token` + `gtoken` (`md5(userId)`).
|
|
242
|
+
* One place so the signed path, the key-exchange and the bearer path can't drift on what "authed" means.
|
|
243
|
+
*/
|
|
244
|
+
private authTokenHeaders;
|
|
245
|
+
/**
|
|
246
|
+
* POST returning an axios-shaped `{status, data}` — `data` is JSON-parsed when possible, else the
|
|
247
|
+
* raw text. Never throws on HTTP status (the callers classify the envelope themselves). 20s timeout.
|
|
248
|
+
*/
|
|
249
|
+
private httpPost;
|
|
250
|
+
/** Step 1: discover the account's region shard (and the bootstrap host). */
|
|
251
|
+
estimateDomain(): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Step 2: ensure a key-exchange SessionEntry for the current region. ONE
|
|
254
|
+
* exchange per region; the same shareKey + key-ident signs login AND every
|
|
255
|
+
* data call. The gateway binds user_id to this key-ident when login succeeds,
|
|
256
|
+
* so it MUST be reused (don't re-exchange after login).
|
|
257
|
+
*/
|
|
258
|
+
ensureSessionKey(targetHost?: string): Promise<SessionEntry>;
|
|
259
|
+
/**
|
|
260
|
+
* Signed + encrypted POST. Content-type auto-falls-back (text/plain ↔ json).
|
|
261
|
+
*
|
|
262
|
+
* `retry` is this method's own bookkeeping across the two recoveries it performs on itself — a re-exchanged
|
|
263
|
+
* session key, and a re-login — so each is attempted once per call. A caller leaves it out.
|
|
264
|
+
*/
|
|
265
|
+
postSigned<T = unknown>(host: string, path: string, body?: unknown, authed?: boolean, retry?: SignedRetry, headerOverrides?: Record<string, string>): Promise<T>;
|
|
266
|
+
/** Authed call to a mega service host: app-{service}-{region}.eufy.com. */
|
|
267
|
+
post<T = unknown>(service: string, path: string, body?: unknown, headerOverrides?: Record<string, string>): Promise<T>;
|
|
268
|
+
/**
|
|
269
|
+
* Fetch the per-station DSK key used in the P2P cloud-lookup payload. Mirrors
|
|
270
|
+
* the legacy `get_dsk_keys` body shape on the mega devicerelation service.
|
|
271
|
+
* The P2P *local* lookup path does not need this; only the cloud path does.
|
|
272
|
+
*/
|
|
273
|
+
getDskKeys(stationSns: string[], priorDsks?: Record<string, string>): Promise<Record<string, {
|
|
274
|
+
dskKey: string;
|
|
275
|
+
expiration: number;
|
|
276
|
+
}>>;
|
|
277
|
+
/**
|
|
278
|
+
* Register an FCM push token with the eufy cloud so it pushes this account's
|
|
279
|
+
* events (motion/doorbell/thumbnail) to us. v6 exposes this on the mega push
|
|
280
|
+
* service; body mirrors the legacy `register_push_token` shape.
|
|
281
|
+
*/
|
|
282
|
+
/**
|
|
283
|
+
* Fetch the per-SKU **data-point (param) schema** from the mega `things` service. This is
|
|
284
|
+
* the authoritative source for what each `param_type` means on a given product code — the
|
|
285
|
+
* same call the v6 app uses, so its ids are guaranteed accepted by the mega API (unlike the
|
|
286
|
+
* possibly-stale third-party catalogue). Works for ANY SKU code, not just owned devices.
|
|
287
|
+
*
|
|
288
|
+
* Body shape is `{ code: <SKU> }` (e.g. "T8210", "90C0"). Returns the raw response so callers
|
|
289
|
+
* can adapt to the (not-yet-pinned) field layout.
|
|
290
|
+
*/
|
|
291
|
+
getProductDataPoint<T = unknown>(code: string): Promise<T>;
|
|
292
|
+
/**
|
|
293
|
+
* Fetch the **live param list** from the mega `devicemanage` service. Body keys off a
|
|
294
|
+
* `device_sns` ARRAY (a bare `device_sn` 400s with "type mismatch"). Each returned entry is
|
|
295
|
+
* `{ param_type, param_value, update_time }` — VALUES ONLY, no name/meaning (the param→meaning
|
|
296
|
+
* mapping is hardcoded in the app, never returned by the API).
|
|
297
|
+
*
|
|
298
|
+
* NOTE: this endpoint is **owner-gated** — a shared/member account gets {@link OWNER_ONLY_CODE}
|
|
299
|
+
* (`"Only the owner can change settings"`), permanently. For those accounts use the `get_devs_list`
|
|
300
|
+
* params instead, which also carry `{param_type, param_value, update_time}` and are not owner-gated.
|
|
301
|
+
*/
|
|
302
|
+
getDeviceParamList<T = unknown>(deviceSn: string): Promise<T>;
|
|
303
|
+
/**
|
|
304
|
+
* Fetch one page of a device's **cleaning history** from the mega `clean` service.
|
|
305
|
+
*
|
|
306
|
+
* Body is `{ device_sn, num, page }` — `num` is the page SIZE and `page` is 1-based. Returns the raw
|
|
307
|
+
* response so the caller owns the shape; `parseCleanRecords` in `model/` is what reads it.
|
|
308
|
+
*
|
|
309
|
+
* Each record carries a `download_url` to a binary detail blob. That blob is NOT fetched here: the
|
|
310
|
+
* host it points at is unconfirmed, and this client's binary path (`downloadMediaResource`) is
|
|
311
|
+
* host-allowlisted by design.
|
|
312
|
+
*/
|
|
313
|
+
getCleanRecords<T = unknown>(deviceSn: string, num?: number, page?: number): Promise<T>;
|
|
314
|
+
/**
|
|
315
|
+
* Fetch one page of a device's stored **map data** from the mega `clean` service.
|
|
316
|
+
*
|
|
317
|
+
* Paginated by both `page` and a byte `last_offset`, because one map is larger than one response: the
|
|
318
|
+
* answer carries `offset`, `last_offset`, `len`, `total` and `is_next_page`.
|
|
319
|
+
*
|
|
320
|
+
* Returns the raw response, `content` included. **The content is not decoded anywhere in this SDK and
|
|
321
|
+
* deliberately so** — the decoder is the vendor's clean-native library, which is absent from the base
|
|
322
|
+
* APK, and the extracted `.so` set contains no clean-native module. Handing over bytes a caller can
|
|
323
|
+
* take elsewhere is the honest surface; a decode here would be invention.
|
|
324
|
+
*/
|
|
325
|
+
getDeviceMapList<T = unknown>(deviceSn: string, channelId?: number, num?: number, page?: number, lastOffset?: number): Promise<T>;
|
|
326
|
+
/**
|
|
327
|
+
* Fetch the stored map content for several channels of one device in a single call.
|
|
328
|
+
*
|
|
329
|
+
* The batch counterpart of {@link getDeviceMapList}, answering `content` keyed by channel id. Same
|
|
330
|
+
* standing on the bytes: returned as they arrive, never decoded here.
|
|
331
|
+
*/
|
|
332
|
+
getManyDeviceMapContent<T = unknown>(deviceSn: string, channelIds: readonly number[]): Promise<T>;
|
|
333
|
+
/**
|
|
334
|
+
* Generic authed signed POST to `app-{service}-{region}.eufy.com{path}` with an arbitrary body.
|
|
335
|
+
* The typed wrappers above cover the known endpoints; this is the generic escape hatch for one that
|
|
336
|
+
* has none yet — {@link fetchLightCatalog} drives the `things` service through it.
|
|
337
|
+
*/
|
|
338
|
+
request<T = unknown>(service: string, path: string, body?: unknown): Promise<T>;
|
|
339
|
+
/**
|
|
340
|
+
* Fetch the per-user secure-MQTT credentials. Pass `appName` to request a specific capability scope
|
|
341
|
+
* on the current session without re-logging in — security devices (locks/garage) need the
|
|
342
|
+
* `eufy_security` scope, which the default scope can't reach.
|
|
343
|
+
*/
|
|
344
|
+
getUserMqttInfo(appName?: string): Promise<SecureMqttCredentials>;
|
|
345
|
+
registerPushToken(token: string): Promise<void>;
|
|
346
|
+
/** Download raw bytes from a push-media URL using the active account session. */
|
|
347
|
+
downloadMedia(url: string): Promise<Buffer>;
|
|
348
|
+
/** Download push image bytes and decrypt a recognized v1 wrapper when its device key input is available. */
|
|
349
|
+
downloadImage(url: string, p2pDid?: string): Promise<Buffer>;
|
|
350
|
+
/** The security-app data host for this region (face recognition, media, etc.). */
|
|
351
|
+
private securityAppHost;
|
|
352
|
+
/**
|
|
353
|
+
* Signed+encrypted POST to the security-app data host (face recognition, etc.).
|
|
354
|
+
* Despite the different host, these endpoints use the SAME algo_ecdh pipeline as
|
|
355
|
+
* the mega hosts (captured header set: `x-encryption-info: algo_ecdh` +
|
|
356
|
+
* x-request-ts/once + ecdh-encrypted body) — NOT a plain JSON POST (that 403s).
|
|
357
|
+
* So we reuse {@link postSigned}, which handles the key exchange, body encryption,
|
|
358
|
+
* signature and response decryption.
|
|
359
|
+
*
|
|
360
|
+
* The `*.eufylife.com` gateway uses a SEPARATE ecdh key from the mega `*.eufy.com` gateway
|
|
361
|
+
* (own bootstrap localKey `118c12c8…`, own `/v3/openapi/oauth/key/exchange` path, and data
|
|
362
|
+
* calls want `Content-Type: text/plain`). {@link ensureSessionKey} keeps a PER-HOST key
|
|
363
|
+
* for eufylife hosts and exchanges against the eufylife host, so `getFaces()`/`getCiphers()` work.
|
|
364
|
+
* Note `getFaces` returns an empty roster for accounts whose faces live on the HomeBase — the P2P
|
|
365
|
+
* database path carries the real roster.
|
|
366
|
+
*/
|
|
367
|
+
securityAppPost<T = any>(path: string, body?: Record<string, unknown>): Promise<T>;
|
|
368
|
+
/**
|
|
369
|
+
* List the account's enrolled AI faces (the recognition roster). Each entry is keyed by `ai_user_id` —
|
|
370
|
+
* the id an `IDENTITY_PERSON_DETECTION` push carries as `person_id` — so this is the lookup table for
|
|
371
|
+
* naming a recognised person. Endpoint: `/v3/aiassis/get_faces` on the security-app host.
|
|
372
|
+
*
|
|
373
|
+
* Answers an empty roster for an account whose faces live on the HomeBase; that roster is read over
|
|
374
|
+
* the P2P database path instead.
|
|
375
|
+
*/
|
|
376
|
+
getFaces(opts?: {
|
|
377
|
+
aiGroupId?: number;
|
|
378
|
+
num?: number;
|
|
379
|
+
page?: number;
|
|
380
|
+
}): Promise<any>;
|
|
381
|
+
/** Resolve specific AI face ids (e.g. a push `person_id`) → face records. */
|
|
382
|
+
getFacesByIds(aiUserIds: number[]): Promise<any>;
|
|
383
|
+
/**
|
|
384
|
+
* Fetch E2E cipher material for a station's `cipher_id`(s). Returns
|
|
385
|
+
* `[{ cipher_id, ecc_private_key, private_key }]` in cleartext (the algo_ecdh transport
|
|
386
|
+
* is the only wrapping). The `ecc_private_key` is the root of trust for the P2P **level-2**
|
|
387
|
+
* session key: `CMD_GATEWAYINFO(1100)` ships an ECIES envelope decrypted with it
|
|
388
|
+
* (see `deriveLevel2KeyFromGatewayInfo` in `p2p/codec`). `userId` must be the station
|
|
389
|
+
* `member.admin_user_id`. Endpoint: `/v3/app/cipher/get_ciphers` on the eufylife host.
|
|
390
|
+
*/
|
|
391
|
+
getCiphers(cipherIds: number[], userId: string, stationSn: string): Promise<Array<{
|
|
392
|
+
cipher_id: number;
|
|
393
|
+
ecc_private_key?: string;
|
|
394
|
+
private_key?: string;
|
|
395
|
+
}>>;
|
|
396
|
+
/** Build the /passport/login body (verify_code empty unless 2FA). */
|
|
397
|
+
private loginBody;
|
|
398
|
+
/** Fetch a fresh captcha challenge: { captchaId, image (data:image/png;base64) }. */
|
|
399
|
+
generateCaptcha(): Promise<{
|
|
400
|
+
captchaId: string;
|
|
401
|
+
image: string;
|
|
402
|
+
}>;
|
|
403
|
+
/**
|
|
404
|
+
* Trigger the 2FA verify code. POST app-push-{region}/app/sendmsg/verify_code
|
|
405
|
+
* with biz_type 1004 (login 2FA). message_type 2 = email, 1 = SMS.
|
|
406
|
+
* Requires the (limited) token from the first login attempt.
|
|
407
|
+
*/
|
|
408
|
+
sendVerifyCode(messageType?: number): Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* Begin (or resume) login. Returns a {@link LoginResult} discriminated union rather than throwing
|
|
411
|
+
* for the expected captcha/2FA flow — the caller switches on `status`:
|
|
412
|
+
* - `ok` → authenticated.
|
|
413
|
+
* - `captcha` → show `image`, then {@link solveCaptcha}(answer).
|
|
414
|
+
* - `2fa` → a code was sent; {@link submitVerifyCode}(code).
|
|
415
|
+
*
|
|
416
|
+
* A restored session short-circuits to `ok` with no network, so `ok` there states that a session was
|
|
417
|
+
* RESTORED, not that the cloud still honours it — `session.raw.restored` marks that case. Nothing is spent
|
|
418
|
+
* proving it here: the first authenticated call is where the cloud says, and a token it rejects is replaced
|
|
419
|
+
* by a fresh login and the call retried, without the caller seeing anything (see {@link postSigned}). What
|
|
420
|
+
* reaches the caller, as {@link SessionExpiredError}, is a replacement this client cannot complete by
|
|
421
|
+
* itself — one needing a captcha or a 2FA code, one with no credentials to use, one attempted while a login
|
|
422
|
+
* is already part-way through, or a login that failed outright.
|
|
423
|
+
*
|
|
424
|
+
* `messageType` picks the 2FA channel (2 = email, 1 = SMS) for the code that gets sent when 2FA is required.
|
|
425
|
+
*/
|
|
426
|
+
login(opts?: {
|
|
427
|
+
messageType?: number;
|
|
428
|
+
}): Promise<LoginResult>;
|
|
429
|
+
/**
|
|
430
|
+
* Whether the session this call was made against has already been replaced.
|
|
431
|
+
*
|
|
432
|
+
* A rejection can arrive after another call's recovery has finished — the request was in flight with the old
|
|
433
|
+
* token, and the answer to it is late news. Such a call needs no recovery of its own, and must not clear the
|
|
434
|
+
* session: doing so discards the token that was just obtained and logs the client out while it is being
|
|
435
|
+
* fixed.
|
|
436
|
+
*/
|
|
437
|
+
private sessionReplacedSince;
|
|
438
|
+
/**
|
|
439
|
+
* Deal with a rejection that means the token is finished; answers what the caller may do about it.
|
|
440
|
+
*
|
|
441
|
+
* The cheap answers first. A session already replaced by another call's recovery just needs using, and a
|
|
442
|
+
* recovery already in flight is JOINED rather than duplicated — a device-list refresh fires several calls at
|
|
443
|
+
* once, and each starting its own login would spend N of them to learn one thing. A store SHARED with
|
|
444
|
+
* another client may already hold that client's token, which is both cheaper than a login and the difference
|
|
445
|
+
* between adopting a working session and destroying it.
|
|
446
|
+
*
|
|
447
|
+
* Only then is a login spent, and its rate is bounded — see {@link recoveryDue}. Dropping the dead session
|
|
448
|
+
* first is what keeps the login state machine from short-circuiting on it.
|
|
449
|
+
*/
|
|
450
|
+
private recoverRejectedSession;
|
|
451
|
+
/**
|
|
452
|
+
* Whether a token may be replaced now, given how recently the last one was.
|
|
453
|
+
*
|
|
454
|
+
* A client's device identity defaults to one derived from its credentials, so two clients on one account look
|
|
455
|
+
* like the same device — and the cloud keeps one session per device. Each finds its token rejected, replaces
|
|
456
|
+
* it, and evicts the other: an unbounded login war, silent, and repeated logins are exactly what makes an
|
|
457
|
+
* account start demanding captchas. The first replacement is immediate, because a token displaced once is
|
|
458
|
+
* the ordinary case; a second one soon after is evidence of contention rather than expiry, so the wait grows
|
|
459
|
+
* and a caller is told the honest reason instead of being served a fight.
|
|
460
|
+
*/
|
|
461
|
+
private recoveryDue;
|
|
462
|
+
/**
|
|
463
|
+
* Note that the held session is working. A replacement that keeps serving calls for long enough is not
|
|
464
|
+
* contention, so the hold-off is forgotten and the next genuine expiry recovers immediately.
|
|
465
|
+
*/
|
|
466
|
+
private noteSessionWorking;
|
|
467
|
+
/**
|
|
468
|
+
* Whether a rejected token is worth trying to replace without asking the host anything.
|
|
469
|
+
*
|
|
470
|
+
* Structural rather than a list of paths: a login round trip is itself an authenticated call while a limited
|
|
471
|
+
* token is held, so re-logging in from inside one would recurse — and a path list would have to be
|
|
472
|
+
* maintained alongside every request the login flow makes. A 2FA code already outstanding is a login a human
|
|
473
|
+
* is part-way through, and restarting it silently would discard it. No credentials means nothing to try,
|
|
474
|
+
* so the rejection surfaces instead.
|
|
475
|
+
*/
|
|
476
|
+
private canReauthenticate;
|
|
477
|
+
/**
|
|
478
|
+
* Replace a rejected token by running the login state machine again, at most once at a time.
|
|
479
|
+
*
|
|
480
|
+
* Answers whether a usable session was obtained; a login that needs a captcha or a 2FA code answers `false`,
|
|
481
|
+
* because neither can be satisfied from here — the caller then surfaces the rejection so the host can drive
|
|
482
|
+
* the flow it owns. A login that fails outright answers `false` too, with the reason logged.
|
|
483
|
+
*
|
|
484
|
+
* One attempt is SHARED by every call that was in flight against the dead token. A device-list refresh fires
|
|
485
|
+
* several at once, and each starting its own login would spend N of them to learn one thing — worse, on an
|
|
486
|
+
* account that limits concurrent sessions, each login displaces the token the previous one just obtained.
|
|
487
|
+
*/
|
|
488
|
+
private reauthenticate;
|
|
489
|
+
/**
|
|
490
|
+
* Continue a login that returned `{status:"captcha"}` — submit the human's answer to the pending
|
|
491
|
+
* challenge. Returns the next {@link LoginResult} (`ok`, another `captcha` if wrong, or `2fa`).
|
|
492
|
+
*/
|
|
493
|
+
solveCaptcha(answer: string, opts?: {
|
|
494
|
+
messageType?: number;
|
|
495
|
+
}): Promise<LoginResult>;
|
|
496
|
+
/**
|
|
497
|
+
* Continue a login that returned `{status:"2fa"}` — submit the verify code that was sent. Returns
|
|
498
|
+
* the next {@link LoginResult} (normally `ok`).
|
|
499
|
+
*/
|
|
500
|
+
submitVerifyCode(code: string): Promise<LoginResult>;
|
|
501
|
+
/**
|
|
502
|
+
* One `/passport/login` round-trip + outcome classification. Shared by {@link login} /
|
|
503
|
+
* {@link solveCaptcha} / {@link submitVerifyCode}; the caller-facing methods only build the body.
|
|
504
|
+
*/
|
|
505
|
+
private attemptLogin;
|
|
506
|
+
/** The round trip itself. Wrapped by {@link attemptLogin}, which marks it in flight. */
|
|
507
|
+
private loginRoundTrip;
|
|
508
|
+
/** Save the current token + session key for reuse across runs. */
|
|
509
|
+
private persist;
|
|
510
|
+
/** Forget the persisted session (e.g. after the token is rejected). */
|
|
511
|
+
clearSession(): void;
|
|
512
|
+
/** True if a usable (restored or fresh) session is held. */
|
|
513
|
+
get loggedIn(): boolean;
|
|
514
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random phone-model + user-agent generation for the account's device identity.
|
|
3
|
+
*
|
|
4
|
+
* The cloud stores a `phone_model` per login and the media CDN wants a real Android `user-agent`. A
|
|
5
|
+
* single hardcoded value (every install reporting the same model/UA) is an obvious fingerprint once
|
|
6
|
+
* more than a handful of people run the SDK. These helpers pick a realistic model instead — SEEDED by
|
|
7
|
+
* the install's `openudid` so it is STABLE across runs (a value that changed each run would look like a
|
|
8
|
+
* new device every launch and trigger a fresh-device 2FA every time). Pass no seed for a one-off random
|
|
9
|
+
* value. An explicitly configured `phoneModel` / `mediaUserAgent` pins the identity and never reaches
|
|
10
|
+
* here.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A realistic random Android phone model (e.g. `SM-G998B`, `Pixel 7 Pro`, `Redmi Note 12 Pro`). With a
|
|
14
|
+
* `seed` the result is stable for that seed — pass the install's `openudid` so it stays put across runs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function randomPhoneModel(seed?: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* A realistic Dalvik `user-agent` for the media download path, consistent with `model` (defaults to a
|
|
19
|
+
* fresh {@link randomPhoneModel}). Deterministic for a given `seed`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function randomUserAgent(seed?: string, model?: string): string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
1
|
+
export type { FfmpegLevel } from "./ffmpeg.js";
|
|
2
|
+
/** The level list `FfmpegLevel` is taken from — published because the union names it. */
|
|
3
|
+
export { FFMPEG_LEVELS } from "./ffmpeg.js";
|
|
4
|
+
/** Whether the ffmpeg/ffprobe a media path would run is present — the media paths that shell out. */
|
|
5
|
+
export { ffmpegAvailable, ffprobeAvailable } from "./ffmpeg.js";
|
|
6
|
+
export * from "./http/index.js";
|
|
7
|
+
export * from "./mqtt/index.js";
|
|
8
|
+
export * from "./p2p/index.js";
|
|
9
|
+
export * from "./push/index.js";
|
|
10
|
+
export * as tuya from "./tuya/index.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AppClientIdInput {
|
|
2
|
+
/** Topic scope, e.g. "eufy_security". */
|
|
3
|
+
appName: string;
|
|
4
|
+
/** The logged-in user id (40-hex) — NOT necessarily the device owner if it's a shared device. */
|
|
5
|
+
uid: string;
|
|
6
|
+
/** Stable per-install identifier (`AIOTDeviceSdk.getMqttUUID()` on the real app). Any stable 16-hex
|
|
7
|
+
* string works for our purposes — generate once and persist it, don't re-randomize per connect. */
|
|
8
|
+
mqttUuid: string;
|
|
9
|
+
/** Connect-time unix SECONDS. Defaults to now; inject for deterministic tests. */
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Build a client_id shaped like `android-{appName}-{uid}-{mqttUuid}-{timestamp}`. */
|
|
13
|
+
export declare function buildAppShapedClientId(input: AppClientIdInput): string;
|
|
14
|
+
/** A fresh stable-looking install UUID (16 hex chars) — generate ONCE per identity and persist it
|
|
15
|
+
* (a new random value on every connect defeats the point of "stable"). */
|
|
16
|
+
export declare function generateMqttUuid(): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Verified fields decoded from the `state_info` wire, before the client assigns domain semantics. */
|
|
2
|
+
export interface StateInfoSignal {
|
|
3
|
+
readonly deviceSn: string;
|
|
4
|
+
readonly status: boolean;
|
|
5
|
+
readonly observedAt?: number;
|
|
6
|
+
readonly sequence?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decode the verified `eufy_life` device-availability wire. The current app handles only
|
|
10
|
+
* `synq/eufy_life/{model}/{deviceSn}/state_info`, parses the envelope's string `payload`, and applies
|
|
11
|
+
* its boolean `status` to the light identified by the topic serial. No station or transport scope is
|
|
12
|
+
* projected from this signal, and non-boolean values are not interpreted.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseStateInfoSignal(topic: string, raw: unknown): StateInfoSignal | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS shape for dialling an AIoT broker instance by bare IP.
|
|
3
|
+
*
|
|
4
|
+
* The broker hostname fronts several independent backend instances that do not share subscribe/publish
|
|
5
|
+
* routing, so both the pinned transport and the reachability probe in `./broker-discovery.ts` connect
|
|
6
|
+
* to one instance's IP directly rather than letting DNS pick. That is the only reason a bare-IP dial
|
|
7
|
+
* exists here, and it is what makes this fragment necessary: Node matches the presented certificate
|
|
8
|
+
* against the name passed to `connect`, which for an IP dial is the IP, and the broker's certificate
|
|
9
|
+
* names the hostname. `servername` keeps SNI on that hostname so the instance answers with the same
|
|
10
|
+
* certificate a DNS-resolved connect would have got, and the check is then run against it — Node's own
|
|
11
|
+
* matcher, given the name the server answered for. Nothing is relaxed or reimplemented.
|
|
12
|
+
*
|
|
13
|
+
* Both call sites need the identical recipe, so it lives in one place.
|
|
14
|
+
*/
|
|
15
|
+
import tls from "node:tls";
|
|
16
|
+
/**
|
|
17
|
+
* The mTLS identity and server-verification options for one bare-IP broker dial.
|
|
18
|
+
*
|
|
19
|
+
* `rejectUnauthorized` is deliberately absent as a caller-tunable: there is no supported way to turn
|
|
20
|
+
* server verification off, because the transport carries a per-user client certificate and the
|
|
21
|
+
* account's device command traffic.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export interface BareIpTlsOptions {
|
|
25
|
+
servername: string;
|
|
26
|
+
cert: string;
|
|
27
|
+
key: string;
|
|
28
|
+
ca: string;
|
|
29
|
+
rejectUnauthorized: true;
|
|
30
|
+
checkServerIdentity: (hostname: string, cert: tls.PeerCertificate) => Error | undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the TLS options for reaching `hostname`'s broker at a specific instance IP.
|
|
34
|
+
*
|
|
35
|
+
* `ca` REPLACES Node's trust store rather than extending it. It arrives per-user from the cloud's
|
|
36
|
+
* `get_user_mqtt_info` response over an already-verified HTTPS channel, alongside the client
|
|
37
|
+
* certificate and key it is used with, so trusting exactly that one root is both sound and strictly
|
|
38
|
+
* narrower than trusting every public CA.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export declare function bareIpTlsOptions(identity: {
|
|
42
|
+
hostname: string;
|
|
43
|
+
cert: string;
|
|
44
|
+
key: string;
|
|
45
|
+
ca: string;
|
|
46
|
+
}): BareIpTlsOptions;
|