@mega-yfue/eufy-sdk 0.3.0-beta.2 → 0.3.0-beta.4
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/types.d.ts
CHANGED
|
@@ -377,8 +377,9 @@ export type EufyMegaEventMap = {
|
|
|
377
377
|
* on `error`.
|
|
378
378
|
*
|
|
379
379
|
* The error carries the rate: `err.retryAfterMs` is how long the next session replacement is barred
|
|
380
|
-
* for, and `err.contended` says
|
|
381
|
-
*
|
|
380
|
+
* for, and `err.contended` says the token was rejected again soon after being replaced, which is how
|
|
381
|
+
* displacement by another client looks and which a re-login does not answer. A login made before that
|
|
382
|
+
* wait elapses extends it.
|
|
382
383
|
*/
|
|
383
384
|
sessionExpired: [err: SessionExpiredError];
|
|
384
385
|
error: [err: Error];
|
package/dist/index.js
CHANGED
|
@@ -1102,8 +1102,9 @@ var SessionExpiredError = class extends Error {
|
|
|
1102
1102
|
/**
|
|
1103
1103
|
* Whether this rejection landed inside that bar — a token replaced recently and rejected again since.
|
|
1104
1104
|
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1105
|
+
* That is the shape displacement by another client signed in on this account has, where replacing the
|
|
1106
|
+
* token again only trades one login for another. It is not proof of one, since a cloud refusing to
|
|
1107
|
+
* re-issue the session looks the same.
|
|
1107
1108
|
*/
|
|
1108
1109
|
contended;
|
|
1109
1110
|
constructor(message, opts = {}) {
|
|
@@ -1149,7 +1150,7 @@ var LoginStatus = {
|
|
|
1149
1150
|
var REAUTH_HOLD_OFF_MS = 6e4;
|
|
1150
1151
|
var REAUTH_HOLD_OFF_CAP_MS = 30 * 6e4;
|
|
1151
1152
|
var REAUTH_STABLE_MS = 10 * 6e4;
|
|
1152
|
-
var CONTENDED_SESSION_HINT = "another client signed in
|
|
1153
|
+
var CONTENDED_SESSION_HINT = "the token was rejected again soon after it was last replaced; one possible cause is another client signed in with the same account and device identity, since the cloud holds about one session per pair and each login ends the other's (where that is another SDK install, give each its own openudid)";
|
|
1153
1154
|
function tokenRejected(code, msg) {
|
|
1154
1155
|
return code === EufyCloudErrorCode.SESSION_KICKED || /user_id is empty|invalid[^,]*\btoken\b|\btoken\b[^,]*(expired|error|not exist)|kicked|(?:\btoken\b|\bsession\b)[^,]*does not exist|unauthor/i.test(msg ?? "");
|
|
1155
1156
|
}
|
|
@@ -1163,6 +1164,8 @@ var MegaHttpClient = class {
|
|
|
1163
1164
|
sessionKey;
|
|
1164
1165
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
1165
1166
|
sessionKeys = /* @__PURE__ */ new Map();
|
|
1167
|
+
/** The in-flight key exchanges, by host and the token they carry — see {@link ensureSessionKey}. */
|
|
1168
|
+
keyExchanges = /* @__PURE__ */ new Map();
|
|
1166
1169
|
/**
|
|
1167
1170
|
* The held credential. `userId` is the login reply's `ap_cloud_user_id` where it has one — the Anker
|
|
1168
1171
|
* Passport cloud's id — while `accountUserId` is the eufy account's own `user_id`.
|
|
@@ -1384,6 +1387,20 @@ var MegaHttpClient = class {
|
|
|
1384
1387
|
return cached2;
|
|
1385
1388
|
}
|
|
1386
1389
|
const host = isEufylife ? targetHost : this.bootstrapDomain ?? `app-openapi-${this.region}.eufy.com`;
|
|
1390
|
+
const flight = `${host}
|
|
1391
|
+
${this.auth_?.authToken ?? ""}`;
|
|
1392
|
+
const joining = this.keyExchanges.get(flight);
|
|
1393
|
+
if (joining)
|
|
1394
|
+
return joining;
|
|
1395
|
+
const exchange = this.exchangeSessionKey(host, isEufylife).finally(() => {
|
|
1396
|
+
if (this.keyExchanges.get(flight) === exchange)
|
|
1397
|
+
this.keyExchanges.delete(flight);
|
|
1398
|
+
});
|
|
1399
|
+
this.keyExchanges.set(flight, exchange);
|
|
1400
|
+
return exchange;
|
|
1401
|
+
}
|
|
1402
|
+
/** One key exchange against `host`, installed as that host's key. */
|
|
1403
|
+
async exchangeSessionKey(host, isEufylife) {
|
|
1387
1404
|
const kxPath = isEufylife ? "/v3/openapi/oauth/key/exchange" : "/openapi/oauth/key/exchange";
|
|
1388
1405
|
const prep = prepareKeyExchange(isEufylife ? EUFYLIFE_LOCAL_KEY_HEX : void 0);
|
|
1389
1406
|
const res = await this.httpPost(`https://${host}${kxPath}`, { client_public_key: prep.encryptedClientPublicKey }, {
|
|
@@ -1399,7 +1416,7 @@ var MegaHttpClient = class {
|
|
|
1399
1416
|
}
|
|
1400
1417
|
const entry = finishKeyExchange(prep, env.data.server_public_key);
|
|
1401
1418
|
if (isEufylife)
|
|
1402
|
-
this.sessionKeys.set(
|
|
1419
|
+
this.sessionKeys.set(host, entry);
|
|
1403
1420
|
else
|
|
1404
1421
|
this.sessionKey = entry;
|
|
1405
1422
|
return entry;
|
|
@@ -1470,9 +1487,10 @@ var MegaHttpClient = class {
|
|
|
1470
1487
|
const identityError = authed && (last?.code === EufyCloudErrorCode.IDENTITY_KEY_STALE || /get identity error|identity error/i.test(last?.msg ?? ""));
|
|
1471
1488
|
if (identityError && !retry.identity && this.auth_) {
|
|
1472
1489
|
this.logger.debug("[mega] identity error \u2192 re-exchanging session key and retrying");
|
|
1473
|
-
if (host.includes(".eufylife.com"))
|
|
1474
|
-
this.sessionKeys.
|
|
1475
|
-
|
|
1490
|
+
if (host.includes(".eufylife.com")) {
|
|
1491
|
+
if (this.sessionKeys.get(host) === entry)
|
|
1492
|
+
this.sessionKeys.delete(host);
|
|
1493
|
+
} else if (this.sessionKey === entry)
|
|
1476
1494
|
this.sessionKey = void 0;
|
|
1477
1495
|
try {
|
|
1478
1496
|
await this.ensureSessionKey(host);
|
|
@@ -1832,14 +1850,14 @@ var MegaHttpClient = class {
|
|
|
1832
1850
|
* like the same device — and the cloud keeps one session per device. Each finds its token rejected, replaces
|
|
1833
1851
|
* it, and evicts the other: an unbounded login war, silent, and repeated logins are exactly what makes an
|
|
1834
1852
|
* account start demanding captchas. The first replacement is immediate, because a token displaced once is
|
|
1835
|
-
* the ordinary case; a second one soon after
|
|
1836
|
-
* and a caller is told
|
|
1853
|
+
* the ordinary case; a second one soon after has the shape of contention rather than expiry, so the wait grows
|
|
1854
|
+
* and a caller is told what was seen instead of being served a fight.
|
|
1837
1855
|
*/
|
|
1838
1856
|
recoveryDue() {
|
|
1839
1857
|
const remaining = this.holdOffRemainingMs();
|
|
1840
1858
|
if (remaining === 0)
|
|
1841
1859
|
return true;
|
|
1842
|
-
this.logger.warn(`[mega]
|
|
1860
|
+
this.logger.warn(`[mega] holding off ${Math.round(remaining / 1e3)}s more before replacing the token again (last replaced ${Math.round((Date.now() - this.lastRecoveryAt) / 1e3)}s ago): ${CONTENDED_SESSION_HINT}`);
|
|
1843
1861
|
return false;
|
|
1844
1862
|
}
|
|
1845
1863
|
/**
|
|
@@ -15141,6 +15159,8 @@ var LIVE_START_ACK_DEADLINE_MS = 3e3;
|
|
|
15141
15159
|
var SEQUENCE_LOOKBACK = 32768;
|
|
15142
15160
|
var STALE_RETRANSMIT_DEPTH = 1024;
|
|
15143
15161
|
var PUNCH_PROBE_SOCKETS = 7;
|
|
15162
|
+
var REORDER_WAIT_MS = 250;
|
|
15163
|
+
var REORDER_MAX_DATAGRAMS = 128;
|
|
15144
15164
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15145
15165
|
var CMD_SET_PAYLOAD = 1350;
|
|
15146
15166
|
var CMD_NOTIFY_PAYLOAD = 1351;
|
|
@@ -15210,9 +15230,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15210
15230
|
selfHost;
|
|
15211
15231
|
/** In-flight multi-datagram frame per data channel (see onData). */
|
|
15212
15232
|
pendingByDataType = /* @__PURE__ */ new Map();
|
|
15213
|
-
/** Last datagram sequence number
|
|
15214
|
-
* mid-frame and drop the (now unrecoverable) partial frame instead of splicing wrong bytes. */
|
|
15233
|
+
/** Last delivered datagram sequence number per data type. */
|
|
15215
15234
|
lastSeqByType = /* @__PURE__ */ new Map();
|
|
15235
|
+
/** Held datagrams and their active gap timer, grouped by data type. */
|
|
15236
|
+
reorderByType = /* @__PURE__ */ new Map();
|
|
15216
15237
|
tracedDatagramGaps = 0;
|
|
15217
15238
|
level1Key;
|
|
15218
15239
|
/** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
|
|
@@ -16467,8 +16488,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16467
16488
|
* Acknowledge and reassemble one DATA datagram, sequenced independently per data type.
|
|
16468
16489
|
*
|
|
16469
16490
|
* The device numbers each data type's datagrams in its own 16-bit space and repeats what it thinks was
|
|
16470
|
-
* lost
|
|
16471
|
-
* retransmission of something already reassembled: it is acknowledged, then ignored. Distance is measured
|
|
16491
|
+
* lost. A repeat of something already delivered is acknowledged, then ignored. Distance is measured
|
|
16472
16492
|
* modulo the sequence space and read as backwards beyond {@link SEQUENCE_LOOKBACK}, which is what lets the
|
|
16473
16493
|
* numbering wrap without the next datagram looking like a jump of nearly a full space.
|
|
16474
16494
|
*
|
|
@@ -16478,9 +16498,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16478
16498
|
* numbering and the half-assembled frame goes — because ignoring it would freeze the mark, and every
|
|
16479
16499
|
* datagram of the new numbering would then be read as behind it too, for as long as it took to climb back.
|
|
16480
16500
|
*
|
|
16481
|
-
*
|
|
16482
|
-
*
|
|
16483
|
-
*
|
|
16501
|
+
* A forward gap means a datagram has not arrived YET, which is not the same as lost. The device repeats
|
|
16502
|
+
* unacknowledged datagrams, so successors are held for a bounded wait and delivered strictly in sequence
|
|
16503
|
+
* if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
|
|
16504
|
+
* own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
|
|
16505
|
+
*
|
|
16506
|
+
* Only once {@link REORDER_WAIT_MS} passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
16507
|
+
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
16484
16508
|
*/
|
|
16485
16509
|
onData(msg, addr) {
|
|
16486
16510
|
const dataTypeBuffer = msg.subarray(4, 6);
|
|
@@ -16491,18 +16515,98 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16491
16515
|
const advance = prevSeq === void 0 ? 1 : seqNo - prevSeq & 65535;
|
|
16492
16516
|
if (advance === 0)
|
|
16493
16517
|
return;
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
}
|
|
16518
|
+
if (advance > SEQUENCE_LOOKBACK) {
|
|
16519
|
+
if (65536 - advance <= STALE_RETRANSMIT_DEPTH)
|
|
16520
|
+
return;
|
|
16521
|
+
if (this.pendingByDataType.has(dataType) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16522
|
+
this.trace({ phase: "sequence-restart", dataType });
|
|
16523
|
+
this.clearReorderTimer(dataType);
|
|
16524
|
+
this.reorderByType.delete(dataType);
|
|
16502
16525
|
this.pendingByDataType.delete(dataType);
|
|
16526
|
+
this.lastSeqByType.set(dataType, seqNo);
|
|
16527
|
+
this.reassemble(dataType, msg.subarray(8));
|
|
16528
|
+
return;
|
|
16529
|
+
}
|
|
16530
|
+
if (advance === 1) {
|
|
16531
|
+
this.lastSeqByType.set(dataType, seqNo);
|
|
16532
|
+
this.reassemble(dataType, msg.subarray(8));
|
|
16533
|
+
this.drainReorder(dataType);
|
|
16534
|
+
return;
|
|
16535
|
+
}
|
|
16536
|
+
this.holdForRetransmit(dataType, seqNo, msg.subarray(8));
|
|
16537
|
+
}
|
|
16538
|
+
/** Hold a datagram that arrived past a hole, and arm the wait for the missing one to be repeated. */
|
|
16539
|
+
holdForRetransmit(dataType, seqNo, body) {
|
|
16540
|
+
let reorder = this.reorderByType.get(dataType);
|
|
16541
|
+
if (!reorder) {
|
|
16542
|
+
reorder = { held: /* @__PURE__ */ new Map() };
|
|
16543
|
+
this.reorderByType.set(dataType, reorder);
|
|
16544
|
+
}
|
|
16545
|
+
if (!reorder.held.has(seqNo))
|
|
16546
|
+
reorder.held.set(seqNo, body);
|
|
16547
|
+
if (reorder.held.size > REORDER_MAX_DATAGRAMS) {
|
|
16548
|
+
this.abandonHole(dataType);
|
|
16549
|
+
return;
|
|
16503
16550
|
}
|
|
16551
|
+
this.armReorderTimer(dataType);
|
|
16552
|
+
}
|
|
16553
|
+
/** Start a full wait for the current hole, leaving an existing wait undisturbed. */
|
|
16554
|
+
armReorderTimer(dataType) {
|
|
16555
|
+
const reorder = this.reorderByType.get(dataType);
|
|
16556
|
+
if (!reorder?.held.size || reorder.timer)
|
|
16557
|
+
return;
|
|
16558
|
+
const timer = setTimeout(() => {
|
|
16559
|
+
reorder.timer = void 0;
|
|
16560
|
+
this.abandonHole(dataType);
|
|
16561
|
+
}, REORDER_WAIT_MS);
|
|
16562
|
+
timer.unref?.();
|
|
16563
|
+
reorder.timer = timer;
|
|
16564
|
+
}
|
|
16565
|
+
/** Deliver held datagrams that now follow directly on from the last one delivered. */
|
|
16566
|
+
drainReorder(dataType) {
|
|
16567
|
+
const reorder = this.reorderByType.get(dataType);
|
|
16568
|
+
if (!reorder?.held.size)
|
|
16569
|
+
return;
|
|
16570
|
+
this.clearReorderTimer(dataType);
|
|
16571
|
+
for (; ; ) {
|
|
16572
|
+
const next = this.lastSeqByType.get(dataType) + 1 & 65535;
|
|
16573
|
+
const body = reorder.held.get(next);
|
|
16574
|
+
if (body === void 0)
|
|
16575
|
+
break;
|
|
16576
|
+
reorder.held.delete(next);
|
|
16577
|
+
this.lastSeqByType.set(dataType, next);
|
|
16578
|
+
this.reassemble(dataType, body);
|
|
16579
|
+
}
|
|
16580
|
+
if (reorder.held.size)
|
|
16581
|
+
this.armReorderTimer(dataType);
|
|
16582
|
+
else
|
|
16583
|
+
this.reorderByType.delete(dataType);
|
|
16584
|
+
}
|
|
16585
|
+
/**
|
|
16586
|
+
* The missing datagram was not repeated in time: report the gap, drop the frame it belonged to, and
|
|
16587
|
+
* resume from the earliest datagram still held so the stream keeps moving.
|
|
16588
|
+
*/
|
|
16589
|
+
abandonHole(dataType) {
|
|
16590
|
+
const held = this.reorderByType.get(dataType).held;
|
|
16591
|
+
if (this.pendingByDataType.has(dataType) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16592
|
+
this.trace({ phase: "datagram-gap", dataType });
|
|
16593
|
+
this.pendingByDataType.delete(dataType);
|
|
16594
|
+
const last = this.lastSeqByType.get(dataType);
|
|
16595
|
+
const earliest = [...held.keys()].sort((a, b) => (a - last & 65535) - (b - last & 65535))[0];
|
|
16596
|
+
this.lastSeqByType.set(dataType, earliest - 1 & 65535);
|
|
16597
|
+
this.drainReorder(dataType);
|
|
16598
|
+
}
|
|
16599
|
+
clearReorderTimer(dataType) {
|
|
16600
|
+
const reorder = this.reorderByType.get(dataType);
|
|
16601
|
+
if (!reorder?.timer)
|
|
16602
|
+
return;
|
|
16603
|
+
clearTimeout(reorder.timer);
|
|
16604
|
+
reorder.timer = void 0;
|
|
16605
|
+
}
|
|
16606
|
+
/** Reassemble one in-sequence datagram body into logical frames. */
|
|
16607
|
+
reassemble(dataType, datagramBody) {
|
|
16504
16608
|
const pending = this.pendingByDataType.get(dataType);
|
|
16505
|
-
let body = pending ? Buffer.concat([pending.buf,
|
|
16609
|
+
let body = pending ? Buffer.concat([pending.buf, datagramBody]) : datagramBody;
|
|
16506
16610
|
const carryHeader = pending?.header;
|
|
16507
16611
|
this.pendingByDataType.delete(dataType);
|
|
16508
16612
|
if (carryHeader) {
|
|
@@ -16533,6 +16637,9 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16533
16637
|
* completed either.
|
|
16534
16638
|
*/
|
|
16535
16639
|
resetInboundSequencing() {
|
|
16640
|
+
for (const dataType of this.reorderByType.keys())
|
|
16641
|
+
this.clearReorderTimer(dataType);
|
|
16642
|
+
this.reorderByType.clear();
|
|
16536
16643
|
this.lastSeqByType.clear();
|
|
16537
16644
|
this.pendingByDataType.clear();
|
|
16538
16645
|
this.tracedDatagramGaps = 0;
|