whalibmob 5.14.7 → 5.14.9

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/lib/Client.js CHANGED
@@ -442,9 +442,11 @@ class WhalibmobClient extends EventEmitter {
442
442
  async init(phoneNumber) {
443
443
  phoneNumber = String(phoneNumber).replace(/\D/g, '');
444
444
  this._phoneNumber = phoneNumber;
445
- // A fresh attempt clears whatever made the last one fatal. Only the public
446
- // entry points reset it — the reconnect loop must not clear its own guard.
447
- this._fatal = false;
445
+ // A fresh attempt clears whatever made the last one fatal, and whatever
446
+ // deliberate disconnect came before it. Only the public entry points reset
447
+ // these — the reconnect loop must not clear its own guards.
448
+ this._fatal = false;
449
+ this._closing = false;
448
450
 
449
451
  this._sessionDir = sessionDirFor(this._baseDir, phoneNumber);
450
452
 
@@ -621,7 +623,9 @@ class WhalibmobClient extends EventEmitter {
621
623
  phoneNumber = String(phoneNumber).replace(/\D/g, '');
622
624
  this._phoneNumber = phoneNumber;
623
625
  this._mode = 'web';
624
- this._fatal = false;
626
+ // Same as init(): a fresh attempt lifts both guards from the last one.
627
+ this._fatal = false;
628
+ this._closing = false;
625
629
 
626
630
  const { createNewWebStore, loadWebStore, saveWebStore, webSessionPath } =
627
631
  require('./WebStore');
@@ -1066,13 +1070,34 @@ class WhalibmobClient extends EventEmitter {
1066
1070
  return idx > 0 ? `${lidUser}:${idx}@lid` : `${lidUser}@lid`;
1067
1071
  }
1068
1072
 
1069
- disconnect() {
1073
+ /**
1074
+ * Close the connection, and keep it closed.
1075
+ *
1076
+ * Connect again through init() / initWeb() — those are where the caller says
1077
+ * it wants a connection, and where the guard below is lifted.
1078
+ *
1079
+ * @param {object} [opts]
1080
+ * @param {boolean} [opts.reconnect=false] drop the current socket but leave
1081
+ * the reconnect loop armed. For a failure the server has told us is
1082
+ * temporary, where coming back is the right answer.
1083
+ */
1084
+ disconnect(opts) {
1070
1085
  // Closing the socket makes it emit 'close', which lands in _onClose and
1071
1086
  // schedules a reconnect. That is right for a dropped connection and wrong
1072
1087
  // for a deliberate teardown — and it is what turned a single <failure> into
1073
1088
  // an endless retry loop, since clearing _reconnecting was not enough to
1074
1089
  // stop the handler that sets it again.
1075
- this._closing = true;
1090
+ //
1091
+ // For that to work the guard has to still be set when the 'close' arrives,
1092
+ // and it arrives on a later tick — a socket is destroyed now and reports it
1093
+ // afterwards. Clearing it at the end of this method, which is what it used
1094
+ // to do, meant _scheduleReconnect() never once saw it set: every deliberate
1095
+ // disconnect dialled straight back in a second later, and /reconnect ended
1096
+ // up with the old session live alongside the new one.
1097
+ //
1098
+ // Only ever set here, never cleared, so a teardown that means it cannot be
1099
+ // undone by a later one that does not.
1100
+ if (!(opts && opts.reconnect)) this._closing = true;
1076
1101
  this._reconnecting = false;
1077
1102
  this._reconnectTry = 0;
1078
1103
  this._stopTimers();
@@ -1082,7 +1107,6 @@ class WhalibmobClient extends EventEmitter {
1082
1107
  }
1083
1108
  this._connected = false;
1084
1109
  this.emit('close');
1085
- this._closing = false;
1086
1110
  }
1087
1111
 
1088
1112
  /**
@@ -1246,8 +1270,11 @@ class WhalibmobClient extends EventEmitter {
1246
1270
 
1247
1271
  this._requestMediaConnection();
1248
1272
  // Ask what the server still has rather than uploading blind. It answers
1249
- // with a number this side has no other way of learning.
1250
- this._ensureServerPreKeys('login');
1273
+ // with a number this side has no other way of learning, and then with the
1274
+ // bundle it is serving, which is the thing that goes quietly wrong and
1275
+ // takes the account's reachability with it.
1276
+ this._ensureServerPreKeys('login')
1277
+ .then(() => this._verifyServerKeyBundle('login'));
1251
1278
 
1252
1279
  // Same telemetry ping the web client sends on every completed login.
1253
1280
  this._sendUnifiedSession();
@@ -3277,7 +3304,11 @@ class WhalibmobClient extends EventEmitter {
3277
3304
  this.emit('auth_failure', { reason, location: loc, loggedOut: spent, node });
3278
3305
  this._fatal = spent;
3279
3306
  this._reconnecting = false;
3280
- this.disconnect();
3307
+ // A reason that did not kill the session is one to come back from, so this
3308
+ // teardown leaves the reconnect loop armed — a 503 is the server having a
3309
+ // bad minute, not a reason to abandon a working registration. A reason that
3310
+ // did kill it is fatal, and _scheduleReconnect() stands down on that itself.
3311
+ this.disconnect({ reconnect: !spent });
3281
3312
  }
3282
3313
 
3283
3314
  // ─── Media connection ─────────────────────────────────────────────────────
@@ -3411,6 +3442,103 @@ class WhalibmobClient extends EventEmitter {
3411
3442
  return Array.isArray(list.content) ? list.content.length : 0;
3412
3443
  }
3413
3444
 
3445
+ // What the server believes this account's key bundle is.
3446
+ //
3447
+ // The digest is the only way to see it. Everything else here is a guess about
3448
+ // what was uploaded once and assumed to have stuck — and when the server's
3449
+ // idea of the bundle and ours part company, nobody can open a session with
3450
+ // this account and there is no error anywhere to say so. It is the state that
3451
+ // registering the number again clears, which is why registering again is what
3452
+ // has been fixing it.
3453
+ async _queryKeyDigest() {
3454
+ if (!this._socket || !this._connected) return null;
3455
+
3456
+ const resp = await this._sendIq(new BinaryNode('iq', {
3457
+ id: this._genMsgId(),
3458
+ to: 's.whatsapp.net',
3459
+ type: 'get',
3460
+ xmlns: 'encrypt'
3461
+ }, [new BinaryNode('digest', {}, null)]));
3462
+
3463
+ if (!resp) return null;
3464
+ if (resp.attrs && resp.attrs.type === 'error') return null;
3465
+
3466
+ const digest = findChild(resp, 'digest');
3467
+ if (!digest) return { missing: true };
3468
+
3469
+ const bytesOf = (parent, tag) => {
3470
+ const n = parent && findChild(parent, tag);
3471
+ if (!n) return null;
3472
+ if (Buffer.isBuffer(n.content)) return n.content;
3473
+ return n.content ? Buffer.from(n.content) : null;
3474
+ };
3475
+
3476
+ const skey = findChild(digest, 'skey');
3477
+ const list = findChild(digest, 'list');
3478
+ return {
3479
+ missing: false,
3480
+ identity: bytesOf(digest, 'identity'),
3481
+ skeyId: bytesOf(skey, 'id'),
3482
+ skeyValue: bytesOf(skey, 'value'),
3483
+ preKeyCount: list && Array.isArray(list.content) ? list.content.length : 0
3484
+ };
3485
+ }
3486
+
3487
+ // Compare that bundle against the one we hold, and re-upload when they differ.
3488
+ //
3489
+ // Only values the server states outright are compared — the identity key and
3490
+ // the signed pre-key it is serving — so nothing here depends on guessing how
3491
+ // it hashes them. A mismatch means the bundle other people are handed is not
3492
+ // the one this session can answer for, and no amount of waiting fixes that.
3493
+ async _verifyServerKeyBundle(reason) {
3494
+ if (!this._signal) return;
3495
+ try {
3496
+ const digest = await this._queryKeyDigest();
3497
+ if (!digest) return; // no answer; nothing to conclude
3498
+
3499
+ if (digest.missing) {
3500
+ _whaDbg('[DBG] KEYBUNDLE server returned no digest (' + reason + ') — re-uploading');
3501
+ this._replenishLocalPreKeys();
3502
+ this._uploadPreKeys();
3503
+ return;
3504
+ }
3505
+
3506
+ const bare = (b) => (b && b.length === 33 && b[0] === 0x05) ? b.slice(1) : b;
3507
+ const ourIdent = bare(this._signal.getIdentityKey());
3508
+ const ourSpk = this._signal.getSignedPreKeyForUpload();
3509
+ const ourSpkPub = ourSpk && bare(ourSpk.keyPair.pubKey);
3510
+
3511
+ const mismatches = [];
3512
+ if (digest.identity && ourIdent &&
3513
+ Buffer.compare(digest.identity, ourIdent) !== 0) {
3514
+ mismatches.push('identity key');
3515
+ }
3516
+ if (digest.skeyValue && ourSpkPub &&
3517
+ Buffer.compare(digest.skeyValue, ourSpkPub) !== 0) {
3518
+ mismatches.push('signed pre-key');
3519
+ }
3520
+ if (digest.skeyId && ourSpk) {
3521
+ let served = 0;
3522
+ for (const b of digest.skeyId) served = (served << 8) | b;
3523
+ if (served !== ourSpk.keyId) mismatches.push('signed pre-key id (' + served +
3524
+ ' vs ' + ourSpk.keyId + ')');
3525
+ }
3526
+
3527
+ if (!mismatches.length) {
3528
+ _whaDbg('[DBG] KEYBUNDLE matches the server (' + reason + '), ' +
3529
+ digest.preKeyCount + ' one-time key(s) there');
3530
+ return;
3531
+ }
3532
+
3533
+ _whaDbg('[DBG] KEYBUNDLE server disagrees on ' + mismatches.join(' and ') +
3534
+ ' (' + reason + ') — re-uploading');
3535
+ this._replenishLocalPreKeys();
3536
+ this._uploadPreKeys();
3537
+ } catch (err) {
3538
+ _whaDbg('[DBG] KEYBUNDLE check failed (' + reason + '): ' + (err && err.message));
3539
+ }
3540
+ }
3541
+
3414
3542
  // Top the server back up when it is running out. Never throws: a session that
3415
3543
  // cannot ask is left exactly as it was rather than losing its connection over
3416
3544
  // a bookkeeping query.
@@ -418,6 +418,149 @@ const COUNTRY_META = {
418
418
  '971': { mcc: '424', mnc: '02', lg: 'ar', lc: 'AE' }, // UAE
419
419
  '972': { mcc: '425', mnc: '01', lg: 'he', lc: 'IL' }, // Israel
420
420
  '880': { mcc: '470', mnc: '01', lg: 'bn', lc: 'BD' }, // Bangladesh
421
+ // Filled in from the ITU mobile-network tables so a number from anywhere
422
+ // reports the carrier its country actually has. Without an entry a session
423
+ // falls back to 000/000, which is what a handset with no SIM card reports.
424
+ // One operational national operator per country; the point is a plausible
425
+ // home network, not a particular one.
426
+ '53': { mcc: '368', mnc: '01', lg: 'es', lc: 'CU' }, // Cuba (CUBACEL)
427
+ '211': { mcc: '659', mnc: '02', lg: 'en', lc: 'SS' }, // South Sudan (MTN)
428
+ '220': { mcc: '607', mnc: '01', lg: 'en', lc: 'GM' }, // Gambia (Gamcel)
429
+ '221': { mcc: '608', mnc: '01', lg: 'fr', lc: 'SN' }, // Senegal (Orange)
430
+ '222': { mcc: '609', mnc: '01', lg: 'ar', lc: 'MR' }, // Mauritania (Mattel)
431
+ '223': { mcc: '610', mnc: '01', lg: 'fr', lc: 'ML' }, // Mali (Malitel)
432
+ '224': { mcc: '611', mnc: '01', lg: 'fr', lc: 'GN' }, // Guinea (Orange)
433
+ '225': { mcc: '612', mnc: '02', lg: 'fr', lc: 'CI' }, // Ivory Coast (Moov)
434
+ '226': { mcc: '613', mnc: '01', lg: 'fr', lc: 'BF' }, // Burkina Faso (Telmob)
435
+ '227': { mcc: '614', mnc: '01', lg: 'fr', lc: 'NE' }, // Niger (SahelCom)
436
+ '228': { mcc: '615', mnc: '01', lg: 'fr', lc: 'TG' }, // Togo (Togo Cell)
437
+ '229': { mcc: '616', mnc: '01', lg: 'fr', lc: 'BJ' }, // Benin (Benin Telecoms Mob)
438
+ '230': { mcc: '617', mnc: '01', lg: 'en', lc: 'MU' }, // Mauritius (my.t)
439
+ '231': { mcc: '618', mnc: '01', lg: 'en', lc: 'LR' }, // Liberia (Lonestar Cell MTN)
440
+ '232': { mcc: '619', mnc: '01', lg: 'en', lc: 'SL' }, // Sierra Leone (Orange)
441
+ '233': { mcc: '620', mnc: '01', lg: 'en', lc: 'GH' }, // Ghana (MTN)
442
+ '235': { mcc: '622', mnc: '01', lg: 'ar', lc: 'TD' }, // Chad (Airtel)
443
+ '236': { mcc: '623', mnc: '01', lg: 'fr', lc: 'CF' }, // Central African Republic (Moov)
444
+ '237': { mcc: '624', mnc: '01', lg: 'en', lc: 'CM' }, // Cameroon (MTN Cameroon)
445
+ '238': { mcc: '625', mnc: '01', lg: 'pt', lc: 'CV' }, // Cape Verde (CVMOVEL)
446
+ '239': { mcc: '626', mnc: '01', lg: 'pt', lc: 'ST' }, // São Tomé and Príncipe (CSTmovel)
447
+ '240': { mcc: '627', mnc: '01', lg: 'fr', lc: 'GQ' }, // Equatorial Guinea (Orange GQ)
448
+ '241': { mcc: '628', mnc: '01', lg: 'fr', lc: 'GA' }, // Gabon (Libertis)
449
+ '242': { mcc: '629', mnc: '01', lg: 'fr', lc: 'CG' }, // Republic of the Congo (Airtel)
450
+ '243': { mcc: '630', mnc: '01', lg: 'fr', lc: 'CD' }, // DR Congo (Vodacom)
451
+ '244': { mcc: '631', mnc: '02', lg: 'pt', lc: 'AO' }, // Angola (UNITEL)
452
+ '245': { mcc: '632', mnc: '01', lg: 'pt', lc: 'GW' }, // Guinea-Bissau (Guinetel)
453
+ '248': { mcc: '633', mnc: '01', lg: 'en', lc: 'SC' }, // Seychelles (Cable & Wireless)
454
+ '249': { mcc: '634', mnc: '01', lg: 'ar', lc: 'SD' }, // Sudan (Zain SD)
455
+ '250': { mcc: '635', mnc: '10', lg: 'en', lc: 'RW' }, // Rwanda (MTN)
456
+ '251': { mcc: '636', mnc: '01', lg: 'am', lc: 'ET' }, // Ethiopia (MTN)
457
+ '252': { mcc: '637', mnc: '01', lg: 'ar', lc: 'SO' }, // Somalia (Telesom)
458
+ '253': { mcc: '638', mnc: '01', lg: 'ar', lc: 'DJ' }, // Djibouti (Evatis)
459
+ '257': { mcc: '642', mnc: '01', lg: 'fr', lc: 'BI' }, // Burundi (econet Leo)
460
+ '258': { mcc: '643', mnc: '01', lg: 'pt', lc: 'MZ' }, // Mozambique (mCel)
461
+ '260': { mcc: '645', mnc: '01', lg: 'en', lc: 'ZM' }, // Zambia (Airtel)
462
+ '261': { mcc: '646', mnc: '01', lg: 'fr', lc: 'MG' }, // Madagascar (Airtel)
463
+ '263': { mcc: '648', mnc: '01', lg: 'en', lc: 'ZW' }, // Zimbabwe (Net*One)
464
+ '264': { mcc: '649', mnc: '01', lg: 'af', lc: 'NA' }, // Namibia (MTC)
465
+ '265': { mcc: '650', mnc: '01', lg: 'en', lc: 'MW' }, // Malawi (TNM)
466
+ '266': { mcc: '651', mnc: '01', lg: 'en', lc: 'LS' }, // Lesotho (Vodacom)
467
+ '267': { mcc: '652', mnc: '01', lg: 'en', lc: 'BW' }, // Botswana (Mascom)
468
+ '268': { mcc: '653', mnc: '02', lg: 'en', lc: 'SZ' }, // Eswatini (Eswatini Mobile Li)
469
+ '269': { mcc: '654', mnc: '01', lg: 'ar', lc: 'KM' }, // Comoros (HURI)
470
+ '290': { mcc: '658', mnc: '01', lg: 'en', lc: 'SH' }, // Saint Helena, Ascension and Tristan da Cunha (Sure)
471
+ '291': { mcc: '657', mnc: '01', lg: 'ar', lc: 'ER' }, // Eritrea (Eritel)
472
+ '297': { mcc: '363', mnc: '01', lg: 'nl', lc: 'AW' }, // Aruba (SETAR)
473
+ '298': { mcc: '288', mnc: '01', lg: 'da', lc: 'FO' }, // Faroe Islands (Føroya Tele)
474
+ '299': { mcc: '290', mnc: '01', lg: 'kl', lc: 'GL' }, // Greenland (tusass)
475
+ '350': { mcc: '266', mnc: '01', lg: 'en', lc: 'GI' }, // Gibraltar (GibTel)
476
+ '352': { mcc: '270', mnc: '01', lg: 'de', lc: 'LU' }, // Luxembourg (POST)
477
+ '354': { mcc: '274', mnc: '01', lg: 'is', lc: 'IS' }, // Iceland (Síminn)
478
+ '355': { mcc: '276', mnc: '01', lg: 'sq', lc: 'AL' }, // Albania (ONE)
479
+ '356': { mcc: '278', mnc: '01', lg: 'en', lc: 'MT' }, // Malta (Epic)
480
+ '357': { mcc: '280', mnc: '01', lg: 'el', lc: 'CY' }, // Cyprus (Cytamobile-Vodafon)
481
+ '359': { mcc: '284', mnc: '01', lg: 'bg', lc: 'BG' }, // Bulgaria (A1 BG)
482
+ '370': { mcc: '246', mnc: '01', lg: 'lt', lc: 'LT' }, // Lithuania (Telia)
483
+ '371': { mcc: '247', mnc: '01', lg: 'lv', lc: 'LV' }, // Latvia (LMT)
484
+ '372': { mcc: '248', mnc: '01', lg: 'et', lc: 'EE' }, // Estonia (Telia)
485
+ '373': { mcc: '259', mnc: '01', lg: 'ro', lc: 'MD' }, // Moldova (Orange)
486
+ '374': { mcc: '283', mnc: '01', lg: 'hy', lc: 'AM' }, // Armenia (Beeline)
487
+ '375': { mcc: '257', mnc: '01', lg: 'be', lc: 'BY' }, // Belarus (A1)
488
+ '376': { mcc: '213', mnc: '03', lg: 'ca', lc: 'AD' }, // Andorra (Som, Mobiland)
489
+ '377': { mcc: '212', mnc: '10', lg: 'fr', lc: 'MC' }, // Monaco (Office des Telepho)
490
+ '378': { mcc: '292', mnc: '01', lg: 'it', lc: 'SM' }, // San Marino (PRIMA)
491
+ '381': { mcc: '220', mnc: '01', lg: 'sr', lc: 'RS' }, // Serbia (Yettel)
492
+ '382': { mcc: '297', mnc: '01', lg: 'en', lc: 'ME' }, // Montenegro (One)
493
+ '383': { mcc: '221', mnc: '01', lg: 'sq', lc: 'XK' }, // Kosovo (Vala)
494
+ '385': { mcc: '219', mnc: '01', lg: 'hr', lc: 'HR' }, // Croatia (HT HR)
495
+ '386': { mcc: '293', mnc: '10', lg: 'sl', lc: 'SI' }, // Slovenia (SŽ - Infrastruktur)
496
+ '387': { mcc: '218', mnc: '03', lg: 'bs', lc: 'BA' }, // Bosnia and Herzegovina (HT-ERONET)
497
+ '389': { mcc: '294', mnc: '01', lg: 'mk', lc: 'MK' }, // North Macedonia (Telekom.mk)
498
+ '421': { mcc: '231', mnc: '01', lg: 'sk', lc: 'SK' }, // Slovakia (Orange)
499
+ '423': { mcc: '295', mnc: '01', lg: 'de', lc: 'LI' }, // Liechtenstein (Swisscom)
500
+ '500': { mcc: '750', mnc: '001', lg: 'en', lc: 'FK' }, // Falkland Islands (Sure)
501
+ '501': { mcc: '702', mnc: '67', lg: 'en', lc: 'BZ' }, // Belize (DigiCell)
502
+ '502': { mcc: '704', mnc: '01', lg: 'es', lc: 'GT' }, // Guatemala (Claro)
503
+ '503': { mcc: '706', mnc: '01', lg: 'es', lc: 'SV' }, // El Salvador (Claro)
504
+ '504': { mcc: '708', mnc: '001', lg: 'es', lc: 'HN' }, // Honduras (Claro)
505
+ '505': { mcc: '710', mnc: '21', lg: 'es', lc: 'NI' }, // Nicaragua (Claro)
506
+ '506': { mcc: '712', mnc: '01', lg: 'es', lc: 'CR' }, // Costa Rica (Kölbi ICE)
507
+ '507': { mcc: '714', mnc: '01', lg: 'es', lc: 'PA' }, // Panama (Cable & Wireless)
508
+ '508': { mcc: '308', mnc: '01', lg: 'fr', lc: 'PM' }, // Saint Pierre and Miquelon (Ameris)
509
+ '509': { mcc: '372', mnc: '02', lg: 'fr', lc: 'HT' }, // Haiti (Digicel)
510
+ '591': { mcc: '736', mnc: '01', lg: 'ay', lc: 'BO' }, // Bolivia (Viva)
511
+ '592': { mcc: '738', mnc: '00', lg: 'en', lc: 'GY' }, // Guyana (E-Networks)
512
+ '593': { mcc: '740', mnc: '00', lg: 'es', lc: 'EC' }, // Ecuador (Movistar)
513
+ '594': { mcc: '340', mnc: '01', lg: 'fr', lc: 'GF' }, // French Guiana (Orange)
514
+ '595': { mcc: '744', mnc: '01', lg: 'gn', lc: 'PY' }, // Paraguay (VOX)
515
+ '597': { mcc: '746', mnc: '02', lg: 'nl', lc: 'SR' }, // Suriname (Telesur)
516
+ '598': { mcc: '748', mnc: '01', lg: 'es', lc: 'UY' }, // Uruguay (Antel)
517
+ '670': { mcc: '514', mnc: '01', lg: 'pt', lc: 'TL' }, // Timor-Leste (Telkomcel)
518
+ '672': { mcc: '505', mnc: '10', lg: 'en', lc: 'NF' }, // Norfolk Island (Norfolk Telecom)
519
+ '673': { mcc: '528', mnc: '02', lg: 'ms', lc: 'BN' }, // Brunei (PCSB)
520
+ '674': { mcc: '536', mnc: '02', lg: 'en', lc: 'NR' }, // Nauru (Digicel)
521
+ '675': { mcc: '537', mnc: '01', lg: 'en', lc: 'PG' }, // Papua New Guinea (bmobile)
522
+ '676': { mcc: '539', mnc: '01', lg: 'en', lc: 'TO' }, // Tonga (U-Call)
523
+ '677': { mcc: '540', mnc: '01', lg: 'en', lc: 'SB' }, // Solomon Islands (BREEZE)
524
+ '678': { mcc: '541', mnc: '00', lg: 'bi', lc: 'VU' }, // Vanuatu (AIL)
525
+ '679': { mcc: '542', mnc: '01', lg: 'en', lc: 'FJ' }, // Fiji (Vodafone)
526
+ '680': { mcc: '552', mnc: '01', lg: 'en', lc: 'PW' }, // Palau (PNCC)
527
+ '681': { mcc: '543', mnc: '01', lg: 'fr', lc: 'WF' }, // Wallis and Futuna (Manuia)
528
+ '682': { mcc: '548', mnc: '01', lg: 'en', lc: 'CK' }, // Cook Islands (Vodafone)
529
+ '683': { mcc: '555', mnc: '01', lg: 'en', lc: 'NU' }, // Niue (Telecom Niue)
530
+ '685': { mcc: '549', mnc: '01', lg: 'en', lc: 'WS' }, // Samoa (Digicel)
531
+ '686': { mcc: '545', mnc: '01', lg: 'en', lc: 'KI' }, // Kiribati (Kiribati - ATH)
532
+ '687': { mcc: '546', mnc: '01', lg: 'fr', lc: 'NC' }, // New Caledonia (Mobilis)
533
+ '688': { mcc: '553', mnc: '01', lg: 'en', lc: 'TV' }, // Tuvalu (TTC)
534
+ '689': { mcc: '547', mnc: '05', lg: 'fr', lc: 'PF' }, // French Polynesia (Ora)
535
+ '690': { mcc: '554', mnc: '01', lg: 'en', lc: 'TK' }, // Tokelau (Teletok)
536
+ '691': { mcc: '550', mnc: '01', lg: 'en', lc: 'FM' }, // Micronesia (FSMTC)
537
+ '692': { mcc: '551', mnc: '01', lg: 'en', lc: 'MH' }, // Marshall Islands (Marshall Islands N)
538
+ '850': { mcc: '467', mnc: '05', lg: 'ko', lc: 'KP' }, // North Korea (Koryolink)
539
+ '852': { mcc: '454', mnc: '00', lg: 'en', lc: 'HK' }, // Hong Kong (1O1O / One2Free / )
540
+ '853': { mcc: '455', mnc: '00', lg: 'pt', lc: 'MO' }, // Macau (SmarTone)
541
+ '855': { mcc: '456', mnc: '01', lg: 'km', lc: 'KH' }, // Cambodia (Cellcard)
542
+ '856': { mcc: '457', mnc: '01', lg: 'lo', lc: 'LA' }, // Laos (LaoTel)
543
+ '886': { mcc: '466', mnc: '01', lg: 'zh', lc: 'TW' }, // Taiwan (FarEasTone)
544
+ '960': { mcc: '472', mnc: '01', lg: 'dv', lc: 'MV' }, // Maldives (Dhiraagu)
545
+ '961': { mcc: '415', mnc: '01', lg: 'ar', lc: 'LB' }, // Lebanon (Alfa)
546
+ '962': { mcc: '416', mnc: '01', lg: 'ar', lc: 'JO' }, // Jordan (zain JO)
547
+ '963': { mcc: '417', mnc: '01', lg: 'ar', lc: 'SY' }, // Syria (Syriatel)
548
+ '964': { mcc: '418', mnc: '00', lg: 'ar', lc: 'IQ' }, // Iraq (Asia Cell)
549
+ '965': { mcc: '419', mnc: '02', lg: 'ar', lc: 'KW' }, // Kuwait (zain KW)
550
+ '967': { mcc: '421', mnc: '01', lg: 'ar', lc: 'YE' }, // Yemen (SabaFon)
551
+ '968': { mcc: '422', mnc: '02', lg: 'ar', lc: 'OM' }, // Oman (Omantel)
552
+ '970': { mcc: '425', mnc: '05', lg: 'ar', lc: 'PS' }, // Palestine (Jawwal)
553
+ '973': { mcc: '426', mnc: '01', lg: 'ar', lc: 'BH' }, // Bahrain (Batelco)
554
+ '974': { mcc: '427', mnc: '01', lg: 'ar', lc: 'QA' }, // Qatar (Ooredoo)
555
+ '975': { mcc: '402', mnc: '11', lg: 'dz', lc: 'BT' }, // Bhutan (B-Mobile)
556
+ '976': { mcc: '428', mnc: '88', lg: 'mn', lc: 'MN' }, // Mongolia (Unitel)
557
+ '977': { mcc: '429', mnc: '01', lg: 'ne', lc: 'NP' }, // Nepal (Namaste / NT Mobil)
558
+ '992': { mcc: '436', mnc: '01', lg: 'ru', lc: 'TJ' }, // Tajikistan (Tcell)
559
+ '993': { mcc: '438', mnc: '02', lg: 'ru', lc: 'TM' }, // Turkmenistan (TM-Cell)
560
+ '994': { mcc: '400', mnc: '01', lg: 'az', lc: 'AZ' }, // Azerbaijan (Azercell)
561
+ '995': { mcc: '282', mnc: '01', lg: 'ka', lc: 'GE' }, // Georgia (Geocell)
562
+ '996': { mcc: '437', mnc: '01', lg: 'ky', lc: 'KG' }, // Kyrgyzstan (Beeline)
563
+ '998': { mcc: '434', mnc: '03', lg: 'ru', lc: 'UZ' }, // Uzbekistan (UzMobile)
421
564
  };
422
565
 
423
566
  function getCountryMeta(cc) {
@@ -431,17 +574,20 @@ function getCountryMeta(cc) {
431
574
  // (Java long conversion removes them automatically)
432
575
 
433
576
  const THREE_DIGIT_CCS = new Set([
434
- '355','213','376','244','672','374','297','994','387','1246','880','226',
435
- '599','1242','975','267','257','229','590','673','591','238','682','237',
436
- '236','242','225','269','506','53','357','253','1767','291','240','251',
437
- '372','500','298','241','995','594','233','299','220','224','245','592',
438
- '504','353','246','964','354','962','996','855','686','850','965','7',
439
- '856','423','231','266','370','352','218','261','692','692','389','223',
440
- '692','222','960','265','692','976','853','258','264','977','674','505',
441
- '687','227','968','507','675','689','680','595','974','692','250','677',
442
- '248','963','503','268','235','228','676','1868','688','255','993','690',
443
- '670','598','678','685','967','692','262','964','993','1784','1242','371',
444
- '1473','692','1268','692','1869','692'
577
+ '211','212','213','216','218','220','221','222','223','224','225','226',
578
+ '227','228','229','230','231','232','233','234','235','236','237','238',
579
+ '239','240','241','242','243','244','245','246','247','248','249','250',
580
+ '251','252','253','254','255','256','257','258','260','261','262','263',
581
+ '264','265','266','267','268','269','290','291','297','298','299','350',
582
+ '351','352','353','354','355','356','357','358','359','370','371','372',
583
+ '373','374','375','376','377','378','380','381','382','383','385','386',
584
+ '387','389','420','421','423','500','501','502','503','504','505','506',
585
+ '507','508','509','590','591','592','593','594','595','596','597','598',
586
+ '599','670','672','673','674','675','676','677','678','679','680','681',
587
+ '682','683','685','686','687','688','689','690','691','692','850','852',
588
+ '853','855','856','880','886','960','961','962','963','964','965','966',
589
+ '967','968','970','971','972','973','974','975','976','977','992','993',
590
+ '994','995','996','998'
445
591
  ]);
446
592
 
447
593
  const TWO_DIGIT_CCS = new Set([
@@ -451,11 +597,10 @@ const TWO_DIGIT_CCS = new Set([
451
597
  '95','98'
452
598
  ]);
453
599
 
454
- // Every country we carry metadata for has to be splittable here, or parsePhone
455
- // takes the wrong prefix and the registration token ends up computed over the
456
- // wrong national number — which the server answers by refusing the code. The
457
- // two lists above were maintained by hand and had already drifted, so the
458
- // country table itself gets the last word.
600
+ // The country table only covers the places we carry carrier metadata for, which
601
+ // is far fewer than the places that can register. Anything it names still has to
602
+ // be splittable, so it is folded in — a no-op while the lists above are complete,
603
+ // and a backstop if a code is ever added to the table and not to them.
459
604
  for (const cc of Object.keys(COUNTRY_META)) {
460
605
  if (cc.length === 3) THREE_DIGIT_CCS.add(cc);
461
606
  else if (cc.length === 2) TWO_DIGIT_CCS.add(cc);
package/lib/image/Bmp.js CHANGED
@@ -13,15 +13,27 @@ function isBmp(buf) {
13
13
  // One channel out of a packed pixel. A mask says where the channel sits and how
14
14
  // wide it is, and the width varies — 5-6-5 and 5-5-5 are both ordinary — so the
15
15
  // value is scaled by its own maximum rather than shifted by a fixed amount.
16
+ //
17
+ // Both scans below stop at 32 on their own, because a shift count in JS is
18
+ // taken mod 32: `bits >>> 32` is `bits >>> 0`, so a mask of all ones sends an
19
+ // unbounded scan back to bit 0 and it never comes out. Nothing upstream could
20
+ // catch that — decodeImage's try/catch cannot interrupt a loop that does not
21
+ // end, and a thumbnail is built on the way to sending an ordinary image, so
22
+ // one crafted BMP would take the whole process down with it.
16
23
  function maskChannel(v, mask) {
17
24
  if (!mask) return 0;
18
25
  let shift = 0;
19
- while (!((mask >>> shift) & 1)) shift++;
26
+ while (shift < 32 && !((mask >>> shift) & 1)) shift++;
27
+ if (shift === 32) return 0; // no bit set — nothing to read
20
28
  const bits = (mask >>> shift) >>> 0;
21
29
  let width = 0;
22
- while ((bits >>> width) & 1) width++;
23
- const max = (1 << width) - 1;
24
- return Math.round(((v >>> shift) & max) * 255 / max);
30
+ while (width < 32 && ((bits >>> width) & 1)) width++;
31
+ // The obvious `(1 << width) - 1` is wrong at both ends of that range: `1 <<
32
+ // 32` is 1, and `1 << 31` is negative, so a 31- or 32-bit channel would be
33
+ // scaled by zero or by a negative maximum. Build the maximum unsigned, and
34
+ // read the value back unsigned too — `& 0xffffffff` alone yields an int32.
35
+ const max = width >= 32 ? 0xffffffff : ((1 << width) >>> 0) - 1;
36
+ return Math.round((((v >>> shift) & max) >>> 0) * 255 / max);
25
37
  }
26
38
 
27
39
  function decodeBmp(buf) {
@@ -14,6 +14,27 @@ const { SessionRecord } = require('./libsignal');
14
14
  // • on process exit, flush synchronously (so state is never lost)
15
15
  //
16
16
  // Result: zero blocking disk I/O during the encryption hot-path.
17
+ //
18
+ // Two rules keep that from costing the account everything it knows:
19
+ //
20
+ // • Every write goes to a temp file and is renamed into place. Writing over
21
+ // the live path is not one step — it is an open, a truncate and as many
22
+ // writes as the file needs — and anything that reads or writes in the
23
+ // middle of that sees a mixture of two snapshots. That mixture is not
24
+ // valid JSON, and _load() below answers unreadable state with an empty
25
+ // store, so a single interleaved write silently costs the account every
26
+ // session, pre-key, identity and LID mapping it had. rename(2) is atomic:
27
+ // the file is one whole snapshot or the other, never a blend.
28
+ //
29
+ // • A write never lands if newer state reached the disk while it was in
30
+ // flight. The async write started by the debounce is not cancellable, and
31
+ // a synchronous flush — detach(), or the exit handlers — can easily
32
+ // overtake it; letting the older snapshot finish last would undo every
33
+ // session written since. _writtenRev records what the file holds, and a
34
+ // write that has been overtaken drops its temp file instead of renaming.
35
+ //
36
+ // In-memory state is always the newest thing there is, so any write we abandon
37
+ // costs nothing: the next one writes what is current.
17
38
 
18
39
  const DEBOUNCE_MS = 300;
19
40
 
@@ -37,6 +58,15 @@ class SignalStore {
37
58
  this._dirty = false;
38
59
  this._rev = 0;
39
60
  this._saveTimer = null;
61
+ // The revision the file on disk actually holds. An async write compares
62
+ // against it before renaming: if a synchronous flush got there first, the
63
+ // write it is holding is stale and must not land.
64
+ this._writtenRev = 0;
65
+ // One async write at a time. Two of them running together on one path
66
+ // interleave, and _writeAgain remembers that another one came due while
67
+ // the first was still going so it can be re-run from a fresh snapshot.
68
+ this._writing = false;
69
+ this._writeAgain = false;
40
70
 
41
71
  // Flush synchronously on exit so no state is ever lost.
42
72
  //
@@ -61,7 +91,14 @@ class SignalStore {
61
91
  process.removeListener('SIGTERM', this._onSigTerm);
62
92
  process.removeListener('SIGINT', this._onSigInt);
63
93
  } catch (_) {}
64
- this._filePath = null;
94
+ // Clearing the path is also what tells a write that is still in flight to
95
+ // stand down — it checks the path it started with before renaming. The
96
+ // caller detaches in order to move these files, so leave nothing of ours
97
+ // behind in the directory it is about to rename.
98
+ const stale = this._filePath ? [this._tmpPath(true), this._tmpPath(false)] : [];
99
+ this._filePath = null;
100
+ this._writeAgain = false;
101
+ for (const f of stale) { try { fs.unlinkSync(f); } catch (_) {} }
65
102
  }
66
103
 
67
104
  init(identityKeyPair, registrationId) {
@@ -100,23 +137,67 @@ class SignalStore {
100
137
  return this._lidMappings;
101
138
  }
102
139
 
103
- // Immediate synchronous flush (used on process exit only)
140
+ // Everything that is persisted, as it goes on disk.
141
+ _snapshot() {
142
+ return JSON.stringify({
143
+ sessions: this._sessions,
144
+ preKeys: this._preKeys,
145
+ signedPreKeys: this._signedPreKeys,
146
+ identities: this._identities,
147
+ lidMappings: this._lidMappings
148
+ });
149
+ }
150
+
151
+ // Where a write assembles the file before it becomes the file. The sync and
152
+ // async writers get separate names so the one can never find the other's
153
+ // half-written temp file under it.
154
+ _tmpPath(sync) {
155
+ return this._filePath + (sync ? '.stmp' : '.atmp');
156
+ }
157
+
158
+ // A rename replaces the file, so the one that lands carries the temp file's
159
+ // permissions and not the old file's. This holds Signal private keys, and an
160
+ // operator who tightened its mode would find it loosened again by the next
161
+ // write — so carry whatever the file already had across. null when there is
162
+ // no file yet, which leaves the default the old writes produced.
163
+ _modeOf(file) {
164
+ try { return fs.statSync(file).mode & 0o777; } catch (_) { return null; }
165
+ }
166
+
167
+ // Immediate synchronous flush (process exit, and detach())
104
168
  _flushSync() {
105
169
  if (!this._dirty || !this._filePath) return;
106
170
  if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
171
+ const rev = this._rev;
172
+ const file = this._filePath;
173
+ const tmp = this._tmpPath(true);
107
174
  try {
108
- const data = JSON.stringify({
109
- sessions: this._sessions,
110
- preKeys: this._preKeys,
111
- signedPreKeys: this._signedPreKeys,
112
- identities: this._identities,
113
- lidMappings: this._lidMappings
114
- });
115
- const dir = path.dirname(this._filePath);
175
+ const dir = path.dirname(file);
116
176
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
117
- fs.writeFileSync(this._filePath, data, 'utf8');
118
- this._dirty = false;
119
- } catch (_) {}
177
+ const mode = this._modeOf(file);
178
+ fs.writeFileSync(tmp, this._snapshot(), mode !== null
179
+ ? { encoding: 'utf8', mode } : 'utf8');
180
+ if (mode !== null) fs.chmodSync(tmp, mode);
181
+ fs.renameSync(tmp, file);
182
+ // Take the async writer's temp file away from it.
183
+ //
184
+ // Its "have I been overtaken?" check runs on this thread, but the rename
185
+ // it authorises is carried out on a pool thread afterwards — and at exit
186
+ // the pool is usually busy, so that rename can still be queued now and
187
+ // execute during teardown, putting this snapshot back to an older one.
188
+ // Its callback never runs there, so nothing would notice or repair it.
189
+ // Removing the source makes that queued rename fail ENOENT instead.
190
+ // detach() does the same thing, which is why detach() was never exposed
191
+ // to this; the exit path simply was not doing it.
192
+ try { fs.unlinkSync(this._tmpPath(false)); } catch (_) {}
193
+ // Nothing ran between the snapshot and the rename — this is synchronous —
194
+ // so the file now holds exactly this revision and there is nothing left
195
+ // to write.
196
+ this._writtenRev = rev;
197
+ this._dirty = false;
198
+ } catch (_) {
199
+ try { fs.unlinkSync(tmp); } catch (_) {}
200
+ }
120
201
  }
121
202
 
122
203
  // Schedule an async (non-blocking) write after DEBOUNCE_MS inactivity.
@@ -128,25 +209,67 @@ class SignalStore {
128
209
  if (this._saveTimer) clearTimeout(this._saveTimer);
129
210
  this._saveTimer = setTimeout(() => {
130
211
  this._saveTimer = null;
131
- // The revision this write is a snapshot of.
132
- const rev = this._rev;
133
- const data = JSON.stringify({
134
- sessions: this._sessions,
135
- preKeys: this._preKeys,
136
- signedPreKeys: this._signedPreKeys,
137
- identities: this._identities,
138
- lidMappings: this._lidMappings
139
- });
140
- const dir = path.dirname(this._filePath);
212
+ this._writeAsync();
213
+ }, DEBOUNCE_MS);
214
+ }
215
+
216
+ // The debounced write itself: temp file, then rename into place, and only if
217
+ // nothing newer reached the disk in the meantime.
218
+ _writeAsync() {
219
+ if (!this._filePath || !this._dirty) return;
220
+ // Fold a write that comes due mid-write into the one already running
221
+ // rather than starting a second one over the same temp file.
222
+ if (this._writing) { this._writeAgain = true; return; }
223
+
224
+ // The revision this write is a snapshot of, and the path it is for. Both
225
+ // are read once, here: by the time the callbacks run either may have moved
226
+ // on, and that is exactly what they have to notice.
227
+ const rev = this._rev;
228
+ const file = this._filePath;
229
+ const tmp = this._tmpPath(false);
230
+
231
+ let data, mode;
232
+ try {
233
+ const dir = path.dirname(file);
141
234
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
142
- // Only clear the flag when nothing changed while the write was running.
143
- // A mutation that arrived after the snapshot is not in `data`, and its
144
- // own debounce timer may never fire — clearing here would make the
145
- // exit-time _flushSync() skip it and lose it.
146
- fs.writeFile(this._filePath, data, 'utf8', () => {
235
+ mode = this._modeOf(file);
236
+ data = this._snapshot();
237
+ } catch (_) { return; }
238
+
239
+ this._writing = true;
240
+ this._writeAgain = false;
241
+
242
+ const finish = () => {
243
+ this._writing = false;
244
+ // A write came due while this one was running and was folded into it.
245
+ // Every other mutation armed its own debounce timer, which is still
246
+ // pending and will write what is current.
247
+ if (this._writeAgain && this._dirty && this._filePath) this._writeAsync();
248
+ };
249
+ const abandon = () => { fs.unlink(tmp, () => {}); finish(); };
250
+
251
+ // Create the temp file already tight rather than widening the window in
252
+ // which a store full of private keys sits at the umask default.
253
+ fs.writeFile(tmp, data, mode !== null ? { encoding: 'utf8', mode } : 'utf8', (err) => {
254
+ if (err) return abandon();
255
+ // detach() let the file go, or a synchronous flush wrote newer state
256
+ // while this one was running. Renaming now would put an older snapshot
257
+ // back and take every session written since with it.
258
+ if (file !== this._filePath || rev <= this._writtenRev) return abandon();
259
+ if (mode !== null) { try { fs.chmodSync(tmp, mode); } catch (_) {} }
260
+ fs.rename(tmp, file, (renameErr) => {
261
+ if (renameErr) return abandon();
262
+ // Never walk it backwards: a synchronous flush can land — and record a
263
+ // higher revision — between the check above and this callback.
264
+ if (rev > this._writtenRev) this._writtenRev = rev;
265
+ // Only clear the flag when nothing changed while the write was
266
+ // running. A mutation that arrived after the snapshot is not in
267
+ // `data`, and clearing here would make the exit-time _flushSync()
268
+ // skip it and lose it.
147
269
  if (this._rev === rev) this._dirty = false;
270
+ finish();
148
271
  });
149
- }, DEBOUNCE_MS);
272
+ });
150
273
  }
151
274
 
152
275
  // ─── Identity / registration ─────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.7",
3
+ "version": "5.14.9",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",