@mega-yfue/eufy-sdk 0.2.0-beta.2 → 0.2.0-beta.3
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/core/contracts.d.ts +55 -0
- package/dist/index.js +85 -12
- package/dist/index.js.map +2 -2
- package/dist/transport/p2p/command-router.d.ts +23 -0
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-trace.d.ts +84 -6
- package/dist/transport/p2p/p2p-session.d.ts +4 -0
- package/package.json +1 -1
package/dist/core/contracts.d.ts
CHANGED
|
@@ -74,6 +74,61 @@ export declare class CameraDisabledError extends Error {
|
|
|
74
74
|
cause?: unknown;
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Work on a station was refused: the station did not provide the session key that work requires.
|
|
79
|
+
*
|
|
80
|
+
* A station reached over its HomeBase encrypts what it is sent under a key negotiated once per connection, and
|
|
81
|
+
* a media start for an attached camera has no unencrypted form at all — so without that key there is nothing
|
|
82
|
+
* to send, however reachable the station is. Naming this apart from a source that failed is what separates an
|
|
83
|
+
* account whose cipher material could not be resolved from a camera that is off, a station that is busy, or a
|
|
84
|
+
* stream that produced nothing: they share no next step.
|
|
85
|
+
*
|
|
86
|
+
* The `level2-unavailable` trace states WHY the key is not coming. This states only that it is not, because
|
|
87
|
+
* that is what the refusal itself knows.
|
|
88
|
+
*
|
|
89
|
+
* `stationSn` is the station that owed the key, which is the parent for an attached camera and therefore not
|
|
90
|
+
* the serial the refused call was made about: several cameras refused at once are one station's outcome, and
|
|
91
|
+
* nothing else in the refusal says so.
|
|
92
|
+
*/
|
|
93
|
+
export declare class StationKeyUnavailableError extends Error {
|
|
94
|
+
/** The station whose session key did not arrive. */
|
|
95
|
+
readonly stationSn: string;
|
|
96
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
97
|
+
readonly retryable = true;
|
|
98
|
+
constructor(
|
|
99
|
+
/** The station whose session key did not arrive. */
|
|
100
|
+
stationSn: string, options?: {
|
|
101
|
+
cause?: unknown;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Work on a station was refused: its session did not connect within the wait it was given.
|
|
106
|
+
*
|
|
107
|
+
* A station is reached over its own session, and nothing addressed to it — a media start, a property read, a
|
|
108
|
+
* still — can be attempted before that session is up. Naming this apart from every other failure is what tells
|
|
109
|
+
* a station that could not be reached at all from one that answered and then refused, or one that served media
|
|
110
|
+
* a caller could not use: those call for opposite next steps, and a caller cannot infer which it had from a
|
|
111
|
+
* message.
|
|
112
|
+
*
|
|
113
|
+
* `waitedMs` is how long was actually waited, which a caller compares against its own deadline to know whether
|
|
114
|
+
* this SDK concluded or its own bound expired first. `stationSn` is the station that could not be reached —
|
|
115
|
+
* the parent for an attached camera, so it is not derivable from the serial the call was made about.
|
|
116
|
+
*/
|
|
117
|
+
export declare class StationUnreachableError extends Error {
|
|
118
|
+
/** The station whose session did not connect. */
|
|
119
|
+
readonly stationSn: string;
|
|
120
|
+
/** How long the session was waited on before this was raised. */
|
|
121
|
+
readonly waitedMs: number;
|
|
122
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
123
|
+
readonly retryable = true;
|
|
124
|
+
constructor(
|
|
125
|
+
/** The station whose session did not connect. */
|
|
126
|
+
stationSn: string,
|
|
127
|
+
/** How long the session was waited on before this was raised. */
|
|
128
|
+
waitedMs: number, options?: {
|
|
129
|
+
cause?: unknown;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
77
132
|
/**
|
|
78
133
|
* A live stream was refused: the station is already serving another of its cameras to a viewer.
|
|
79
134
|
*
|
package/dist/index.js
CHANGED
|
@@ -2556,6 +2556,28 @@ var CameraDisabledError = class extends Error {
|
|
|
2556
2556
|
this.name = "CameraDisabledError";
|
|
2557
2557
|
}
|
|
2558
2558
|
};
|
|
2559
|
+
var StationKeyUnavailableError = class extends Error {
|
|
2560
|
+
stationSn;
|
|
2561
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
2562
|
+
retryable = true;
|
|
2563
|
+
constructor(stationSn, options) {
|
|
2564
|
+
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
2565
|
+
this.stationSn = stationSn;
|
|
2566
|
+
this.name = "StationKeyUnavailableError";
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
var StationUnreachableError = class extends Error {
|
|
2570
|
+
stationSn;
|
|
2571
|
+
waitedMs;
|
|
2572
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
2573
|
+
retryable = true;
|
|
2574
|
+
constructor(stationSn, waitedMs, options) {
|
|
2575
|
+
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
2576
|
+
this.stationSn = stationSn;
|
|
2577
|
+
this.waitedMs = waitedMs;
|
|
2578
|
+
this.name = "StationUnreachableError";
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2559
2581
|
var StationBusyError = class extends Error {
|
|
2560
2582
|
servingChannel;
|
|
2561
2583
|
/** Always true: the station is busy now, and stops being busy when the other stream is released. */
|
|
@@ -14201,19 +14223,27 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
14201
14223
|
* cameras from that second group streamed normally at level-1 — including one of the same firmware as an
|
|
14202
14224
|
* own-session camera that delivered no video at all for a reason of its own. An expired grace therefore
|
|
14203
14225
|
* separates nothing on this path, and a start failure on such a session is not evidence about it.
|
|
14226
|
+
*
|
|
14227
|
+
* Every `false` answer carries a `level2-unavailable` trace naming its reason, wherever the wait ended: a
|
|
14228
|
+
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
14229
|
+
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
14204
14230
|
*/
|
|
14205
14231
|
async awaitLevel2Key(graceMs, graceFrom = "call") {
|
|
14206
|
-
if (this.closed)
|
|
14232
|
+
if (this.closed) {
|
|
14233
|
+
this.trace({ phase: "level2-unavailable", reason: "session-closed" });
|
|
14207
14234
|
return false;
|
|
14235
|
+
}
|
|
14208
14236
|
if (this.level2Key)
|
|
14209
14237
|
return true;
|
|
14210
|
-
if (!this.level2Pending)
|
|
14238
|
+
if (!this.level2Pending) {
|
|
14239
|
+
this.trace({ phase: "level2-unavailable", reason: "not-negotiating" });
|
|
14211
14240
|
return false;
|
|
14241
|
+
}
|
|
14212
14242
|
const since = graceFrom === "call" ? Date.now() : this.connectedAtMs ?? Date.now();
|
|
14213
14243
|
const remaining = graceMs - (Date.now() - since);
|
|
14214
14244
|
if (remaining <= 0) {
|
|
14215
14245
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} no level-2 key and its ${graceMs}ms grace has elapsed`);
|
|
14216
|
-
this.trace({ phase: "level2-
|
|
14246
|
+
this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: graceMs });
|
|
14217
14247
|
return false;
|
|
14218
14248
|
}
|
|
14219
14249
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} waiting up to ${remaining}ms for the level-2 key`);
|
|
@@ -14239,10 +14269,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
14239
14269
|
});
|
|
14240
14270
|
if (outcome === "timeout") {
|
|
14241
14271
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key did not arrive within its grace`);
|
|
14272
|
+
this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: remaining });
|
|
14242
14273
|
} else if (outcome === "terminal") {
|
|
14243
14274
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 negotiation concluded without a key`);
|
|
14244
14275
|
} else if (outcome === "closed") {
|
|
14245
14276
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} session closed before the level-2 key arrived`);
|
|
14277
|
+
this.trace({ phase: "level2-unavailable", reason: "session-closed" });
|
|
14246
14278
|
}
|
|
14247
14279
|
return outcome === "key";
|
|
14248
14280
|
}
|
|
@@ -14293,6 +14325,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
14293
14325
|
this.level2Negotiating = true;
|
|
14294
14326
|
const generation = this.connectionGeneration;
|
|
14295
14327
|
const cipherId = gatewayInfoCipherId(gwPayload);
|
|
14328
|
+
this.trace({ phase: "level2-negotiating", cipherId });
|
|
14296
14329
|
void (async () => {
|
|
14297
14330
|
try {
|
|
14298
14331
|
const eccPrivHex = await this.cfg.resolveCipherKey?.(cipherId);
|
|
@@ -14300,11 +14333,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
14300
14333
|
return;
|
|
14301
14334
|
if (!eccPrivHex) {
|
|
14302
14335
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} no ECC key for cipher_id ${cipherId}`);
|
|
14336
|
+
this.trace({ phase: "level2-unavailable", reason: "no-cipher-key", cipherId });
|
|
14303
14337
|
this.settleLevel2();
|
|
14304
14338
|
return;
|
|
14305
14339
|
}
|
|
14306
14340
|
const key = deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex);
|
|
14307
14341
|
if (!key) {
|
|
14342
|
+
this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
|
|
14308
14343
|
this.settleLevel2();
|
|
14309
14344
|
this.emit("error", new Error(`level-2 key derivation failed (cipher_id ${cipherId})`));
|
|
14310
14345
|
return;
|
|
@@ -14316,6 +14351,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
14316
14351
|
} catch (e) {
|
|
14317
14352
|
if (this.closed || generation !== this.connectionGeneration)
|
|
14318
14353
|
return;
|
|
14354
|
+
this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
|
|
14319
14355
|
this.settleLevel2();
|
|
14320
14356
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
14321
14357
|
}
|
|
@@ -18869,6 +18905,11 @@ function abortable(work, signal) {
|
|
|
18869
18905
|
}
|
|
18870
18906
|
var LEVEL2_GRACE_MS = 25e3;
|
|
18871
18907
|
var LEVEL2_SETTLE_MS = 8e3;
|
|
18908
|
+
var P2P_STATION_WAITS = {
|
|
18909
|
+
connect: CONNECT_WAIT_MS,
|
|
18910
|
+
level2Grace: LEVEL2_GRACE_MS,
|
|
18911
|
+
level2Settle: LEVEL2_SETTLE_MS
|
|
18912
|
+
};
|
|
18872
18913
|
var RTSP_URL_READ_TIMEOUT_MS = 12e3;
|
|
18873
18914
|
var SHARED_LIVE_OPT_KEYS = [
|
|
18874
18915
|
"eccPrivateKey",
|
|
@@ -18920,6 +18961,14 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
18920
18961
|
}
|
|
18921
18962
|
return normalized;
|
|
18922
18963
|
}
|
|
18964
|
+
/**
|
|
18965
|
+
* Emit a live trace under a station session's handle, for work this router does ON that session before
|
|
18966
|
+
* the session itself records anything — reaching the station, and resolving what a device is on it. Same
|
|
18967
|
+
* handle as everything the session goes on to trace, which is what groups one attempt.
|
|
18968
|
+
*/
|
|
18969
|
+
traceOnStation(session, trace) {
|
|
18970
|
+
traceLiveStart(this.deps.logger ?? noopLogger, trace, session.traceId);
|
|
18971
|
+
}
|
|
18923
18972
|
/**
|
|
18924
18973
|
* Whether this transport stack drives `dev`'s `ff09-*` commands — true when the device has its own
|
|
18925
18974
|
* usable P2P endpoint (a non-empty `p2p_did`). The command sink asks each stack this to route a
|
|
@@ -19060,7 +19109,15 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19060
19109
|
let ecc;
|
|
19061
19110
|
try {
|
|
19062
19111
|
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
19063
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key
|
|
19112
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
19113
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
19114
|
+
ecc = ciphers[0].ecc_private_key;
|
|
19115
|
+
this.traceOnStation(session, {
|
|
19116
|
+
phase: "cipher-fallback",
|
|
19117
|
+
cipherId,
|
|
19118
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
19119
|
+
});
|
|
19120
|
+
}
|
|
19064
19121
|
} catch (e) {
|
|
19065
19122
|
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
19066
19123
|
}
|
|
@@ -19691,9 +19748,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19691
19748
|
s.sendStringPayloadCommand(P2P_ENVELOPE.CONTROL_PAYLOAD, json, ch);
|
|
19692
19749
|
return Promise.resolve();
|
|
19693
19750
|
},
|
|
19694
|
-
l2: async ({ session: s, channel: ch }) => {
|
|
19751
|
+
l2: async ({ session: s, channel: ch, parentSn }) => {
|
|
19695
19752
|
if (!await s.awaitLevel2Key(LEVEL2_GRACE_MS, "call")) {
|
|
19696
|
-
throw new
|
|
19753
|
+
throw new StationKeyUnavailableError(parentSn);
|
|
19697
19754
|
}
|
|
19698
19755
|
s.sendRawLevel2(json, ch, P2P_ENVELOPE.CONTROL_PAYLOAD);
|
|
19699
19756
|
}
|
|
@@ -19860,15 +19917,28 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19860
19917
|
}
|
|
19861
19918
|
this.manager.bumpCommand(parentSn);
|
|
19862
19919
|
const channel = typeof raw.device_channel === "number" ? raw.device_channel : 0;
|
|
19863
|
-
const
|
|
19920
|
+
const stationAdminId = raw.member?.admin_user_id;
|
|
19921
|
+
const accountId = stationAdminId ?? this.deps.mega.auth?.userId ?? "";
|
|
19864
19922
|
const t0 = Date.now();
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19923
|
+
let waitedMs = 0;
|
|
19924
|
+
if (!session.isConnected) {
|
|
19925
|
+
this.traceOnStation(session, { phase: "session-connect-wait", waitMs: CONNECT_WAIT_MS });
|
|
19926
|
+
while (!session.isConnected && Date.now() - t0 < CONNECT_WAIT_MS) {
|
|
19927
|
+
opts.signal?.throwIfAborted();
|
|
19928
|
+
await sleep2(200);
|
|
19929
|
+
}
|
|
19930
|
+
waitedMs = Date.now() - t0;
|
|
19931
|
+
this.traceOnStation(session, session.isConnected ? { phase: "session-connected", waitedMs } : { phase: "session-unreachable", waitedMs });
|
|
19868
19932
|
}
|
|
19869
19933
|
opts.signal?.throwIfAborted();
|
|
19870
19934
|
if (!session.isConnected)
|
|
19871
|
-
throw new
|
|
19935
|
+
throw new StationUnreachableError(parentSn, waitedMs);
|
|
19936
|
+
this.traceOnStation(session, {
|
|
19937
|
+
phase: "station-resolved",
|
|
19938
|
+
topology: homeBaseAttached ? "attached" : "own",
|
|
19939
|
+
channel,
|
|
19940
|
+
stationAdmin: typeof stationAdminId !== "string" ? "unstated" : stationAdminId === this.deps.mega.auth?.userId ? "self" : "other"
|
|
19941
|
+
});
|
|
19872
19942
|
if (opts.waitLevel2) {
|
|
19873
19943
|
if (opts.waitLevel2 === "settle") {
|
|
19874
19944
|
await abortable(session.awaitLevel2Key(LEVEL2_SETTLE_MS, "session"), opts.signal);
|
|
@@ -19882,7 +19952,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19882
19952
|
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), opts.signal);
|
|
19883
19953
|
}
|
|
19884
19954
|
if (!ready)
|
|
19885
|
-
throw new
|
|
19955
|
+
throw new StationKeyUnavailableError(parentSn);
|
|
19886
19956
|
}
|
|
19887
19957
|
return { session, parentSn, channel, accountId, homeBaseAttached };
|
|
19888
19958
|
}
|
|
@@ -25397,6 +25467,7 @@ export {
|
|
|
25397
25467
|
P256,
|
|
25398
25468
|
P2PSession,
|
|
25399
25469
|
P2P_ENVELOPE,
|
|
25470
|
+
P2P_STATION_WAITS,
|
|
25400
25471
|
PRINTER_CATEGORY_RE,
|
|
25401
25472
|
PTZ_MEMBERS,
|
|
25402
25473
|
PowerSource,
|
|
@@ -25430,6 +25501,8 @@ export {
|
|
|
25430
25501
|
SmartDropPushEvent,
|
|
25431
25502
|
StateConvergenceError,
|
|
25432
25503
|
StationBusyError,
|
|
25504
|
+
StationKeyUnavailableError,
|
|
25505
|
+
StationUnreachableError,
|
|
25433
25506
|
StoredSnapshotUnavailableError,
|
|
25434
25507
|
StreamingQuality,
|
|
25435
25508
|
SuctionLevel,
|