whalibmob 5.5.57 → 5.5.59

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.
Files changed (35) hide show
  1. package/README.md +108 -0
  2. package/cli.js +1 -1
  3. package/lib/Attestation.js +212 -0
  4. package/lib/Client.js +28 -26
  5. package/lib/DeviceManager.js +234 -203
  6. package/lib/MediaService.js +63 -29
  7. package/lib/Registration.js +164 -61
  8. package/lib/Store.js +14 -0
  9. package/lib/WAUSync/Protocols/USyncContactProtocol.js +55 -0
  10. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +76 -0
  11. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +29 -0
  12. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +34 -0
  13. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +40 -0
  14. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  15. package/lib/WAUSync/Protocols/index.js +17 -0
  16. package/lib/WAUSync/USyncQuery.js +113 -0
  17. package/lib/WAUSync/USyncUser.js +40 -0
  18. package/lib/WAUSync/index.js +11 -0
  19. package/lib/messages/MessageSender.js +40 -17
  20. package/lib/signal/SenderKey.js +12 -0
  21. package/lib/signal/SignalProtocol.js +68 -44
  22. package/lib/signal/WaSignalGroup/ciphertext-message.js +15 -0
  23. package/lib/signal/WaSignalGroup/group-session-builder.js +45 -0
  24. package/lib/signal/WaSignalGroup/group_cipher.js +92 -0
  25. package/lib/signal/WaSignalGroup/index.js +27 -0
  26. package/lib/signal/WaSignalGroup/keyhelper.js +24 -0
  27. package/lib/signal/WaSignalGroup/sender-chain-key.js +35 -0
  28. package/lib/signal/WaSignalGroup/sender-key-distribution-message.js +136 -0
  29. package/lib/signal/WaSignalGroup/sender-key-message.js +139 -0
  30. package/lib/signal/WaSignalGroup/sender-key-name.js +59 -0
  31. package/lib/signal/WaSignalGroup/sender-key-record.js +63 -0
  32. package/lib/signal/WaSignalGroup/sender-key-state.js +101 -0
  33. package/lib/signal/WaSignalGroup/sender-message-key.js +34 -0
  34. package/package.json +8 -9
  35. package/.env.example +0 -49
