@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 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 RTC signalling signs on: the regional prefix of the estimated domain
1734
- * (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The eu/us classification cannot name shards
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
- const m = /-([a-z]{2})(?:[.-]|$)/i.exec(this.estimatedDomain);
1739
- return m ? `${m[1].toLowerCase()}-pr` : this.region;
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 url = `https://mega-${this.region}.eufy.com/passport/estimate_domain`;
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
- /** Share a dedicated-call source and its consumer operations under one route key. */
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) => attachUnlessAborted(sourceFor(opts), opts?.signal),
25884
- openReadable: async (opts) => openReadableFromConsumer(attachUnlessAborted(sourceFor(opts), opts?.signal), opts),
25885
- snapshotLive: async (opts) => captureSnapshotFromShared(sourceFor(opts), {
25886
- ...opts,
25887
- logger: this.deps.logger,
25888
- ffmpegPath: this.deps.ffmpegPath
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: this.deps.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 existing = this.sessions.get(sn);
26113
- if (existing) {
26114
- await existing.ready;
26115
- if (existing.session.isConnected) {
26116
- this.touch(sn, existing);
26117
- return existing;
26118
- }
26119
- this.drop(sn, existing);
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,