@mega-yfue/eufy-sdk 0.6.0-beta.4 → 0.6.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.
- package/dist/client/device-registry.d.ts +6 -0
- package/dist/client/eufy-mega.d.ts +103 -14
- package/dist/client/push-repeats.d.ts +10 -0
- package/dist/client/types.d.ts +12 -9
- package/dist/core/contracts.d.ts +28 -14
- package/dist/core/store.d.ts +12 -1
- package/dist/core/types.d.ts +4 -2
- package/dist/index.js +3332 -2431
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/access.d.ts +3 -2
- package/dist/model/capabilities/arming.d.ts +2 -1
- package/dist/model/capabilities/battery.d.ts +8 -7
- package/dist/model/capabilities/camera.d.ts +13 -2
- package/dist/model/capabilities/doorbell.d.ts +15 -3
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/members.d.ts +3 -1
- package/dist/model/capabilities/mower.d.ts +3 -2
- package/dist/model/capabilities/ptz.d.ts +11 -5
- package/dist/model/capabilities/vacuum-clean.d.ts +23 -0
- package/dist/model/device-family.d.ts +3 -1
- package/dist/model/device-types.d.ts +1 -0
- package/dist/transport/http/mega-client.d.ts +18 -5
- package/dist/transport/mqtt/biz-stream.d.ts +22 -25
- package/dist/transport/p2p/audio-frame.d.ts +13 -0
- package/dist/transport/p2p/codec.d.ts +8 -2
- package/dist/transport/p2p/command-router.d.ts +48 -13
- package/dist/transport/p2p/database-wire.d.ts +30 -0
- package/dist/transport/p2p/level2-settle.d.ts +9 -0
- package/dist/transport/p2p/live-stream.d.ts +34 -7
- package/dist/transport/p2p/live-trace.d.ts +7 -5
- package/dist/transport/p2p/p2p-session.d.ts +112 -16
- package/dist/transport/p2p/readable-egress.d.ts +3 -1
- package/dist/transport/p2p/recording-download.d.ts +109 -13
- package/dist/transport/p2p/shared-live-source.d.ts +9 -3
- package/dist/transport/p2p/video.d.ts +12 -6
- package/dist/transport/push/store.d.ts +7 -6
- package/dist/transport/rtc/command-router.d.ts +86 -9
- package/dist/transport/rtc/live.d.ts +4 -1
- package/dist/transport/rtc/portal-packet.d.ts +3 -13
- package/dist/transport/rtc/session.d.ts +19 -2
- package/dist/transport/stored-image-cache.d.ts +14 -0
- package/package.json +1 -1
|
@@ -62,6 +62,8 @@ export interface P2PRouterDeps {
|
|
|
62
62
|
onClose: (stationSn: string) => void;
|
|
63
63
|
onError: (err: Error) => void;
|
|
64
64
|
onLevel2Ready: (stationSn: string, cipherId: number) => void;
|
|
65
|
+
/** The station answered on its own session at `atMs` — a PONG or an ACK from the connected peer. */
|
|
66
|
+
onPeerAnswer?: (stationSn: string, atMs: number) => void;
|
|
65
67
|
/** A raw decoded frame — the client emits the low-level `p2p` event + runs the semantic decode. */
|
|
66
68
|
onFrame: (stationSn: string, frame: P2PFrame) => void;
|
|
67
69
|
/**
|
|
@@ -102,6 +104,18 @@ export declare class P2PCommandRouter {
|
|
|
102
104
|
* {@link liveSources} is a claim in flight.
|
|
103
105
|
*/
|
|
104
106
|
private readonly liveSessionKeys;
|
|
107
|
+
/**
|
|
108
|
+
* The live source build in flight per `${parentSn}:${channel}`, settled once its connection is opened
|
|
109
|
+
* or refused.
|
|
110
|
+
*
|
|
111
|
+
* A build awaits its media session before it can file the source in {@link liveSources}, so a second
|
|
112
|
+
* request for the same camera in that window finds nothing cached. Building a second source there
|
|
113
|
+
* overwrites the first, and the orphan's teardown then closes the connection the cached one streams
|
|
114
|
+
* on. A request that finds a build here waits for it, until its own signal aborts, then resolves its
|
|
115
|
+
* session again and reuses what the build filed, or builds its own when that build failed. The entry
|
|
116
|
+
* is removed when the build settles and its promise never rejects, so each wait ends on a later state.
|
|
117
|
+
*/
|
|
118
|
+
private readonly liveSourceBuilds;
|
|
105
119
|
/**
|
|
106
120
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
107
121
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -220,6 +234,11 @@ export declare class P2PCommandRouter {
|
|
|
220
234
|
* so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
|
|
221
235
|
* shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
|
|
222
236
|
* carry the start is not a connected session, so this refuses rather than returning one.
|
|
237
|
+
*
|
|
238
|
+
* A session refused its key within the grace, or never connected, is closed and dropped from the manager at
|
|
239
|
+
* once. Nothing else releases it: no source holds it, a wired station's session never idles out, and the
|
|
240
|
+
* manager hands the same session to every later open of the camera, so the next open dials a fresh
|
|
241
|
+
* connection instead. A session another open has since replaced is left to its own close.
|
|
223
242
|
*/
|
|
224
243
|
private openMediaSession;
|
|
225
244
|
/**
|
|
@@ -295,24 +314,28 @@ export declare class P2PCommandRouter {
|
|
|
295
314
|
*/
|
|
296
315
|
mediaProviderFor(sn: string): MediaProvider;
|
|
297
316
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
317
|
+
* The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
|
|
318
|
+
* station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
|
|
319
|
+
* station, answers `undefined`.
|
|
300
320
|
*/
|
|
301
|
-
private
|
|
321
|
+
private recordingStationFor;
|
|
302
322
|
/**
|
|
303
|
-
* Download one recording
|
|
304
|
-
* ({@link decodeRecording}).
|
|
305
|
-
* on the camera's channel, so a download starts only
|
|
306
|
-
* ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
307
|
-
* while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
308
|
-
* turn is held until the transfer drains.
|
|
323
|
+
* Download one recording `station` holds for the attached camera `sn`, then decode it
|
|
324
|
+
* ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
|
|
325
|
+
* station: the station streams one recording at a time on the camera's channel, so a download starts only
|
|
326
|
+
* once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
327
|
+
* has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
328
|
+
* once and the station's turn is held until the transfer drains.
|
|
309
329
|
*/
|
|
310
330
|
private downloadRecording;
|
|
311
331
|
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
312
332
|
private waitTurn;
|
|
313
333
|
/**
|
|
314
334
|
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
315
|
-
* and when the station is done sending it.
|
|
335
|
+
* and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
|
|
336
|
+
* it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
|
|
337
|
+
* is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
|
|
338
|
+
* reads no cipher, and `opts.cipherId` is not read.
|
|
316
339
|
*/
|
|
317
340
|
private startRecordingDownload;
|
|
318
341
|
/**
|
|
@@ -394,8 +417,9 @@ export declare class P2PCommandRouter {
|
|
|
394
417
|
* {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
|
|
395
418
|
* reuse branch, so a reuse frees the station as a cold start does.
|
|
396
419
|
*
|
|
397
|
-
* `
|
|
398
|
-
* connection of its own for it
|
|
420
|
+
* `continuous` decides whether a camera that finds the station's own session claimed may open a
|
|
421
|
+
* connection of its own for it; on an {@link OWN_LIVE_SESSION_MODELS} station it always gets one, and an
|
|
422
|
+
* unwatched source left on the station's session is rebuilt there. Only the continuous-pull egresses pass it. A still must not: it wants one
|
|
399
423
|
* frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
|
|
400
424
|
* so a still asked for while a sibling is being watched contends as it always did, and the caller's
|
|
401
425
|
* retained image answers it. A live view still outranks a tile; what changed is that two live views no
|
|
@@ -404,7 +428,7 @@ export declare class P2PCommandRouter {
|
|
|
404
428
|
* The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
|
|
405
429
|
* place.
|
|
406
430
|
*/
|
|
407
|
-
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts,
|
|
431
|
+
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, continuous?: boolean): Promise<SharedLiveSource>;
|
|
408
432
|
/**
|
|
409
433
|
* Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
|
|
410
434
|
*
|
|
@@ -559,12 +583,17 @@ export declare class P2PCommandRouter {
|
|
|
559
583
|
* yield CMAF fragments (init segment first, then a `moof`+`mdat` per keyframe boundary) muxed by the
|
|
560
584
|
* dependency-free internal fMP4 muxer. The returned recording handle exposes battery-budget notices
|
|
561
585
|
* and detaches its consumer on `stop`, iterator return, or iterator throw. No ffmpeg.
|
|
586
|
+
*
|
|
587
|
+
* It is a continuous pull only on an {@link OWN_LIVE_SESSION_MODELS} station; elsewhere it never opens a
|
|
588
|
+
* connection of its own.
|
|
562
589
|
*/
|
|
563
590
|
recordFragments(sn: string, opts?: {
|
|
564
591
|
fragmentSeconds?: number;
|
|
565
592
|
eccPrivateKey?: Buffer;
|
|
566
593
|
keepAliveMs?: number;
|
|
567
594
|
} & SharedSourceHints): FragmentRecording;
|
|
595
|
+
/** Whether `stationSn` is an {@link OWN_LIVE_SESSION_MODELS} station. */
|
|
596
|
+
private ownsLiveSession;
|
|
568
597
|
/**
|
|
569
598
|
* **Generic P2P request/reply query.** Sends a `SET_PAYLOAD` (1350) wrapper carrying `subCmd` on the
|
|
570
599
|
* device channel, then resolves with the reply frame's `payload` — the `NOTIFY_PAYLOAD` (1351)
|
|
@@ -668,6 +697,9 @@ export declare class P2PCommandRouter {
|
|
|
668
697
|
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
669
698
|
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
670
699
|
* than closed again.
|
|
700
|
+
*
|
|
701
|
+
* `clearFallback` hands over a session whose station {@link P2PSession.refusedLevel2} instead of throwing, for
|
|
702
|
+
* a standalone device of a model in {@link CLEAR_FF09_MODELS}.
|
|
671
703
|
*/
|
|
672
704
|
private resolveSession;
|
|
673
705
|
/**
|
|
@@ -693,6 +725,9 @@ export declare class P2PCommandRouter {
|
|
|
693
725
|
* **Station-scoped scalar** (`p2p-station-scalar` intent): the 132-byte channel-less body
|
|
694
726
|
* ({@link buildDirectBinaryBody} with no `channel`) on an EXPLICIT channel, signCode 8. The
|
|
695
727
|
* HomeBase's own controls ride the station broadcast channel 255 (alarm/speaker volume 1235).
|
|
728
|
+
*
|
|
729
|
+
* `form` `"auto"` seals it at the session's level, as {@link sendBySessionLevel} defines it: level-2 on a
|
|
730
|
+
* keyed session, the same body sealed level-1 on a keyless one. Absent, the send waits for the level-2 key.
|
|
696
731
|
*/
|
|
697
732
|
private sendStationScalar;
|
|
698
733
|
/** Send a level-1 int-plus-string frame with authenticated account identity injected by the transport. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database wire — the ids and the whole-table query a station's on-station table and stored-image
|
|
3
|
+
* reads are built from.
|
|
4
|
+
*
|
|
5
|
+
* A HomeBase answers them over its P2P session and a T9000 over its portal control channel, with the
|
|
6
|
+
* same inner payloads, so both transports build their reads from this one module. The barrel does not
|
|
7
|
+
* re-export it: these are wire ids, not part of the package surface.
|
|
8
|
+
*
|
|
9
|
+
* @module p2p/database-wire
|
|
10
|
+
*/
|
|
11
|
+
/** CMD_DATABASE_IMAGE (1308) — fetch one stored image by its on-station path. */
|
|
12
|
+
export declare const CMD_DATABASE_IMAGE = 1308;
|
|
13
|
+
/** CMD_DATABASE (1306) — query one on-station table; reply carries `{cmd:10000,count,data:[…]}`. */
|
|
14
|
+
export declare const CMD_DATABASE = 1306;
|
|
15
|
+
/**
|
|
16
|
+
* Inner query cmds carried in a {@link CMD_DATABASE} payload. `FULL_TABLE` reads one table whole;
|
|
17
|
+
* `COMBINATION_WITH_AI` is the AI event-history read that bundles the face roster.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DB_QUERY: {
|
|
20
|
+
readonly FULL_TABLE: 10000;
|
|
21
|
+
readonly COMBINATION_WITH_AI: 10011;
|
|
22
|
+
readonly QUERY_LOCAL: 10017;
|
|
23
|
+
};
|
|
24
|
+
/** How long a station's database reply has to arrive whole, when the caller states no bound. */
|
|
25
|
+
export declare const DB_TABLE_TIMEOUT_MS = 15000;
|
|
26
|
+
/**
|
|
27
|
+
* The query params the vendor app sends with a whole-table `CMD_DATABASE` read (inner `cmd 10000`).
|
|
28
|
+
* `count` bounds the row count.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fullTableQuery(count?: number): Record<string, unknown>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bound a caller waits for the level-2 negotiation to SETTLE, measured from connect.
|
|
3
|
+
*
|
|
4
|
+
* For a caller that picks its seal once from `P2PSession.hasLevel2Key` and has no second chance. A media
|
|
5
|
+
* start reads the key on every send and is re-issued by the warm-up, so it needs no wait; a property write
|
|
6
|
+
* framed level-1 to a family that only accepts level-2 is ignored, and nothing re-frames it. Session-scoped,
|
|
7
|
+
* so a station that offers no key does not charge this to every later command.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEVEL2_SETTLE_MS = 8000;
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* - **Plaintext** (`signCode 0`, e.g. HomeBase-attached cameras): a fixed 22-byte frame header
|
|
8
8
|
* (`parseVideoFrameHeader`) followed directly by Annex-B NAL units — verified live (960×540,
|
|
9
9
|
* keyframe = header flag bit0). We strip the 22-byte header.
|
|
10
|
-
* - **Encrypted
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - **Encrypted keyframes**: either the AES-256-GCM body under a media key wrapped in an ECIES envelope
|
|
11
|
+
* for an offered P-256 key ({@link VideoFrameDecoder}), or an AES-ECB head under a media key wrapped for
|
|
12
|
+
* the session's RSA key ({@link P2PSession.decodeVideoFrame}). An attached camera's start on a station
|
|
13
|
+
* with a {@link P2PSession.mediaEccPrivateKey} offers that P-256 key first, so its stream decodes with
|
|
14
|
+
* that pair unless `eccPrivateKey` names another.
|
|
13
15
|
*
|
|
14
16
|
* Emits: `video` ({@link LiveVideoFrame} — Annex-B), `audio` ({@link LiveAudioFrame}), `start`, `stop`,
|
|
15
17
|
* `error`.
|
|
@@ -54,7 +56,10 @@ export interface LiveStreamOptions {
|
|
|
54
56
|
* however long the station keeps serving that sibling instead of this one.
|
|
55
57
|
*/
|
|
56
58
|
channel?: number;
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* P-256 private key (32B) whose public point the camera wraps each keyframe's media key for. Defaults, on an
|
|
61
|
+
* attached camera, to the session's `P2PSession.mediaEccPrivateKey`, the pair its start offers.
|
|
62
|
+
*/
|
|
58
63
|
eccPrivateKey?: Buffer;
|
|
59
64
|
/** Admin account id — required for the level-2 (`signCode 8`) media-start payload selecting a camera. */
|
|
60
65
|
accountId?: string;
|
|
@@ -112,6 +117,8 @@ export declare class LiveStream extends EventEmitter {
|
|
|
112
117
|
private tracedFirstVideoUnit;
|
|
113
118
|
private tracedFirstKeyframe;
|
|
114
119
|
private tracedDecodeFailures;
|
|
120
|
+
/** How the frame that opened the latest access unit was read; see {@link settleStartKey}. */
|
|
121
|
+
private unitOpenedBy;
|
|
115
122
|
private tracedFirstForeignFrame;
|
|
116
123
|
private stallTimer?;
|
|
117
124
|
/**
|
|
@@ -173,6 +180,7 @@ export declare class LiveStream extends EventEmitter {
|
|
|
173
180
|
stop(): void;
|
|
174
181
|
/** Extract the Annex-B payload from a plaintext 1300 frame (fixed 22-byte header), or undefined. */
|
|
175
182
|
private plaintextAnnexB;
|
|
183
|
+
/** Emit one accepted media frame; an audio frame with no bytes past its header emits nothing. */
|
|
176
184
|
private onFrame;
|
|
177
185
|
/**
|
|
178
186
|
* Whether this stream may take `frame` — the demultiplexer for a station that serves several cameras.
|
|
@@ -198,11 +206,30 @@ export declare class LiveStream extends EventEmitter {
|
|
|
198
206
|
*/
|
|
199
207
|
private acceptsMedia;
|
|
200
208
|
/**
|
|
201
|
-
* The Annex-B payload of a frame that starts an access unit. The
|
|
202
|
-
*
|
|
203
|
-
*
|
|
209
|
+
* The Annex-B payload of a frame that starts an access unit. The authenticated ECIES/GCM
|
|
210
|
+
* {@link VideoFrameDecoder} goes first: its HMAC and GCM checks fail closed on a frame not sealed for its
|
|
211
|
+
* key. The session's RSA path cannot: on Node ≥ 24.5, PKCS#1 v1.5 decryption of a key not wrapped for it
|
|
212
|
+
* returns pseudo-random bytes instead of throwing (implicit rejection), which would decode an ECC
|
|
213
|
+
* keyframe into noise. The session's decode, which also passes plaintext frames, and the header strip
|
|
214
|
+
* follow. A payload that opens an access unit records how it was read: by the ECIES path, or otherwise by
|
|
215
|
+
* its sign code, sealed (`"rsa"`) or in the clear (`"plain"`).
|
|
204
216
|
*/
|
|
205
217
|
private annexbOf;
|
|
218
|
+
/**
|
|
219
|
+
* Tell the session how this keyframe was opened, which answers the attached starts on this channel
|
|
220
|
+
* ({@link P2PSession.settleAttachedKey}), and re-issue the start when the session asks for one: a plaintext
|
|
221
|
+
* keyframe answering the session's P-256 offer, from a camera that did not use the point, which the next
|
|
222
|
+
* start asks with RSA. Only a delivered keyframe answers: a frame no key opens, or a continuation without the
|
|
223
|
+
* frame that opened its unit, delivers nothing.
|
|
224
|
+
*/
|
|
225
|
+
private settleStartKey;
|
|
226
|
+
/**
|
|
227
|
+
* The payload of an audio frame. On a stream with an ECIES decoder, a sealed frame ({@link isSealedAudio}) is
|
|
228
|
+
* opened under the media key of the latest keyframe, and answers `undefined` when it does not open or arrives
|
|
229
|
+
* before any keyframe gave a media key, since its bytes are ciphertext. Every other frame is the bytes after
|
|
230
|
+
* its header, empty for a frame that carries only its header.
|
|
231
|
+
*/
|
|
232
|
+
private audioPayload;
|
|
206
233
|
/**
|
|
207
234
|
* Report an access unit the transport could not complete.
|
|
208
235
|
*
|
|
@@ -23,6 +23,8 @@ export type LiveTrace =
|
|
|
23
23
|
topology: "attached" | "own";
|
|
24
24
|
action: "start" | "keepalive";
|
|
25
25
|
level2: boolean;
|
|
26
|
+
/** The media-key type an attached start offered. */
|
|
27
|
+
key?: "ecc" | "rsa";
|
|
26
28
|
}
|
|
27
29
|
/** The device acknowledged a retained start, or that start was repeated / abandoned unacknowledged. */
|
|
28
30
|
| {
|
|
@@ -137,16 +139,16 @@ export type LiveTrace =
|
|
|
137
139
|
*
|
|
138
140
|
* Every ending of a level-2 wait carries one of these reasons, so a start refused for want of a key is
|
|
139
141
|
* accounted for however it ended. `grace-elapsed` is a wait that ran out and states how long was waited;
|
|
140
|
-
* the rest are answered without waiting, because
|
|
141
|
-
*
|
|
142
|
-
* material, `not-negotiating` and `session-closed` about the station or its connection —
|
|
143
|
-
* reason reached under a negotiation has a cipher to name.
|
|
142
|
+
* the rest are answered without waiting, because a concluded negotiation stays concluded until a
|
|
143
|
+
* re-prompt for `CMD_GATEWAYINFO` re-opens it. `no-cipher-key` and `derivation-failed` are about this
|
|
144
|
+
* account's cipher material, `not-negotiating` and `session-closed` about the station or its connection —
|
|
145
|
+
* and only a reason reached under a negotiation has a cipher to name.
|
|
144
146
|
*
|
|
145
147
|
* A `grace-elapsed` start proceeds at level 1 where it has such a form, and not at all where it does not.
|
|
146
148
|
*/
|
|
147
149
|
| {
|
|
148
150
|
phase: "level2-unavailable";
|
|
149
|
-
reason: "no-cipher-key" | "derivation-failed" | "not-negotiating" | "session-closed" | "grace-elapsed";
|
|
151
|
+
reason: "no-cipher-key" | "derivation-failed" | "not-negotiating" | "session-closed" | "grace-elapsed" | "gateway-refused";
|
|
150
152
|
cipherId?: number;
|
|
151
153
|
waitedMs?: number;
|
|
152
154
|
}
|
|
@@ -95,6 +95,12 @@ export interface P2PFrame extends P2PDataFrameHeader {
|
|
|
95
95
|
* the values as device state without any per-capability decoder.
|
|
96
96
|
*/
|
|
97
97
|
params?: Record<number, string>;
|
|
98
|
+
/**
|
|
99
|
+
* A station's camera-info table, one `param_type → param_value` map per channel it names (255 is the
|
|
100
|
+
* station). Set instead of {@link params} when the station answered on {@link STATION_CHANNEL}, so each
|
|
101
|
+
* channel's state can land on the device attached there.
|
|
102
|
+
*/
|
|
103
|
+
channelParams?: Record<number, Record<number, string>>;
|
|
98
104
|
}
|
|
99
105
|
/**
|
|
100
106
|
* A live PPCS session. Internal transport; a host drives cameras through the capability surface.
|
|
@@ -134,7 +140,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
134
140
|
private audioStalled;
|
|
135
141
|
private audioRetransmitTimer?;
|
|
136
142
|
private lastPongData?;
|
|
137
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* When the selected peer last answered outbound P2P traffic on this connection.
|
|
145
|
+
*
|
|
146
|
+
* Each answer also emits `peerAnswer` with this time: a PONG or an ACK from the connected address is the
|
|
147
|
+
* station replying on this connection now, which a heartbeat draws every {@link HEARTBEAT_MS}.
|
|
148
|
+
*/
|
|
138
149
|
private lastPeerAt?;
|
|
139
150
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
140
151
|
private pathStaleTraced;
|
|
@@ -180,11 +191,21 @@ export declare class P2PSession extends EventEmitter {
|
|
|
180
191
|
private level2Seq;
|
|
181
192
|
private rsaPrivateKey?;
|
|
182
193
|
private rsaModulusHex?;
|
|
183
|
-
/**
|
|
194
|
+
/** The P-256 pair an attached camera's media start offers; see {@link mediaEccPrivateKey}. */
|
|
195
|
+
private mediaEcc?;
|
|
196
|
+
/**
|
|
197
|
+
* Per attached channel, the media-key type its latest start offered on this connection, or that last answered,
|
|
198
|
+
* and whether a keyframe has answered it. See {@link attachedStartKey} and {@link settleAttachedKey}.
|
|
199
|
+
*/
|
|
200
|
+
private readonly attachedStartKeys;
|
|
201
|
+
/**
|
|
202
|
+
* Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
|
|
203
|
+
* settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
|
|
204
|
+
*/
|
|
184
205
|
private level2Negotiating;
|
|
185
206
|
/**
|
|
186
207
|
* Whether waiting for a level-2 key can still change the answer: `false` once one has been negotiated,
|
|
187
|
-
* once
|
|
208
|
+
* once a negotiation concluded without one, and from the start when nothing can negotiate one.
|
|
188
209
|
*/
|
|
189
210
|
private level2Pending;
|
|
190
211
|
/** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
|
|
@@ -203,6 +224,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
203
224
|
private readonly liveStartedChannels;
|
|
204
225
|
/** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
|
|
205
226
|
private gatewayCipherId?;
|
|
227
|
+
/** Whether this connection's latest `CMD_GATEWAYINFO` reply was a refusal — see {@link gatewayInfoRefused}. */
|
|
228
|
+
private gatewayRefused;
|
|
206
229
|
/** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
|
|
207
230
|
private clearCommandCount;
|
|
208
231
|
private readonly unackedLiveStarts;
|
|
@@ -260,7 +283,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
260
283
|
* The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
|
|
261
284
|
* beginning later does not restart it: best-effort media can proceed without the key, and restarting
|
|
262
285
|
* would charge another grace to a source that may already be streaming. A session whose negotiation has
|
|
263
|
-
* settled answers without waiting at all
|
|
286
|
+
* settled answers without waiting at all: the answer stands until {@link repromptLevel2Key} re-opens the
|
|
287
|
+
* wait, since the station is prompted for `CMD_GATEWAYINFO` only on connect and on that re-prompt.
|
|
264
288
|
*
|
|
265
289
|
* An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
|
|
266
290
|
* on one account: own-session cameras of two device types negotiated a key, three others never did, and
|
|
@@ -276,12 +300,13 @@ export declare class P2PSession extends EventEmitter {
|
|
|
276
300
|
/**
|
|
277
301
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
278
302
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
303
|
+
* Without this the station is prompted once on connect, and a negotiation that settles without a key
|
|
304
|
+
* leaves {@link awaitLevel2Key} answering `false` at once for the rest of that connection. That is the
|
|
281
305
|
* right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
|
|
282
306
|
* level-2 is then refused for the whole life of that connection, while a fresh session over the same
|
|
283
307
|
* device negotiates a key normally. Measured: a session that had settled refused every such operation
|
|
284
|
-
* until it was rebuilt, at which point the station answered with a cipher id straight away.
|
|
308
|
+
* until it was rebuilt, at which point the station answered with a cipher id straight away. The reply to
|
|
309
|
+
* this ask runs the negotiation again and settles the re-opened wait either way.
|
|
285
310
|
*
|
|
286
311
|
* Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
|
|
287
312
|
* flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
|
|
@@ -296,13 +321,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
296
321
|
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
297
322
|
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
298
323
|
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
299
|
-
* key.
|
|
324
|
+
* key. Emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
300
325
|
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` when the ECC unwrap
|
|
301
326
|
* fails; an RSA unwrap failure is only traced (non-fatal either way — level-1 traffic keeps working).
|
|
302
327
|
*
|
|
303
|
-
* Every outcome settles the wait in {@link awaitLevel2Key},
|
|
304
|
-
*
|
|
305
|
-
*
|
|
328
|
+
* Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
|
|
329
|
+
* left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
|
|
330
|
+
* GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
|
|
331
|
+
* generation ended settles nothing. A {@link gatewayInfoRefused} reply names no cipher, so it settles at once
|
|
332
|
+
* with no cloud lookup, and a later reply naming a cipher withdraws the refusal.
|
|
306
333
|
*/
|
|
307
334
|
private negotiateLevel2Key;
|
|
308
335
|
/**
|
|
@@ -327,7 +354,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
327
354
|
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
328
355
|
*/
|
|
329
356
|
private requestReceiveBuffer;
|
|
330
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* Open the socket and start the lookup → hole-punch handshake.
|
|
359
|
+
*
|
|
360
|
+
* A socket closed or failing while binding never invokes its bind callback, so the bind wait also
|
|
361
|
+
* settles on the socket's `close` and `error`. On `close`, a connect whose session was closed meanwhile
|
|
362
|
+
* returns without starting the handshake, so it never sends a lookup or arms a timer on a closed
|
|
363
|
+
* session. On `error`, connect closes the session and rejects with {@link StationUnreachableError}
|
|
364
|
+
* carrying the bind error as its `cause`.
|
|
365
|
+
*/
|
|
331
366
|
connect(): Promise<void>;
|
|
332
367
|
/** Cached across every `P2PSession` in this process — the local outbound IPv4 doesn't vary by
|
|
333
368
|
* station, so there's no reason to re-probe it per session (e.g. once per station in a fleet). Only
|
|
@@ -494,6 +529,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
494
529
|
sendIntStringCommand(commandType: number, value: number, valueSub: number, strValue: string, channel?: number): void;
|
|
495
530
|
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
496
531
|
sendStringPairCommand(commandType: number, strValue: string, strValueSub: string, channel: number): void;
|
|
532
|
+
/**
|
|
533
|
+
* Whether this connection's station answered `CMD_GATEWAYINFO` with a refusal ({@link gatewayInfoRefused}) and
|
|
534
|
+
* no key that frames a set-payload is held.
|
|
535
|
+
*/
|
|
536
|
+
get refusedLevel2(): boolean;
|
|
497
537
|
/**
|
|
498
538
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
499
539
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -504,8 +544,12 @@ export declare class P2PSession extends EventEmitter {
|
|
|
504
544
|
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
505
545
|
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
506
546
|
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
547
|
+
*
|
|
548
|
+
* With `clearOnRefusal` on a session that {@link refusedLevel2}, the frame goes in the clear in the vendor
|
|
549
|
+
* app's form for such a device: header bytes `[01 00]`, signCode 0, and a body of `{cmd, payload}` alone.
|
|
550
|
+
* Without it, a refused session answers `false` and sends nothing.
|
|
507
551
|
*/
|
|
508
|
-
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
|
|
552
|
+
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number, clearOnRefusal?: boolean): boolean;
|
|
509
553
|
/**
|
|
510
554
|
* Send a **level-2 (GCM, signCode 8) frame whose plaintext is exactly `json`** — no
|
|
511
555
|
* `{account_id,cmd,mChannel,…}` envelope. The target device is selected by the frame-header
|
|
@@ -612,6 +656,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
612
656
|
* Send a CMD_SET_PAYLOAD(1350) wrapping `{account_id, cmd:<subCmd>, mChannel, mValue3:<subCmd>,
|
|
613
657
|
* payload}`, encrypted level-2 (AES-256-GCM, signCode 8) — the app's media-control path. The
|
|
614
658
|
* camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
|
|
659
|
+
* A start (HomeBase-controlled camera path, the V6 app's media-start envelope) also carries an
|
|
660
|
+
* `accountId` inside its payload and the media key `key` offers (see {@link attachedStartKey}). A start
|
|
661
|
+
* offering the P-256 point labels itself `ClientOS: "IOS"`, as every simple attached start in the app
|
|
662
|
+
* captures that carry that key form does; the RSA start keeps `"Android"`.
|
|
615
663
|
*/
|
|
616
664
|
private sendMediaPayloadLevel2;
|
|
617
665
|
/**
|
|
@@ -632,6 +680,41 @@ export declare class P2PSession extends EventEmitter {
|
|
|
632
680
|
* (T8171 2560×1440, T8210 640×480). Returns `undefined` on failure (frame is skipped).
|
|
633
681
|
*/
|
|
634
682
|
private rsaUnwrapKey;
|
|
683
|
+
/**
|
|
684
|
+
* The media-key type the next attached start sent on `channel` offers, recorded as offered: ECC, the current
|
|
685
|
+
* app's form, for the first start this connection sends there; RSA for every start after one that no keyframe
|
|
686
|
+
* answered before it was re-issued; and, once a keyframe has answered, the type that opened it, until
|
|
687
|
+
* {@link settleAttachedKey} records another. Only an opened keyframe answers: frames no key opens, and deltas,
|
|
688
|
+
* do not. A re-issue is a full restart on an attached camera, so a camera that cannot read the point is asked
|
|
689
|
+
* with RSA, the start every station answers, and is not asked with the point again while that goes unanswered.
|
|
690
|
+
* A stream decoding with the session's pair reads either answer. A station without {@link mediaEccPrivateKey}
|
|
691
|
+
* is offered RSA only. A start that cannot be sent offers nothing and records nothing.
|
|
692
|
+
*/
|
|
693
|
+
private attachedStartKey;
|
|
694
|
+
/**
|
|
695
|
+
* Record how a keyframe of `channel` was opened, which answers the starts there. A keyframe opened under a
|
|
696
|
+
* key type is an answer of that type, whichever type the latest start offered: a station can serve the
|
|
697
|
+
* P-256 start after the RSA re-issue went out. A plaintext keyframe
|
|
698
|
+
* answering an unanswered P-256 offer is a camera that did not use the point, and the next start there offers
|
|
699
|
+
* RSA; once a P-256 answer has been opened it changes nothing, and answering RSA, it settles RSA. Answers
|
|
700
|
+
* whether a new start is needed. A channel no attached start has offered a key on is left as it is.
|
|
701
|
+
*/
|
|
702
|
+
settleAttachedKey(channel: number, opened: "ecc" | "rsa" | "plain"): boolean;
|
|
703
|
+
/**
|
|
704
|
+
* The 32-byte P-256 private scalar paired with the point an attached camera's media start offers as `key`.
|
|
705
|
+
* A camera answering that start wraps each keyframe's AES-256-GCM media key in an ECIES envelope for this
|
|
706
|
+
* key ({@link VideoFrameDecoder}). Generated once per session, like the RSA pair.
|
|
707
|
+
*
|
|
708
|
+
* `undefined` unless the station is a HomeBase 3 (`T8030`): the app's P-256 start and the device runs
|
|
709
|
+
* that answered it were both observed on that station only.
|
|
710
|
+
*/
|
|
711
|
+
get mediaEccPrivateKey(): Buffer | undefined;
|
|
712
|
+
/**
|
|
713
|
+
* The session's P-256 pair: the private scalar as 32 bytes, and the public point as 128 hex characters,
|
|
714
|
+
* `x ‖ y` without the uncompressed-point prefix, the form the current app's attached-camera start carries in
|
|
715
|
+
* `key`. `ECDH.getPrivateKey()` drops a scalar's leading zero bytes, so the scalar is padded back to 32.
|
|
716
|
+
*/
|
|
717
|
+
private mediaEccPair;
|
|
635
718
|
/**
|
|
636
719
|
* The RSA-1024 public-key modulus (128-byte hex) the station uses to wrap the per-stream media
|
|
637
720
|
* key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
|
|
@@ -677,10 +760,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
677
760
|
innerCmd?: number;
|
|
678
761
|
}): void;
|
|
679
762
|
/**
|
|
680
|
-
*
|
|
681
|
-
*
|
|
763
|
+
* The `CMD_SET_PAYLOAD` payload of a database read or stored-image request: the JSON level-1 encrypted, or
|
|
764
|
+
* in the clear with header bytes from {@link nextClearCommandBytes} on a session that {@link sendsInTheClear}
|
|
765
|
+
* and holds no level-2 key.
|
|
682
766
|
*/
|
|
683
|
-
private
|
|
767
|
+
private databasePayload;
|
|
684
768
|
/**
|
|
685
769
|
* Request the on-device edge-AI **face roster** over P2P — the phone-free path.
|
|
686
770
|
*
|
|
@@ -719,6 +803,9 @@ export declare class P2PSession extends EventEmitter {
|
|
|
719
803
|
*
|
|
720
804
|
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
721
805
|
* session throws, so a second reply cannot land in this buffer.
|
|
806
|
+
*
|
|
807
|
+
* The query's framing follows the cipher the gateway-info reply names, so on a session still negotiating
|
|
808
|
+
* its level-2 key the query waits for that to settle, up to {@link LEVEL2_SETTLE_MS} from connect.
|
|
722
809
|
*/
|
|
723
810
|
readDatabase(table: string, opts?: {
|
|
724
811
|
accountId?: string;
|
|
@@ -788,6 +875,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
788
875
|
*
|
|
789
876
|
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
790
877
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
878
|
+
*
|
|
879
|
+
* A datagram shorter than the 8-byte DATA header carries no sequence number and is dropped unacknowledged.
|
|
791
880
|
*/
|
|
792
881
|
private onData;
|
|
793
882
|
/** Hold a datagram that arrived past a hole, and arm the wait for the missing one to be repeated. */
|
|
@@ -821,6 +910,13 @@ export declare class P2PSession extends EventEmitter {
|
|
|
821
910
|
*/
|
|
822
911
|
private resetInboundSequencing;
|
|
823
912
|
private ackTypeHeader;
|
|
913
|
+
/**
|
|
914
|
+
* Decrypt and dispatch one inbound data frame.
|
|
915
|
+
*
|
|
916
|
+
* A `CMD_GATEWAYINFO` (1100) reply carries the ECIES envelope for the level-2 session key. When a
|
|
917
|
+
* cipher-key resolver is configured, no key that seals a set-payload is held and no negotiation is in
|
|
918
|
+
* flight, the reply starts one (async, fire-and-forget); a reply landing while one is in flight is ignored.
|
|
919
|
+
*/
|
|
824
920
|
private handleFrame;
|
|
825
921
|
private send;
|
|
826
922
|
/** Finish the END send on the current socket before closing it; send completion is not a peer ACK. */
|
|
@@ -25,6 +25,8 @@ export interface ReadableEgressOptions {
|
|
|
25
25
|
/**
|
|
26
26
|
* Wrap a shared-source {@link Consumer} in a fresh Readable. The consumer is detached when the
|
|
27
27
|
* Readable is destroyed/ended, so callers own the lifetime by owning the stream.
|
|
28
|
-
* Frames arrive through onMedia, preserving their source-arrival metadata.
|
|
28
|
+
* Frames arrive through onMedia, preserving their source-arrival metadata. The consumer's battery
|
|
29
|
+
* `budget` notice is re-emitted unchanged as a `budget` event carrying a {@link StreamBudgetNotice};
|
|
30
|
+
* without an `extend()` within its grace the shared source ends the consumer and the Readable reaches EOF.
|
|
29
31
|
*/
|
|
30
32
|
export declare function openReadableFromConsumer(consumer: Consumer, opts?: ReadableEgressOptions): Readable;
|