@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,107 @@
1
+ /**
2
+ * Secure MQTT ("smqtt") — Anker AIoT broker, mutual-TLS.
3
+ *
4
+ * Broker: mqtts://aiot-mqtt-{region}.anker.com:8883. Credentials come from the
5
+ * mega API `get_user_mqtt_info` call: a per-user client certificate + RSA key,
6
+ * and a server CA (the `aws_root_ca1_pem` field, despite the name, is a GoDaddy
7
+ * root). The default clientId is the cert CN = `{user_id}-{app_name}`.
8
+ *
9
+ * Topic vocabulary and the per-line credential scope live in `./topics.ts` — publish to `/req`
10
+ * (cloud→device commands), subscribe to whichever inbound legs that line uses. Granted for eufy_mega +
11
+ * eufy_home devices; eufy_security (cameras) are DENIED here (they use P2P) — see src/types.ts
12
+ * classifyDevice.
13
+ */
14
+ import { EventEmitter } from "node:events";
15
+ import type { EufyDevice, RealtimeTransport } from "../../core/types.js";
16
+ import { type Logger } from "../../core/logger.js";
17
+ /**
18
+ * Per-user mTLS credentials as returned by get_user_mqtt_info.
19
+ *
20
+ * Broker credentials as the cloud returns them — internal transport detail.
21
+ * @internal
22
+ */
23
+ export interface SecureMqttCredentials {
24
+ /** Broker host, e.g. aiot-mqtt-eu.anker.com (port assumed 8883). */
25
+ endpoint_addr: string;
26
+ endpoint_port?: number;
27
+ /** PEM client certificate (CN = {user_id}-{app_name}). */
28
+ certificate_pem: string;
29
+ /** PEM client private key. */
30
+ private_key: string;
31
+ /** PEM server CA (field is misleadingly named aws_root_ca1_pem — it's a GoDaddy root). */
32
+ aws_root_ca1_pem: string;
33
+ /** Default clientId (cert CN = thing_name). */
34
+ thing_name?: string;
35
+ /** App scope the cert is granted for (the `{app_name}` topic segment) — e.g. `eufy_mega`. */
36
+ app_name?: string;
37
+ /** The account/user id the cert is bound to. */
38
+ user_id?: string;
39
+ }
40
+ /**
41
+ * Construction options for the internal broker client.
42
+ * @internal
43
+ */
44
+ export interface SecureMqttOptions {
45
+ credentials: SecureMqttCredentials;
46
+ clientId?: string;
47
+ /** Dial this IP directly instead of resolving `credentials.endpoint_addr` via DNS — the broker
48
+ * hostname fronts multiple independent backend instances that do NOT share subscribe/publish
49
+ * routing (see `transport/mqtt/broker-discovery.ts`); pin to the instance a device's session was
50
+ * confirmed to be on, found via {@link discoverReachableInstance}. */
51
+ instanceIp?: string;
52
+ /**
53
+ * mqtt.js's own `reconnectPeriod` (ms) — auto-reconnect on an unexpected drop. Default `5000`,
54
+ * right for the one long-lived connection (the facade's auto-started `this.transport`). A **one-shot**
55
+ * connection (opened for a single command, torn down right after — see `MqttCommandRouter.ensureSecurityMqttFor`)
56
+ * must pass `0` here: with a nonzero period, a connect that never establishes (or drops right after)
57
+ * leaves the underlying mqtt.js client retrying against that instance forever, orphaned in the
58
+ * background — {@link SecureMqtt.connect} rejecting/resolving doesn't stop it, only `end()` does, and
59
+ * a one-shot caller has no reason to ever call `end()` again after its one command is done.
60
+ */
61
+ reconnectPeriod?: number;
62
+ /** Diagnostics sink. Omit for silence. */
63
+ logger?: Logger;
64
+ }
65
+ /**
66
+ * The broker client. Internal transport; a host reaches appliances through the capability surface.
67
+ * @internal
68
+ */
69
+ export declare class SecureMqtt extends EventEmitter implements RealtimeTransport {
70
+ readonly kind: "smqtt";
71
+ private client?;
72
+ private readonly o;
73
+ private readonly logger;
74
+ constructor(opts: SecureMqttOptions);
75
+ get id(): string;
76
+ /**
77
+ * Open the broker connection, resolving once it is established. Pinned to a broker instance's IP, or
78
+ * to the plain hostname; only the former needs its own TLS shape, see `./bare-ip-tls.ts`.
79
+ */
80
+ connect(): Promise<void>;
81
+ /**
82
+ * Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
83
+ * four topics for `eufy_life`).
84
+ *
85
+ * The grants are INSPECTED, not assumed: AWS IoT answers a policy-denied filter with a
86
+ * `SUBACK_FAILURE` (`0x80`) grant rather than failing the SUBSCRIBE, so subscribing with a credential
87
+ * whose scope doesn't cover the topic looks identical to success and then delivers nothing. A denied
88
+ * topic is reported via `error` naming the credential scope; only an all-denied device throws, so a
89
+ * line that grants its state channel but refuses (say) the OTA leg still works.
90
+ */
91
+ subscribeDevice(device: EufyDevice): Promise<void>;
92
+ /**
93
+ * Publish a raw payload to an MQTT topic (the command leg — `cmd/{app}/{pn}/{sn}/req`). The `body`
94
+ * is a pre-built envelope the caller supplies (the command router builds it). QoS 1 by default (the
95
+ * broker acks). This is the outbound leg — commands, alongside the receive-only `/res` subscriptions.
96
+ *
97
+ * NOTE: which cert scope is used does NOT gate publishing to a security device's topic — confirmed
98
+ * live, an `eufy_mega`-scoped cert and an `eufy_security`-scoped cert get the identical grant/deny
99
+ * pattern. What actually matters is landing on the broker instance that currently holds the device's
100
+ * session (`ensureSecurityMqttFor`/`broker-discovery.ts` — the NLB fronts several instances that
101
+ * don't share subscribe/publish routing).
102
+ */
103
+ publish(topic: string, body: string | Buffer, opts?: {
104
+ qos?: 0 | 1 | 2;
105
+ }): Promise<void>;
106
+ disconnect(): Promise<void>;
107
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Anker secure-MQTT topic vocabulary + the per-line credential scope.
3
+ *
4
+ * Two facts about this broker drive everything here, both CONFIRMED live (2026-07-28) against a real
5
+ * `eufy_life` light and cross-read from the disassembled V6 app
6
+ * (`com.anker.esiotkit.security_device.mqtt.SecurityMqttConstant`, `mqtt_serve.dart`):
7
+ *
8
+ * - **The device→app leg is not `/res` for every line.** `eufy_life` reports land on `/app/res`;
9
+ * the app subscribes four topics per light device and `/res` carries nothing. Other lines use
10
+ * `/res` alone.
11
+ * - **The topic space is partitioned by credential.** `get_user_mqtt_info` returns a DIFFERENT
12
+ * certificate per `app-name`, and the AWS IoT policy granting `eufy_life/...` is attached only to
13
+ * the `eufy_life` cert. Subscribing to a light's topics with the default credential is *silently*
14
+ * denied — the broker answers SUBACK with a 128 grant rather than rejecting, which is why this
15
+ * produced total silence instead of an error.
16
+ *
17
+ * Scope is derived from `EufyDevice.category` — a device-record field, not a capability — so this
18
+ * stays on the transport side of the capability↔transport boundary.
19
+ */
20
+ import type { EufyDevice } from "../../core/types.js";
21
+ /**
22
+ * Which `get_user_mqtt_info` credential a device's realtime traffic rides on. `"default"` is the
23
+ * headerless credential (observed `eufy_mega`-scoped) that serves every non-life MQTT device.
24
+ *
25
+ * A scope exists only where a DISTINCT credential does. `eufy_life` is one because its topic space is
26
+ * denied to the default credential. The Clean line (`eufy_home`) is NOT: its
27
+ * `cmd/eufy_home/<model>/<sn>/res` subscribe is confirmed against a T2351 on the
28
+ * default credential, which is the only one `get_user_mqtt_info` was ever asked for. Giving it a scope
29
+ * of its own would open a second connection under the SAME `thing_name` — AWS IoT treats a duplicate
30
+ * client id as a takeover and evicts the incumbent, so both lines would flap.
31
+ */
32
+ export type MqttScope = "default" | "eufy_life";
33
+ /**
34
+ * The credential scope a device's topics are granted under. Derived from `EufyDevice.category` — a
35
+ * device record field, not a capability — so this stays on the transport side of the boundary.
36
+ */
37
+ export declare function mqttScopeFor(device: EufyDevice): MqttScope;
38
+ /**
39
+ * The `app-name` request header value for a scope, or `undefined` for the default (headerless)
40
+ * credential — matching the optional argument of `getUserMqttInfo`.
41
+ */
42
+ export declare function mqttAppName(scope: MqttScope): string | undefined;
43
+ /**
44
+ * Build a topic for a device: `cmd/<prefix>/<model>/<sn>/<req|res>`. `leg` is required — the two
45
+ * directions are one word apart and publishing to the wrong one fails silently. The prefix is
46
+ * derived by the module-local `topicPrefix` — `eufy_home` for clean-line devices, `device.category` elsewhere.
47
+ */
48
+ export declare function secureTopic(device: EufyDevice, leg: "req" | "res"): string;
49
+ /**
50
+ * Every topic to subscribe for a device's inbound traffic.
51
+ *
52
+ * `eufy_life` (smart lights) gets the app's full four-topic set: `/app/res` (the state channel),
53
+ * `/res` (present in the app's subscribe list but observed to carry nothing on this line),
54
+ * `synq/…/state_info` (online/offline), and `/app/ota/res` (OTA progress).
55
+ *
56
+ * Clean-line devices (vacuum/mower) subscribe four topics on the `eufy_home` prefix:
57
+ * - `cmd/…/res` — device→app DP reports and command replies (confirmed live on T2351)
58
+ * - `biz/…/res` — cloud→app business-layer responses (TopicManager.getBizReqTopic())
59
+ * - `biz/…/req` — cloud ACKs for app→cloud business requests (subscribe for ACKs)
60
+ * - `dt/…/param_info` — device-twin parameter push
61
+ *
62
+ * Every other line subscribes `/res` alone.
63
+ */
64
+ export declare function subscribeTopics(device: EufyDevice): readonly string[];
65
+ /** A parsed inbound topic. `tail` is everything after the serial (`res`, `app/res`, `state_info`, …). */
66
+ export interface ParsedTopic {
67
+ root: string;
68
+ category: string;
69
+ model: string;
70
+ sn: string;
71
+ tail: string;
72
+ }
73
+ /**
74
+ * Split an inbound topic into its parts. All roots this broker uses (`cmd/…`, `synq/…`, `biz/…`,
75
+ * `dt/…`) put the serial at index 3 regardless of how deep the tail runs, so the serial is read
76
+ * positionally rather than from the end — `…/<sn>/app/res` and `…/<sn>/app/ota/res` would otherwise
77
+ * yield the tail segment as the device id. Returns `undefined` on any shape this doesn't recognise,
78
+ * so an unparsed topic leaves `deviceSn` unset instead of carrying a guess.
79
+ */
80
+ export declare function parseSecureTopic(topic: string): ParsedTopic | undefined;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ADTS (Audio Data Transport Stream) helpers — the framing the device expects on the audio path a
3
+ * host pushes toward a camera. A host hands over an arbitrarily-chunked byte stream (an encoder's
4
+ * stdout, a file read), so frame boundaries have to be RECOVERED from the stream rather than assumed
5
+ * to align with the chunks; this is the single place that scans for them.
6
+ *
7
+ * An ADTS frame is a 7-byte header (9 with CRC) followed by the raw audio payload, and the header's
8
+ * own `frameLength` field spans header + payload — so the scan is self-delimiting once synced.
9
+ * Header layout, MSB-first, as the V6 app writes it (`media/player/audio/AacEncode.java:43-51`):
10
+ *
11
+ * ```
12
+ * byte 0 1111 1111 syncword high
13
+ * byte 1 1111 iilp syncword low · i=MPEG version+layer · p=1 ⇒ NO CRC (2 fewer header bytes)
14
+ * byte 2 ppff ffpc p=profile-1 · f=sampling-frequency index · c=channel-config high bit
15
+ * byte 3 cc.. LLL c=channel-config low 2 bits · L=frameLength bits 12..11
16
+ * byte 4 LLLL LLLL frameLength bits 10..3
17
+ * byte 5 LLLb bbbb frameLength bits 2..0 · b=buffer fullness high
18
+ * byte 6 bbbb bbnn buffer fullness low · n=frames-in-block minus one
19
+ * ```
20
+ *
21
+ * The device's parameters are fixed and verified against the app's own encoder configuration
22
+ * (`AacEncode.java:23-41`, `player/audio/BDBAudioConfig.java:6-66`): AAC-LC, 16 kHz, mono. One AAC-LC
23
+ * frame is 1024 samples, so at 16 kHz each frame carries exactly 64 ms of audio — the cadence the
24
+ * send side paces on.
25
+ *
26
+ * @module p2p/adts
27
+ */
28
+ /** Samples per AAC-LC frame — fixed by the codec, not by these parameters. */
29
+ export declare const AAC_SAMPLES_PER_FRAME = 1024;
30
+ /** Sample rate the device's audio path runs at. */
31
+ export declare const AAC_SAMPLE_RATE = 16000;
32
+ /**
33
+ * Duration one frame represents, in milliseconds — `1024 / 16000`, exactly 64 ms. The send side both
34
+ * paces on this and steps its frame timestamp by it.
35
+ */
36
+ export declare const AAC_FRAME_MS: number;
37
+ /**
38
+ * Largest frame the device accepts. The app drops anything longer before it reaches the wire
39
+ * (`media/recorder/recorder/BDBRawAudioRecorder.java:98`), so a longer frame is a caller bug rather
40
+ * than something to split or truncate.
41
+ */
42
+ export declare const MAX_AUDIO_FRAME_BYTES = 640;
43
+ /** Parsed fields of one ADTS header, with the payload it introduces. */
44
+ export interface AdtsHeader {
45
+ /** Header + payload length, as declared by the header itself. */
46
+ frameLength: number;
47
+ /** Bytes of header ahead of the payload — 7 without a CRC, 9 with one. */
48
+ headerLength: number;
49
+ /** Profile field, `1` for AAC-LC. */
50
+ profile: number;
51
+ /** Index into the ADTS sampling-frequency table, `8` for 16 kHz. */
52
+ frequencyIndex: number;
53
+ /** Channel configuration, `1` for mono. */
54
+ channels: number;
55
+ }
56
+ /**
57
+ * Read an ADTS header at `offset`, or `undefined` when the bytes there are not a plausible header —
58
+ * no syncword, a `frameLength` shorter than its own header, or not enough bytes to decide. Says
59
+ * nothing about whether the frame's payload has arrived yet; that is the scanner's job.
60
+ */
61
+ export declare function parseAdtsHeader(buf: Buffer, offset?: number): AdtsHeader | undefined;
62
+ /**
63
+ * Whether a header describes the audio parameters the device's path is fixed at — AAC-LC, 16 kHz,
64
+ * mono. A stream at any other rate or channel count is rejected rather than resampled: the device has
65
+ * no way to be told otherwise, so passing it through would produce audio at the wrong pitch and speed.
66
+ */
67
+ export declare function isSupportedAdts(h: AdtsHeader): boolean;
68
+ /**
69
+ * Human-readable reason a header is unsupported, for the error a caller sees. Reports the decoded
70
+ * values so a mis-configured encoder is obvious from the message alone.
71
+ */
72
+ export declare function describeAdts(h: AdtsHeader): string;
73
+ /**
74
+ * Incremental ADTS frame scanner. Feed it byte chunks in arrival order and it yields whole frames,
75
+ * holding a partial frame across calls — which is the whole point: a host's chunk boundaries have no
76
+ * relationship to frame boundaries, and a naive per-chunk parse would either split frames or resync
77
+ * mid-payload and emit garbage.
78
+ *
79
+ * A byte run that does not begin with a syncword is skipped one byte at a time until one is found, so
80
+ * a stream that starts mid-frame (or carries an encoder's preamble) recovers instead of failing.
81
+ */
82
+ export declare class AdtsFrameReader {
83
+ private buffered;
84
+ /**
85
+ * Append `chunk` and return every complete frame now available, each a standalone buffer that still
86
+ * carries its own ADTS header (the device expects the header on the wire).
87
+ */
88
+ push(chunk: Buffer): Buffer[];
89
+ /** Bytes held back awaiting the rest of their frame — non-zero mid-stream, zero on a clean boundary. */
90
+ get pending(): number;
91
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Annex-B elementary-stream helpers — the single NAL-sniffing source of truth for the P2P media
3
+ * pipeline. Both the live source (codec tag on every frame) and the fMP4 muxer (parameter sets for
4
+ * the init segment) read the stream through here, so the start-code scan + NAL-type decode lives in
5
+ * exactly one place.
6
+ *
7
+ * H.264 (Annex-B): NAL type = `byte & 0x1f`; SPS = 7, PPS = 8, IDR = 5.
8
+ * H.265/HEVC (Annex-B): NAL type = `(byte >> 1) & 0x3f`; VPS = 32, SPS = 33, PPS = 34, IDR = 19/20.
9
+ *
10
+ * @module p2p/annexb
11
+ */
12
+ import type { VideoCodec } from "../../core/contracts.js";
13
+ /** Split an Annex-B access unit into its individual NAL bodies (start codes stripped). */
14
+ export declare function splitAnnexbNals(buf: Buffer): Buffer[];
15
+ /**
16
+ * Sniff the codec of an Annex-B access unit by its parameter-set NAL units. Returns `undefined` when
17
+ * the buffer carries no config NAL (a plain delta frame) — the caller should carry the last-known
18
+ * codec rather than guess. Only the first 64 bytes are scanned (config NALs lead the access unit).
19
+ */
20
+ export declare function sniffAnnexbCodec(buf: Buffer): VideoCodec | undefined;
21
+ /** Parameter sets extracted from a keyframe access unit — the input to an fMP4 init segment. */
22
+ export interface ParamSets {
23
+ codec: VideoCodec;
24
+ /** H.264 SPS / H.265 SPS NAL bodies (start-code stripped), in stream order. */
25
+ sps: Buffer[];
26
+ /** H.264 PPS / H.265 PPS NAL bodies. */
27
+ pps: Buffer[];
28
+ /** H.265 VPS NAL bodies (empty for H.264). */
29
+ vps: Buffer[];
30
+ }
31
+ /**
32
+ * Extract the SPS/PPS (and, for H.265, VPS) parameter-set NAL bodies from a keyframe access unit.
33
+ * Each returned buffer is the raw NAL (start-code stripped), ready to embed in an `avcC` / `hvcC`
34
+ * decoder-config record. Returns `undefined` if no parameter sets are present.
35
+ */
36
+ export declare function extractParamSets(buf: Buffer): ParamSets | undefined;
37
+ /**
38
+ * The parameter sets in force after `buf`, folding what it announces into `current`.
39
+ *
40
+ * A camera commonly announces SPS/PPS ONCE, with the first keyframe of a stream, so anything that will
41
+ * later hand a burst to a decoder has to watch EVERY unit go past — including ones it discards.
42
+ *
43
+ * Folds per kind rather than replacing wholesale, because a decoder retains the last set it was given of
44
+ * EACH kind: a unit announcing an SPS alone re-states that SPS and says nothing about the PPS, so
45
+ * replacing the whole record would drop a PPS that is still in force. A codec change replaces
46
+ * everything — sets from another codec describe a different bitstream.
47
+ *
48
+ * Cheap on the overwhelmingly common case: {@link extractParamSets} answers from a bounded head scan when
49
+ * a unit carries no config NAL, so an ordinary delta frame costs no full-buffer walk.
50
+ */
51
+ export declare function updatedParamSets(buf: Buffer, current: ParamSets | undefined): ParamSets | undefined;
52
+ /**
53
+ * Re-emit `sets` as Annex-B NALs immediately ahead of `annexb`, so a unit whose parameter sets were
54
+ * sent earlier in the stream becomes decodable on its own.
55
+ *
56
+ * A decoder reads parameter sets in stream order, so they are emitted VPS → SPS → PPS: a PPS ahead of
57
+ * the SPS it references is as useless as none at all. Sets carrying no NALs return the input unchanged
58
+ * rather than an equal copy.
59
+ *
60
+ * Emitting a duplicate set is harmless — a decoder overwrites the entry with the same id — which is why
61
+ * this needs no knowledge of what the unit already carries; {@link extractParamSets} answers what a
62
+ * unit carries already.
63
+ */
64
+ export declare function prefixParamSets(annexb: Buffer, sets: ParamSets): Buffer;
65
+ /**
66
+ * Whether an Annex-B access unit contains an IDR (keyframe) NAL. Cheap scan used to key-align the
67
+ * ring buffer / fragment boundaries when the frame header's keyframe flag isn't authoritative.
68
+ */
69
+ export declare function hasIdr(buf: Buffer, codec: VideoCodec): boolean;
70
+ /** A width/height pair in luma samples. */
71
+ export interface Size {
72
+ width: number;
73
+ height: number;
74
+ }
75
+ /**
76
+ * What a parameter set says a picture's dimensions are — both of them.
77
+ *
78
+ * A stream states TWO sizes and a consumer needs whichever matches what it holds. {@link CodedGeometry.width}
79
+ * and `height` are the DISPLAY size, the picture as a viewer should see it. {@link CodedGeometry.coded} is the
80
+ * size it is actually coded at, rounded up to the macroblock (H.264) or CTU (H.265) grid, and
81
+ * {@link CodedGeometry.crop} is the window between them, already scaled from the chroma units the syntax
82
+ * states them in into luma samples.
83
+ *
84
+ * The distinction is not a refinement: 1080 is not a multiple of 16, so the commonest geometry there is codes
85
+ * 1088 rows and crops 8 away. A caller that muxes or measures wants the display size; a caller DECODING frames
86
+ * itself gets the coded size back from its decoder and needs the window to crop with — one that assumes the two
87
+ * are the same emits eight rows of encoder padding and calls the result 1088 tall.
88
+ */
89
+ export interface CodedGeometry extends Size {
90
+ /** The size the picture is coded at — macroblock- or CTU-aligned, and never smaller than the display size. */
91
+ coded: Size;
92
+ /** The crop (H.264) or conformance (H.265) window, in LUMA samples. */
93
+ crop: {
94
+ left: number;
95
+ top: number;
96
+ right: number;
97
+ bottom: number;
98
+ };
99
+ }
100
+ /**
101
+ * Profiles whose SPS carries the chroma, bit-depth and scaling-matrix fields (H.264 Annex A).
102
+ *
103
+ * `144` is the 2005-edition High 4:4:4 that `244` replaced. It is absent from the current standard's list but
104
+ * present in streams, and a set read without its branch lands mid-element — so it is carried here rather
105
+ * than left to misparse into a plausible geometry.
106
+ */
107
+ export declare const H264_CHROMA_PROFILES: Set<number>;
108
+ /**
109
+ * The picture geometry a decoder will produce from the parameter sets in force, or `undefined` when no
110
+ * SPS could be read.
111
+ *
112
+ * This is the authority on a live stream's geometry. A frame header states the geometry at capture start,
113
+ * and a source that reconfigures mid-session leaves it contradicting the bytes it is sending — so a
114
+ * consumer that rebuilt a decoder from the header would size it for a picture the stream is not carrying.
115
+ * The SPS is what the picture actually is.
116
+ *
117
+ * The crop and conformance offsets are part of the answer rather than a refinement of it: 1080 is not a
118
+ * multiple of the 16-sample macroblock, so a 1080p H.264 stream codes 1088 rows and crops 8 away. A read
119
+ * that stopped at the coded size would be wrong by exactly that on the commonest geometry there is.
120
+ *
121
+ * Answers from the LAST SPS of the set, which is the one in force. AV1 is not parsed — the SDK decodes no
122
+ * AV1 sequence header, and a size from another codec's syntax would be a fabrication.
123
+ */
124
+ export declare function codedGeometry(sets: ParamSets): CodedGeometry | undefined;
@@ -0,0 +1,166 @@
1
+ export interface Address {
2
+ host: string;
3
+ port: number;
4
+ }
5
+ /** Magic word at the start of every P2P data frame. */
6
+ export declare const MAGIC_WORD = "XZYH";
7
+ /** UDP message types (1st byte always 0xf1). */
8
+ export declare const RequestMessageType: {
9
+ readonly LOCAL_LOOKUP: Buffer<ArrayBuffer>;
10
+ readonly LOOKUP_WITH_KEY: Buffer<ArrayBuffer>;
11
+ readonly LOOKUP_WITH_KEY2: Buffer<ArrayBuffer>;
12
+ readonly CHECK_CAM: Buffer<ArrayBuffer>;
13
+ readonly CHECK_CAM2: Buffer<ArrayBuffer>;
14
+ readonly PING: Buffer<ArrayBuffer>;
15
+ readonly PONG: Buffer<ArrayBuffer>;
16
+ readonly DATA: Buffer<ArrayBuffer>;
17
+ readonly ACK: Buffer<ArrayBuffer>;
18
+ readonly END: Buffer<ArrayBuffer>;
19
+ };
20
+ export declare const ResponseMessageType: {
21
+ readonly LOCAL_LOOKUP_RESP: Buffer<ArrayBuffer>;
22
+ readonly LOOKUP_ADDR: Buffer<ArrayBuffer>;
23
+ readonly LOOKUP_ADDR2: Buffer<ArrayBuffer>;
24
+ readonly CAM_ID: Buffer<ArrayBuffer>;
25
+ readonly TURN_SERVER_CAM_ID: Buffer<ArrayBuffer>;
26
+ readonly PING: Buffer<ArrayBuffer>;
27
+ readonly PONG: Buffer<ArrayBuffer>;
28
+ readonly DATA: Buffer<ArrayBuffer>;
29
+ readonly ACK: Buffer<ArrayBuffer>;
30
+ readonly END: Buffer<ArrayBuffer>;
31
+ };
32
+ /** P2P data-channel types (2nd byte of the data-type header). */
33
+ export declare const P2PDataType: {
34
+ readonly DATA: 0;
35
+ readonly VIDEO: 1;
36
+ readonly CONTROL: 2;
37
+ readonly BINARY: 3;
38
+ };
39
+ export declare const P2PDataTypeHeader: {
40
+ readonly DATA: Buffer<ArrayBuffer>;
41
+ readonly VIDEO: Buffer<ArrayBuffer>;
42
+ readonly CONTROL: Buffer<ArrayBuffer>;
43
+ readonly BINARY: Buffer<ArrayBuffer>;
44
+ };
45
+ /** The fixed-width 16-byte header of every P2P data frame (after "XZYH"). */
46
+ export declare const P2P_DATA_HEADER_BYTES = 16;
47
+ export interface P2PDataFrameHeader {
48
+ commandId: number;
49
+ bytesToRead: number;
50
+ channel: number;
51
+ signCode: number;
52
+ type: number;
53
+ }
54
+ /** Frame a UDP packet: `[msgType:2][payloadLen:2 BE][payload]`. */
55
+ export declare function frameMessage(msgType: Buffer, payload?: Buffer): Buffer;
56
+ /** True if `msg` begins with the given 2-byte message type. */
57
+ export declare function hasHeader(msg: Buffer, type: Buffer): boolean;
58
+ /** Pad a string into a fixed-width (multiple-of-chunk) zero-filled buffer. */
59
+ export declare function stringWithLength(input: string, chunkLength?: number): Buffer;
60
+ /** `EUPRCAM-000000-XXXXX` → the 20-byte buffer used in lookup/check payloads. */
61
+ export declare function p2pDidToBuffer(p2pDid: string): Buffer;
62
+ /**
63
+ * Decode a station's `p2p_conn` / `app_conn` string into the eufy cloud lookup
64
+ * server addresses (port 32100). XOR cipher with a fixed lookup table, seed 0x39.
65
+ */
66
+ export declare function decodeP2PCloudIPs(data: string): Address[];
67
+ /**
68
+ * The "Level 1" AES-128-ECB key for P2P command/control encryption — derivable
69
+ * from sn + p2p_did alone (no cipher needed). HomeBase control-channel
70
+ * notifications (sensor/alarm events) use exactly this key.
71
+ */
72
+ export declare function p2pCommandEncryptionKey(stationSn: string, p2pDid: string): string;
73
+ /** AES-128-ECB decrypt of a P2P payload (no padding). */
74
+ export declare function decryptP2PData(data: Buffer, key: Buffer): Buffer;
75
+ /** The `cipher_id` named in a decrypted `CMD_GATEWAYINFO` (1100) payload (uint16 LE @0). */
76
+ export declare function gatewayInfoCipherId(gwPayload: Buffer): number;
77
+ /** Options for {@link eciesUnwrap}. */
78
+ export interface EciesUnwrapOptions {
79
+ /**
80
+ * When true, the envelope carries a trailing 32-byte HMAC tag over `iv ‖ ct` (the video
81
+ * `CMD_VIDEO_FRAME` envelope, which is `ephPub(33) ‖ iv(16) ‖ ct(48) ‖ HMAC(32)`). The tag is
82
+ * verified with `kdf[16:48]` as the MAC key and a mismatch yields `undefined`. When false (the
83
+ * `CMD_GATEWAYINFO` envelope, `ephPub(33) ‖ iv(16) ‖ ct(48)`) there is no HMAC step.
84
+ */
85
+ verifyHmac?: boolean;
86
+ /** Whether the AES-128-CBC plaintext is PKCS7-padded (video: true). Default false (zero-/no-pad). */
87
+ pkcs7?: boolean;
88
+ }
89
+ /**
90
+ * The shared ECIES unwrap primitive used by both the gateway and the video paths.
91
+ *
92
+ * `envelope` = `ephemeralPub(33, compressed P-256) ‖ iv(16) ‖ ct(48) [‖ HMAC(32)]`. Derivation:
93
+ * `S = ECDH_X(eccPriv, ephPub)` → `kdf = eufyKDF(S)` → `aesKey = kdf[0:16]`; (optionally verify
94
+ * `HMAC(kdf[16:48], iv‖ct) == tag`) → `AES-128-CBC(aesKey, iv, ct)` → plaintext. Returns the
95
+ * decrypted bytes, or `undefined` on any parse / HMAC / cipher failure (fails closed, never throws).
96
+ */
97
+ export declare function eciesUnwrap(envelope: Buffer, eccPrivateKeyHex: string, options?: EciesUnwrapOptions): Buffer | undefined;
98
+ /**
99
+ * Derive the P2P **level-2** (signCode 2/8) AES-256-GCM session key from a *decrypted*
100
+ * `CMD_GATEWAYINFO` (1100) payload and the station's ECC private key (from `get_ciphers`).
101
+ *
102
+ * Layout of the decrypted payload: `cipher_id(2 LE) ‖ 0000 ‖ ECIES-envelope(129)`, where the
103
+ * envelope is `ephemeralPub(33, compressed P-256) ‖ iv(16) ‖ ciphertext(48)`. ECIES =
104
+ * ECDH(eccPriv, ephemeralPub) → eufyKDF(SHA-256/HMAC, label "ECIES") → AES-128-CBC decrypt →
105
+ * 32-byte session key. Reversed from `libmega_media_sdk.so`; verified to reproduce the live key.
106
+ * Returns the 32-byte key, or undefined if the inputs don't parse.
107
+ */
108
+ export declare function deriveLevel2KeyFromGatewayInfo(gwPayload: Buffer, eccPrivateKeyHex: string): Buffer | undefined;
109
+ /** Local-lookup payload: two zero bytes. */
110
+ export declare function buildLocalLookupPayload(): Buffer;
111
+ /** Cloud-lookup payload (variant 2): `[p2pDid:20][dskKey][0x00000000]`. */
112
+ export declare function buildLookupWithKeyPayload2(p2pDid: string, dskKey: string): Buffer;
113
+ /**
114
+ * Cloud-lookup payload — the CLASSIC variant (`LOOKUP_WITH_KEY`, 0xf126), reverse-engineered
115
+ * byte-exact against the app's own request (cold-start, phone
116
+ * force-stopped then reopened). Distinct from {@link buildLookupWithKeyPayload2} (`LOOKUP_WITH_KEY2`,
117
+ * 0xf16a) which eufy-sdk already sent: that variant only ever got relay-pool candidates back for a
118
+ * remote (cross-WAN) target; THIS variant is what got the real app a genuine direct-device address.
119
+ *
120
+ * `[p2pDid:20][selfAddr:16][clientVersion:4 = 02 05 01 05][dskKey][0x00000000]`. `selfAddr` is the
121
+ * caller's own observed LAN host:port (the socket's own bound address) — a STUN-like self-report, NOT
122
+ * the target's address. `clientVersion` was `02 05 01 05` in every capture; reproduced verbatim since
123
+ * its exact semantics (and whether it's validated) are unconfirmed.
124
+ */
125
+ export declare function buildLookupWithKeyPayload(p2pDid: string, selfHost: string, selfPort: number, dskKey: string): Buffer;
126
+ /** CHECK_CAM hole-punch payload: `[p2pDid:20][0x000000]`. */
127
+ export declare function buildCheckCamPayload(p2pDid: string): Buffer;
128
+ /** 10-byte command header: `[dataTypeHeader:2][seq:2 BE]["XZYH"][cmd:2 LE]`. */
129
+ export declare function buildCommandHeader(seqNumber: number, commandType: number, dataTypeHeader?: Buffer): Buffer;
130
+ /** 10-byte empty command body for the given channel (used by CMD_GATEWAYINFO etc.). */
131
+ export declare function buildVoidCommandPayload(channel?: number): Buffer;
132
+ /** AES-128-ECB encrypt (no auto-padding) — P2P control payloads. Inverse of decryptP2PData. */
133
+ export declare function encryptP2PData(data: Buffer, key: Buffer): Buffer;
134
+ /** Zero-pad to a multiple of `blocksize` (eufy P2P pads with 0x00, NOT PKCS7). */
135
+ export declare function paddingP2PData(data: Buffer, blocksize?: number): Buffer;
136
+ /**
137
+ * Command body carrying a string `value` (e.g. CMD_SET_PAYLOAD JSON). Mirrors the
138
+ * app's `buildCommandWithStringTypePayload`: `[len:2 LE][00 00][01 00][channel,encType][00 00][data]`.
139
+ * When `key` is given the data is zero-padded to 16 and AES-128-ECB encrypted, and
140
+ * `encType` is written in the channel word (level-1 encryption = 1).
141
+ */
142
+ export declare function buildStringCommandPayload(value: string, channel?: number, key?: Buffer, encType?: number): Buffer;
143
+ /**
144
+ * Build an **int+string** command body: `valueSub(u32 LE) ‖ value(u32 LE) ‖ strValue(len-prefixed)`,
145
+ * AES-128-ECB encrypted (level-1) like {@link buildStringCommandPayload}. This is the wire shape the
146
+ * eufy app uses for the floodlight/spotlight manual switch (`CMD_SET_FLOODLIGHT_MANUAL_SWITCH` 1400)
147
+ * on IndoorOutdoor / SoloCam-spotlight / Cam2C/3 families — where `value` = 0/1, `valueSub` = the
148
+ * device channel, and `strValue` = the admin `account_id`. The `strValue` uses the 128-byte-chunk
149
+ * length prefix ({@link stringWithLength}).
150
+ */
151
+ export declare function buildIntStringCommandPayload(value: number, valueSub: number, strValue: string, channel?: number, key?: Buffer, encType?: number): Buffer;
152
+ /**
153
+ * Build a command body around an ALREADY-encrypted (or plaintext) `data` buffer with an explicit
154
+ * `signCode` — used for level-2 (`signCode 8`, AES-256-GCM) commands like the media-start 1350 the
155
+ * app sends. Same on-wire layout as `buildStringCommandPayload` but the caller supplies the body and
156
+ * the signCode verbatim (no ECB step): `len(u16) ‖ 0000 ‖ 0100 ‖ [channel, signCode] ‖ 0000 ‖ data`.
157
+ */
158
+ export declare function buildRawCommandPayload(data: Buffer, channel?: number, signCode?: number, magic?: [number, number], streamId?: number): Buffer;
159
+ /** ACK payload for a received DATA frame: `[dataTypeHeader:2][count:2 BE][seqNo:2 BE]`. */
160
+ export declare function buildAckPayload(dataTypeHeader: Buffer, seqNo: number): Buffer;
161
+ /** Parse the 16-byte data-frame header that follows the "XZYH" magic. */
162
+ export declare function parseDataFrameHeader(frame: Buffer): P2PDataFrameHeader;
163
+ /** Parse a LOOKUP_ADDR / LOOKUP_ADDR2 response into a device address. */
164
+ export declare function parseLookupAddr(msg: Buffer): Address;
165
+ /** Read a NUL-terminated UTF-8 string (CMD_NOTIFY_PAYLOAD carries JSON this way). */
166
+ export declare function readNullTerminatedString(data: Buffer, encoding?: BufferEncoding): string;