@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.
@@ -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-absent", waitedMs: graceMs });
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 ?? ciphers[0]?.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 Error(`level-2 key not ready for ${sn} \u2014 cannot query`);
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 accountId = raw.member?.admin_user_id ?? this.deps.mega.auth?.userId ?? "";
19920
+ const stationAdminId = raw.member?.admin_user_id;
19921
+ const accountId = stationAdminId ?? this.deps.mega.auth?.userId ?? "";
19864
19922
  const t0 = Date.now();
19865
- while (!session.isConnected && Date.now() - t0 < CONNECT_WAIT_MS) {
19866
- opts.signal?.throwIfAborted();
19867
- await sleep2(200);
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 Error(`P2P session for ${parentSn} did not connect`);
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 Error(`level-2 key not ready for ${parentSn}`);
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,