@mega-yfue/eufy-sdk 0.6.0-beta.31 → 0.6.0-beta.32
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/index.js +101 -32
- package/dist/index.js.map +2 -2
- package/dist/transport/http/mega-client.d.ts +15 -3
- package/dist/transport/rtc/command-router.d.ts +19 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1609,11 +1609,17 @@ var CONTENDED_SESSION_HINT = "the token was rejected again soon after it was las
|
|
|
1609
1609
|
function tokenRejected(code, msg) {
|
|
1610
1610
|
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 ?? "");
|
|
1611
1611
|
}
|
|
1612
|
+
function shardOfDomain(domain) {
|
|
1613
|
+
const m = /-([a-z]{2})(?:[.-]|$)/i.exec(domain);
|
|
1614
|
+
return m ? `${m[1].toLowerCase()}-pr` : void 0;
|
|
1615
|
+
}
|
|
1612
1616
|
var MegaHttpClient = class {
|
|
1613
1617
|
cfg;
|
|
1614
1618
|
region;
|
|
1615
1619
|
/** The host `estimate_domain` answered, persisted with the session. */
|
|
1616
1620
|
estimatedDomain = "";
|
|
1621
|
+
/** The in-flight (then settled, so cached) RTC-sign shard request from {@link portalShard}. */
|
|
1622
|
+
portalShardInFlight;
|
|
1617
1623
|
bootstrapDomain;
|
|
1618
1624
|
sessionKey;
|
|
1619
1625
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
@@ -1730,13 +1736,36 @@ var MegaHttpClient = class {
|
|
|
1730
1736
|
return this.region;
|
|
1731
1737
|
}
|
|
1732
1738
|
/**
|
|
1733
|
-
* The shard the
|
|
1734
|
-
*
|
|
1735
|
-
* like `ie-pr`, whose accounts need the IE sign host and cluster.
|
|
1739
|
+
* The shard the estimated domain names (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The
|
|
1740
|
+
* eu/us classification cannot name shards like `ie-pr`.
|
|
1736
1741
|
*/
|
|
1737
1742
|
get rtcShard() {
|
|
1738
|
-
|
|
1739
|
-
|
|
1743
|
+
return shardOfDomain(this.estimatedDomain) ?? this.region;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* The shard the RTC signalling signs on, from `estimate_domain` asked with the portal's WEB identity
|
|
1747
|
+
* (`Model-Type: WEB`, `App-Name: eufy_mega`) — the identity the sign itself is sent with, which can name
|
|
1748
|
+
* a different shard than the PHONE identity does for one account. The in-flight request is shared by
|
|
1749
|
+
* concurrent first callers, and once it settles to a shard the resolved promise is the cache; a call
|
|
1750
|
+
* that fails or names no shard answers {@link rtcShard}, clears the field and is asked again next time.
|
|
1751
|
+
*/
|
|
1752
|
+
async portalShard() {
|
|
1753
|
+
return this.portalShardInFlight ??= this.fetchPortalShard();
|
|
1754
|
+
}
|
|
1755
|
+
async fetchPortalShard() {
|
|
1756
|
+
try {
|
|
1757
|
+
const res = await this.estimateDomainRequest({ "Model-Type": "WEB", "App-Name": "eufy_mega" });
|
|
1758
|
+
const domain = res.data?.data?.domain;
|
|
1759
|
+
const shard = typeof domain === "string" ? shardOfDomain(domain) : void 0;
|
|
1760
|
+
if (shard) {
|
|
1761
|
+
this.logger.debug("[mega] portal shard:", shard);
|
|
1762
|
+
return shard;
|
|
1763
|
+
}
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
this.logger.debug("[mega] portal estimate_domain failed:", e instanceof Error ? e.message : String(e));
|
|
1766
|
+
}
|
|
1767
|
+
this.portalShardInFlight = void 0;
|
|
1768
|
+
return this.rtcShard;
|
|
1740
1769
|
}
|
|
1741
1770
|
/**
|
|
1742
1771
|
* The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
|
|
@@ -1833,13 +1862,13 @@ var MegaHttpClient = class {
|
|
|
1833
1862
|
});
|
|
1834
1863
|
return { status: res.status, data: parseMaybeJson(await res.text()) };
|
|
1835
1864
|
}
|
|
1865
|
+
/** POST `estimate_domain` with the given identity headers. */
|
|
1866
|
+
estimateDomainRequest(headers) {
|
|
1867
|
+
return this.httpPost(`https://mega-${this.region}.eufy.com/passport/estimate_domain`, { ab: this.cfg.countryCode.toLowerCase(), mode: 1 }, { ...headers, "content-type": "application/json" });
|
|
1868
|
+
}
|
|
1836
1869
|
/** Step 1: discover the account's region shard (and the bootstrap host). */
|
|
1837
1870
|
async estimateDomain() {
|
|
1838
|
-
const
|
|
1839
|
-
const res = await this.httpPost(url, { ab: this.cfg.countryCode.toLowerCase(), mode: 1 }, {
|
|
1840
|
-
...this.baseHeaders(),
|
|
1841
|
-
"content-type": "application/json"
|
|
1842
|
-
});
|
|
1871
|
+
const res = await this.estimateDomainRequest(this.baseHeaders());
|
|
1843
1872
|
this.logger.debug("[mega] estimate_domain raw:", JSON.stringify(res.data).slice(0, 400));
|
|
1844
1873
|
const env = res.data;
|
|
1845
1874
|
const d = env?.data ?? {};
|
|
@@ -25727,6 +25756,8 @@ var RtcCommandRouter = class {
|
|
|
25727
25756
|
warm = /* @__PURE__ */ new Map();
|
|
25728
25757
|
callSources = /* @__PURE__ */ new Map();
|
|
25729
25758
|
lastTransaction = 0;
|
|
25759
|
+
/** The latest shard each `call` source resolved, read by its `makeStream` (see {@link callMediaProviderFor}). */
|
|
25760
|
+
callShards = /* @__PURE__ */ new Map();
|
|
25730
25761
|
constructor(deps) {
|
|
25731
25762
|
this.deps = deps;
|
|
25732
25763
|
}
|
|
@@ -25853,7 +25884,13 @@ var RtcCommandRouter = class {
|
|
|
25853
25884
|
views
|
|
25854
25885
|
};
|
|
25855
25886
|
}
|
|
25856
|
-
/**
|
|
25887
|
+
/**
|
|
25888
|
+
* Share a dedicated-call source and its consumer operations under one route key.
|
|
25889
|
+
*
|
|
25890
|
+
* `SharedLiveSource` calls `makeStream` synchronously, so the shard cannot be awaited inside it. Each
|
|
25891
|
+
* async entry point resolves the shard and records it in {@link callShards} before touching the source;
|
|
25892
|
+
* `makeStream` reads the latest recorded value.
|
|
25893
|
+
*/
|
|
25857
25894
|
callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
|
|
25858
25895
|
const sourceFor = (opts = {}) => {
|
|
25859
25896
|
opts.signal?.throwIfAborted();
|
|
@@ -25867,7 +25904,7 @@ var RtcCommandRouter = class {
|
|
|
25867
25904
|
if (!identity)
|
|
25868
25905
|
throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
|
|
25869
25906
|
const accountId = route.adminUserId || identity.userId;
|
|
25870
|
-
return makeStream(this.newSession(route, accountId, identity), accountId, view);
|
|
25907
|
+
return makeStream(this.newSession(route, accountId, identity, this.callShards.get(key)), accountId, view);
|
|
25871
25908
|
},
|
|
25872
25909
|
label: sourceKey,
|
|
25873
25910
|
logger: this.deps.logger,
|
|
@@ -25879,14 +25916,26 @@ var RtcCommandRouter = class {
|
|
|
25879
25916
|
}
|
|
25880
25917
|
return source;
|
|
25881
25918
|
};
|
|
25919
|
+
const resolveShard = async () => {
|
|
25920
|
+
this.callShards.set(key, await this.deps.shard(route));
|
|
25921
|
+
};
|
|
25882
25922
|
return {
|
|
25883
|
-
live: async (opts) =>
|
|
25884
|
-
|
|
25885
|
-
|
|
25886
|
-
|
|
25887
|
-
|
|
25888
|
-
|
|
25889
|
-
|
|
25923
|
+
live: async (opts) => {
|
|
25924
|
+
await resolveShard();
|
|
25925
|
+
return attachUnlessAborted(sourceFor(opts), opts?.signal);
|
|
25926
|
+
},
|
|
25927
|
+
openReadable: async (opts) => {
|
|
25928
|
+
await resolveShard();
|
|
25929
|
+
return openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts);
|
|
25930
|
+
},
|
|
25931
|
+
snapshotLive: async (opts) => {
|
|
25932
|
+
await resolveShard();
|
|
25933
|
+
return captureSnapshotFromShared(sourceFor(opts), {
|
|
25934
|
+
...opts,
|
|
25935
|
+
logger: this.deps.logger,
|
|
25936
|
+
ffmpegPath: this.deps.ffmpegPath
|
|
25937
|
+
});
|
|
25938
|
+
},
|
|
25890
25939
|
record: async () => {
|
|
25891
25940
|
throw new Error(recordError);
|
|
25892
25941
|
}
|
|
@@ -25901,6 +25950,7 @@ var RtcCommandRouter = class {
|
|
|
25901
25950
|
this.callSources.delete(sn);
|
|
25902
25951
|
src.dispose();
|
|
25903
25952
|
}
|
|
25953
|
+
this.callShards.clear();
|
|
25904
25954
|
for (const [sn, live] of this.lives) {
|
|
25905
25955
|
this.lives.delete(sn);
|
|
25906
25956
|
live.source.dispose();
|
|
@@ -26094,31 +26144,50 @@ var RtcCommandRouter = class {
|
|
|
26094
26144
|
}
|
|
26095
26145
|
});
|
|
26096
26146
|
}
|
|
26097
|
-
newSession(route, adminUserId, identity) {
|
|
26147
|
+
newSession(route, adminUserId, identity, shard) {
|
|
26098
26148
|
return (this.deps.createSession ?? ((o) => new RtcSession(o)))({
|
|
26099
26149
|
authToken: identity.authToken,
|
|
26100
26150
|
gtoken: identity.gtoken,
|
|
26101
26151
|
stationSn: route.stationSn,
|
|
26102
26152
|
adminUserId,
|
|
26103
|
-
shard
|
|
26153
|
+
shard,
|
|
26104
26154
|
country: this.deps.country ?? "US",
|
|
26105
26155
|
logger: this.deps.logger,
|
|
26106
26156
|
signalingMode: route.signalingMode,
|
|
26107
26157
|
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy, mediaTrack: route.mediaTrack }
|
|
26108
26158
|
});
|
|
26109
26159
|
}
|
|
26160
|
+
/** A connected session for `sn` to reuse, or undefined (dropping a dead one). */
|
|
26161
|
+
async reusable(sn) {
|
|
26162
|
+
const st = this.sessions.get(sn);
|
|
26163
|
+
if (!st)
|
|
26164
|
+
return void 0;
|
|
26165
|
+
await st.ready;
|
|
26166
|
+
if (st.session.isConnected) {
|
|
26167
|
+
this.touch(sn, st);
|
|
26168
|
+
return st;
|
|
26169
|
+
}
|
|
26170
|
+
this.drop(sn, st);
|
|
26171
|
+
return void 0;
|
|
26172
|
+
}
|
|
26173
|
+
/**
|
|
26174
|
+
* Get or open the station's session. A connected session is reused before the shard is awaited, so a
|
|
26175
|
+
* command on an already-connected station never waits on the unauthenticated `estimate_domain` round
|
|
26176
|
+
* trip — only the create path awaits it. After the await the map is re-checked, with no `await` between
|
|
26177
|
+
* the lookup and `this.sessions.set`, so concurrent creators still share one session.
|
|
26178
|
+
*/
|
|
26110
26179
|
async stationSession(route, adminUserId, identity) {
|
|
26111
26180
|
const sn = route.stationSn;
|
|
26112
|
-
const
|
|
26113
|
-
if (
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
}
|
|
26121
|
-
const session = this.newSession(route, adminUserId, identity);
|
|
26181
|
+
const reused = await this.reusable(sn);
|
|
26182
|
+
if (reused)
|
|
26183
|
+
return reused;
|
|
26184
|
+
const shard = await this.deps.shard(route);
|
|
26185
|
+
if (this.sessions.has(sn)) {
|
|
26186
|
+
const again = await this.reusable(sn);
|
|
26187
|
+
if (again)
|
|
26188
|
+
return again;
|
|
26189
|
+
}
|
|
26190
|
+
const session = this.newSession(route, adminUserId, identity, shard);
|
|
26122
26191
|
const ready = this.bringUp(sn, session);
|
|
26123
26192
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve(), leases: 0 };
|
|
26124
26193
|
session.on("error", (e) => this.deps.onError?.(e));
|
|
@@ -29768,7 +29837,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
|
|
|
29768
29837
|
});
|
|
29769
29838
|
this.rtc = new RtcCommandRouter({
|
|
29770
29839
|
identity: () => this.mega.rtcIdentity(),
|
|
29771
|
-
shard: () => this.mega.rtcShard,
|
|
29840
|
+
shard: (route) => route.mediaTrack ? this.mega.rtcShard : this.mega.portalShard(),
|
|
29772
29841
|
country: opts.countryCode,
|
|
29773
29842
|
logger: opts.logger,
|
|
29774
29843
|
ffmpegPath: opts.ffmpegPath,
|