@mega-yfue/eufy-sdk 0.2.0-beta.20 → 0.2.0-beta.21
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 +6 -2
- package/dist/index.js +63 -12
- package/dist/index.js.map +2 -2
- package/dist/transport/http/mega-client.d.ts +50 -3
- package/package.json +1 -1
package/dist/client/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `interface EufyMega` (the typed on/once/off/emit overloads) stays in `eufy-mega.ts` next to the
|
|
6
6
|
* class — TS declaration merging requires both in the same module.
|
|
7
7
|
*/
|
|
8
|
-
import type { MegaClientConfig } from "../transport/http/mega-client.js";
|
|
8
|
+
import type { MegaClientConfig, SessionExpiredError } from "../transport/http/mega-client.js";
|
|
9
9
|
import type { FcmStore } from "../transport/push/store.js";
|
|
10
10
|
import type { FfmpegLevel } from "../transport/ffmpeg.js";
|
|
11
11
|
import type { DeviceEventMap } from "../model/capabilities/index.js";
|
|
@@ -375,8 +375,12 @@ export type EufyMegaEventMap = {
|
|
|
375
375
|
* token expired. The SDK has already cleared the persisted session, so recovery is a fresh `login()`
|
|
376
376
|
* (which usually needs 2FA). Distinct from `error`: a session error is emitted ONLY here, not also
|
|
377
377
|
* on `error`.
|
|
378
|
+
*
|
|
379
|
+
* The error carries the rate: `err.retryAfterMs` is how long the next session replacement is barred
|
|
380
|
+
* for, and `err.contended` says this session is being displaced by another client rather than expiring
|
|
381
|
+
* — which a re-login does not answer. A login made before that wait elapses extends it.
|
|
378
382
|
*/
|
|
379
|
-
sessionExpired: [err:
|
|
383
|
+
sessionExpired: [err: SessionExpiredError];
|
|
380
384
|
error: [err: Error];
|
|
381
385
|
};
|
|
382
386
|
/** Event names {@link EufyMega} can emit. */
|
package/dist/index.js
CHANGED
|
@@ -1092,9 +1092,25 @@ var MegaApiError = class extends Error {
|
|
|
1092
1092
|
};
|
|
1093
1093
|
var OWNER_ONLY_CODE = 20004;
|
|
1094
1094
|
var SessionExpiredError = class extends Error {
|
|
1095
|
-
|
|
1095
|
+
/**
|
|
1096
|
+
* How long the next session replacement is barred for, in milliseconds; `0` when nothing bars one now.
|
|
1097
|
+
*
|
|
1098
|
+
* The remainder of the client's own hold-off, which doubles per consecutive replacement and is capped —
|
|
1099
|
+
* and which every replacement extends, whether the client spent it or a login made on this error did.
|
|
1100
|
+
*/
|
|
1101
|
+
retryAfterMs;
|
|
1102
|
+
/**
|
|
1103
|
+
* Whether this rejection landed inside that bar — a token replaced recently and rejected again since.
|
|
1104
|
+
*
|
|
1105
|
+
* It says the session is being DISPLACED rather than expiring: something else is signing in on this
|
|
1106
|
+
* account, and replacing the token again only trades one login for another.
|
|
1107
|
+
*/
|
|
1108
|
+
contended;
|
|
1109
|
+
constructor(message, opts = {}) {
|
|
1096
1110
|
super(message);
|
|
1097
1111
|
this.name = "SessionExpiredError";
|
|
1112
|
+
this.retryAfterMs = opts.retryAfterMs ?? 0;
|
|
1113
|
+
this.contended = opts.contended ?? false;
|
|
1098
1114
|
}
|
|
1099
1115
|
};
|
|
1100
1116
|
var EufyCloudErrorCode = {
|
|
@@ -1133,7 +1149,7 @@ var LoginStatus = {
|
|
|
1133
1149
|
var REAUTH_HOLD_OFF_MS = 6e4;
|
|
1134
1150
|
var REAUTH_HOLD_OFF_CAP_MS = 30 * 6e4;
|
|
1135
1151
|
var REAUTH_STABLE_MS = 10 * 6e4;
|
|
1136
|
-
var CONTENDED_SESSION_HINT = "another client
|
|
1152
|
+
var CONTENDED_SESSION_HINT = "another client signed in on this account keeps displacing this session \u2014 the cloud holds about one session per account and device identity, so each login ends the other's; where the other client is another SDK install, give each its own openudid";
|
|
1137
1153
|
function tokenRejected(code, msg) {
|
|
1138
1154
|
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 ?? "");
|
|
1139
1155
|
}
|
|
@@ -1183,9 +1199,11 @@ var MegaHttpClient = class {
|
|
|
1183
1199
|
loggingIn = false;
|
|
1184
1200
|
/** The one in-flight re-login every call rejected on the same dead token waits on. */
|
|
1185
1201
|
reauthAttempt;
|
|
1186
|
-
/** Replacements since the held session last proved stable, and when the last one ran — see {@link
|
|
1202
|
+
/** Replacements since the held session last proved stable, and when the last one ran — see {@link holdOffRemainingMs}. */
|
|
1187
1203
|
recoveries = 0;
|
|
1188
1204
|
lastRecoveryAt = 0;
|
|
1205
|
+
/** A token of ours has been rejected and not yet replaced — see {@link noteTokenReplacement}. */
|
|
1206
|
+
rejectedTokenPending = false;
|
|
1189
1207
|
constructor(cfg) {
|
|
1190
1208
|
this.cfg = {
|
|
1191
1209
|
appName: "eufy_mega",
|
|
@@ -1468,12 +1486,17 @@ var MegaHttpClient = class {
|
|
|
1468
1486
|
const tokenFinished = identityError && retry.identity || authed && last?.status === 401 && tokenRejected(last.code, last.msg);
|
|
1469
1487
|
if (tokenFinished) {
|
|
1470
1488
|
const reason = `${path} failed (${last?.status}/${last?.code}): ${last?.msg}`;
|
|
1489
|
+
this.rejectedTokenPending = true;
|
|
1471
1490
|
const recovery = retry.reauth ? "unrecoverable" : await this.recoverRejectedSession(sentToken);
|
|
1472
1491
|
if (recovery === "retry")
|
|
1473
1492
|
return this.postSigned(host, path, body, authed, { ...retry, reauth: true }, headerOverrides);
|
|
1474
1493
|
if (!this.sessionReplacedSince(sentToken))
|
|
1475
1494
|
this.clearSession();
|
|
1476
|
-
|
|
1495
|
+
const contended = recovery === "held-off";
|
|
1496
|
+
throw new SessionExpiredError(contended ? `${reason} \u2014 ${CONTENDED_SESSION_HINT}` : reason, {
|
|
1497
|
+
retryAfterMs: this.holdOffRemainingMs(),
|
|
1498
|
+
contended
|
|
1499
|
+
});
|
|
1477
1500
|
}
|
|
1478
1501
|
throw new MegaApiError(`${path} failed (${last?.status}/${last?.code}): ${last?.msg}`, last?.code, last?.status);
|
|
1479
1502
|
}
|
|
@@ -1798,8 +1821,7 @@ var MegaHttpClient = class {
|
|
|
1798
1821
|
return "unrecoverable";
|
|
1799
1822
|
if (!this.recoveryDue())
|
|
1800
1823
|
return "held-off";
|
|
1801
|
-
this.
|
|
1802
|
-
this.lastRecoveryAt = Date.now();
|
|
1824
|
+
this.noteTokenReplacement();
|
|
1803
1825
|
this.clearSession();
|
|
1804
1826
|
return await this.reauthenticate() ? "retry" : "unrecoverable";
|
|
1805
1827
|
}
|
|
@@ -1814,20 +1836,47 @@ var MegaHttpClient = class {
|
|
|
1814
1836
|
* and a caller is told the honest reason instead of being served a fight.
|
|
1815
1837
|
*/
|
|
1816
1838
|
recoveryDue() {
|
|
1817
|
-
|
|
1839
|
+
const remaining = this.holdOffRemainingMs();
|
|
1840
|
+
if (remaining === 0)
|
|
1818
1841
|
return true;
|
|
1819
|
-
|
|
1820
|
-
const waited = Date.now() - this.lastRecoveryAt;
|
|
1821
|
-
if (waited >= wait)
|
|
1822
|
-
return true;
|
|
1823
|
-
this.logger.warn(`[mega] token rejected ${Math.round(waited / 1e3)}s after the last replacement \u2014 holding off ${Math.round(wait / 1e3)}s. ${CONTENDED_SESSION_HINT}`);
|
|
1842
|
+
this.logger.warn(`[mega] token rejected ${Math.round((Date.now() - this.lastRecoveryAt) / 1e3)}s after the last replacement \u2014 holding off ${Math.round(remaining / 1e3)}s more. ${CONTENDED_SESSION_HINT}`);
|
|
1824
1843
|
return false;
|
|
1825
1844
|
}
|
|
1845
|
+
/**
|
|
1846
|
+
* How much longer a token replacement must wait, in milliseconds; `0` when one may run now.
|
|
1847
|
+
*
|
|
1848
|
+
* The wait doubles per consecutive replacement and is capped, and it is what {@link recoveryDue} gates
|
|
1849
|
+
* this client's own recovery on — and what {@link SessionExpiredError.retryAfterMs} hands a host that
|
|
1850
|
+
* drives its own. One function so the two cannot disagree about the rate, which they would have to for
|
|
1851
|
+
* a host to be told it may retry while this client is still holding off.
|
|
1852
|
+
*/
|
|
1853
|
+
holdOffRemainingMs() {
|
|
1854
|
+
if (this.recoveries === 0)
|
|
1855
|
+
return 0;
|
|
1856
|
+
const wait = Math.min(REAUTH_HOLD_OFF_MS * 2 ** (this.recoveries - 1), REAUTH_HOLD_OFF_CAP_MS);
|
|
1857
|
+
return Math.max(0, wait - (Date.now() - this.lastRecoveryAt));
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Count one token replacement against the hold-off, and clear the rejection it answered.
|
|
1861
|
+
*
|
|
1862
|
+
* Every replacement passes through here, wherever it was spent from: {@link recoverRejectedSession}, and
|
|
1863
|
+
* a {@link login} that follows a rejection this client surfaced. A hold-off that counted only its own
|
|
1864
|
+
* would be no bound at all — the wait would sit at its first value however many sessions had been spent,
|
|
1865
|
+
* and {@link SessionExpiredError.retryAfterMs} would report a minute while logins ran every few seconds.
|
|
1866
|
+
* Which of the two counted a given replacement is the flag: the recovery path clears it before logging
|
|
1867
|
+
* in, so the login cannot count the same one again.
|
|
1868
|
+
*/
|
|
1869
|
+
noteTokenReplacement() {
|
|
1870
|
+
this.recoveries++;
|
|
1871
|
+
this.lastRecoveryAt = Date.now();
|
|
1872
|
+
this.rejectedTokenPending = false;
|
|
1873
|
+
}
|
|
1826
1874
|
/**
|
|
1827
1875
|
* Note that the held session is working. A replacement that keeps serving calls for long enough is not
|
|
1828
1876
|
* contention, so the hold-off is forgotten and the next genuine expiry recovers immediately.
|
|
1829
1877
|
*/
|
|
1830
1878
|
noteSessionWorking() {
|
|
1879
|
+
this.rejectedTokenPending = false;
|
|
1831
1880
|
if (this.recoveries > 0 && Date.now() - this.lastRecoveryAt > REAUTH_STABLE_MS)
|
|
1832
1881
|
this.recoveries = 0;
|
|
1833
1882
|
}
|
|
@@ -1944,6 +1993,8 @@ var MegaHttpClient = class {
|
|
|
1944
1993
|
this.sessionKey = void 0;
|
|
1945
1994
|
await this.ensureSessionKey();
|
|
1946
1995
|
this.persist();
|
|
1996
|
+
if (this.rejectedTokenPending)
|
|
1997
|
+
this.noteTokenReplacement();
|
|
1947
1998
|
return { status: LoginStatus.Ok, session: { userId, authToken, geoKey: this.auth_.geoKey, raw: res } };
|
|
1948
1999
|
}
|
|
1949
2000
|
/** Save the current token + session key for reuse across runs. */
|