whalibmob 5.5.22 → 5.5.24

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/.env.example ADDED
@@ -0,0 +1,49 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # whalibmob — Device Emulation Configuration
3
+ # Copy this file to .env in your project root and set the values you need.
4
+ # All variables are optional; defaults emulate an iPhone 15 Pro running iOS 17.
5
+ # ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ # Operating system to emulate. Accepted values: ios | android
8
+ # Default: ios
9
+ # WA_OS=android
10
+
11
+ # Named device profile.
12
+ #
13
+ # iOS profiles (WA_OS=ios):
14
+ # iphone_15_pro, iphone_15, iphone_14_pro, iphone_14, iphone_13_pro,
15
+ # iphone_13, iphone_12_pro, iphone_12, iphone_11_pro, iphone_11,
16
+ # iphone_se3, iphone_xs
17
+ #
18
+ # Android profiles (WA_OS=android):
19
+ # pixel_8_pro, pixel_8, pixel_7, pixel_7a,
20
+ # samsung_s24_ultra, samsung_s24, samsung_s23_ultra, samsung_s23, samsung_a55,
21
+ # oneplus_12, oneplus_11, xiaomi_14, xiaomi_13, oppo_find_x7, realme_gt5
22
+ #
23
+ # Default: iphone_15_pro (or pixel_8_pro when WA_OS=android)
24
+ # WA_DEVICE=pixel_8_pro
25
+
26
+ # ── Custom device overrides (applied on top of the selected profile) ─────────
27
+ # Use these to fine-tune any field without creating a new profile.
28
+
29
+ # WA_DEVICE_MODEL=SM-S928B
30
+ # WA_DEVICE_MANUFACTURER=samsung
31
+ # WA_DEVICE_OS_VERSION=14
32
+ # WA_DEVICE_BUILD=UP1A.231005.007
33
+ # WA_DEVICE_MODEL_ID=samsung-sm-s928b
34
+
35
+ # ── Version & token overrides ─────────────────────────────────────────────────
36
+
37
+ # Pin the WhatsApp version string instead of fetching the latest from the store.
38
+ # Format: 2.x.x.x (four-part)
39
+ # WA_VERSION=2.24.13.80
40
+
41
+ # Override the static token used in registration token computation.
42
+ # Only needed if WhatsApp rotates the bundled token.
43
+ # WA_STATIC_TOKEN=Y29Cs6AVNR2bj5PBeKSYFd1nAKuvNQ3h
44
+
45
+ # ── Proxy / Tor ───────────────────────────────────────────────────────────────
46
+
47
+ # Route registration HTTP traffic through a SOCKS5 proxy or Tor.
48
+ # TOR_PROXY=socks5://127.0.0.1:9050
49
+ # SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
package/README.md CHANGED
@@ -6,6 +6,20 @@
6
6
  > [!CAUTION]
7
7
  > Use a dedicated phone number with this library. Connecting with a number that is already active on a real device will cause WhatsApp to log that device out.
8
8
 
9
+ > [!CAUTION]
10
+ > Whalibmob now It needs to be rewritten because WhatsApp mobile and has changed the protocol lately and now whalibmob is in testing and some updates by Me Any pull request is accepted.
11
+
12
+
13
+ > [!IMPORTANT]
14
+ > If you like what I do and want to support me I can leave you here my Crypto usdc address for any donation and support any Small donation is accepted because the WhatsApp protocol changes very often : "0x8AD64F47a715eC24DeF193FBb9aC64d4E857f0f3"
15
+
16
+ Usdc ethereum network.
17
+
18
+
19
+
20
+
21
+
22
+
9
23
  > [!IMPORTANT]
10
24
  > This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or affiliates. "WhatsApp" and related names are registered trademarks of their respective owners. Use at your own discretion.
11
25
 
