@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.
Files changed (143) hide show
  1. package/README.md +31 -12
  2. package/dist/client/device-registry.d.ts +343 -0
  3. package/dist/client/eufy-mega.d.ts +872 -0
  4. package/dist/client/index.d.ts +1 -6
  5. package/dist/client/map-channels.d.ts +22 -0
  6. package/dist/client/types.d.ts +383 -0
  7. package/dist/core/contracts.d.ts +898 -0
  8. package/dist/core/crypto.d.ts +98 -0
  9. package/dist/core/index.d.ts +9 -7
  10. package/dist/core/logger.d.ts +53 -0
  11. package/dist/core/lz4-block.d.ts +35 -0
  12. package/dist/core/raw-dp-hex.d.ts +32 -0
  13. package/dist/core/raw-dp-writer.d.ts +83 -0
  14. package/dist/core/store.d.ts +43 -0
  15. package/dist/core/types.d.ts +169 -0
  16. package/dist/core/util.d.ts +78 -0
  17. package/dist/index.d.ts +4 -7
  18. package/dist/index.js +25526 -14
  19. package/dist/index.js.map +7 -1
  20. package/dist/model/capabilities/access.d.ts +127 -0
  21. package/dist/model/capabilities/arming.d.ts +201 -0
  22. package/dist/model/capabilities/audio.d.ts +154 -0
  23. package/dist/model/capabilities/battery.d.ts +363 -0
  24. package/dist/model/capabilities/camera.d.ts +564 -0
  25. package/dist/model/capabilities/co.d.ts +40 -0
  26. package/dist/model/capabilities/contact.d.ts +133 -0
  27. package/dist/model/capabilities/doorbell.d.ts +345 -0
  28. package/dist/model/capabilities/dp-catalog.d.ts +38 -0
  29. package/dist/model/capabilities/index.d.ts +561 -0
  30. package/dist/model/capabilities/info.d.ts +28 -0
  31. package/dist/model/capabilities/keypad.d.ts +61 -0
  32. package/dist/model/capabilities/leak.d.ts +43 -0
  33. package/dist/model/capabilities/light.d.ts +174 -0
  34. package/dist/model/capabilities/locate.d.ts +63 -0
  35. package/dist/model/capabilities/lock.d.ts +242 -0
  36. package/dist/model/capabilities/manifest.d.ts +107 -0
  37. package/dist/model/capabilities/members.d.ts +647 -0
  38. package/dist/model/capabilities/motion.d.ts +377 -0
  39. package/dist/model/capabilities/person-detection.d.ts +8 -0
  40. package/dist/model/capabilities/ptz.d.ts +289 -0
  41. package/dist/model/capabilities/rtsp.d.ts +221 -0
  42. package/dist/model/capabilities/siren.d.ts +218 -0
  43. package/dist/model/capabilities/smart-light.d.ts +172 -0
  44. package/dist/model/capabilities/smoke.d.ts +40 -0
  45. package/dist/model/capabilities/snapshot.d.ts +6 -0
  46. package/dist/model/capabilities/storage.d.ts +11 -0
  47. package/dist/model/capabilities/suction.d.ts +104 -0
  48. package/dist/model/capabilities/types.d.ts +484 -0
  49. package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
  50. package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
  51. package/dist/model/capabilities/video.d.ts +6 -0
  52. package/dist/model/classify.d.ts +78 -0
  53. package/dist/model/clean-record-detail.d.ts +65 -0
  54. package/dist/model/clean-records.d.ts +69 -0
  55. package/dist/model/device-family.d.ts +73 -0
  56. package/dist/model/device-types.d.ts +123 -0
  57. package/dist/model/device.d.ts +265 -0
  58. package/dist/model/index.d.ts +29 -4
  59. package/dist/model/infer.d.ts +23 -0
  60. package/dist/model/inspect.d.ts +61 -0
  61. package/dist/model/life-params.d.ts +21 -0
  62. package/dist/model/map-pixels.d.ts +70 -0
  63. package/dist/model/param-dictionary.d.ts +28 -0
  64. package/dist/model/param-namespace.d.ts +21 -0
  65. package/dist/model/proto-read.d.ts +53 -0
  66. package/dist/model/push-events.d.ts +147 -0
  67. package/dist/model/registry.d.ts +54 -0
  68. package/dist/model/types.d.ts +301 -0
  69. package/dist/model/vacuum-map-store.d.ts +92 -0
  70. package/dist/model/vacuum-map.d.ts +286 -0
  71. package/dist/model/vacuum-scenes.d.ts +76 -0
  72. package/dist/model/vacuum-schedules.d.ts +85 -0
  73. package/dist/transport/dp-preset.d.ts +102 -0
  74. package/dist/transport/ff09.d.ts +444 -0
  75. package/dist/transport/ffmpeg.d.ts +86 -0
  76. package/dist/transport/http/decodeImageV1.d.ts +20 -0
  77. package/dist/transport/http/decodeImageV2.d.ts +19 -0
  78. package/dist/transport/http/index.d.ts +5 -0
  79. package/dist/transport/http/light-catalog.d.ts +62 -0
  80. package/dist/transport/http/media-download.d.ts +14 -0
  81. package/dist/transport/http/mega-client.d.ts +514 -0
  82. package/dist/transport/http/phone-model.d.ts +21 -0
  83. package/dist/transport/index.d.ts +10 -7
  84. package/dist/transport/mqtt/app-client-id.d.ts +16 -0
  85. package/dist/transport/mqtt/availability.d.ts +14 -0
  86. package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
  87. package/dist/transport/mqtt/biz-stream.d.ts +98 -0
  88. package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
  89. package/dist/transport/mqtt/clean-codec.d.ts +14 -0
  90. package/dist/transport/mqtt/command-router.d.ts +285 -0
  91. package/dist/transport/mqtt/dp-codec.d.ts +58 -0
  92. package/dist/transport/mqtt/dp-color.d.ts +14 -0
  93. package/dist/transport/mqtt/engine.d.ts +16 -0
  94. package/dist/transport/mqtt/index.d.ts +5 -0
  95. package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
  96. package/dist/transport/mqtt/topics.d.ts +80 -0
  97. package/dist/transport/p2p/adts.d.ts +91 -0
  98. package/dist/transport/p2p/annexb.d.ts +124 -0
  99. package/dist/transport/p2p/codec.d.ts +166 -0
  100. package/dist/transport/p2p/command-router.d.ts +655 -0
  101. package/dist/transport/p2p/commands.d.ts +550 -0
  102. package/dist/transport/p2p/envelope.d.ts +46 -0
  103. package/dist/transport/p2p/fmp4.d.ts +89 -0
  104. package/dist/transport/p2p/fragment-recording.d.ts +33 -0
  105. package/dist/transport/p2p/index.d.ts +13 -0
  106. package/dist/transport/p2p/lan-ip.d.ts +25 -0
  107. package/dist/transport/p2p/live-stream.d.ts +223 -0
  108. package/dist/transport/p2p/live-trace.d.ts +123 -0
  109. package/dist/transport/p2p/media.d.ts +105 -0
  110. package/dist/transport/p2p/p2p-session.d.ts +620 -0
  111. package/dist/transport/p2p/readable-egress.d.ts +27 -0
  112. package/dist/transport/p2p/session-manager.d.ts +154 -0
  113. package/dist/transport/p2p/shared-live-source.d.ts +431 -0
  114. package/dist/transport/p2p/talkback.d.ts +187 -0
  115. package/dist/transport/p2p/video.d.ts +150 -0
  116. package/dist/transport/p2p/write-commands.d.ts +21 -0
  117. package/dist/transport/protobuf.d.ts +5 -0
  118. package/dist/transport/push/fcm.d.ts +23 -0
  119. package/dist/transport/push/index.d.ts +6 -0
  120. package/dist/transport/push/message-tags.d.ts +26 -0
  121. package/dist/transport/push/parser.d.ts +27 -0
  122. package/dist/transport/push/proto.d.ts +11 -0
  123. package/dist/transport/push/push-client.d.ts +64 -0
  124. package/dist/transport/push/store.d.ts +23 -0
  125. package/dist/transport/push/types.d.ts +180 -0
  126. package/dist/transport/raw-dp.d.ts +6 -0
  127. package/dist/transport/stored-image-cache.d.ts +23 -0
  128. package/dist/transport/tuya/account.d.ts +44 -0
  129. package/dist/transport/tuya/client.d.ts +89 -0
  130. package/dist/transport/tuya/command-router.d.ts +79 -0
  131. package/dist/transport/tuya/dp-codec.d.ts +56 -0
  132. package/dist/transport/tuya/index.d.ts +28 -0
  133. package/dist/transport/tuya/request.d.ts +157 -0
  134. package/dist/transport/tuya/sign.d.ts +64 -0
  135. package/package.json +12 -13
  136. package/dist/client/index.js +0 -2
  137. package/dist/client/index.js.map +0 -1
  138. package/dist/core/index.js +0 -2
  139. package/dist/core/index.js.map +0 -1
  140. package/dist/model/index.js +0 -2
  141. package/dist/model/index.js.map +0 -1
  142. package/dist/transport/index.js +0 -2
  143. package/dist/transport/index.js.map +0 -1
