@mega-yfue/eufy-sdk 0.5.0-beta.4 → 0.5.0-beta.40

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 (40) hide show
  1. package/dist/client/device-registry.d.ts +24 -2
  2. package/dist/client/eufy-mega.d.ts +50 -6
  3. package/dist/client/types.d.ts +1 -1
  4. package/dist/core/contracts.d.ts +52 -26
  5. package/dist/core/store.d.ts +2 -0
  6. package/dist/core/types.d.ts +2 -0
  7. package/dist/index.js +2308 -277
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/arming.d.ts +31 -4
  10. package/dist/model/capabilities/camera.d.ts +24 -10
  11. package/dist/model/capabilities/dp-catalog.d.ts +4 -3
  12. package/dist/model/capabilities/light.d.ts +3 -0
  13. package/dist/model/capabilities/motion.d.ts +5 -0
  14. package/dist/model/capabilities/suction.d.ts +5 -5
  15. package/dist/model/capabilities/types.d.ts +6 -0
  16. package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
  17. package/dist/model/classify.d.ts +1 -2
  18. package/dist/model/device-family.d.ts +4 -0
  19. package/dist/model/registry.d.ts +3 -2
  20. package/dist/model/types.d.ts +6 -0
  21. package/dist/transport/abortable.d.ts +8 -0
  22. package/dist/transport/http/mega-client.d.ts +26 -0
  23. package/dist/transport/p2p/command-router.d.ts +14 -26
  24. package/dist/transport/p2p/fmp4.d.ts +12 -2
  25. package/dist/transport/p2p/live-stream.d.ts +3 -0
  26. package/dist/transport/p2p/p2p-session.d.ts +23 -3
  27. package/dist/transport/p2p/recording-download.d.ts +4 -3
  28. package/dist/transport/p2p/shared-live-source.d.ts +9 -0
  29. package/dist/transport/p2p/talkback.d.ts +21 -10
  30. package/dist/transport/p2p/video.d.ts +5 -2
  31. package/dist/transport/rtc/command-router.d.ts +109 -0
  32. package/dist/transport/rtc/live.d.ts +69 -0
  33. package/dist/transport/rtc/peer.d.ts +101 -0
  34. package/dist/transport/rtc/portal-packet.d.ts +72 -0
  35. package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
  36. package/dist/transport/rtc/scall-sdp.d.ts +42 -0
  37. package/dist/transport/rtc/session.d.ts +88 -0
  38. package/dist/transport/rtc/signaling.d.ts +139 -0
  39. package/dist/transport/token-echo.d.ts +8 -0
  40. package/package.json +2 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * One T9000 command session end to end: sign → signalling socket → `scall` → the hub's SDP offer →