@@ -102,6 +116,11 @@ npm install -g whalibmob
102
116
  - [Register a New Number](#register-a-new-number)
103
117
  - [Connect](#connect)
104
118
  - [Saving & Restoring Sessions](#saving--restoring-sessions)
119
+ - [Signal Store Utilities](#signal-store-utilities)
120
+ - [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
121
+ - [addTransactionCapability](#addtransactioncapability)
122
+ - [assertMeId](#assertmeid)
123
+ - [initAuthCreds](#initauthcreds)
105
124
  - [Handling Events](#handling-events)
106
125
  - [Example to Start](#example-to-start)
107
126
  - [All Events](#all-events)
@@ -267,6 +286,138 @@ await client.init('919634847671')
267
286
  > [!NOTE]
268
287
  > Each phone number uses its own session file. The library handles Signal Protocol key persistence automatically.
269
288
 
289
+ ## Signal Store Utilities
290
+
291
+ `auth-utils` is a collection of optional helpers for power users who manage their own `SignalStore` instances directly (e.g. custom storage backends, multi-account servers).
292
+
293
+ ```js
294
+ const {
295
+ makeCacheableSignalKeyStore,
296
+ addTransactionCapability,
297
+ assertMeId,
298
+ initAuthCreds
299
+ } = require('whalibmob')
300
+ ```
301
+
302
+ ### `makeCacheableSignalKeyStore`
303
+
304
+ Wraps a `SignalStore` with an in-memory NodeCache layer (5-minute TTL). All `get` calls for `sessions`, `preKeys`, `signedPreKeys`, and `identities` are served from cache on subsequent accesses. Writes invalidate the cache automatically.
305
+
306
+ `useClones` is set to `false` so that `SessionRecord` objects — which carry internal state and methods — are returned by reference and never deep-cloned.
307
+
308
+ The wrapper also forwards `transaction()` and `isInTransaction()` calls to the underlying store when present, making it safe to stack with `addTransactionCapability`.
309
+
310
+ ```js
311
+ const { SignalStore } = require('whalibmob')
312
+ const { makeCacheableSignalKeyStore } = require('whalibmob')
313
+
314
+ const store = new SignalStore(/* ... */)
315
+ const cached = makeCacheableSignalKeyStore(store)
316
+
317
+ // reads hit cache after first access
318
+ const session = await cached.getSession('919634847671@s.whatsapp.net:0')
319
+ ```
320
+
321
+ **When to use:** whenever your `SignalStore` is backed by a remote or disk-based store (database, Redis, file system) and you want to reduce repeated lookups for sessions that haven't changed between sends.
322
+
323
+ ### `addTransactionCapability`
324
+
325
+ Wraps a `SignalStore` with batched-write (transaction) semantics. During a transaction all writes are buffered in memory; they are flushed to the underlying store atomically when `commit()` is called at the end of the transaction.
326
+
327
+ Uses `AsyncLocalStorage` to propagate transaction context across async call chains, and a per-key-type `Mutex` with reference-counting to serialize concurrent writers safely.
328
+
329
+ ```js
330
+ const { addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
331
+
332
+ // recommended: cache first, then transactions on top
333
+ const base = new SignalStore(/* ... */)
334
+ const cached = makeCacheableSignalKeyStore(base)
335
+ const txnStore = addTransactionCapability(cached)
336
+
337
+ // inside a send flow
338
+ await txnStore.transaction(async () => {
339
+ // all writes are buffered
340
+ await txnStore.setSession('919634847671@s.whatsapp.net:0', sessionRecord)
341
+ await txnStore.setPreKey(1, preKeyPair)
342
+ // commit is called automatically at the end of the transaction callback
343
+ })
344
+ ```
345
+
346
+ Stacking order matters: put `makeCacheableSignalKeyStore` below `addTransactionCapability` so that the cache always sees the committed state.
347
+
348
+ **When to use:** for high-throughput servers that send to many recipients concurrently and need to batch Signal key writes into a single atomic flush per message.
349
+
350
+ ### `assertMeId`
351
+
352
+ Validates that a store object has a registered phone number and returns the canonical `@s.whatsapp.net` JID. Throws an `Error` if the store lacks a `phoneNumber` or has `registered !== true`.
353
+
354
+ ```js
355
+ const { assertMeId } = require('whalibmob')
356
+
357
+ const store = loadStore(sessFile)
358
+
359
+ try {
360
+ const jid = assertMeId(store)
361
+ // jid === '919634847671@s.whatsapp.net'
362
+ console.log('account JID:', jid)
363
+ } catch (err) {
364
+ console.error('store is not registered:', err.message)
365
+ }
366
+ ```
367
+
368
+ **When to use:** as a guard before calling `client.init()` to give a clear error message when a corrupted or unregistered session file is accidentally loaded.
369
+
370
+ ### `initAuthCreds`
371
+
372
+ Creates a fresh credential store for the given phone number. Functionally equivalent to `createNewStore` but also initialises the Baileys-compatible extra fields that the library expects for account sync: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, and `advSecretKey`.
373
+
374
+ ```js
375
+ const { initAuthCreds, saveStore } = require('whalibmob')
376
+ const path = require('path')
377
+ const fs = require('fs')
378
+
379
+ const phone = '919634847671'
380
+ const sessDir = path.join(process.env.HOME, '.waSession')
381
+ const sessFile = path.join(sessDir, phone + '.json')
382
+
383
+ fs.mkdirSync(sessDir, { recursive: true })
384
+
385
+ const store = initAuthCreds(phone)
386
+ saveStore(store, sessFile)
387
+ ```
388
+
389
+ This is the function used internally by the CLI for all new session creation. Prefer it over `createNewStore` for forward compatibility.
390
+
391
+ > [!NOTE]
392
+ > `initAuthCreds` and `createNewStore` produce equivalent stores for all current library operations. The additional fields from `initAuthCreds` are there for future-proofing and interoperability.
393
+
394
+ ### Recommended Stacking Pattern
395
+
396
+ For a production multi-account server:
397
+
398
+ ```js
399
+ const {
400
+ SignalStore,
401
+ makeCacheableSignalKeyStore,
402
+ addTransactionCapability,
403
+ initAuthCreds,
404
+ saveStore,
405
+ loadStore
406
+ } = require('whalibmob')
407
+
408
+ // 1. load or create the credential store
409
+ let store = loadStore(sessFile) || initAuthCreds(phone)
410
+
411
+ // 2. build the layered Signal key store
412
+ const signalStore = new SignalStore(store)
413
+ const cachedStore = makeCacheableSignalKeyStore(signalStore)
414
+ const txnStore = addTransactionCapability(cachedStore)
415
+
416
+ // 3. pass to the client (advanced usage — most users should use WhalibmobClient directly)
417
+ ```
418
+
419
+ For standard usage, `WhalibmobClient` handles all of this internally. These helpers are for advanced scenarios where you need direct control over Signal key storage.
420
+
270
421
  ## Handling Events
271
422
 
272
423
  whalibmob uses the EventEmitter syntax for events.
package/cli.js CHANGED
@@ -35,6 +35,8 @@ const {
35
35
  loadStore
36
36
  } = require('./lib/Client');
37
37
 
38
+ const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
39
+
38
40
  const VERSION = '5.1.21';
39
41
 
40
42
  // ─── output helpers ───────────────────────────────────────────────────────────
@@ -435,14 +437,18 @@ async function handleLine(line) {
435
437
  if (_client) { try { _client.disconnect(); } catch (_) {} }
436
438
  process.exit(0);
437
439
 
438
- case '/session':
440
+ case '/session': {
439
441
  if (!_client || !_client.store) { fail('not connected'); break; }
442
+ let _meJid = '—';
443
+ try { _meJid = assertMeId(_client.store); } catch (_) {}
440
444
  hr();
441
445
  kv('phone', _client.store.phoneNumber);
446
+ kv('jid', _meJid);
442
447
  kv('name', _client.store.pushName || _client.store.name || '—');
443
448
  kv('session', _sessDir);
444
449
  hr();
445
450
  break;
451
+ }
446
452
 
447
453
  case '/connect': {
448
454
  const ph = p[1];
@@ -1153,7 +1159,7 @@ async function handleLine(line) {
1153
1159
  const sessFile = path.join(_sessDir, `${ph}.json`);
1154
1160
  let store = loadStore(sessFile);
1155
1161
  if (!store) {
1156
- store = createNewStore(ph);
1162
+ store = initAuthCreds(ph);
1157
1163
  saveStore(store, sessFile);
1158
1164
  } else if (!store.codePending && !store.registered) {
1159
1165
  // Only check /exist when keys were never used to request a code.
@@ -1162,7 +1168,7 @@ async function handleLine(line) {
1162
1168
  const fresh = await assertRegistrationKeys(store, waVersion);
1163
1169
  if (!fresh) {
1164
1170
  out(' device keys already registered — generating new keys...');
1165
- store = createNewStore(ph);
1171
+ store = initAuthCreds(ph);
1166
1172
  saveStore(store, sessFile);
1167
1173
  out(' new keys saved — proceed with code below');
1168
1174
  }
@@ -1180,7 +1186,7 @@ async function handleLine(line) {
1180
1186
  const code = p[3];
1181
1187
  if (!ph || !code) { fail('usage: /reg confirm <phone> <code>'); break; }
1182
1188
  const file = path.join(_sessDir, `${ph}.json`);
1183
- const store = loadStore(file) || createNewStore(ph);
1189
+ const store = loadStore(file) || initAuthCreds(ph);
1184
1190
  out('verifying...');
1185
1191
  const r = await verifyCode(store, code);
1186
1192
  if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
@@ -1477,7 +1483,7 @@ async function main() {
1477
1483
  let store = loadStore(sessFile);
1478
1484
  if (!store) {
1479
1485
  // Brand new — generate fresh keys, save immediately, no need to check /exist
1480
- store = createNewStore(ph);
1486
+ store = initAuthCreds(ph);
1481
1487
  saveStore(store, sessFile);
1482
1488
  } else if (!store.codePending && !store.registered) {
1483
1489
  // Existing store but code was never sent and not registered — check if
@@ -1488,7 +1494,7 @@ async function main() {
1488
1494
  const fresh = await assertRegistrationKeys(store, waVersion);
1489
1495
  if (!fresh) {
1490
1496
  out(' device keys already registered — generating new keys...');
1491
- store = createNewStore(ph);
1497
+ store = initAuthCreds(ph);
1492
1498
  saveStore(store, sessFile);
1493
1499
  }
1494
1500
  }
@@ -1518,7 +1524,7 @@ async function main() {
1518
1524
  if (!ph) { fail('phone number required'); process.exit(1); }
1519
1525
  if (!code) { fail('--code is required'); process.exit(1); }
1520
1526
  const file = path.join(_sessDir, `${ph}.json`);
1521
- const store = loadStore(file) || createNewStore(ph);
1527
+ const store = loadStore(file) || initAuthCreds(ph);
1522
1528
  out('verifying code for +' + ph + '...');
1523
1529
  try {
1524
1530
  const r = await verifyCode(store, code);
package/index.js CHANGED
@@ -11,6 +11,12 @@ const { SenderKeyStore, SenderKeyCrypto } = require('./lib/signal/SenderKey');
11
11
  const { DeviceManager } = require('./lib/DeviceManager');
12
12
  const { encryptMedia, decryptMedia, uploadMedia, downloadMedia } = require('./lib/MediaService');
13
13
  const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/MessageSender');
14
+ const {
15
+ makeCacheableSignalKeyStore,
16
+ addTransactionCapability,
17
+ assertMeId,
18
+ initAuthCreds
19
+ } = require('./lib/auth-utils');
14
20
 
15
21
  module.exports = {
16
22
  WhalibmobClient,
@@ -48,5 +54,10 @@ module.exports = {
48
54
  // Message helpers
49
55
  MessageSender,
50
56
  makeJid,
51
- generateMessageId
57
+ generateMessageId,
58
+ // Auth utilities (mirrors Baileys' auth-utils)
59
+ makeCacheableSignalKeyStore,
60
+ addTransactionCapability,
61
+ assertMeId,
62
+ initAuthCreds
52
63
  };
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { BinaryNode } = require('./BinaryNode');
3
+ const { BinaryNode } = require('./BinaryNode');
4
+ const { NodeCache } = require('@cacheable/node-cache');
5
+
6
+ const DEVICE_CACHE_TTL = '5m';
4
7
 
5
8
  // ─── JID helpers ─────────────────────────────────────────────────────────────
6
9
 
@@ -131,12 +134,37 @@ function parseBundleFromUserNode(userNode) {
131
134
  class DeviceManager {
132
135
  constructor(client) {
133
136
  this._client = client;
134
- // phone → Set<deviceId> (populated after first usync)
135
- this._deviceCache = new Map();
137
+ // phone → deviceId[] (TTL-based: expires after 5 min, triggers fresh usync)
138
+ this._deviceCache = new NodeCache({ ttl: DEVICE_CACHE_TTL });
136
139
  // phone → Promise<jids[]> (in-flight usync dedup)
137
140
  this._usyncInflight = new Map();
138
- // own device list cache (populated once per connection)
139
- this._ownDeviceJids = null;
141
+ // own device list cache + expiry timestamp (refreshed every 5 min)
142
+ this._ownDeviceJids = null;
143
+ this._ownDeviceExpiry = 0;
144
+ }
145
+
146
+ // ─── NodeCache helper methods ─────────────────────────────────────────────
147
+ // _deviceCache stores deviceId arrays (not Sets) because NodeCache serialises
148
+ // values and Sets don't round-trip reliably. All callers use these helpers.
149
+
150
+ _dcHas(key) { return this._deviceCache.has(key); }
151
+ _dcGet(key) { return this._deviceCache.get(key) || null; } // number[] | null
152
+ _dcSet(key, arr) { this._deviceCache.set(key, arr); }
153
+ _dcAdd(key, id) {
154
+ const arr = this._deviceCache.get(key) || [];
155
+ if (!arr.includes(id)) arr.push(id);
156
+ this._deviceCache.set(key, arr);
157
+ }
158
+ _dcEnsure(key) {
159
+ if (!this._deviceCache.has(key)) this._deviceCache.set(key, []);
160
+ }
161
+ _dcDel(keys) {
162
+ for (const k of keys) this._deviceCache.del(k);
163
+ }
164
+ _dcFlush() {
165
+ this._deviceCache.flushAll();
166
+ this._ownDeviceJids = null;
167
+ this._ownDeviceExpiry = 0;
140
168
  }
141
169
 
142
170
  // ─── Fetch pre-key bundles for a list of JIDs via encrypt IQ ───────────────
@@ -222,8 +250,8 @@ class DeviceManager {
222
250
  // Only truly unknown phones trigger a network IQ.
223
251
 
224
252
  async _usyncGetDevices(phones) {
225
- // Fast path: all phones cached
226
- const uncached = phones.filter(p => !this._deviceCache.has(p));
253
+ // Fast path: all phones cached (TTL-based: NodeCache returns null for expired entries)
254
+ const uncached = phones.filter(p => !this._dcHas(p));
227
255
  if (uncached.length === 0) {
228
256
  return this._jidsFromCache(phones);
229
257
  }
@@ -250,8 +278,8 @@ class DeviceManager {
250
278
  _jidsFromCache(phones) {
251
279
  const jids = [];
252
280
  for (const p of phones) {
253
- const devices = this._deviceCache.get(p);
254
- if (devices && devices.size > 0) {
281
+ const devices = this._dcGet(p);
282
+ if (devices && devices.length > 0) {
255
283
  for (const dev of devices) jids.push(makeDeviceJid(p, dev));
256
284
  } else {
257
285
  jids.push(makeDeviceJid(p, 0));
@@ -374,9 +402,7 @@ class DeviceManager {
374
402
  const devicesNode = findChild(userNode, 'devices');
375
403
  const deviceListNode = devicesNode ? findChild(devicesNode, 'device-list') : null;
376
404
 
377
- if (!this._deviceCache.has(cachePhone)) {
378
- this._deviceCache.set(cachePhone, new Set());
379
- }
405
+ this._dcEnsure(cachePhone);
380
406
 
381
407
  if (deviceListNode && Array.isArray(deviceListNode.content)) {
382
408
  for (const devNode of deviceListNode.content) {
@@ -385,13 +411,13 @@ class DeviceManager {
385
411
  const devId = devNode.attrs && devNode.attrs.id != null
386
412
  ? parseInt(String(devNode.attrs.id), 10)
387
413
  : 0;
388
- this._deviceCache.get(cachePhone).add(devId);
414
+ this._dcAdd(cachePhone, devId);
389
415
  }
390
416
  process.stderr.write('[DBG] USYNC_DEVICES phone=' + cachePhone +
391
- ' ids=[' + [...this._deviceCache.get(cachePhone)].join(',') + ']\n');
417
+ ' ids=[' + (this._dcGet(cachePhone) || []).join(',') + ']\n');
392
418
  } else {
393
419
  // No device-list in response — fall back to device 0
394
- this._deviceCache.get(cachePhone).add(0);
420
+ this._dcAdd(cachePhone, 0);
395
421
  process.stderr.write('[DBG] USYNC_NO_DEVLIST phone=' + cachePhone + ' → fallback id=0\n');
396
422
  }
397
423
  }
@@ -409,8 +435,8 @@ class DeviceManager {
409
435
 
410
436
  // For phones with no usync response at all, cache device 0 so we don't re-query
411
437
  for (const p of phones) {
412
- if (!this._deviceCache.has(p)) {
413
- this._deviceCache.set(p, new Set([0]));
438
+ if (!this._dcHas(p)) {
439
+ this._dcSet(p, [0]);
414
440
  process.stderr.write('[DBG] USYNC_FALLBACK phone=' + p + '\n');
415
441
  }
416
442
  }
@@ -476,19 +502,23 @@ class DeviceManager {
476
502
  const lidJid = `${lidUser}@lid`;
477
503
  const cacheKey = `lid:${lidUser}`;
478
504
 
479
- // skipUsync=true: caller already knows the LID (from _pnToLid) skip the
480
- // usync round-trip and assume primary device 0. Saves up to 15 s.
505
+ // skipUsync=true: skip usync ONLY if we already have a cache hit (i.e. we
506
+ // fetched this LID's devices in the last 5 min). On a cache miss we MUST
507
+ // run a real usync even with skipUsync=true, otherwise linked devices (tablets,
508
+ // desktop, web) are silently ignored and only device 0 (primary phone) is used.
481
509
  if (skipUsync) {
482
- if (!this._deviceCache.has(cacheKey)) {
483
- this._deviceCache.set(cacheKey, new Set([0]));
484
- process.stderr.write('[DBG] LID_SKIP_USYNC lidUser=' + lidUser + ' assume device=0\n');
510
+ if (!this._dcHas(cacheKey)) {
511
+ process.stderr.write('[DBG] LID_SKIP_USYNC_CACHE_MISS lidUser=' + lidUser + ' → real usync\n');
512
+ await this._doUsyncIqByJid(lidJid, cacheKey, lidUser);
513
+ } else {
514
+ process.stderr.write('[DBG] LID_SKIP_USYNC_HIT lidUser=' + lidUser + ' → cache\n');
485
515
  }
486
- } else if (!this._deviceCache.has(cacheKey)) {
516
+ } else if (!this._dcHas(cacheKey)) {
487
517
  await this._doUsyncIqByJid(lidJid, cacheKey, lidUser);
488
518
  }
489
519
 
490
- const deviceIds = this._deviceCache.get(cacheKey) || new Set([0]);
491
- const deviceJids = [...deviceIds].map(d => makeDeviceJid(lidUser, d, 'lid'));
520
+ const deviceIds = this._dcGet(cacheKey) || [0];
521
+ const deviceJids = deviceIds.map(d => makeDeviceJid(lidUser, d, 'lid'));
492
522
 
493
523
  process.stderr.write('[DBG] LID_DEVICES lidUser=' + lidUser +
494
524
  ' deviceJids=[' + deviceJids.join(',') + ']\n');
@@ -635,9 +665,7 @@ class DeviceManager {
635
665
  return null;
636
666
  });
637
667
 
638
- if (!this._deviceCache.has(cacheKey)) {
639
- this._deviceCache.set(cacheKey, new Set());
640
- }
668
+ this._dcEnsure(cacheKey);
641
669
 
642
670
  if (response) {
643
671
  const usyncNode = findChild(response, 'usync');
@@ -655,22 +683,22 @@ class DeviceManager {
655
683
  if (!devNode || devNode.description !== 'device') continue;
656
684
  const devId = devNode.attrs && devNode.attrs.id != null
657
685
  ? parseInt(String(devNode.attrs.id), 10) : 0;
658
- this._deviceCache.get(cacheKey).add(devId);
686
+ this._dcAdd(cacheKey, devId);
659
687
  }
660
688
  process.stderr.write('[DBG] USYNC_LID_DEVICES jid=' + jid +
661
- ' ids=[' + [...this._deviceCache.get(cacheKey)].join(',') + ']\n');
689
+ ' ids=[' + (this._dcGet(cacheKey) || []).join(',') + ']\n');
662
690
  } else {
663
- this._deviceCache.get(cacheKey).add(0);
691
+ this._dcAdd(cacheKey, 0);
664
692
  process.stderr.write('[DBG] USYNC_LID_NO_DEVLIST jid=' + jid + ' → fallback id=0\n');
665
693
  }
666
694
  }
667
695
  } else {
668
696
  process.stderr.write('[DBG] USYNC_LID_NO_LIST jid=' + jid + '\n');
669
- this._deviceCache.get(cacheKey).add(0);
697
+ this._dcAdd(cacheKey, 0);
670
698
  }
671
699
  } else {
672
700
  process.stderr.write('[DBG] USYNC_LID_NULL_RESP jid=' + jid + '\n');
673
- this._deviceCache.get(cacheKey).add(0);
701
+ this._dcAdd(cacheKey, 0);
674
702
  }
675
703
  }
676
704
 
@@ -681,15 +709,16 @@ class DeviceManager {
681
709
  // from cache — zero extra network round-trips.
682
710
  //
683
711
  async ensureOwnDeviceSessions(ownPhone, signalProto, allowPkmsg = true) {
684
- // If own device list is already cached, use it directly
685
- if (this._ownDeviceJids !== null) {
712
+ // Use cached own device list if it hasn't expired (5-min TTL matches _deviceCache)
713
+ if (this._ownDeviceJids !== null && Date.now() < this._ownDeviceExpiry) {
686
714
  const others = this._ownDeviceJids.filter(j => stripUser(j).device !== 0);
687
715
  return this._ensureSessions(others, signalProto, allowPkmsg);
688
716
  }
689
717
 
690
- // First call: query usync for own devices (same as before, but we cache the result)
691
- const deviceJids = await this._usyncGetDevices([ownPhone]);
692
- this._ownDeviceJids = deviceJids;
718
+ // Cache miss or expired: re-query usync for own devices
719
+ const deviceJids = await this._usyncGetDevices([ownPhone]);
720
+ this._ownDeviceJids = deviceJids;
721
+ this._ownDeviceExpiry = Date.now() + 5 * 60 * 1000;
693
722
 
694
723
  const others = deviceJids.filter(j => stripUser(j).device !== 0);
695
724
  return this._ensureSessions(others, signalProto, allowPkmsg);
@@ -730,10 +759,9 @@ class DeviceManager {
730
759
  // ─── Invalidate cached device list (used on phash mismatch / 421 retry) ───
731
760
  clearCache(phones) {
732
761
  if (!phones || phones.length === 0) {
733
- this._deviceCache.clear();
734
- this._ownDeviceJids = null;
762
+ this._dcFlush();
735
763
  } else {
736
- for (const p of phones) this._deviceCache.delete(p);
764
+ this._dcDel(phones);
737
765
  }
738
766
  }
739
767
 
@@ -370,6 +370,7 @@ function toBase64Url(buf) {
370
370
 
371
371
  function buildPayload(store, waVersion, useToken, extraPairs) {
372
372
  const { cc, national } = parsePhone(store.phoneNumber);
373
+ const meta = getCountryMeta(cc);
373
374
  const token = useToken ? computeToken(waVersion, national) : null;
374
375
  const fdid = store.fdid.toUpperCase();
375
376
 
@@ -377,8 +378,8 @@ function buildPayload(store, waVersion, useToken, extraPairs) {
377
378
  'cc', cc,
378
379
  'in', national,
379
380
  'rc', String(RELEASE_CHANNEL),
380
- 'lg', 'en',
381
- 'lc', 'US',
381
+ 'lg', meta.lg,
382
+ 'lc', meta.lc,
382
383
  'authkey', toBase64Url(stripKeyPrefix(store.noiseKeyPair.public)),
383
384
  'e_regid', toBase64Url(intToBytes(store.registrationId, 4)),
384
385
  'e_keytype', toBase64Url(Buffer.from([SIGNAL_KEY_TYPE])),
@@ -601,11 +602,15 @@ async function requestSmsCode(store, method) {
601
602
  const fallbackMethod = method === 'wa_old' ? 'sms' : (method === 'sms' ? 'wa_old' : 'sms');
602
603
  let autoFallbackDone = false;
603
604
 
605
+ // Derive real MCC/MNC from the phone number being registered.
606
+ const { cc: _regCc } = parsePhone(store.phoneNumber);
607
+ const _regMeta = getCountryMeta(_regCc);
608
+
604
609
  async function _tryMethod(m) {
605
610
  const extra = [
606
611
  'method', m,
607
- 'sim_mcc', '000',
608
- 'sim_mnc', '000',
612
+ 'sim_mcc', _regMeta.mcc,
613
+ 'sim_mnc', _regMeta.mnc,
609
614
  'reason', '',
610
615
  'cellular_strength', '1'
611
616
  ];
@@ -706,4 +711,4 @@ async function verifyCode(store, code) {
706
711
  throw new Error(`Verification failed: ${reason || JSON.stringify(result)}`);
707
712
  }
708
713
 
709
- module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, assertRegistrationKeys };
714
+ module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys };