@mega-yfue/eufy-sdk 0.2.0-beta.15 → 0.2.0-beta.16
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/store.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ import type { RegionShard } from "../transport/http/mega-client.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export interface PersistedSession {
|
|
7
7
|
userId: string;
|
|
8
|
+
/**
|
|
9
|
+
* The eufy account's own `user_id` — the id the `gtoken` header is hashed from, which is not always the
|
|
10
|
+
* `ap_cloud_user_id` that `userId` prefers.
|
|
11
|
+
*
|
|
12
|
+
* Always written, equal to `userId` when the login reply carried only the one id. Absent therefore means a
|
|
13
|
+
* record from before this was tracked, which {@link isSessionValid} refuses rather than restore.
|
|
14
|
+
*/
|
|
15
|
+
accountUserId?: string;
|
|
8
16
|
authToken: string;
|
|
9
17
|
geoKey?: string;
|
|
10
18
|
region: RegionShard;
|
|
@@ -50,5 +58,14 @@ export declare class FileSessionStore<T = PersistedSession> implements SessionSt
|
|
|
50
58
|
* (e.g. Solix) whose session shape differs but whose freshness rule is identical.
|
|
51
59
|
*/
|
|
52
60
|
export declare function tokenNotExpired(tokenExpiresAt: number | undefined, skewSec?: number): boolean;
|
|
53
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* A persisted session is usable if it carries a complete credential — token, bound ECDH key, and the account
|
|
63
|
+
* id the `gtoken` header is hashed from — whose token isn't (near-)expired.
|
|
64
|
+
*
|
|
65
|
+
* `accountUserId` is part of the credential, not an optional extra: a record without it was written before
|
|
66
|
+
* that id was tracked, so it can only be restored as the other id, which is what the gateway rejects the
|
|
67
|
+
* header on. Nothing in a restored session can recover it either — the id arrives with a login reply. So such
|
|
68
|
+
* a record is refused and one login re-establishes it, rather than reinstating a session whose every
|
|
69
|
+
* authenticated call fails identically.
|
|
70
|
+
*/
|
|
54
71
|
export declare function isSessionValid(s: PersistedSession | null, skewSec?: number): boolean;
|
package/dist/index.js
CHANGED
|
@@ -153,7 +153,7 @@ function tokenNotExpired(tokenExpiresAt, skewSec = 300) {
|
|
|
153
153
|
return true;
|
|
154
154
|
}
|
|
155
155
|
function isSessionValid(s, skewSec = 300) {
|
|
156
|
-
if (!s?.authToken || !s.shareKey || !s.keyIdent)
|
|
156
|
+
if (!s?.authToken || !s.shareKey || !s.keyIdent || !s.accountUserId)
|
|
157
157
|
return false;
|
|
158
158
|
return tokenNotExpired(s.tokenExpiresAt, skewSec);
|
|
159
159
|
}
|
|
@@ -1135,7 +1135,7 @@ var REAUTH_HOLD_OFF_CAP_MS = 30 * 6e4;
|
|
|
1135
1135
|
var REAUTH_STABLE_MS = 10 * 6e4;
|
|
1136
1136
|
var CONTENDED_SESSION_HINT = "another client may be signed in with the same account and device identity, and each login displaces the other's session; give each client its own openudid";
|
|
1137
1137
|
function tokenRejected(code, msg) {
|
|
1138
|
-
return code === EufyCloudErrorCode.SESSION_KICKED || /user_id is empty|invalid
|
|
1138
|
+
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
1139
|
}
|
|
1140
1140
|
function withoutTokenEcho(text2) {
|
|
1141
1141
|
return text2.replace(/token\s*[=:]\s*"?[A-Za-z0-9._-]{8,}"?/gi, "token = <redacted>");
|
|
@@ -1147,6 +1147,13 @@ var MegaHttpClient = class {
|
|
|
1147
1147
|
sessionKey;
|
|
1148
1148
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
1149
1149
|
sessionKeys = /* @__PURE__ */ new Map();
|
|
1150
|
+
/**
|
|
1151
|
+
* The held credential. `userId` is the login reply's `ap_cloud_user_id` where it has one — the Anker
|
|
1152
|
+
* Passport cloud's id — while `accountUserId` is the eufy account's own `user_id`.
|
|
1153
|
+
*
|
|
1154
|
+
* The `gtoken` header is hashed from `accountUserId`: that is the id the gateway recomputes the header
|
|
1155
|
+
* from, rejecting a disagreement with `"gtoken not equal userid error"`.
|
|
1156
|
+
*/
|
|
1150
1157
|
auth_;
|
|
1151
1158
|
/** captcha_id of an in-flight challenge, held between login() and solveCaptcha(). */
|
|
1152
1159
|
pendingCaptchaId;
|
|
@@ -1212,7 +1219,12 @@ var MegaHttpClient = class {
|
|
|
1212
1219
|
if (!isSessionValid(saved) || !saved)
|
|
1213
1220
|
return void 0;
|
|
1214
1221
|
this.region = saved.region;
|
|
1215
|
-
this.auth_ = {
|
|
1222
|
+
this.auth_ = {
|
|
1223
|
+
userId: saved.userId,
|
|
1224
|
+
accountUserId: saved.accountUserId,
|
|
1225
|
+
authToken: saved.authToken,
|
|
1226
|
+
geoKey: saved.geoKey
|
|
1227
|
+
};
|
|
1216
1228
|
this.tokenExpiresAt = saved.tokenExpiresAt;
|
|
1217
1229
|
this.sessionKey = {
|
|
1218
1230
|
keyIdent: saved.keyIdent,
|
|
@@ -1275,7 +1287,16 @@ var MegaHttpClient = class {
|
|
|
1275
1287
|
};
|
|
1276
1288
|
}
|
|
1277
1289
|
/**
|
|
1278
|
-
* The
|
|
1290
|
+
* The id `gtoken` is hashed from — the account's own `user_id`, which is what the gateway recomputes the
|
|
1291
|
+
* header from. One place so the two header paths cannot drift on which of the session's ids that is.
|
|
1292
|
+
*
|
|
1293
|
+
* Call only where `auth_` is already established; every header path guards it.
|
|
1294
|
+
*/
|
|
1295
|
+
gtokenUserId() {
|
|
1296
|
+
return this.auth_.accountUserId ?? this.auth_.userId;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* The account-credential headers every authed call carries — `x-auth-token` + `gtoken`.
|
|
1279
1300
|
* One place so the signed path, the key-exchange and the bearer path can't drift on what "authed" means.
|
|
1280
1301
|
*/
|
|
1281
1302
|
authTokenHeaders() {
|
|
@@ -1284,7 +1305,7 @@ var MegaHttpClient = class {
|
|
|
1284
1305
|
return {
|
|
1285
1306
|
"x-auth-token": this.auth_.authToken,
|
|
1286
1307
|
authorization: this.auth_.authToken,
|
|
1287
|
-
gtoken: gtoken(this.
|
|
1308
|
+
gtoken: gtoken(this.gtokenUserId())
|
|
1288
1309
|
};
|
|
1289
1310
|
}
|
|
1290
1311
|
/**
|
|
@@ -1588,7 +1609,7 @@ var MegaHttpClient = class {
|
|
|
1588
1609
|
try {
|
|
1589
1610
|
return await downloadMediaResource(url, {
|
|
1590
1611
|
"x-auth-token": this.auth_.authToken,
|
|
1591
|
-
gtoken: gtoken(this.
|
|
1612
|
+
gtoken: gtoken(this.gtokenUserId()),
|
|
1592
1613
|
"app-name": "eufy_mega",
|
|
1593
1614
|
"model-type": "PHONE",
|
|
1594
1615
|
"user-agent": this.mediaUserAgent
|
|
@@ -1890,17 +1911,19 @@ var MegaHttpClient = class {
|
|
|
1890
1911
|
{
|
|
1891
1912
|
const cap = Object.keys(res).filter((k) => /captcha|answer|picture|image|fa_/i.test(k));
|
|
1892
1913
|
this.logger.debug("[mega] login resp keys:", Object.keys(res).join(","));
|
|
1914
|
+
this.logger.debug("[mega] ids agree:", res.ap_cloud_user_id === res.user_id);
|
|
1893
1915
|
if (cap.length)
|
|
1894
1916
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1895
1917
|
}
|
|
1896
1918
|
const userId = res.ap_cloud_user_id ?? res.user_id ?? res.userId;
|
|
1919
|
+
const accountUserId = res.user_id ?? res.userId;
|
|
1897
1920
|
const authToken = res.auth_token ?? res.token;
|
|
1898
1921
|
if (!userId || !authToken)
|
|
1899
1922
|
throw new Error(`login returned no session: ${JSON.stringify(res).slice(0, 200)}`);
|
|
1900
1923
|
const faInfo = res.fa_info ?? {};
|
|
1901
1924
|
const needs2fa = !isVerify && !!faInfo.info;
|
|
1902
1925
|
if (needs2fa) {
|
|
1903
|
-
this.auth_ = { userId, authToken, geoKey: res.geo_key };
|
|
1926
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
1904
1927
|
this.pendingCaptchaId = void 0;
|
|
1905
1928
|
this.pending2fa = true;
|
|
1906
1929
|
await this.sendVerifyCode(messageType);
|
|
@@ -1908,7 +1931,7 @@ var MegaHttpClient = class {
|
|
|
1908
1931
|
}
|
|
1909
1932
|
this.pendingCaptchaId = void 0;
|
|
1910
1933
|
this.pending2fa = false;
|
|
1911
|
-
this.auth_ = { userId, authToken, geoKey: res.geo_key };
|
|
1934
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
1912
1935
|
this.tokenExpiresAt = Number(res.token_expires_at ?? 0) || 0;
|
|
1913
1936
|
this.sessionKey = void 0;
|
|
1914
1937
|
await this.ensureSessionKey();
|
|
@@ -1921,6 +1944,7 @@ var MegaHttpClient = class {
|
|
|
1921
1944
|
return;
|
|
1922
1945
|
this.store.save({
|
|
1923
1946
|
userId: this.auth_.userId,
|
|
1947
|
+
accountUserId: this.auth_.accountUserId ?? this.auth_.userId,
|
|
1924
1948
|
authToken: this.auth_.authToken,
|
|
1925
1949
|
geoKey: this.auth_.geoKey,
|
|
1926
1950
|
region: this.region,
|
|
@@ -26406,6 +26430,11 @@ var SolixClient = class {
|
|
|
26406
26430
|
* Turn a decrypted `/passport/login` payload into an `ok`/`2fa` result, establishing the session on
|
|
26407
26431
|
* `ok`. The passport marks a pending 2FA with a non-empty `fa_info.info`, and empties it once the code
|
|
26408
26432
|
* has been satisfied.
|
|
26433
|
+
*
|
|
26434
|
+
* `gtoken` is hashed from `ap_cloud_user_id` where the reply carries one, `user_id` otherwise. Whether
|
|
26435
|
+
* this gateway recomputes the header from `user_id` specifically — as the mega gateway does, rejecting a
|
|
26436
|
+
* disagreement with `"gtoken not equal userid error"` — is unverified here: no Solix response has been
|
|
26437
|
+
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
26409
26438
|
*/
|
|
26410
26439
|
classifyLogin(data, isVerify) {
|
|
26411
26440
|
const userId = data.ap_cloud_user_id ?? data.user_id;
|