3
+ * our answer → ICE → DTLS → the command data channel open.
4
+ *
5
+ * The signalling exchange, as the portal runs it and as confirmed live:
6
+ *
7
+ * → scall (action 3, channel 0, the hub's own session)
8
+ * ← scall {status: 100, turn} the hub granted the session and handed out TURN credentials
9
+ * ← info {sdp | format:"SDP"} the hub's offer, as scall JSON
10
+ * → info {sdp} the answer
11
+ * ↔ info {candidate} trickle ICE on the same channel; "" ends it
12
+ * ← scall {status: 200} → ack
13
+ * ← scall {status: 486 | 408} busy / timeout: the session fails
14
+ * ← hangup the hub ended it
15
+ *
16
+ * The command data channel is the session: `close` fires once, when the channel closes, the peer fails,
17
+ * the signalling socket drops or {@link RtcSession.close} is called, even while the peer connection is
18
+ * nominally up.
19
+ *
20
+ * Both the signalling client and the peer are injectable.
21
+ */
22
+ import { EventEmitter } from "node:events";
23
+ import { RtcPeer, type RtcPeerOptions } from "./peer.js";
24
+ import { RtcSignalingClient, type RtcSignalingOptions } from "./signaling.js";
25
+ export interface RtcSessionOptions extends RtcSignalingOptions {
26
+ peer?: RtcPeerOptions;
27
+ createSignaling?: (opts: RtcSignalingOptions) => RtcSignalingClient;
28
+ createPeer?: (opts: RtcPeerOptions) => RtcPeer;
29
+ }
30
+ export interface RtcSessionEvents {
31
+ connected: [];
32
+ close: [];
33
+ error: [err: Error];
34
+ /** A reassembled command or notify frame: portal packet bytes + the link type it arrived on. */
35
+ commandData: [frame: Buffer, linkType: number];
36
+ /** A reassembled live-video frame. */
37
+ mediaData: [frame: Buffer];
38
+ }
39
+ export declare class RtcSession extends EventEmitter<RtcSessionEvents> {
40
+ private readonly opts;
41
+ private readonly signaling;
42
+ private readonly peer;
43
+ private readonly logger;
44
+ private turn?;
45
+ private authOk;
46
+ private authWaiter?;
47
+ private connected;
48
+ private closed;
49
+ private closeAnnounced;
50
+ private sdpHandled;
51
+ private chain;
52
+ constructor(opts: RtcSessionOptions);
53
+ get isConnected(): boolean;
54
+ /**
55
+ * Start the sequence; resolves once the call is sent. `connected` fires when the channel opens. A session
56
+ * closed by the time the sign fetch or the auth completes rejects instead of opening the socket or
57
+ * placing the call.
58
+ */
59
+ connect(): Promise<void>;
60
+ /** Send one portal packet; false when the command channel isn't open. */
61
+ sendCommand(portalPacket: Buffer): boolean;
62
+ /** Send unframed bytes on the command channel; false when it isn't open. */
63
+ sendRaw(bytes: Buffer): boolean;
64
+ /**
65
+ * Hang up and tear both sides down; `close` fires once, here if nothing announced it before. A
66
+ * `connect()` still waiting for the signalling auth is rejected and its timer stopped.
67
+ */
68
+ close(): void;
69
+ /**
70
+ * Trickle one local candidate, or the empty end-of-candidates. The native peer can report one after the
71
+ * signalling socket has closed, and sending then throws inside a native callback, where the throw can't
72
+ * be caught and aborts the process. A candidate with no open socket is dropped.
73
+ */
74
+ private trickle;
75
+ private announceClose;
76
+ private waitForAuth;
77
+ /** Settle a pending auth wait: resolve it, or reject it with `err`; either way its timer stops. */
78
+ private settleAuth;
79
+ private onSignaling;
80
+ /** A `scall` status: `100` grants the relay, `200` is acknowledged, `486`/`408` fail the session. */
81
+ private onCall;
82
+ /**
83
+ * An `info`: trickle ICE, in either of the two shapes the hub sends, or the hub's SDP offer. The offer
84
+ * is answered only once a TURN grant is in: the peer is relay-only, so an offer ahead of `scall 100`
85
+ * is reported and left unanswered.
86
+ */
87
+ private onInfo;
88
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The T9000's signalling channel: the WebSocket through which a client and a HomeBase S1 Pro agree on a
3
+ * WebRTC session. It is the wire the security.eufy.com web client uses (`/v1/rtc/ws/join`), and the hub
4
+ * accepts it from any client presenting the account's mega token.
5
+ *
6
+ * Sequence:
7
+ *
8
+ * 1. `GET https://<smart host>/v1/smart/nvr/ws/sign` with the mega token → a `sign` blob.
9
+ * 2. WebSocket to `wss://<smart host>/v1/rtc/ws/join?reqtype=nvr`, subprotocols `["v1", <base64url
10
+ * JSON>]` carrying the cluster region, station serial, token, `gtoken` and the sign. HTTP headers on
11
+ * the upgrade alone are refused: the JSON subprotocol is what authenticates.
12
+ * 3. `action 1` auth on open; `action 3` session messages after: `scall` (start), `info` (SDP and
13
+ * trickle ICE), `ack`, `hangup`. Every session message carries an HMAC-SHA256 `account` over
14
+ * `channelId + adminUserId + ts`, keyed by the token.
15
+ *
16
+ * Region is two values: the HTTP sign request names the account's **country** (`Web-Country`), the
17
+ * WebSocket payload names the **cluster**, the shard prefix uppercased (`ie-pr` → `IE`). The smart host
18
+ * is `security-smart` for `us` and `security-smart-<prefix>` for any other shard.
19
+ *
20
+ * `fetch` and the `WebSocket` constructor are injectable.
21
+ */
22
+ import { EventEmitter } from "node:events";
23
+ import { type Logger } from "../../core/logger.js";
24
+ /** The portal's origin; the sign endpoint and the socket upgrade both check it. */
25
+ export declare const PORTAL_ORIGIN = "https://security.eufy.com";
26
+ /** The hub drops an idle signalling socket after ~83 s; re-sending auth inside that keeps it. */
27
+ export declare const SIGNALING_KEEPALIVE_MS = 25000;
28
+ /** How long the socket has to open after the sign. */
29
+ export declare const SIGNALING_CONNECT_TIMEOUT_MS = 15000;
30
+ /** Outer wire envelope. */
31
+ export interface RtcWsEnvelope {
32
+ msgid: string;
33
+ data: string;
34
+ }
35
+ /** Inner message (the envelope's `data`, JSON-parsed). */
36
+ export interface RtcInnerMessage {
37
+ code?: number;
38
+ action?: number;
39
+ sessionId?: string;
40
+ sn?: string;
41
+ subSn?: string;
42
+ channelId?: number;
43
+ isResponse?: number;
44
+ dataType?: string;
45
+ source?: string;
46
+ ts?: number;
47
+ data?: string;
48
+ msgid?: string;
49
+ }
50
+ /** What a socket event carries; only the fields each event type fills are read. */
51
+ export interface SignalingSocketEvent {
52
+ data?: unknown;
53
+ code?: number;
54
+ reason?: string;
55
+ }
56
+ /** The subset of a `WebSocket` the client uses. */
57
+ export interface SignalingSocket {
58
+ readonly readyState: number;
59
+ send(data: string): void;
60
+ close(code?: number, reason?: string): void;
61
+ addEventListener(type: "open" | "message" | "close" | "error", listener: (ev: SignalingSocketEvent) => void): void;
62
+ }
63
+ export interface SignalingSocketInit {
64
+ /** The subprotocols; slot 2 carries the base64url auth JSON. */
65
+ protocols: string[];
66
+ /** The upgrade Origin, which the server checks like the sign call. */
67
+ origin: string;
68
+ }
69
+ export type SignalingSocketFactory = (url: string, init: SignalingSocketInit) => SignalingSocket;
70
+ export interface RtcSignalingOptions {
71
+ /** Full SDP (`call`) or compact SDP (`scall`, the default). */
72
+ signalingMode?: "call" | "scall";
73
+ /** The mega session's auth token. */
74
+ authToken: string;
75
+ /** The `gtoken` header value the mega session's authed HTTP calls carry. */
76
+ gtoken: string;
77
+ stationSn: string;
78
+ /** The station's `member.admin_user_id`, the account the session HMAC names. */
79
+ adminUserId: string;
80
+ /** The mega shard the account lives on; picks the host and the cluster name. */
81
+ shard: string;
82
+ /** The account's ISO country, sent on the sign request (`Web-Country`). */
83
+ country: string;
84
+ fetch?: typeof fetch;
85
+ createSocket?: SignalingSocketFactory;
86
+ logger?: Logger;
87
+ now?: () => number;
88
+ makeMsgId?: () => string;
89
+ }
90
+ export interface RtcSignalingEvents {
91
+ message: [inner: RtcInnerMessage, envelope: RtcWsEnvelope];
92
+ open: [];
93
+ close: [code: number, reason: string];
94
+ error: [err: Error];
95
+ }
96
+ /** The portal's `account` field: HMAC-SHA256 of `channelId + adminUserId + ts`, keyed by the token. */
97
+ export declare function sessionAccount(channelId: number, adminUserId: string, ts: number, authToken: string): string;
98
+ export declare class RtcSignalingClient extends EventEmitter<RtcSignalingEvents> {
99
+ private readonly opts;
100
+ private ws?;
101
+ private sign?;
102
+ private keepalive?;
103
+ private readonly wsUrl;
104
+ private readonly signUrl;
105
+ private readonly wsRegion;
106
+ private readonly fetchImpl;
107
+ private readonly createSocket;
108
+ private readonly logger;
109
+ private readonly now;
110
+ private readonly makeMsgId;
111
+ constructor(opts: RtcSignalingOptions);
112
+ get isOpen(): boolean;
113
+ /** Step 1: the sign blob the socket and every auth message carry. A non-JSON body is reported by status. */
114
+ fetchSign(): Promise<string>;
115
+ /**
116
+ * Step 2: open the socket and send the first auth; resolves on `open`. A socket that closes or errors
117
+ * before it ever opened rejects, so the call always settles.
118
+ */
119
+ connect(): Promise<void>;
120
+ /** `action 1`, also what keeps the socket alive when re-sent. */
121
+ sendAuth(sign?: string): void;
122
+ /**
123
+ * `action 3`, a session message on channel 0, the hub's own session; `scall` opens the negotiation.
124
+ * `subSn` is empty for the hub's session.
125
+ */
126
+ sendSession(dataType: string, payload?: Record<string, unknown>): void;
127
+ sendCall(): void;
128
+ sendAck(): void;
129
+ /** The SDP answer, encoded for the negotiated call format, in an `info`. */
130
+ sendInfoSdp(sdp: string): void;
131
+ /** Trickle a candidate on the session's channel, like its SDP answer. An empty candidate ends them. */
132
+ sendInfoCandidate(candidate: string): void;
133
+ sendHangup(): void;
134
+ close(): void;
135
+ private startKeepalive;
136
+ private stopKeepalive;
137
+ private sendEnvelope;
138
+ private handleWireMessage;
139
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strip a token the gateway echoed back into its own error message.
3
+ *
4
+ * `"token not exist, token = <the rejected token>"` is the vendor's wording, and that message travels into the
5
+ * error the SDK surfaces. A credential that has just been rejected is still a credential, and it is
6
+ * never the part of the message that explains anything.
7
+ */
8
+ export declare function withoutTokenEcho(text: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.5.0-beta.4",
3
+ "version": "0.5.0-beta.40",
4
4
  "description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
5
5
  "license": "Apache-2.0",
6
6
  "author": "mega-yfue",
@@ -74,6 +74,7 @@
74
74
  "dependencies": {
75
75
  "jpeg-js": "^0.4.4",
76
76
  "mqtt": "^5.16.0",
77
+ "node-datachannel": "^0.33.4",
77
78
  "protobufjs": "^8.8.0"
78
79
  },
79
80
  "devDependencies": {