package/README.md CHANGED
@@ -126,6 +126,7 @@ npm install -g whalibmob
126
126
  - [Library API](#library-api)
127
127
  - [Connecting Account](#connecting-account)
128
128
  - [Register a New Number](#register-a-new-number)
129
+ - [Device Attestation with Frida (optional)](#device-attestation-with-frida-optional)
129
130
  - [Connect](#connect)
130
131
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
131
132
  - [Signal Store Utilities](#signal-store-utilities)
@@ -272,6 +273,113 @@ if (result.status === 'ok') {
272
273
  }
273
274
  ```
274
275
 
276
+ ### Device Attestation with Frida (optional)
277
+
278
+ WhatsApp's registration servers score every `/code` and `/register` request on how
279
+ much it looks like a genuine phone. A real device proves itself with a hardware
280
+ attestation token — **Play Integrity** plus a **Keystore**-signed request on
281
+ Android, **App Attest** on iOS. whalibmob sends these fields on every registration
282
+ request, but it can only fill them with real values if it can talk to an actual
283
+ device.
284
+
285
+ This repository ships a **`frida/` folder** containing the on-device scripts that
286
+ produce those tokens. It is entirely **optional**: with no device attached,
287
+ whalibmob sends the same empty attestation fields a real phone sends when its
288
+ integrity check fails, and registration still works. Attaching a device raises the
289
+ trust score, which helps when you keep hitting `no_routes` or block screens.
290
+
291
+ > Requires a **rooted Android phone** or a **jailbroken iPhone** with the official
292
+ > WhatsApp app installed from the Play Store / App Store. Sideloaded APKs do not
293
+ > work — the attestation is bound to the store-signed build.
294
+
295
+ #### What's in the folder
296
+
297
+ ```
298
+ frida/
299
+ android/ Play Integrity + Keystore attestation server (/integrity, /cert, /info)
300
+ ios/ App Attest attestation server (/integrity)
301
+ ```
302
+
303
+ Each platform folder has its own `README.md` with the full device prerequisites.
304
+
305
+ #### 1. Build the script
306
+
307
+ Install [Frida](https://frida.re) on your computer, then build the bundle for your
308
+ platform:
309
+
310
+ ```bash
311
+ cd frida/android # or: cd frida/ios
312
+ npm install
313
+ npm run build # produces server_with_dependencies.js
314
+ ```
315
+
316
+ #### 2. Run it on the device
317
+
318
+ Make sure the Frida server is running on the phone, then attach to WhatsApp:
319
+
320
+ ```bash
321
+ # Android — open WhatsApp and reach the "register a number" screen FIRST,
322
+ # otherwise the integrity components are not loaded yet
323
+ frida -U "WhatsApp" -l server_with_dependencies.js
324
+
325
+ # iOS
326
+ frida -U -l server_with_dependencies.js -f "net.whatsapp.WhatsApp"
327
+ ```
328
+
329
+ The script starts a small HTTP server **on the phone**, listening on port `1119`
330
+ (or `1120` if you attached to WhatsApp Business). Wait for:
331
+
332
+ ```
333
+ [*] Server ready on port 1119
334
+ ```
335
+
336
+ #### 3. Make the port reachable
337
+
338
+ whalibmob talks to that server over plain HTTP, so the port has to be reachable
339
+ from the machine running your bot. Over USB, forward it with adb:
340
+
341
+ ```bash
342
+ adb forward tcp:1119 tcp:1119 # Android
343
+ iproxy 1119 1119 # iOS (libimobiledevice)
344
+ ```
345
+
346
+ Alternatively, if the phone is on the same Wi-Fi, use its LAN IP directly.
347
+
348
+ #### 4. Point whalibmob at it
349
+
350
+ Set the host — and the port, if it isn't the default `1119`:
351
+
352
+ ```bash
353
+ WA_FRIDA_HOST=127.0.0.1
354
+ WA_FRIDA_PORT=1119 # optional; use 1120 for WhatsApp Business
355
+ ```
356
+
357
+ Or in `.env`:
358
+
359
+ ```
360
+ WA_FRIDA_HOST=127.0.0.1
361
+ WA_FRIDA_PORT=1119
362
+ ```
363
+
364
+ That is the whole integration. On the next `requestSmsCode()` / `verifyCode()`
365
+ call, whalibmob queries the device automatically and attaches the attestation to
366
+ the request:
367
+
368
+ | Platform | Fields it fills |
369
+ |----------|--------------------------------------------------------------------|
370
+ | Android | `gpia` (Play Integrity) in the body, plus a Keystore signature and certificate chain on the request |
371
+ | iOS | An App Attest assertion and attestation on the request |
372
+
373
+ Keep the Frida session open while you register. When you unset `WA_FRIDA_HOST`,
374
+ whalibmob silently goes back to the empty-attestation path — no code changes, and
375
+ nothing else in your bot is affected.
376
+
377
+ > **Troubleshooting.** If the fields come back empty, check in this order: the
378
+ > Frida session is still attached, the port is forwarded, and — on Android — that
379
+ > you opened WhatsApp's registration screen at least once before attaching, since
380
+ > the integrity components are lazy-loaded. whalibmob never fails a registration
381
+ > because attestation is unavailable; it just falls back to empty values.
382
+
275
383
  ### Connect
276
384
 
277
385
  ```js
package/cli.js CHANGED
@@ -37,7 +37,7 @@ const {
37
37
 
38
38
  const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
39
39
 
40
- const VERSION = '5.1.21';
40
+ const VERSION = '5.5.59';
41
41
 
42
42
  // ─── output helpers ───────────────────────────────────────────────────────────
43
43
 
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ // ─── Device attestation client ────────────────────────────────────────────────
4
+ //
5
+ // Talks to the on-device Frida attestation server shipped in ./frida (Android
6
+ // Play Integrity / Keystore attestation, iOS App Attest) and folds its output
7
+ // into the registration request, mirroring how Auties00/cobalt consumes the
8
+ // same scripts.
9
+ //
10
+ // Design (matches Cobalt's LinkedWhatsAppClientDeviceAttestor):
11
+ // • Two data shapes are produced —
12
+ // Android: { gpia, gg, gi, gp, ge, ga } (Play Integrity legs)
13
+ // iOS: { attestation, assertion, keyId } (App Attest)
14
+ // • A NONE fallback returns empty strings for every field. The WhatsApp
15
+ // registration server tolerates empty attestation as a low-trust signal,
16
+ // so registration still works with no rooted/jailbroken device attached.
17
+ //
18
+ // Activation: set WA_FRIDA_HOST (e.g. 127.0.0.1) — and optionally WA_FRIDA_PORT
19
+ // (default 1119 for WhatsApp consumer, 1120 for Business) — to make the client
20
+ // query the local Frida server. When WA_FRIDA_HOST is unset every method
21
+ // returns the empty NONE fallback and performs no network I/O.
22
+
23
+ const crypto = require('crypto');
24
+ const http = require('http');
25
+
26
+ const DEFAULT_PORT = 1119; // WhatsApp consumer; Business build listens on 1120
27
+ const REQUEST_TIMEMS = 15000;
28
+
29
+ // ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
30
+
31
+ const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
32
+ const EMPTY_IOS = { attestation: '', assertion: '', keyId: '' };
33
+ const EMPTY_BODY = { bodyAttestation: null, authorizationHeader: null };
34
+
35
+ function fridaHost() {
36
+ return process.env.WA_FRIDA_HOST || null;
37
+ }
38
+
39
+ function fridaPort() {
40
+ return parseInt(process.env.WA_FRIDA_PORT, 10) || DEFAULT_PORT;
41
+ }
42
+
43
+ function isEnabled() {
44
+ return !!fridaHost();
45
+ }
46
+
47
+ // ─── base64 / hex helpers ─────────────────────────────────────────────────────
48
+
49
+ function toBase64Url(buf) {
50
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
51
+ }
52
+
53
+ function sha256(buf) {
54
+ return crypto.createHash('sha256').update(buf).digest();
55
+ }
56
+
57
+ function toHex(buf) {
58
+ return Buffer.from(buf).toString('hex');
59
+ }
60
+
61
+ // ─── Local Frida HTTP GET (best-effort; never throws) ─────────────────────────
62
+ // Returns the parsed JSON object on success, or null on any failure/timeout so
63
+ // the caller can fall back to the empty NONE attestation.
64
+
65
+ function fridaGet(pathname, query) {
66
+ return new Promise((resolve) => {
67
+ const host = fridaHost();
68
+ if (!host) return resolve(null);
69
+
70
+ const qs = query
71
+ ? '?' + Object.keys(query)
72
+ .filter(k => query[k] != null)
73
+ .map(k => k + '=' + encodeURIComponent(query[k]))
74
+ .join('&')
75
+ : '';
76
+
77
+ const req = http.request(
78
+ { host, port: fridaPort(), path: pathname + qs, method: 'GET', timeout: REQUEST_TIMEMS },
79
+ (res) => {
80
+ const chunks = [];
81
+ res.on('data', d => chunks.push(d));
82
+ res.on('end', () => {
83
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
84
+ catch (_) { resolve(null); }
85
+ });
86
+ }
87
+ );
88
+ req.on('error', () => resolve(null));
89
+ req.on('timeout', () => { req.destroy(); resolve(null); });
90
+ req.end();
91
+ });
92
+ }
93
+
94
+ // ─── Android: Play Integrity legs (gpia + _gg _gi _gp _ge _ga) ────────────────
95
+ //
96
+ // Queries /integrity?authKey=<nonce>. The Frida server returns { token }, which
97
+ // becomes the Play Integrity verdict token (gpia). The _gg/_gi/_gp/_ge/_ga GMS
98
+ // legs are only produced by the full native attestor, so a Frida-only setup
99
+ // leaves them empty — the server accepts a gpia-only attestation.
100
+ //
101
+ // nonceB64 is the URL-safe base64 request nonce (see attestationFieldsAndroid).
102
+ async function androidIntegrity(nonceB64) {
103
+ if (!isEnabled()) return { ...EMPTY_ANDROID };
104
+ const res = await fridaGet('/integrity', { authKey: nonceB64 });
105
+ if (!res || !res.token) return { ...EMPTY_ANDROID };
106
+ return { gpia: res.token, gg: '', gi: '', gp: '', ge: '', ga: '' };
107
+ }
108
+
109
+ // ─── Android: Keystore attestation over the ENC body (&H= + Authorization) ────
110
+ //
111
+ // Queries /cert?authKey=<authKeyB64>&enc=<encB64>. Both parameters are decoded
112
+ // with the STANDARD base64 decoder on the device, so they must not be URL-safe:
113
+ // authKey — the 32-byte raw auth key (the Noise public key, no 0x05 prefix);
114
+ // the server embeds it in the Keystore attestation challenge and
115
+ // locates it in the certificate by its 64-hex-char representation.
116
+ // enc — the UTF-8 bytes of the ENC payload string, base64-encoded; the
117
+ // server signs exactly those bytes.
118
+ // The server returns { signature, certificate }: signature → the &H= body
119
+ // suffix, certificate → the Authorization cert-chain header.
120
+ async function androidAttestBody(encB64, authKeyB64) {
121
+ if (!isEnabled()) return { ...EMPTY_BODY };
122
+ const encParam = Buffer.from(encB64, 'utf8').toString('base64');
123
+ const res = await fridaGet('/cert', { authKey: authKeyB64, enc: encParam });
124
+ if (!res || !res.signature || !res.certificate) return { ...EMPTY_BODY };
125
+ return { bodyAttestation: res.signature, authorizationHeader: res.certificate };
126
+ }
127
+
128
+ // ─── Android: device /info (apk hashes / signature / secret key) ──────────────
129
+ async function androidInfo() {
130
+ if (!isEnabled()) return null;
131
+ return fridaGet('/info');
132
+ }
133
+
134
+ // ─── iOS: App Attest (attestation + assertion + keyId) ────────────────────────
135
+ //
136
+ // Queries /integrity?authKey=<clientDataHashB64>. The Frida server returns
137
+ // { attestation, assertion }; the keyId is generated per call on-device and
138
+ // echoed by the server when available. clientDataHash is the base64 of the
139
+ // Noise public key (the server SHA-256-hashes it internally).
140
+ async function iosAppAttest(noisePubB64) {
141
+ if (!isEnabled()) return { ...EMPTY_IOS };
142
+ const res = await fridaGet('/integrity', { authKey: noisePubB64 });
143
+ if (!res || !res.attestation || !res.assertion) return { ...EMPTY_IOS };
144
+ return {
145
+ attestation: res.attestation,
146
+ assertion: res.assertion,
147
+ keyId: res.keyId || ''
148
+ };
149
+ }
150
+
151
+ // ─── High-level: attestation fields shipped on every attested endpoint ────────
152
+ //
153
+ // Returns the alternating name/value pairs Cobalt ships via attestationFields():
154
+ // Android → gpia,_gg,_gi,_gp,_ge,_ga,push_token
155
+ // iOS → push_token
156
+ //
157
+ // These fields are part of the plaintext that gets AES-GCM-sealed into the
158
+ // ENC envelope, so the Play Integrity nonce is derived from the store (a stable
159
+ // per-session value), NOT from the ENC body. pushToken comes from the optional
160
+ // push client — empty by default.
161
+ async function attestationFields(device, nonceB64, opts) {
162
+ opts = opts || {};
163
+ const pushToken = opts.pushToken || '';
164
+
165
+ if (device && device.os === 'android') {
166
+ const a = await androidIntegrity(nonceB64 || '');
167
+ return [
168
+ 'gpia', a.gpia,
169
+ '_gg', a.gg,
170
+ '_gi', a.gi,
171
+ '_gp', a.gp,
172
+ '_ge', a.ge,
173
+ '_ga', a.ga,
174
+ 'push_token', pushToken
175
+ ];
176
+ }
177
+
178
+ // iOS ships only push_token in the form body; App Attest rides in &H= + header.
179
+ return ['push_token', pushToken];
180
+ }
181
+
182
+ // ─── High-level: body attestation (&H= suffix + Authorization header) ─────────
183
+ //
184
+ // encB64 — the base64url ENC payload (without the "ENC=" prefix)
185
+ // device — { os }
186
+ // keys.noisePubB64 — standard base64 of the 32-byte Noise public key. Android
187
+ // uses it as the Keystore attestation challenge auth key;
188
+ // iOS uses it as the App Attest clientDataHash source.
189
+ async function attestBody(encB64, device, keys) {
190
+ keys = keys || {};
191
+ if (device && device.os === 'android') {
192
+ return androidAttestBody(encB64, keys.noisePubB64 || '');
193
+ }
194
+ const data = await iosAppAttest(keys.noisePubB64 || '');
195
+ if (!data.attestation || !data.assertion || !data.keyId) return { ...EMPTY_BODY };
196
+ return {
197
+ bodyAttestation: '{"assertion":"' + data.assertion + '"}',
198
+ authorizationHeader: data.attestation + '|' + data.keyId
199
+ };
200
+ }
201
+
202
+ module.exports = {
203
+ isEnabled,
204
+ attestationFields,
205
+ attestBody,
206
+ androidInfo,
207
+ toHex,
208
+ toBase64Url,
209
+ EMPTY_ANDROID,
210
+ EMPTY_IOS,
211
+ EMPTY_BODY
212
+ };
package/lib/Client.js CHANGED
@@ -676,7 +676,7 @@ class WhalibmobClient extends EventEmitter {
676
676
  _whaDbg('[DBG] DM_DECRYPT id=' + id + ' type=' + encType + ' sigJid=' + sigJid + ' cipherLen=' + cipherBuf.length);
677
677
 
678
678
  this._signal.decrypt(sigJid, encType, cipherBuf)
679
- .then(plaintext => {
679
+ .then(async plaintext => {
680
680
  const decoded = this._decodeMsg(plaintext);
681
681
  _whaDbg('[DBG] DM_DECODED id=' + id + ' type=' + decoded.type + ' ptLen=' + (plaintext ? plaintext.length : 0) + ' hasSKDM=' + !!(decoded.skdm || decoded.type === 'senderKeyDistribution'));
682
682
 
@@ -685,7 +685,7 @@ class WhalibmobClient extends EventEmitter {
685
685
  // axolotlBytes may be absent in newer WA proto versions — that's OK for DM pkmsg flow.
686
686
  if (decoded.skdm && decoded.skdm.axolotlBytes && decoded.skdm.axolotlBytes[0] === 0x33) {
687
687
  try {
688
- this._signal.processSKDM(decoded.skdm.groupId, sigJid, decoded.skdm.axolotlBytes);
688
+ await this._signal.processSKDM(decoded.skdm.groupId, sigJid, decoded.skdm.axolotlBytes);
689
689
  _whaDbg('[DBG] SKDM_PROCESSED groupId=' + decoded.skdm.groupId + ' sender=' + sigJid);
690
690
  } catch (e) {
691
691
  _whaDbg('[DBG] SKDM_ERR ' + e.message);
@@ -696,7 +696,7 @@ class WhalibmobClient extends EventEmitter {
696
696
  if (decoded.type === 'senderKeyDistribution') {
697
697
  if (decoded.axolotlBytes && decoded.axolotlBytes[0] === 0x33) {
698
698
  try {
699
- this._signal.processSKDM(decoded.groupId, sigJid, decoded.axolotlBytes);
699
+ await this._signal.processSKDM(decoded.groupId, sigJid, decoded.axolotlBytes);
700
700
  _whaDbg('[DBG] SKDM_ONLY_PROCESSED groupId=' + decoded.groupId + ' sender=' + sigJid);
701
701
  } catch (e) {
702
702
  _whaDbg('[DBG] SKDM_ONLY_ERR ' + e.message);
@@ -745,24 +745,25 @@ class WhalibmobClient extends EventEmitter {
745
745
  ? skmsgNode.content
746
746
  : Buffer.from(skmsgNode.content || '');
747
747
 
748
- try {
749
- const plaintext = this._signal.senderKeyDecrypt(from, participant, cipherBuf);
750
- const decoded = this._decodeMsg(plaintext);
751
- if (decoded.type === 'senderKeyDistribution') return;
752
- if (decoded.type === 'protocol') {
753
- if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
754
- this._handleHistorySync(decoded.historySyncNotification, id);
755
- }
756
- if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
757
- this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
748
+ this._signal.senderKeyDecrypt(from, participant, cipherBuf)
749
+ .then(plaintext => {
750
+ const decoded = this._decodeMsg(plaintext);
751
+ if (decoded.type === 'senderKeyDistribution') return;
752
+ if (decoded.type === 'protocol') {
753
+ if (decoded.subtype === 'history_sync' && decoded.historySyncNotification) {
754
+ this._handleHistorySync(decoded.historySyncNotification, id);
755
+ }
756
+ if (decoded.subtype === 'app_state_sync_key_share' && decoded.appStateSyncKeyShare) {
757
+ this._handleAppStateSyncKeyShare(decoded.appStateSyncKeyShare);
758
+ }
759
+ return;
758
760
  }
759
- return;
760
- }
761
- this.emit('message', { id, from, participant, ts, decoded, isGroup: true, node });
762
- this._sendReadReceipt(id, fromRaw, partRaw);
763
- } catch (err) {
764
- this.emit('decrypt_error', { id, from, participant, err });
765
- }
761
+ this.emit('message', { id, from, participant, ts, decoded, isGroup: true, node });
762
+ this._sendReadReceipt(id, fromRaw, partRaw);
763
+ })
764
+ .catch(err => {
765
+ this.emit('decrypt_error', { id, from, participant, err });
766
+ });
766
767
  }
767
768
 
768
769
  _decodeMsg(buf) {
@@ -801,14 +802,14 @@ class WhalibmobClient extends EventEmitter {
801
802
 
802
803
  tasks.push(
803
804
  this._signal.decrypt(senderJid, encType, cipherBuf)
804
- .then(decrypted => {
805
+ .then(async decrypted => {
805
806
  const decoded = this._decodeMsg(decrypted);
806
807
  if (decoded && decoded.type === 'senderKeyDistribution' &&
807
808
  decoded.groupId && decoded.axolotlBytes) {
808
- this._signal.processSKDM(decoded.groupId, senderJid, decoded.axolotlBytes);
809
+ await this._signal.processSKDM(decoded.groupId, senderJid, decoded.axolotlBytes);
809
810
  } else if (decoded && decoded.skdm &&
810
811
  decoded.skdm.groupId && decoded.skdm.axolotlBytes) {
811
- this._signal.processSKDM(decoded.skdm.groupId, senderJid, decoded.skdm.axolotlBytes);
812
+ await this._signal.processSKDM(decoded.skdm.groupId, senderJid, decoded.skdm.axolotlBytes);
812
813
  }
813
814
  })
814
815
  .catch(() => {})
@@ -1255,6 +1256,7 @@ class WhalibmobClient extends EventEmitter {
1255
1256
  const connNode = findChild(node, 'media_conn');
1256
1257
  if (!connNode) return;
1257
1258
  const auth = (connNode.attrs && connNode.attrs.auth) || '';
1259
+ const ttl = connNode.attrs && connNode.attrs.ttl ? parseInt(connNode.attrs.ttl, 10) : 300;
1258
1260
  const hosts = [];
1259
1261
  if (Array.isArray(connNode.content)) {
1260
1262
  for (const child of connNode.content) {
@@ -1264,9 +1266,9 @@ class WhalibmobClient extends EventEmitter {
1264
1266
  }
1265
1267
  }
1266
1268
  }
1267
- this._mediaConn = { hosts, auth };
1268
- if (this._sender) this._sender.setMediaConnection(hosts, auth);
1269
- this.emit('media_conn', { hosts, auth });
1269
+ this._mediaConn = { hosts, auth, ttl, fetchDate: Date.now() };
1270
+ if (this._sender) this._sender.setMediaConnection(hosts, auth, ttl, Date.now());
1271
+ this.emit('media_conn', { hosts, auth, ttl });
1270
1272
  }
1271
1273
 
1272
1274
  // ─── Pre-key upload ───────────────────────────────────────────────────────