@@ -0,0 +1,180 @@
1
+ /**
2
+ * eufy FCM push payload types.
3
+ *
4
+ * A push arrives as an MCS DataMessageStanza whose `app_data` has a `payload`
5
+ * entry = base64( NUL-terminated JSON ). That JSON is the EufyPushMessage; its
6
+ * nested `payload` is device-type specific. The v6 app enriches these with AI
7
+ * detection fields (person/vehicle/pet/package/faces/crops/short video) — see
8
+ * PushEnrichment.
9
+ */
10
+ /**
11
+ * Raw MCS frame: a tag + the decoded protobuf object.
12
+ *
13
+ * A framed message off the push socket — internal wire shape.
14
+ * @internal
15
+ */
16
+ export interface McsMessage {
17
+ tag: number;
18
+ object: any;
19
+ }
20
+ /**
21
+ * Outer FCM message after app_data extraction.
22
+ *
23
+ * The undecoded push envelope, surfaced on the facade's diagnostic event only.
24
+ * @internal
25
+ */
26
+ export interface RawPushMessage {
27
+ id?: string;
28
+ from?: string;
29
+ to?: string;
30
+ category?: string;
31
+ persistentId?: string;
32
+ ttl?: number;
33
+ sent?: string;
34
+ /** The decoded eufy payload (the `payload` app_data entry, JSON-parsed). */
35
+ payload: EufyPushMessage;
36
+ }
37
+ /** The eufy JSON envelope inside the push. */
38
+ export interface EufyPushMessage {
39
+ type?: string | number;
40
+ title?: string;
41
+ content?: string;
42
+ device_sn?: string;
43
+ station_sn?: string;
44
+ event_time?: string;
45
+ push_time?: string;
46
+ doorbell?: string;
47
+ "google.c.sender.id"?: string;
48
+ /** Device-type specific body (often a JSON string that we further parse). */
49
+ payload?: PushPayload;
50
+ [k: string]: unknown;
51
+ }
52
+ /** v6 AI/media enrichment fields (optional; present on newer cameras/doorbells). */
53
+ export interface PushEnrichment {
54
+ /** Comma list of detection types, e.g. "0,1,2,3" (person/vehicle/pet/package). */
55
+ ai_detect_type?: string;
56
+ /** Detected object class names. */
57
+ objects?: string[] | {
58
+ names?: string[];
59
+ };
60
+ person?: string | number;
61
+ person_id?: number;
62
+ person_count?: number;
63
+ ai_faces?: Array<{
64
+ face_id?: number;
65
+ confidence?: number;
66
+ x?: number;
67
+ y?: number;
68
+ width?: number;
69
+ height?: number;
70
+ }>;
71
+ face_id?: number;
72
+ face_ids?: number[];
73
+ familiar_faces?: unknown[];
74
+ vehicle_types?: string[];
75
+ pet_type?: string | number;
76
+ /** Sound/cry detection (indoor). */
77
+ crying?: number | boolean;
78
+ sound_detection?: number | boolean;
79
+ sound_type?: string | number;
80
+ /** Media enrichment. */
81
+ thumbnail?: string;
82
+ short_video_url?: string;
83
+ video_url?: string;
84
+ cover_path?: string;
85
+ crop_local_path?: string;
86
+ crop_url?: string;
87
+ /** Timing. */
88
+ trigger_time?: number;
89
+ duration?: number;
90
+ event_end_time?: number;
91
+ }
92
+ /**
93
+ * Normalised device push payload. Superset of the common fields across device
94
+ * types plus the v6 enrichment; concrete pushes populate a subset. The short
95
+ * single-letter keys are eufy's wire names (kept for fidelity).
96
+ */
97
+ export interface PushPayload extends PushEnrichment {
98
+ device_sn?: string;
99
+ station_sn?: string;
100
+ s?: string;
101
+ name?: string;
102
+ device_name?: string;
103
+ nick_name?: string;
104
+ n?: string;
105
+ channel?: number;
106
+ c?: number;
107
+ a?: number;
108
+ event_type?: number;
109
+ msg_type?: number;
110
+ type?: number;
111
+ alarm_type?: number;
112
+ mode?: number;
113
+ arming?: number;
114
+ notification_style?: number;
115
+ e?: string;
116
+ /** Unverified vendor field; polarity and entity scope are not established. */
117
+ m?: number;
118
+ pic_url?: string;
119
+ file_path?: string;
120
+ p?: string;
121
+ storage_type?: number;
122
+ cipher?: number;
123
+ k?: number;
124
+ create_time?: number;
125
+ event_time?: number;
126
+ trigger_time?: number;
127
+ session_id?: string;
128
+ unique_id?: string;
129
+ push_count?: number;
130
+ user_id?: string;
131
+ user_name?: string;
132
+ short_user_id?: string;
133
+ [k: string]: unknown;
134
+ }
135
+ /** Facts available to attribute a push-provided thumbnail candidate. */
136
+ export type ThumbnailCandidateAttribution = {
137
+ kind: "device";
138
+ deviceSn: string;
139
+ } | {
140
+ kind: "station";
141
+ stationSn?: string;
142
+ } | {
143
+ kind: "ambiguous";
144
+ };
145
+ /** A push-provided thumbnail URL together with its transport-level attribution facts. */
146
+ export interface ThumbnailCandidate {
147
+ url: string;
148
+ attribution: ThumbnailCandidateAttribution;
149
+ }
150
+ /** A normalised, consumer-facing push event emitted by the client. */
151
+ export interface PushEvent {
152
+ /** Best-effort device serial (falls back to station). */
153
+ deviceSn?: string;
154
+ stationSn?: string;
155
+ /** Numeric event type as the cloud sends it; `eventName` carries the resolved label. */
156
+ eventType?: number;
157
+ /** Resolved event name where known. */
158
+ eventName?: string;
159
+ /** The thumbnail URL, if the push carried one (pic_url/thumbnail). */
160
+ thumbnailUrl?: string;
161
+ /** A usable thumbnail URL and the identity claims carried beside it. */
162
+ thumbnailCandidate?: ThumbnailCandidate;
163
+ /** Cipher id for decoding the media (when present). */
164
+ cipher?: number;
165
+ /** The full parsed eufy payload. */
166
+ payload: PushPayload;
167
+ /** The whole raw push (envelope + app_data). */
168
+ raw: RawPushMessage;
169
+ }
170
+ /** Persisted FCM credentials so we register once and reconnect across runs. */
171
+ export interface FcmCredentials {
172
+ fid: string;
173
+ androidId: string;
174
+ securityToken: string;
175
+ /** The FCM registration token (what we hand to eufy's cloud). */
176
+ fcmToken: string;
177
+ /** Firebase installation refresh token (for auth-token renewal). */
178
+ refreshToken?: string;
179
+ createdAt: number;
180
+ }
@@ -0,0 +1,6 @@
1
+ import type { RawDpCodec } from "../core/contracts.js";
2
+ /**
3
+ * The codec every Raw-DP reader shares. Stateless, so one frozen instance serves every device — there
4
+ * is nothing per-device or per-DP to configure.
5
+ */
6
+ export declare const rawDpCodec: RawDpCodec;
@@ -0,0 +1,23 @@
1
+ import type { Logger } from "../core/logger.js";
2
+ /** In-memory state for push thumbnails acquired before a caller passively reads them. */
3
+ export declare class StoredImageCache {
4
+ private readonly downloader;
5
+ private readonly logger;
6
+ private readonly clock;
7
+ private readonly isLifecycleError;
8
+ private readonly devices;
9
+ private readonly activeDevices;
10
+ private activeDownloads;
11
+ private generation;
12
+ constructor(downloader: (url: string, deviceKey: string) => Promise<Buffer>, logger: Logger, clock?: () => number, isLifecycleError?: (error: unknown) => boolean);
13
+ /** Observe a normalized thumbnail URL and start acquisition eagerly. */
14
+ observe(deviceKey: string, url: string): void;
15
+ /** Return retained bytes without starting or awaiting network work. */
16
+ snapshotStored(deviceKey: string): Promise<Buffer>;
17
+ /** Invalidate all retained and candidate state. */
18
+ clear(): void;
19
+ private pump;
20
+ private complete;
21
+ private isValidJpeg;
22
+ private diagnose;
23
+ }
@@ -0,0 +1,44 @@
1
+ /** Username/plaintext prefix the app prepends to the eufy user id. */
2
+ export declare const TUYA_USERNAME_PREFIX = "eufyhome-";
3
+ /** AES-128-CBC key used to derive the Tuya password (16 bytes). */
4
+ export declare const TUYA_PASSWORD_KEY: Buffer<ArrayBuffer>;
5
+ /** AES-128-CBC IV used to derive the Tuya password (16 bytes). */
6
+ export declare const TUYA_PASSWORD_IV: Buffer<ArrayBuffer>;
7
+ /** The Tuya username for a eufy user id (`"eufyhome-" + eufyUserId`). */
8
+ export declare function tuyaUsername(eufyUserId: string): string;
9
+ /**
10
+ * Derive the Tuya password for a eufy user id:
11
+ * `UPPERCASE-HEX( AES-128-CBC-NoPadding( pad16("eufyhome-"+eufyUserId), KEY, IV ) )`.
12
+ * Deterministic (fixed KEY/IV) — the same user id always yields the same password.
13
+ */
14
+ export declare function deriveTuyaPassword(eufyUserId: string): string;
15
+ /**
16
+ * Map an ISO 3166-1 alpha-2 country code (e.g. `"GB"`, `"DE"`) to its E.164 numeric dial code.
17
+ * Covers the main eufy device markets. Returns `undefined` for unlisted codes — callers fall back
18
+ * to the region-based heuristic.
19
+ */
20
+ export declare function isoToDialCode(iso: string): string | undefined;
21
+ /**
22
+ * Resolve the Tuya `countryCode` login field. Priority:
23
+ * 1. `phoneCode` — an explicit numeric dial code (e.g. `"49"`) when the caller already has one.
24
+ * 2. `isoCode` — an ISO 3166-1 alpha-2 code (e.g. `"DE"` from `MegaClientConfig.countryCode`) looked up
25
+ * via {@link isoToDialCode}. Covers the full eufy market range, so a German user on the EU
26
+ * shard correctly receives `"49"` rather than the coarse region fallback's `"44"`.
27
+ * 3. `region` — coarse mega shard prefix fallback: `"EU"`→`"44"`, `"CN"`→`"86"`, else `"1"`.
28
+ */
29
+ export declare function resolveCountryCode(phoneCode?: string, region?: string, isoCode?: string): string;
30
+ /** A derived Tuya login identity — everything needed to call the uid token/password login actions. */
31
+ export interface TuyaAccount {
32
+ /** `"eufyhome-" + eufyUserId`. */
33
+ username: string;
34
+ /** AES-derived password (uppercase hex). See {@link deriveTuyaPassword}. */
35
+ password: string;
36
+ /** Tuya `countryCode` field. See {@link resolveCountryCode}. */
37
+ countryCode: string;
38
+ }
39
+ /**
40
+ * Derive the full {@link TuyaAccount} (username + password + countryCode) for a eufy user id.
41
+ * `phoneCode` is the eufy account's phone country code; when omitted the countryCode defaults to
42
+ * "1" (see {@link resolveCountryCode} for the region-based fallback).
43
+ */
44
+ export declare function deriveTuyaAccount(eufyUserId: string, phoneCode?: string): TuyaAccount;
@@ -0,0 +1,89 @@
1
+ import { type TuyaSigner } from "./sign.js";
2
+ import { type TuyaAction, type TuyaEnv, type TuyaEnvelope, type TuyaSession, type TuyaHttpPost } from "./request.js";
3
+ /** Result of a successful {@link TuyaClient.login}. */
4
+ export interface TuyaLoginResult {
5
+ sid: string;
6
+ /** Tuya user id (`uid`) from the login response. */
7
+ uid: string;
8
+ }
9
+ export interface TuyaClientConfig {
10
+ /**
11
+ * The native sign seam. Defaults to {@link HmacSigner} with the built-in app key — no config
12
+ * needed. Override only for tests (pass a {@link StubSigner} or custom impl).
13
+ */
14
+ signer?: TuyaSigner;
15
+ /**
16
+ * Channel key — defaults to {@link TUYA_CHKEY} (`"7cbfe6d8"`), the constant extracted from
17
+ * the eufy Security/Mega app. Override only for non-standard builds.
18
+ */
19
+ chKey?: string;
20
+ /** Per-install device id; a random 44-hex one is generated if omitted (see {@link genDeviceId}). */
21
+ deviceId?: string;
22
+ /** Restore a prior session id (skip login). */
23
+ sid?: string;
24
+ /** Override the environment/static fields (see {@link TuyaEnv}). */
25
+ env?: TuyaEnv;
26
+ /** api.json endpoint override (region shard). */
27
+ endpoint?: string;
28
+ /** Inject a POST transport (test stub); default = native fetch. */
29
+ http?: TuyaHttpPost;
30
+ }
31
+ /**
32
+ * Generate a per-install `deviceId` (44 hex chars). The app derives it deterministically per
33
+ * install from device fingerprints, but the scheme is not reversed — we mint a random id instead.
34
+ * TODO(scheme): replace with the app's real derivation once known.
35
+ */
36
+ export declare function genDeviceId(): string;
37
+ export declare class TuyaClient {
38
+ private readonly signer;
39
+ private readonly env;
40
+ private readonly endpoint?;
41
+ private readonly http?;
42
+ private session;
43
+ constructor(config?: TuyaClientConfig);
44
+ /** The current per-install session identity (sid empty until {@link login}). */
45
+ getSession(): Readonly<TuyaSession>;
46
+ /** True once a login has populated a session id. */
47
+ get loggedIn(): boolean;
48
+ /**
49
+ * Build the full signed param map for an action against the current session. Does not send.
50
+ * Requires a working signer (the sign step).
51
+ */
52
+ buildRequest(action: TuyaAction): Record<string, string>;
53
+ /** Build + POST an action, returning the parsed envelope. Requires a working signer. */
54
+ call<T = unknown>(action: TuyaAction): Promise<TuyaEnvelope<T>>;
55
+ /**
56
+ * Log into the Tuya cloud from a eufy user id (wire-confirmed from the eufy Security app).
57
+ *
58
+ * Flow:
59
+ * 1. `smartlife.m.user.username.token.get` → `{ token, publicKey, exponent }` (RSA-2048 key).
60
+ * If this returns USER_NOT_EXIST the shadow account has never been provisioned — the vacuum
61
+ * must be added via the eufy Security app (`com.oceanwing.battery.cam`) at least once.
62
+ * 2. Derive password: RSA/PKCS1-encrypt( MD5hex(aesPassword), serverKey ) → hex.
63
+ * 3. `smartlife.m.user.uid.password.login.reg` → `{ sid, uid }`.
64
+ * On USER_PASSWD_WRONG: re-fetch a token and retry once with the hardcoded fallback
65
+ * password `"12345678"` (wire-confirmed from the eufy Security app).
66
+ * ⚠️ Two failed attempts in a row can contribute to Tuya-side rate-limiting or lockout — do not
67
+ * add further retry loops on top of this one.
68
+ */
69
+ login(eufyUserId: string, phoneCode?: string): Promise<TuyaLoginResult>;
70
+ /**
71
+ * READ/dump a device's cached data-points (`thing.m.device.cache.dp.get`).
72
+ * Builds the request without needing a working signer; the signer is only exercised on send.
73
+ */
74
+ getDeviceDps<T = unknown>(devId: string, dpCacheType?: number): Promise<TuyaEnvelope<T>>;
75
+ /**
76
+ * CONTROL: publish data-points to a device (`thing.m.device.dp.publish`). `dps` is
77
+ * `{ "<dpId>": <value> }`. `gwId` is the gateway/parent id (equals `devId` for a standalone gw).
78
+ *
79
+ * ⚠️ UNVERIFIED write — refuses to send by default. The `dp.publish` param shape
80
+ * ({@link buildPublishDpsAction}) is derived, NOT pinned against a confirmed exchange,
81
+ * and the login round-trip that yields a real `sid` is unproven too. A wrong shape comes back as a
82
+ * generic Tuya error indistinguishable from a real device rejection, so blindly sending would hide
83
+ * that ambiguity. Pass `{ allowUnverified: true }` to send anyway; the gate drops when the write is
84
+ * captured + confirmed against a device.
85
+ */
86
+ publishDps<T = unknown>(devId: string, gwId: string, dps: Record<string, unknown>, opts?: {
87
+ allowUnverified?: boolean;
88
+ }): Promise<TuyaEnvelope<T>>;
89
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `TuyaCommandRouter` — the transport-side command router for eufy Home/Clean Tuya vacuums (X8,
3
+ * G-series and other `eufy_home_tuya` category devices).
4
+ *
5
+ * Usage: call {@link bind} after a successful mega login, then {@link registerDevice} for each
6
+ * `eufy_home_tuya` device (the facade does both). On the first {@link dispatchCommand} the router
7
+ * logs into the Tuya cloud lazily (once, shared across all subsequent sends). The dp.publish write
8
+ * is sent via `TuyaClient.publishDps` — note the `dp.publish` param shape is not yet confirmed from
9
+ * a live eufy Home/Clean capture (gated by {@link TuyaCommandRouterConfig.allowUnverified}, default `false`).
10
+ *
11
+ * Layering: imports `../../core` only — no `model/` import, consistent with the capability↔transport
12
+ * decorrelation invariant. Sibling tuya/* imports are same-layer (transport).
13
+ */
14
+ import type { Command } from "../../core/contracts.js";
15
+ import type { TuyaHttpPost } from "./request.js";
16
+ export interface TuyaCommandRouterConfig {
17
+ /**
18
+ * Opt-in to unverified Tuya DP writes. Defaults to `false` — {@link dispatchCommand} will throw
19
+ * until a live `publishDps` capture confirms the dp.publish round-trip. Set `true` only after that
20
+ * confirmation and remove the gate when the write is shipped as verified.
21
+ */
22
+ allowUnverified?: boolean;
23
+ /** Inject a POST transport (test stub); default = native fetch. Forwarded to the internal TuyaClient. */
24
+ http?: TuyaHttpPost;
25
+ }
26
+ export declare class TuyaCommandRouter {
27
+ private readonly allowUnverified;
28
+ private readonly http;
29
+ private userId;
30
+ private dialCode;
31
+ private client;
32
+ /** Promise that resolves once Tuya login has completed. Reset by {@link bind}. */
33
+ private loginOnce;
34
+ /**
35
+ * eufy SN → Tuya device ids, populated by the facade via {@link registerDevice}.
36
+ * The facade extracts the Tuya id from the `raw` device record fields (`tuya_uuid`,
37
+ * `tuya_virtual_id`, `tuya_device_id`, `virtualId`) and registers it once the device list loads.
38
+ */
39
+ private readonly snMap;
40
+ constructor(config?: TuyaCommandRouterConfig);
41
+ /**
42
+ * Supply credentials for lazy Tuya login. Called by the facade after a successful mega login.
43
+ * The router logs into Tuya on the first {@link dispatchCommand}, not immediately.
44
+ *
45
+ * `regionShard` is the mega shard string (`"eu-pr"`, `"us-pr"`, …) used as a coarse fallback;
46
+ * `isoCode` is the ISO 3166-1 alpha-2 country code from {@link MegaClientConfig} (e.g. `"DE"`)
47
+ * and takes precedence — a German user on the EU shard gets dial code `"49"`, not `"44"`.
48
+ */
49
+ bind(userId: string, regionShard?: string, isoCode?: string): void;
50
+ /**
51
+ * Register a eufy SN → Tuya devId mapping. Called by the facade for each `eufy_home_tuya`
52
+ * device after the cloud device list loads. The facade extracts the Tuya id from the device's
53
+ * raw record (`tuya_uuid` / `tuya_virtual_id` / `tuya_device_id` / `virtualId` fields).
54
+ * `gwId` defaults to `devId` — standalone devices share the two.
55
+ */
56
+ registerDevice(sn: string, devId: string, gwId?: string): void;
57
+ private ensureLoggedIn;
58
+ /**
59
+ * Fetch a device's cached DPs from the Tuya cloud (`thing.m.device.cache.dp.get`) and deliver
60
+ * the raw DP map to the caller. Used for initial state hydration after MQTT subscribe — gets the
61
+ * last-known state without waiting for the first realtime push.
62
+ *
63
+ * The response shape from `getDeviceDps` is not yet pinned from a live capture. The defensive
64
+ * extraction tries both `result.dps` (a nested map) and bare `result` (a flat map), and returns
65
+ * `null` when neither yields a non-empty record.
66
+ */
67
+ fetchDps(sn: string): Promise<Record<string, unknown> | null>;
68
+ /**
69
+ * Route an `aiot-dp` {@link Command} to the Tuya REST API.
70
+ *
71
+ * Logs in lazily on first call. The eufy SN must have been registered via {@link registerDevice}
72
+ * before dispatch — the facade does this when the device list is loaded.
73
+ *
74
+ * ⚠️ `dp.publish` is unverified — see {@link TuyaCommandRouterConfig.allowUnverified}. By default
75
+ * this throws. Pass `allowUnverified: true` in the router config only after a live capture
76
+ * confirms the full round-trip, then remove the gate.
77
+ */
78
+ dispatchCommand(sn: string, cmd: Command): Promise<void>;
79
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Inbound Tuya DP event parsing and routing for eufy Home/Clean (`eufy_home_tuya`) devices.
3
+ *
4
+ * {@link parseTuyaDpReport} unwraps the Anker MQTT `{head, payload}` envelope and extracts the
5
+ * raw DP map. {@link parseTuyaDpEvent} then validates and normalises it to a numeric-keyed record.
6
+ * {@link TuyaDpRouter} delivers the result to the registered {@link TuyaDpInbound} listener so the
7
+ * capability layer can consume Tuya state through the same typed getters as AIoT.
8
+ *
9
+ * Inbound messages arrive on `cmd/eufy_home/{model}/{sn}/res` with `sign_code: 0` (no additional
10
+ * encryption — confirmed from clean-device captures). If a future capture shows `sign_code ≠ 0` the
11
+ * payload will be AES-128-ECB encrypted with the device's localKey before the JSON can be parsed.
12
+ */
13
+ import type { TuyaDpInbound } from "../../core/contracts.js";
14
+ /**
15
+ * Validate and convert a raw ThingClips DP callback payload to a numeric-keyed record.
16
+ *
17
+ * The callback delivers `{ "<dpId>": <value>, … }` with string keys and typed values; this
18
+ * normalises the keys to positive integer DP ids. Unrecognised keys (non-positive-integer, non-scalar
19
+ * values) are skipped rather than rejecting the whole map — the same defensive posture as
20
+ * {@link parseAiotDpReport}. Returns `null` only when the input is not a non-empty object or when no
21
+ * valid DP entry was found.
22
+ */
23
+ export declare function parseTuyaDpEvent(payload: unknown): Record<number, boolean | number | string> | null;
24
+ /**
25
+ * Extract a raw DP map from an inbound Anker MQTT message for a `eufy_home_tuya` device.
26
+ *
27
+ * Messages arrive in the same `{head, payload}` envelope the AIoT path uses, where `payload` is a
28
+ * JSON **string**:
29
+ * ```json
30
+ * { "head": { "cmd": 65537, "cmd_status": 2, "sign_code": 0, ... },
31
+ * "payload": "{\"t\":\"…\",\"protocol\":2,\"data\":{\"104\":80,\"106\":0}}" }
32
+ * ```
33
+ * Two DP layouts inside the decoded payload are handled: Tuya-native (`data.dps`) and AIoT-direct
34
+ * (`data` with integer-keyed DP ids). Falls back to flat `envelope.dps` / `envelope.data.dps`
35
+ * shapes for any pre-parsed delivery. Returns `undefined` when no recognised shape is found.
36
+ *
37
+ * @internal
38
+ */
39
+ export declare function parseTuyaDpReport(raw: unknown): Record<string, unknown> | undefined;
40
+ /**
41
+ * Routes validated inbound Tuya DP events to the registered {@link TuyaDpInbound} listener.
42
+ *
43
+ * Mirrors the AIoT MQTT inbound path: the transport owns parsing; the capability layer owns
44
+ * semantics. A malformed or empty payload is silently dropped; the listener sees only
45
+ * successfully parsed DP maps.
46
+ */
47
+ export declare class TuyaDpRouter {
48
+ private listener;
49
+ /** Register the inbound listener. Replaces any previously registered one. */
50
+ setListener(listener: TuyaDpInbound): void;
51
+ /**
52
+ * Parse `dps` and, if valid, deliver it to the registered listener for `sn`.
53
+ * Silently drops malformed or empty payloads.
54
+ */
55
+ deliver(sn: string, dps: unknown): void;
56
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Tuya / Thingclips cloud-protocol client.
3
+ *
4
+ * A clean, typed base for the eufy app's Tuya backbone: deterministic eufy→Tuya account derivation,
5
+ * exact `api.json` request assembly, the sign, and a small {@link TuyaClient}. No configuration is
6
+ * required — the built-in signing key ({@link TUYA_SIGN_K}) works out of the box.
7
+ *
8
+ * Quick start:
9
+ *
10
+ * import { tuya } from "@mega-yfue/eufy-sdk";
11
+ * const client = new tuya.TuyaClient(); // zero-config
12
+ * await client.login(eufyUserId, phoneCode); // derives Tuya account + logs in
13
+ * const dps = await client.getDeviceDps(devId); // read DPs with the live sid
14
+ *
15
+ * // Or inject a previously obtained sid to skip login:
16
+ * const client = new tuya.TuyaClient({ sid });
17
+ *
18
+ * STATUS:
19
+ * ✅ sign = HMAC-SHA256(TUYA_SIGN_K, preimage) — reproduces a live-captured signature
20
+ * ✅ chKey = "7cbfe6d8" per-appId constant
21
+ * ✅ login() — username.token.get → RSA-encrypt(MD5(password)) → password.login.reg
22
+ * (shadow account must be provisioned via eufy Security app first)
23
+ * ✅ getDeviceDps / publishDps builders ready; publishDps gated behind allowUnverified
24
+ */
25
+ export * from "./account.js";
26
+ export * from "./sign.js";
27
+ export * from "./request.js";
28
+ export * from "./client.js";
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tuya / Thingclips `api.json` request assembly + transport.
3
+ *
4
+ * Every call is a single `POST https://a1.tuyaeu.com/api.json` (EU) whose body is
5
+ * **form-urlencoded plaintext params** (NOT an encrypted blob) plus a `sign`. This module builds
6
+ * that param map for a given action, form-encodes it, POSTs it, and parses Tuya's response
7
+ * envelope. The one non-plaintext piece — the `sign` digest — is delegated to a {@link TuyaSigner}.
8
+ */
9
+ /** Injectable POST transport — takes (url, form-body), returns the parsed JSON envelope. Native `fetch` by default. */
10
+ export type TuyaHttpPost = (url: string, body: string) => Promise<unknown>;
11
+ import { type TuyaSigner } from "./sign.js";
12
+ /** EU api.json endpoint. (Other regions swap the `tuyaeu` shard, e.g. `tuyaus`/`tuyacn`.) */
13
+ export declare const TUYA_API_ENDPOINT = "https://a1.tuyaeu.com/api.json";
14
+ /** Hardcoded public app key (a.k.a. clientId) shipped in the eufy app. */
15
+ export declare const TUYA_APP_KEY = "w8x4ppqkdxvqnd73ahj9";
16
+ /**
17
+ * Hardcoded app secret. Public only in the sense that it ships in the vendor app; it feeds the NATIVE
18
+ * sign digest (see {@link TuyaSigner}), so it is not used directly in this module.
19
+ */
20
+ export declare const TUYA_APP_SECRET = "pt585qhmt75hwcynchnps9dnxh9suhwd";
21
+ /** Thingclips SDK version the captured traffic used. */
22
+ export declare const TUYA_SDK_VERSION = "7.5.0";
23
+ /** App version the client reports (also feeds the sign via `appVersion`). */
24
+ export declare const TUYA_APP_VERSION = "6.0.51_26722";
25
+ /** A single action to call: the `a` (action) + `v` (version) + optional pre-serialized `postData`. */
26
+ export interface TuyaAction {
27
+ /** Action name, e.g. `thing.m.device.dp.publish`. */
28
+ a: string;
29
+ /** Per-action version, e.g. `"1.0"` / `"2.0"`. */
30
+ v: string;
31
+ /** The `postData` param value — already a JSON STRING (Tuya carries the body as a string param). */
32
+ postData?: string;
33
+ }
34
+ /** Per-install session identity carried on every request. */
35
+ export interface TuyaSession {
36
+ /** Session id from a successful login; empty string pre-login. */
37
+ sid: string;
38
+ /** Per-install device id (44-hex in the capture). See {@link TuyaClient} for generation. */
39
+ deviceId: string;
40
+ /**
41
+ * 8-hex `chKey`. ✅ SOLVED: `getChKey` in `libthing_security.so` is a pure
42
+ * function of the **appId only** (never touches time/session/Context), so it's a per-appId CONSTANT —
43
+ * `"7cbfe6d8"` for this app's appId. Hardcode it.
44
+ */
45
+ chKey: string;
46
+ }
47
+ /**
48
+ * The constant / environment fields of a request. All are plaintext and (except the sign-relevant
49
+ * `clientId`/`os`/`ttid`/`lang`/`et`/`appVersion`) do NOT feed the sign. Values marked TODO are
50
+ * cosmetic os-info fields whose exact contents were not pinned from the capture; they do not affect
51
+ * the sign and can be tuned freely.
52
+ */
53
+ export interface TuyaEnv {
54
+ /** appKey. */
55
+ clientId: string;
56
+ sdkVersion: string;
57
+ appVersion: string;
58
+ /** "Android". */
59
+ os: string;
60
+ /** "android". */
61
+ ttid: string;
62
+ /** "en_GB". */
63
+ lang: string;
64
+ /** "3". */
65
+ et: string;
66
+ /** "gzip". */
67
+ cp: string;
68
+ /** "sdk". */
69
+ channel: string;
70
+ /** "1". */
71
+ nd: string;
72
+ /** JSON string, e.g. `{"customDomainSupport":"1","sdkInt":"36","nd":"1","brand":"google"}`. */
73
+ bizData: string;
74
+ /** TODO(unknown): device platform string; cosmetic (not in sign). */
75
+ platform: string;
76
+ /** TODO(unknown): OS system version; cosmetic (not in sign). */
77
+ osSystem: string;
78
+ /** IANA time zone id, e.g. "Europe/London". */
79
+ timeZoneId: string;
80
+ /** TODO(unknown): Thingclips device-core version; cosmetic (not in sign). */
81
+ deviceCoreVersion: string;
82
+ }
83
+ /** Default environment fields (see {@link TuyaEnv} for which are verified vs. TODO/cosmetic). */
84
+ export declare const DEFAULT_TUYA_ENV: TuyaEnv;
85
+ /** Inputs to {@link buildApiParams} beyond the action itself. */
86
+ export interface BuildRequestOptions {
87
+ session: TuyaSession;
88
+ signer: TuyaSigner;
89
+ env?: TuyaEnv;
90
+ /** Override the unix-second `time` (default: now) — for deterministic builds/tests. */
91
+ time?: number;
92
+ /** Override the `requestId` uuid (default: random v4) — for deterministic builds/tests. */
93
+ requestId?: string;
94
+ }
95
+ /**
96
+ * Build the full form param map for an action: fills the static env + per-install session fields +
97
+ * generated `time`/`requestId`, computes the sign preimage over the allowlisted subset, and appends
98
+ * the `sign` from the signer. Returns a flat `Record<string,string>` ready to form-encode.
99
+ *
100
+ * The sign is the only step that needs a working {@link TuyaSigner}; a {@link StubSigner} throws
101
+ * here. Callers that only want to inspect the param shape can read {@link BuildRequestOptions} with
102
+ * a fake signer.
103
+ */
104
+ export declare function buildApiParams(action: TuyaAction, opts: BuildRequestOptions): Record<string, string>;
105
+ /** Tuya's standard response envelope. */
106
+ export interface TuyaEnvelope<T = unknown> {
107
+ success: boolean;
108
+ /** Server unix-ms timestamp. */
109
+ t?: number;
110
+ /** Decoded result payload (present on success). */
111
+ result?: T;
112
+ errorCode?: string;
113
+ errorMsg?: string;
114
+ /** Occasionally present status string. */
115
+ status?: string;
116
+ }
117
+ /** Transport knobs for {@link sendApiRequest}. */
118
+ export interface SendOptions {
119
+ endpoint?: string;
120
+ /** Inject a POST transport (default: native `fetch`). Used to stub the network in tests. */
121
+ http?: TuyaHttpPost;
122
+ }
123
+ /**
124
+ * Form-encode a param map and POST it to `api.json`, returning the parsed {@link TuyaEnvelope}.
125
+ * The body is `application/x-www-form-urlencoded` (Tuya rejects JSON here).
126
+ */
127
+ export declare function sendApiRequest<T = unknown>(params: Record<string, string>, opts?: SendOptions): Promise<TuyaEnvelope<T>>;
128
+ /**
129
+ * CONTROL: publish device data-points. `dps` is `{ "<dpId>": <value> }`; note it is JSON-stringified
130
+ * INSIDE the postData (Tuya nests the dps map as a string). Action `thing.m.device.dp.publish` v2.0.
131
+ */
132
+ export declare function buildPublishDpsAction(devId: string, gwId: string, dps: Record<string, unknown>): TuyaAction;
133
+ /**
134
+ * READ/dump: fetch a device's cached data-points. Action `thing.m.device.cache.dp.get` v2.0.
135
+ * TODO(verify): `dpCacheType` default (1) was not pinned from the capture.
136
+ */
137
+ export declare function buildGetDeviceDpsAction(devId: string, dpCacheType?: number): TuyaAction;
138
+ /**
139
+ * LOGIN step 1: fetch a pre-login RSA token for a Tuya uid.
140
+ * Wire-confirmed from the eufy Security app (`com.oceanwing.battery.cam`):
141
+ * wire action `smartlife.m.user.username.token.get`, v=2.0.
142
+ * Returns `{ token, publicKey, exponent }` (RSA-2048 modulus + exponent as decimal strings).
143
+ */
144
+ export declare function buildUsernameTokenGetAction(countryCode: string, username: string): TuyaAction;
145
+ /**
146
+ * LOGIN step 2: uid password login + auto-register.
147
+ * Wire-confirmed from the eufy Security app: wire action
148
+ * `smartlife.m.user.uid.password.login.reg`, v=1.0. `passwd` = hex of RSA-PKCS1-encrypt(MD5hex(aesPassword)).
149
+ * On success returns `{ sid, uid }`.
150
+ */
151
+ export declare function buildPasswordLoginRegAction(countryCode: string, uid: string, passwd: string, token: string): TuyaAction;
152
+ /**
153
+ * Older Tuya API path that auto-creates a shadow account if it does not yet exist.
154
+ * Used only by `tuya-login-diag.mjs` as a last-resort re-provisioning probe — not part of the
155
+ * normal login flow (`smartlife.m.user.username.token.get` is step 1).
156
+ */
157
+ export declare function buildUidTokenCreateAction(countryCode: string, uid: string): TuyaAction;