@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,444 @@
1
+ /**
2
+ * `ff09` frame encoder — the AES-128-CBC framed control protocol shared by every eufy device that
3
+ * takes a lock-style on/off actuation command (currently: the T8531 video smart lock and the T85D0
4
+ * garage door; likely other actuators later — see below).
5
+ *
6
+ * The command bytes are a self-contained `ff09…` frame, NOT a plain param write, and BOTH transports
7
+ * carry the exact same frame:
8
+ * - **P2P** (`transport/p2p/command-router.ts`, T8531): wrapped in a `SET_PAYLOAD` (1350) envelope
9
+ * whose inner `cmd` is `TRANSFER_PAYLOAD` (1940) and `payload` = `{apiCommand:6018,
10
+ * lock_payload:"ff09…", seq_num, time}`.
11
+ * - **MQTT** (`transport/mqtt/command-router.ts`, standalone/garage families): base64 of the identical
12
+ * frame under the `trans` field.
13
+ *
14
+ * So cracking the frame solves both — this module builds ONLY the frame, nothing about either
15
+ * envelope, and sits at the transport root (like `ffmpeg.ts`) specifically so neither transport
16
+ * imports the other for it. Named after the wire frame, not the capability: the two current callers
17
+ * both happen to be locks, but nothing here is lock-specific beyond the {@link LOCK_COMMAND_CODE}
18
+ * opcode value itself — a future actuator (e.g. a curtain) reusing this wire would just be a new
19
+ * opcode constant, not a new encoder.
20
+ *
21
+ * ## Wire (verified byte-exact against captured T8531 lock+unlock frames)
22
+ *
23
+ * **Outer frame** (`lockPayload`, hex):
24
+ * ```
25
+ * ff 09 | size(u16 LE) | 03(versionCode) 00(reserved) 02(dataType) | cmdEnc(u16 BE) | ct… | xorHash(1)
26
+ * ```
27
+ * `size` = total frame length incl. header + xor. `cmdEnc` = `0x4000 | commandCode` (bit 0x4000 =
28
+ * "encrypted payload"). `xorHash` = XOR of every preceding byte.
29
+ *
30
+ * **Inner plaintext** — an incrementing-separator TLV (`0xA1, 0xA2, …`, each `sep | len | bytes`),
31
+ * then **zero-padded to a 16-byte boundary** before AES (the app pads the buffer, so the real PKCS#7
32
+ * block AES adds is a full 0x10 block):
33
+ * ```
34
+ * A1 04 <time u32 LE> — freshness stamp
35
+ * A2 <len> <admin_user_id ASCII> — the lock owner's id (40 hex chars)
36
+ * A3 01 <lockByte> — 0 = lock, 1 = unlock
37
+ * A4 <len> <username ASCII> — the acting user's display name (email local-part)
38
+ * A5 <len> <short_user_id bytes> — the acting member's short id (hex→bytes, e.g. "0003")
39
+ * ```
40
+ *
41
+ * **Cipher**: AES-128-CBC + PKCS#7.
42
+ * - key = `ASCII(admin_user_id[-12:])` (12 B) ‖ `uint32 BE(time)` (4 B) = 16 B.
43
+ * - iv = `ASCII(deviceSn)` zero-padded to 16 B (a 16-char SN is used verbatim).
44
+ * - `time` is the nonce that makes each command's key differ; it is emitted CLEARTEXT in the
45
+ * envelope `time` field so the lock can re-derive the key. The app computes it as
46
+ * `floor(unixSec) | floor(random*100)` — the low bits are randomized. We reproduce that:
47
+ * `keyTime = unixSec | nonce`, and the TLV `A1` stamp carries the un-noised `unixSec`.
48
+ *
49
+ * ## Settings GET/SET (T85D0 garage/lock auto-lock, 2026-07-16 capture — same cipher/frame family)
50
+ *
51
+ * Two more `apiCommand`/`commandCode` pairs on the SAME outer frame + cipher as above, verified
52
+ * byte-exact against a live GET query + a live GET response + two live SET writes (off→on) on a
53
+ * T85D0:
54
+ * - **GET** (`LOCK_API_COMMAND.GET_SETTINGS` 6016 / `LOCK_COMMAND_CODE.GET_SETTINGS` 53/0x35,
55
+ * {@link buildFf09QueryFrame}): plaintext is JUST `[A1 time][A2 adminUserId]` — no user-attribution
56
+ * fields at all (narrower than even `omitUserFields`, which still keeps A3).
57
+ * - **SET** (`LOCK_API_COMMAND.SET_SETTINGS` 6015 / `LOCK_COMMAND_CODE.SET_SETTINGS` 52/0x34,
58
+ * {@link buildFf09SettingsFrame}): `[A1 time][A2 adminUserId][A3 1B][A4 1B enable][A5 2B LE delay-
59
+ * seconds][A6 1B][A7 2B][A8 2B][A9 1B]`. `A3` is the same **setting-type selector** the Rain Mode
60
+ * compact write uses (see below) — `0x00` selects auto-lock. `A4`-`A8` are now FULLY CONFIRMED
61
+ * against the app's own JS (`smartLockSetParamsDataParse`'s `case 0`, carved from a live `/proc/mem`
62
+ * dump, 2026-07-18 — not just a capture-derived guess): `A4`=`isAutoLock`, `A5`=`autoLockTime`
63
+ * (seconds, LE u16), `A6`=`isSchedule` (bool), `A7`=`scheduleStartTime`, `A8`=`scheduleEndTime`.
64
+ * `A7`/`A8` are each `[hourByte, minuteByte]` (the app encodes them as `hour.toString(16)‖
65
+ * minute.toString(16)`, NOT a packed LE u16) — we still treat them as an opaque 2-byte passthrough
66
+ * (read from a prior GET, written back unchanged), which stays correct either way since we never
67
+ * interpret their contents, only preserve them. **`A9` is UNRESOLVED, not "confirmed reserved" —
68
+ * the app's own `case 0` builder writes exactly 5 fields (`A4`-`A8`) and produces no 6th field at
69
+ * all.** The original capture this encoder was built from showed a 6th real tagged byte
70
+ * (`0xA9`, value `0x00`) after `A8`; two independent copies of the app's JS (including a newer
71
+ * 15-case build variant) agree there's no `A9` in `case 0`. This is a genuine, unresolved
72
+ * discrepancy between the captured wire bytes and the decompiled source — not yet re-verified with
73
+ * a fresh capture. Left as-is in the shipped encoder (still sends `A9=0x00`) since changing it
74
+ * without a fresh capture risks breaking a live-verified write over an unconfirmed hypothesis; the
75
+ * device has never rejected the extra field in any live test.
76
+ * - **The device's `/res` reply to a GET** is NOT a normal command ack: no `mChannel`/`mValue3`/
77
+ * `apiCommand`/`seq_num`, just `{cmd:1940, payload:{dev_sn, lock_payload, time}}` where `time` is a
78
+ * **hex string** (not decimal) that equals the keyTime of the GET that triggered it — matched back
79
+ * to the query to survive interleaved traffic. Its `cmdEnc` has an extra flag bit set vs. a request
80
+ * (`0x48xx` not `0x40xx`) but decodes with the exact same key/iv derivation. Decrypted plaintext:
81
+ * a leading `0x00` status byte, then TLV fields using the same `sep|len|bytes` encoding but the
82
+ * response's OWN independent tag numbering (does not line up positionally with the GET/SET request
83
+ * TLVs) — our own capture only ever showed `a1..ad` (13 fields, tags `0xa1`-`0xad`), but this is NOT
84
+ * the auto-lock write's own reply scoped to auto-lock: **a single `GET_SETTINGS` reply is a FLAT
85
+ * DUMP of the device's ENTIRE settings state**, one field per tag, covering every setting in the
86
+ * `A3`/setting-type enum below (not just the one being written) — confirmed against
87
+ * the app's own `smartLockGetParamsDataParse` (2026-07-18, same JS dump as the case-0 finding
88
+ * above), which explicit-tag-reads (not sequentially, `getByteParam(<tag>)`/`getSecondParam(<tag>)`/
89
+ * `getTimeParam(<tag>)`) THIRTY-FOUR fields, tags `0xa1` through `0xc2`:
90
+ *
91
+ * | tag | field | tag | field |
92
+ * |------|-------------------------------------|------|-------------------------------------|
93
+ * | 0xa1 | `isAutoLock` **(current enable state — use THIS, not `A4`, to read it back)** | 0xb3 | *(read, unused in this build)* |
94
+ * | 0xa2 | `autoLockTime` (= our `A5`/delay readback) | 0xb4 | *(read, unused in this build)* |
95
+ * | 0xa3 | `isSchedule` (= our `A6`) | 0xb5 | `isPowerSavingMode` |
96
+ * | 0xa4 | `scheduleStartTime` (= our `A7` readback) | 0xb6 | `powerSavingModeStartTime` |
97
+ * | 0xa5 | `scheduleEndTime` (= our `A8` readback) | 0xb7 | `powerSavingModeEndTime` |
98
+ * | 0xa6 | `isOneTouchLock` (setting-type 1) | 0xb8 | `keepAliveTime` |
99
+ * | 0xa7 | `isScramblePasscode` (setting-type 3) | 0xb9 | `isOneTouchRearLock` (setting-type 11) |
100
+ * | 0xa8 | `isWrongTryProtect` (setting-type 2) | 0xba | `multiFunctionBtnSinglePressMode` (12) |
101
+ * | 0xa9 | `wrongTryTime` | 0xbb | `multiFunctionBtnDoublePressMode` |
102
+ * | 0xaa | `lockDownTime` | 0xbc | `multiFunctionBtnLongPressMode` |
103
+ * | 0xab | `lockVolume` (setting-type 4) | 0xbd | `isSoundNotDisturbSchedule` |
104
+ * | 0xac | `wifiStatus` (setting-type 5) | 0xbe | `soundNotDisturbScheduleStart` |
105
+ * | 0xad | `isEnableLog` (setting-type 6) | 0xbf | `soundNotDisturbScheduleEnd` |
106
+ * | 0xae | `isRainMode` (setting-type 7) | 0xc0 | `faceEnabled` (setting-type 13, default 1) |
107
+ * | 0xaf | `isPassageMode` (setting-type 8) | 0xc1 | `faceWakeType` (default 0) |
108
+ * | 0xb0 | `passageModeStartTime` | 0xc2 | `fingerprintEnabled` (default 1) |
109
+ * | 0xb1 | `passageModeEndTime` | | |
110
+ * | 0xb2 | `isPrivacyMode` (setting-type 9) | | |
111
+ *
112
+ * `a6` is `isOneTouchLock`, an entirely different setting, which is why it never moves with
113
+ * auto-lock's own on/off state — the REAL current-enable-state field is `a1` (our own
114
+ * `parseFf09SettingsResponse` already parses it correctly; it is simply not READ by
115
+ * `sendFf09Autolock`/`dispatchFf09Autolock`, which only pull `a2`/`a4`/`a5`). Not yet
116
+ * wired into any code path — flagged here as a documented opportunity, not a shipped read.
117
+ * See {@link decryptFf09Frame} + {@link parseFf09SettingsResponse}.
118
+ * - **Also live-verified on the T8531 over P2P** (2026-07-18): the same GET/SET frame shape, live-
119
+ * captured off the P2P wire and confirmed byte-identical in structure to the T85D0 capture above
120
+ * (same key derivation, same TLV layout, `A5`'s captured value there was 120s/"2 min" vs. this
121
+ * doc's 90s/"1.5 min" — different device, same field semantics). `dev.lock()?.setAutoLock()` driven
122
+ * end-to-end through the P2P command router against a real T8531, both directions confirmed via the
123
+ * app UI — not just byte-exact against a capture. See `transport/p2p/command-router.ts`'s
124
+ * `sendFf09Autolock`.
125
+ *
126
+ * ## Rain Mode (T8531, 2026-07-18 capture — a SECOND, shorter `SET_SETTINGS` shape)
127
+ *
128
+ * Same `apiCommand`/`commandCode` pair as the auto-lock SET above (6015/52), but a genuinely different,
129
+ * SHORTER plaintext — captured live off the P2P wire toggling Rain Mode off→on: `[A1 time]
130
+ * [A2 adminUserId][A3 1B settingId][A4 1B value]`, nothing past `A4` (no `A5`-`A9` at all). `A3` here
131
+ * is NOT the unconfirmed constant from the full-blob write above — it's a setting-id SELECTOR (`0x07`
132
+ * for Rain Mode), making this a compact single-setting write rather than a read-modify-write of a fixed
133
+ * blob. Because it doesn't touch any field besides the one being set, it's a pure blind write — no GET
134
+ * pass needed first. See {@link FF09_SETTING_ID} + {@link buildFf09SettingToggleFrame}. The `A3`
135
+ * selector addresses at least 14 T8531 settings — the app's own decompiled JS
136
+ * (`smartLockSetParamsDataParse`, 2026-07-18) confirmed the id→setting map; `RAIN_MODE` is the only one
137
+ * independently WIRE-CAPTURED so far, and 6 more (one-touch lock/scramble/wifi/enable-log/privacy/
138
+ * one-touch-rear) are wired but THROW "wire unverified" until each clears that bar — see
139
+ * {@link FF09_SETTING_ID} for the full id list.
140
+ * ✅ LIVE-VERIFIED end-to-end (2026-07-18): `dev.lock()?.setRainMode()` driven through the P2P command
141
+ * router against a real T8531, both directions (off→on and on→off) confirmed via the app UI showing
142
+ * the new state afterward — not just byte-exact against a capture. See
143
+ * `transport/p2p/command-router.ts`'s `sendFf09SettingToggle`.
144
+ */
145
+ import type { AutoLockSnapshot } from "../core/contracts.js";
146
+ /**
147
+ * `apiCommand` values for the `TRANSFER_PAYLOAD` (1940) envelope `payload`. `ON_OFF_LOCK` (6018)
148
+ * is THE lock/unlock command for both the video lock (T8531) and the garage door (T85D0) — proven
149
+ * end-to-end against a T8531 (unlock/lock/unlock/lock capture) and, 2026-07-16, against the garage
150
+ * door too (2 independent live captures of the real app closing it, both `apiCommand 6018`, matching
151
+ * eufy-sdk's own already-verified open path). Direction is the ff09 frame's `A3` byte, not the
152
+ * apiCommand: both lock and unlock issue `apiCommand 6018`.
153
+ */
154
+ export declare const LOCK_API_COMMAND: {
155
+ /** Lock / unlock the deadbolt — video lock AND garage door, both directions (see above). */
156
+ readonly ON_OFF_LOCK: 6018;
157
+ /**
158
+ * NOT used by this encoder — kept only as a real, observed constant. `apiCommand 6012` genuinely
159
+ * appears in live traffic (captured from a real but UNIDENTIFIED iOS app instance on the same
160
+ * account, 2026-07-16), but sending it from eufy-sdk never once produced a physical actuation in
161
+ * several live tests. The app's own `ESLCommand` enum (carved from a live `/proc/mem` dump,
162
+ * 2026-07-18) names 6012 `QUERY_STATUS_IN_LOCK` — a read-only status POLL, not an actuator at all,
163
+ * which is exactly why it never once opened/closed anything. The name `OPEN_DOOR` is a misnomer,
164
+ * kept as-is to avoid API churn on a constant that is not usable — do not use it for open, close, or
165
+ * a status read (this encoder has no query-status support; `GET_SETTINGS`/6016 is the only supported
166
+ * read path).
167
+ */
168
+ readonly OPEN_DOOR: 6012;
169
+ /** Read the current auto-lock/settings TLV — {@link buildFf09QueryFrame}. Verified live 2026-07-16. */
170
+ readonly GET_SETTINGS: 6016;
171
+ /** Write the auto-lock/settings TLV — {@link buildFf09SettingsFrame}. Verified live 2026-07-16. */
172
+ readonly SET_SETTINGS: 6015;
173
+ };
174
+ /**
175
+ * `commandCode` values encoded into the `ff09` frame's `cmdEnc` field (`0x4000 | commandCode`).
176
+ * `ON_OFF_LOCK` (35 = 0x23 → cmdEnc 0x4023) is lock/unlock — video lock AND garage door, both
177
+ * directions. Distinct from {@link LOCK_API_COMMAND}: this is the inner frame opcode, that is the
178
+ * outer envelope's `apiCommand`.
179
+ */
180
+ export declare const LOCK_COMMAND_CODE: {
181
+ /** Lock / unlock the deadbolt (inner frame opcode) — video lock + garage door, both directions. */
182
+ readonly ON_OFF_LOCK: 35;
183
+ /** NOT used by this encoder — see {@link LOCK_API_COMMAND.OPEN_DOOR}; kept as an observed constant only. */
184
+ readonly OPEN_DOOR: 34;
185
+ /** Settings GET inner opcode (53 = 0x35 → cmdEnc 0x4035) — see {@link buildFf09QueryFrame}. */
186
+ readonly GET_SETTINGS: 53;
187
+ /** Settings SET inner opcode (52 = 0x34 → cmdEnc 0x4034) — see {@link buildFf09SettingsFrame}. */
188
+ readonly SET_SETTINGS: 52;
189
+ };
190
+ /** The `TRANSFER_PAYLOAD` inner-command id both envelopes carry (P2P `SET_PAYLOAD.cmd` / MQTT `trans.cmd`). */
191
+ export declare const CMD_TRANSFER_PAYLOAD = 1940;
192
+ export interface Ff09FrameInput {
193
+ /** Actuation direction — the `A3` byte: `true` = engage (byte 0, lock/close), `false` = release (byte 1, unlock/open). */
194
+ engage: boolean;
195
+ /** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
196
+ adminUserId: string;
197
+ /** The acting user's display name (the login email's local-part) — the `A4` field. Omitted when {@link omitUserFields}. */
198
+ username?: string;
199
+ /** The acting member's short id (`member.short_user_id`, hex, e.g. `"0003"`) — the `A5` field. Omitted when {@link omitUserFields}. */
200
+ shortUserId?: string;
201
+ /**
202
+ * Skip the user-attribution TLV fields (A4 username, A5 shortUserId). ✅ The garage-door OPEN command
203
+ * (`apiCommand 6012` / opcode 34) omits them — its plaintext is just `[A1 time, A2 adminUserId, A3 byte]`
204
+ * — verified byte-exact from a live capture. Lock/unlock + garage CLOSE keep them (default false).
205
+ */
206
+ omitUserFields?: boolean;
207
+ /** The lock's serial number — the AES IV (zero-padded to 16). */
208
+ deviceSn: string;
209
+ /** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
210
+ unixTime?: number;
211
+ /** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
212
+ nonce?: number;
213
+ /** Override the envelope `seq_num` (testing). Default = `unixTime`. */
214
+ seqNum?: number;
215
+ }
216
+ export interface Ff09Frame {
217
+ /** The `ff09…` frame, hex — goes verbatim into the envelope `lock_payload` (P2P) or `trans` (MQTT). */
218
+ lockPayload: string;
219
+ /** The cleartext `time` the envelope must carry so the device re-derives the key. */
220
+ time: number;
221
+ /** The envelope `seq_num`. */
222
+ seqNum: number;
223
+ /**
224
+ * The `apiCommand` the envelope must carry for THIS frame (the `TRANSFER_PAYLOAD` `payload.apiCommand`
225
+ * — see {@link LOCK_API_COMMAND}). Returned by the builder so neither transport has to know the
226
+ * frame↔apiCommand pairing: the P2P/MQTT envelope just carries it through. Distinct from the inner
227
+ * frame `commandCode` the builder also picks internally.
228
+ */
229
+ apiCommand: number;
230
+ }
231
+ /**
232
+ * The `TRANSFER_PAYLOAD` (1940) `payload` object both transports carry for an ff09 frame. The index
233
+ * signature keeps it assignable to the routers' generic `Record<string, unknown>` envelope-body param
234
+ * (every field is already `number | string`).
235
+ */
236
+ export interface Ff09TransferPayload {
237
+ apiCommand: number;
238
+ lock_payload: string;
239
+ seq_num: number;
240
+ time: number;
241
+ [field: string]: number | string;
242
+ }
243
+ /**
244
+ * Project a built {@link Ff09Frame} into the `TRANSFER_PAYLOAD` (1940) `payload` — the ONE place the
245
+ * frame→envelope field mapping (`lockPayload`→`lock_payload`, `seqNum`→`seq_num`) lives, so neither the
246
+ * P2P router (`set-payload` body) nor the MQTT router (`trans.payload`) re-spells it.
247
+ */
248
+ export declare function ff09TransferPayload(frame: Ff09Frame): Ff09TransferPayload;
249
+ /**
250
+ * Build an `ff09` command frame. Deterministic given `unixTime`/`nonce`/`seqNum`, so a test can
251
+ * reproduce a captured frame byte-for-byte; otherwise it stamps a fresh `time` per call.
252
+ *
253
+ * Throws on missing identity (empty `adminUserId`/`shortUserId`/`deviceSn`) — an all-empty field would
254
+ * silently produce a frame the device rejects, and both transports send this fire-and-forget (no
255
+ * error comes back), so we fail loud here instead.
256
+ */
257
+ export declare function buildFf09Frame(input: Ff09FrameInput): Ff09Frame;
258
+ export interface Ff09QueryFrameInput {
259
+ /** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
260
+ adminUserId: string;
261
+ /** The lock's serial number — the AES IV (zero-padded to 16). */
262
+ deviceSn: string;
263
+ /** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
264
+ unixTime?: number;
265
+ /** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
266
+ nonce?: number;
267
+ /** Override the envelope `seq_num` (testing). Default = `unixTime`. */
268
+ seqNum?: number;
269
+ }
270
+ /**
271
+ * Build the settings **GET** query frame (`LOCK_COMMAND_CODE.GET_SETTINGS`) — plaintext is just
272
+ * `[A1 time][A2 adminUserId]`, no user-attribution fields (narrower than `omitUserFields` on
273
+ * {@link buildFf09Frame}, which still keeps A3). See the module doc's "Settings GET/SET" section.
274
+ */
275
+ export declare function buildFf09QueryFrame(input: Ff09QueryFrameInput): Ff09Frame;
276
+ export interface Ff09SettingsFrameInput {
277
+ /** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
278
+ adminUserId: string;
279
+ /** The lock's serial number — the AES IV (zero-padded to 16). */
280
+ deviceSn: string;
281
+ /** `A4` — auto-lock enable (confirmed semantics). */
282
+ autoLockEnabled: boolean;
283
+ /** `A5` (u16 LE) — auto-lock delay in seconds (confirmed semantics; captured value 90 = "1.5 min"). */
284
+ autoLockDelaySeconds: number;
285
+ /**
286
+ * `A7` (u16 LE) — semantics UNCONFIRMED, but the value IS readable back from a GET response
287
+ * (response field `a4`), so callers should pass through a just-read current value rather than a
288
+ * guess. See the module doc's "Settings GET/SET" section.
289
+ */
290
+ a7: number;
291
+ /** `A8` (u16 LE) — same caveat as {@link a7} (readable back as GET response field `a5`). */
292
+ a8: number;
293
+ /** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
294
+ unixTime?: number;
295
+ /** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
296
+ nonce?: number;
297
+ /** Override the envelope `seq_num` (testing). Default = `unixTime`. */
298
+ seqNum?: number;
299
+ }
300
+ /**
301
+ * Build the settings **SET** frame (`LOCK_COMMAND_CODE.SET_SETTINGS`) — plaintext is `[A1 time]
302
+ * [A2 adminUserId][A3 1B][A4 1B enable][A5 2B LE delay][A6 1B][A7 2B LE][A8 2B LE][A9 1B]`. `A3`/`A6`/
303
+ * `A9` are sent as the literal `0x00` observed in the one capture available (semantics unconfirmed —
304
+ * see the module doc). `A7`/`A8` are NOT hardcoded here — the caller (the read-modify-write orchestration
305
+ * in the client) is expected to pass through values just read from a GET response.
306
+ */
307
+ export declare function buildFf09SettingsFrame(input: Ff09SettingsFrameInput): Ff09Frame;
308
+ /**
309
+ * Setting-id selectors for the COMPACT single-setting `SET_SETTINGS` write ({@link
310
+ * buildFf09SettingToggleFrame}) — a SHORTER TLV than the full-blob write ({@link buildFf09SettingsFrame},
311
+ * which always sends `A3=0x00` plus all of `A4`-`A9` together for auto-lock). Captured live on a T8531
312
+ * (2026-07-18): toggling Rain Mode off→on produced just `[A1 time][A2 adminUserId][A3=0x07][A4 0/1]` —
313
+ * nothing past `A4`, and `A3` here is a setting-id SELECTOR rather than an unconfirmed constant. The
314
+ * app's own decompiled JS (2026-07-18) mapped the full selector enum (≥14 settings); `RAIN_MODE` is the
315
+ * only one independently wire-captured, the other 6 ids below ship throwing "wire unverified" pending
316
+ * their own capture. See the module doc's "Rain Mode" section.
317
+ *
318
+ * NOT consulted by {@link buildFf09SettingToggleFrame} or anything else on the production path — the
319
+ * wire byte actually comes from the `Command`'s opaque `settingId` field, sourced from `model/`'s OWN
320
+ * copy (`lock.ts`'s `LOCK_SETTING_ID.RAIN_MODE`, per the capability↔transport decorrelation rule this
321
+ * module's doc already covers). This constant is layer-local wire vocab for THIS file's own callers/
322
+ * spec only — it documents the value but carries no production reference, so it can't structurally
323
+ * catch the two copies drifting apart; see `lock.ts`'s doc for that caveat.
324
+ */
325
+ export declare const FF09_SETTING_ID: {
326
+ /** One-touch lock toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
327
+ readonly ONE_TOUCH_LOCK: 1;
328
+ /** Scramble-passcode toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
329
+ readonly SCRAMBLE_PASSCODE: 3;
330
+ /** Wifi-status toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
331
+ readonly WIFI_STATUS: 5;
332
+ /** Event-log-enable toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
333
+ readonly ENABLE_LOG: 6;
334
+ /** Rain Mode toggle on the T8531 video lock. `A4` = 0 off / 1 on. Verified live 2026-07-18. */
335
+ readonly RAIN_MODE: 7;
336
+ /** Privacy-mode toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
337
+ readonly PRIVACY_MODE: 9;
338
+ /** One-touch rear-lock toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
339
+ readonly ONE_TOUCH_REAR_LOCK: 11;
340
+ };
341
+ export interface Ff09SettingToggleFrameInput {
342
+ /** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
343
+ adminUserId: string;
344
+ /** The lock's serial number — the AES IV (zero-padded to 16). */
345
+ deviceSn: string;
346
+ /** Which setting this write targets — the `A3` field. See {@link FF09_SETTING_ID}. */
347
+ settingId: number;
348
+ /** The new value — the `A4` field (0/1). */
349
+ value: boolean;
350
+ /** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
351
+ unixTime?: number;
352
+ /** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
353
+ nonce?: number;
354
+ /** Override the envelope `seq_num` (testing). Default = `unixTime`. */
355
+ seqNum?: number;
356
+ }
357
+ /**
358
+ * Build the settings **SET** frame using the COMPACT single-setting shape (`LOCK_COMMAND_CODE.SET_SETTINGS`,
359
+ * same opcode as {@link buildFf09SettingsFrame}) — plaintext is just `[A1 time][A2 adminUserId]
360
+ * [A3 1B settingId][A4 1B value]`, no `A5`-`A9` at all. Unlike the full-blob write, this is a pure blind
361
+ * write — no GET-then-preserve is needed since it doesn't touch any field the device isn't being told to
362
+ * change. See the module doc's "Rain Mode" section + {@link FF09_SETTING_ID}.
363
+ */
364
+ export declare function buildFf09SettingToggleFrame(input: Ff09SettingToggleFrameInput): Ff09Frame;
365
+ /**
366
+ * Decrypt any `ff09` response frame (e.g. the GET-settings reply) back to its plaintext TLV bytes.
367
+ * Same key/iv derivation as the encoder (`adminUserId.slice(-12) ‖ u32be(keyTime)` / `deviceSn` padded
368
+ * 16), but normal PKCS#7 unpadding (`createDecipheriv` with auto-padding ON) — unlike the encoder,
369
+ * which zero-pads its OWN plaintext before letting AES add a genuine PKCS#7 block on top, a device
370
+ * response is a plain PKCS#7-padded ciphertext with no extra zero-padding layer.
371
+ *
372
+ * Validates the `ff09` magic and the trailing XOR checksum (the frame's own self-check), but does NOT
373
+ * validate `cmdEnc` beyond that — a response's `cmdEnc` carries an extra flag bit vs. a request's
374
+ * (`0x48xx` not `0x40xx`) and this decoder is generic across whatever inner opcode it's carrying.
375
+ *
376
+ * `keyTime` is the response envelope's `time` field parsed as the hex string it actually is (NOT
377
+ * decimal, unlike the `seq_num`/other envelope fields) — see the module doc.
378
+ */
379
+ export declare function decryptFf09Frame(input: {
380
+ lockPayload: string;
381
+ keyTime: number;
382
+ adminUserId: string;
383
+ deviceSn: string;
384
+ }): Buffer;
385
+ /** A decoded settings-response TLV: the leading status byte + the `a1..ad` fields, keyed by separator byte. */
386
+ export interface Ff09SettingsResponse {
387
+ /** The leading status byte (byte 0 of the decrypted plaintext, before the TLV fields). */
388
+ status: number;
389
+ /** `sep` (e.g. `0xa2`) → field bytes. The response's own tag numbering — does NOT line up positionally
390
+ * with the GET/SET request TLVs (see the module doc). */
391
+ fields: Map<number, Buffer>;
392
+ }
393
+ /**
394
+ * Parse a decrypted settings-response plaintext ({@link decryptFf09Frame}'s output) into its leading
395
+ * status byte + `sep|len|bytes` TLV fields. Stops at a `0x00` separator (real tags start at `0xa1`;
396
+ * a `0x00` sep only ever shows up as trailing zero-padding, never a real field) so it doesn't choke on
397
+ * padding past the last real field.
398
+ */
399
+ export declare function parseFf09SettingsResponse(plain: Buffer): Ff09SettingsResponse;
400
+ /** Read a little-endian u16 out of a TLV field buffer (throws on a missing/short field — a caller-side bug, not a wire ambiguity). */
401
+ export declare function readFf09U16LE(field: Buffer | undefined, name: string): number;
402
+ /** Read a single byte out of a TLV field buffer (throws on a missing field — same convention as {@link readFf09U16LE}). */
403
+ export declare function readFf09U8(field: Buffer | undefined, name: string): number;
404
+ /**
405
+ * Read a 2-byte `[hour, minute]` time pair out of a TLV field buffer — the app encodes schedule times
406
+ * as `hour.toString(16)‖minute.toString(16)` (two raw byte VALUES, not a packed LE u16 — see the
407
+ * module doc's "Settings GET/SET" section). Throws on a missing/short field, same convention as
408
+ * {@link readFf09U16LE}.
409
+ */
410
+ export declare function readFf09HourMinute(field: Buffer | undefined, name: string): [number, number];
411
+ /**
412
+ * Decode a parsed `GET_SETTINGS` response into the auto-lock snapshot both transports return from
413
+ * `getAutoLockState` — the response tag map's `a1`=enabled, `a2`=delaySeconds, `a3`=isSchedule,
414
+ * `a4`/`a5`=schedule start/end (`[hour,minute]`). Shared here (rather than copied into each router's
415
+ * `getAutoLockState`) so the P2P and MQTT read paths can't drift on the field mapping.
416
+ */
417
+ export declare function decodeFf09AutoLockSnapshot(parsed: Ff09SettingsResponse): AutoLockSnapshot;
418
+ /**
419
+ * Coerce a settings-response `time` field back to the numeric keyTime, for matching the reply against
420
+ * the GET's own keyTime. Shared by both transports' autolock GET-reply matchers. A string is parsed
421
+ * base-16 (the reply `time` is a hex string on the working, test-backed path — `ff09-mqtt-settings-
422
+ * dispatch.spec` drives the full GET→decrypt→SET flow with a `time.toString(16)` reply); a JSON number
423
+ * is taken as the value directly (unambiguous). A *decimal string* is deliberately NOT handled — it's
424
+ * indistinguishable from hex and never observed on the wire, so parsing an all-digit decimal as hex
425
+ * would silently mis-match. Returns the keyTime, or `undefined` if it doesn't parse to a finite number.
426
+ */
427
+ export declare function ff09ReplyKeyTime(rawTime: string | number): number | undefined;
428
+ /**
429
+ * The read-modify-write core of an auto-lock settings write, shared by both transports' autolock
430
+ * dispatchers ({@link buildFf09QueryFrame} GET → this → send): decrypt the GET-settings reply, pull the
431
+ * current delay + `A7`/`A8` passthrough values off it, and build the SET frame that changes only
432
+ * enable/delay while preserving everything else. The transport owns getting the reply (its own GET wire
433
+ * + reply match) and wrapping/sending the returned frame; the decrypt→read→rebuild sequence lives here
434
+ * once so the two routers can't drift (e.g. the pending `a2`/`a4`/`a5`→`a1` enable-readback fix the
435
+ * module doc flags is then a one-place change).
436
+ */
437
+ export declare function buildFf09AutolockSetFrame(input: {
438
+ lockPayload: string;
439
+ keyTime: number;
440
+ adminUserId: string;
441
+ deviceSn: string;
442
+ enabled: boolean;
443
+ delaySeconds?: number;
444
+ }): Ff09Frame;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared `ffmpeg` spawn helper — the ONE place that shells out to ffmpeg. It decides ffmpeg's own
3
+ * verbosity, prepends the common flags, and routes stderr into the SDK's {@link Logger}, so no caller
4
+ * touches `spawn("ffmpeg", …)` directly. Used by the media paths that mux via ffmpeg (`p2p/media.ts`
5
+ * snapshot/record). Not byte-on-a-wire, so it sits at the transport root
6
+ * rather than in a wire subfolder.
7
+ *
8
+ * Two independent dials:
9
+ * - **ffmpeg verbosity** — how chatty ffmpeg itself is, via `-loglevel`, from the host's
10
+ * `new EufyMega({ ffmpegLogLevel })` config (default `"error"`).
11
+ * - **where it lands** — the host {@link Logger} and its own min-level gate what's actually shown.
12
+ *
13
+ * So `new EufyMega({ ffmpegLogLevel: "trace", logger: new ConsoleLogger() })` surfaces ffmpeg's full
14
+ * trace at `[ffmpeg]` debug lines; the default level + no logger stays silent.
15
+ *
16
+ * WHICH binary runs is a third, orthogonal dial: `new EufyMega({ ffmpegPath })`. See
17
+ * {@link ffmpegExecutable} — the SDK never edits the process `PATH`, so an explicit path is what names
18
+ * a binary that is not on it.
19
+ *
20
+ * @module transport/ffmpeg
21
+ */
22
+ import { type ChildProcess, type StdioOptions } from "node:child_process";
23
+ import { type Logger } from "../core/logger.js";
24
+ /**
25
+ * ffmpeg's `-loglevel` values, quiet → loud. `trace` is the firehose.
26
+ *
27
+ * Exported as the set in data form; not published — `FfmpegLevel` states the same members.
28
+ * @internal
29
+ */
30
+ export declare const FFMPEG_LEVELS: readonly ["quiet", "panic", "fatal", "error", "warning", "info", "verbose", "debug", "trace"];
31
+ /** A valid ffmpeg `-loglevel`. Set via `new EufyMega({ ffmpegLogLevel })`. */
32
+ export type FfmpegLevel = (typeof FFMPEG_LEVELS)[number];
33
+ /**
34
+ * Resolve ffmpeg's own verbosity from the SDK config a host passes (`new EufyMega({ ffmpegLogLevel })`),
35
+ * defaulting to `"error"` (quiet); an unset/invalid value yields the default. This is ffmpeg's
36
+ * `-loglevel`, NOT the SDK's {@link LogLevel} — the two are orthogonal (ffmpeg decides what to emit,
37
+ * the Logger decides what's shown).
38
+ */
39
+ export declare function ffmpegLogLevel(level?: FfmpegLevel): FfmpegLevel;
40
+ /**
41
+ * Resolve WHICH ffmpeg to run from the SDK config a host passes (`new EufyMega({ ffmpegPath })`),
42
+ * defaulting to the bare name `"ffmpeg"` so it is looked up on `PATH`.
43
+ *
44
+ * An environment where no `ffmpeg` is on `PATH` is ordinary, and the SDK never mutates
45
+ * `process.env.PATH` process-wide to reach one — this option is how such a binary is named.
46
+ *
47
+ * A **blank** value counts as absent: an unset host config commonly arrives as `""` or as whitespace
48
+ * from a config file, and neither can name a binary, so spawning it would fail as an `ENOENT` on the
49
+ * empty string — the misleading message this option exists to remove. Any non-blank value is passed
50
+ * through EXACTLY as given, never trimmed: a leading or trailing space is legal in a POSIX path, and
51
+ * rewriting one would make a real file unreachable.
52
+ *
53
+ * The value is NOT probed here. Spawn failure surfaces to the caller as the media path's own "not
54
+ * runnable" rejection, which is the same signal a missing `PATH` entry gives, so there is nothing for
55
+ * an extra `stat` to add. {@link ffmpegAvailable} is the probe.
56
+ */
57
+ export declare function ffmpegExecutable(path?: string): string;
58
+ /**
59
+ * Spawn ffmpeg. Prepends `-hide_banner` + the env-driven `-loglevel` (see {@link ffmpegLogLevel}) to
60
+ * `args`, then forwards the child's stderr to `logger.debug` under an `[ffmpeg]` prefix — the logger's
61
+ * min-level gates visibility, so a `noopLogger` (the default) swallows it. Callers pass only their
62
+ * ffmpeg-specific args and read stdin/stdout off the returned child.
63
+ *
64
+ * `stdio` defaults to all-pipe; pass e.g. `["pipe", "ignore", "pipe"]` to drop stdout (stderr must
65
+ * stay piped for forwarding to work). `level` overrides the resolved `-loglevel` (see
66
+ * {@link ffmpegLogLevel} for precedence); `executable` picks the binary (see
67
+ * {@link ffmpegExecutable}).
68
+ */
69
+ export interface FfmpegSpawnOptions {
70
+ logger?: Logger;
71
+ stdio?: StdioOptions;
72
+ level?: FfmpegLevel;
73
+ /** The ffmpeg binary to run. Default: the bare name, looked up on `PATH`. */
74
+ executable?: string;
75
+ }
76
+ export declare function spawnFfmpeg(args: string[], opts?: FfmpegSpawnOptions): ChildProcess;
77
+ /**
78
+ * Whether the resolved ffmpeg is runnable, by running `-version` on it. Resolves the executable exactly
79
+ * as the media paths do, so the answer is about the SAME binary they will launch.
80
+ */
81
+ export declare function ffmpegAvailable(executable?: string): boolean;
82
+ /**
83
+ * Whether `ffprobe` is on `PATH`. The SDK never spawns it, so there is no executable to resolve and
84
+ * none is taken.
85
+ */
86
+ export declare function ffprobeAvailable(): boolean;
@@ -0,0 +1,20 @@
1
+ export declare const V1_PREFIX = "eufysecurity";
2
+ /** Derive the numeric id-suffix from the station `p2p_did` — a digit-mixing seed used by the key derivation. */
3
+ export declare function getIdSuffix(p2pDid: string): number;
4
+ /** Derive the image "base code" from the camera serial + `p2p_did` — first input to {@link getImageKey}. */
5
+ export declare function getImageBaseCode(serialNumber: string, p2pDid: string): string;
6
+ /** Derive the per-image seed from the `p2p_did` + the image's own code — second input to {@link getImageKey}. */
7
+ export declare function getImageSeed(p2pDid: string, code: string): string;
8
+ /** Derive the AES-128-ECB key for a v1 thumbnail: SHA-256 over the base code + seed, rotated by a hash byte. */
9
+ export declare function getImageKey(serialNumber: string, p2pDid: string, code: string): string;
10
+ /** True if the blob is a legacy v1 `eufysecurity:` image (NOT the v2 variant). */
11
+ export declare function isV1Image(data: Buffer): boolean;
12
+ /**
13
+ * Decode a v1 `eufysecurity:` blob to a plain JPEG buffer using the station's
14
+ * `p2p_did`. Returns null if the blob isn't v1. Mirrors the legacy decode: parse
15
+ * SERIAL + CODE, derive the key, AES-128-ECB-decrypt the first 256 bytes, splice
16
+ * the decrypted head back in front of the plaintext tail.
17
+ */
18
+ export declare function decodeImageV1(data: Buffer, p2pDid: string): Buffer | null;
19
+ /** Decode a recognized v1 (device-key) or v2 (keyless) wrapper; leave other media unchanged. */
20
+ export declare function normalizePushImage(data: Buffer, p2pDid?: string): Buffer;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-channel auto-contrast, a byte-exact port of PIL `ImageOps.autocontrast` (cutoff 0.5%). The lost
3
+ * quant tables leave the reconstruction low-contrast ("foggy"); stretching each channel's clipped range
4
+ * to full scale restores a natural-looking image. Mutates `data` (RGBA) in place.
5
+ *
6
+ * Parity notes vs PIL: the cutoff count is `n * cutoff // 100` (integer floor); it is trimmed off each
7
+ * end by zeroing whole histogram bins until the count is spent; the range is then the first/last
8
+ * non-empty bins; and the LUT truncates toward zero (`int()`), NOT rounds — rounding would shift pixels.
9
+ */
10
+ export declare function autoContrast(data: Uint8Array, width: number, height: number, cutoff?: number): void;
11
+ /** True if the blob is a v2 `v2_eufysecurity:` push thumbnail. */
12
+ export declare function isV2Image(data: Buffer): boolean;
13
+ /**
14
+ * Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2 or the
15
+ * plaintext scan can't be located. The search first chooses subsampling and coarse geometry, derives
16
+ * the fixed MCU count, refines width by row shear, and pins the exact fill height before applying
17
+ * auto-contrast and re-encoding. See the module doc for the keyless-splice rationale.
18
+ */
19
+ export declare function decodeImageV2(data: Buffer): Buffer | null;
@@ -0,0 +1,5 @@
1
+ export * from "./mega-client.js";
2
+ export { randomPhoneModel, randomUserAgent } from "./phone-model.js";
3
+ export * from "./decodeImageV1.js";
4
+ export * from "./decodeImageV2.js";
5
+ export { listLightEffects, listAiSceneRecommendations, type LightEffectSummary } from "./light-catalog.js";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Light-effect **catalogue** over the vendor's `light` HTTP service — `/app/light/discover/list`,
3
+ * `/app/light/lighteffect/batchget` and `/app/light/aigc/recommend/list`, with their response parsing.
4
+ * `light` here is the service path the vendor publishes, which is why this sits with the rest of the
5
+ * HTTP surface.
6
+ *
7
+ * This module fetches a catalogue definition; `transport/dp-preset.ts` turns one into wire fields, and
8
+ * sits at the transport root because both this and the MQTT router serialize through it.
9
+ *
10
+ * The catalogue is keyed by numeric id throughout and carries no model vocabulary.
11
+ */
12
+ import type { MegaHttpClient } from "./mega-client.js";
13
+ import type { DpPresetSpec } from "../dp-preset.js";
14
+ /**
15
+ * One entry in the light-effect gallery — the browsable catalogue behind {@link listLightEffects}.
16
+ * `buildable` says whether the entry's definition can be turned into wire fields at all: false when it
17
+ * carries no layers, or a layer shape this SDK cannot encode.
18
+ */
19
+ export interface LightEffectSummary {
20
+ /** The catalogue id that identifies this effect on the wire. */
21
+ lightId: number;
22
+ /** Display name from the catalogue (e.g. "Presidents Day"), when present. */
23
+ name?: string;
24
+ /** Preview swatch — `"RRGGBB|RRGGBB…"` — when the catalogue entry carries one. */
25
+ colors?: string;
26
+ /** Whether this entry can be serialized to wire fields. */
27
+ buildable: boolean;
28
+ }
29
+ /**
30
+ * Browse the light-effect gallery — the catalogue of `lightId`s that `setEffect` accepts. Unions every
31
+ * scene/light id found anywhere in the `/app/light/discover/list` carousel (walked recursively) with a
32
+ * scan over the default `LIGHT_EFFECT_ID_WINDOW` (the dense id band holding the app's category tabs) plus any
33
+ * `ids` given, then resolves them via `/app/light/lighteffect/batchget` (which returns an entry only
34
+ * for ids that exist). Pass `idRange` to widen/narrow the scan or `ids` to fetch specific ones.
35
+ *
36
+ * `batchget` is issued in chunks of `BATCHGET_CHUNK` (100) ids — the scanned set runs to the hundreds,
37
+ * so one request does not swallow it whole.
38
+ */
39
+ export declare function listLightEffects(mega: MegaHttpClient, opts?: {
40
+ idRange?: [number, number];
41
+ ids?: number[];
42
+ }): Promise<LightEffectSummary[]>;
43
+ /**
44
+ * List the **AI-generated ambient scenes** ("aigc") — e.g. "Enchanting Starry Night", "Moonlit
45
+ * Serenity". A DIFFERENT resource from {@link listLightEffects}'s id-addressable catalogue:
46
+ * `/app/light/aigc/recommend/list` returns scene **keywords only, with NO `lightId`**, so they can't be
47
+ * driven through `setEffect` — the app turns a chosen keyword into an applied effect via a server-side
48
+ * generate step the SDK hasn't reversed yet. Surfaced for visibility. Needs a `region` (400s
49
+ * without one) and, for some accounts, a device `sn`; `region` defaults to `"eu"` (the endpoint
50
+ * returned an identical list across every shard tried).
51
+ */
52
+ export declare function listAiSceneRecommendations(mega: MegaHttpClient, opts?: {
53
+ region?: string;
54
+ sn?: string;
55
+ }): Promise<string[]>;
56
+ /**
57
+ * Fetch a gallery effect by exact catalog `lightId` and parse it into the {@link DpPresetSpec} the
58
+ * MQTT router serializes — the "Auto inside setEffect" resolution. Matches the EXACT id (never
59
+ * substitutes a neighbour — the DP write is fire-and-forget). Only effects with directly-serializable
60
+ * `params.layer` data are supported; flat/grouped entries throw a clear error rather than emit a guess.
61
+ */
62
+ export declare function resolveLightEffect(mega: MegaHttpClient, lightId: number): Promise<DpPresetSpec>;