whalibmob 5.14.18 → 5.14.19

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/README.md CHANGED
@@ -2149,7 +2149,8 @@ History arrives in chunks over the first minute or so after linking, largest fir
2149
2149
  | File | Holds |
2150
2150
  |---|---|
2151
2151
  | `<phone>.web.json` | link state — keys, `advSecretKey`, the device slot you were given |
2152
- | `<phone>.web.signal.json` | Signal sessions and pre-keys for the linked device |
2152
+ | `<phone>.web.signal.json` | Signal sessions, signed pre-key, identities, LID map, pre-key counters |
2153
+ | `<phone>.web.pre-key-<id>.json` | one one-time pre-key — 812 of them |
2153
2154
  | `<phone>.web.sk.json` | group SenderKeys |
2154
2155
  | `<phone>.web.tctoken.json` | privacy tokens |
2155
2156
  | `<phone>.web.appState.json` | app-state version and hash per collection |
@@ -2538,7 +2539,11 @@ whalibmob_auth/
2538
2539
  ├── android-apk-material-business.json
2539
2540
  ├── 919634847671/
2540
2541
  │ ├── 919634847671.json ← the store: keys, device, version
2541
- │ ├── 919634847671.signal.json ← Signal sessions
2542
+ │ ├── 919634847671.signal.json ← Signal sessions, signed pre-key, identities
2543
+ │ ├── 919634847671.pre-key-1.json ← one-time pre-keys, one file each
2544
+ │ ├── 919634847671.pre-key-2.json
2545
+ │ ├── … 812 of them
2546
+ │ ├── 919634847671.pre-key-812.json
2542
2547
  │ ├── 919634847671.sk.json ← sender keys
2543
2548
  │ ├── 919634847671.tctoken.json
2544
2549
  │ ├── 919634847671.device-cache.json
@@ -2566,6 +2571,66 @@ of a pile by prefix.
2566
2571
  > is moved unless you ask — `wa migrate-sessions` does that, one number or all
2567
2572
  > of them, and re-running it is safe.
2568
2573
 
2574
+ ### One-time Pre-keys
2575
+
2576
+ A pre-key is a one-shot Diffie-Hellman key the account leaves with the server so
2577
+ somebody who wants to message it can open a Signal session without it being
2578
+ online. Each one is handed out once and then gone. Run out and nobody can start
2579
+ a conversation with the number at all — the messages are not delayed, they are
2580
+ never sent, and nothing anywhere reports it.
2581
+
2582
+ whalibmob keeps a pool of **812**, generated the moment a session is created and
2583
+ written one key per file, in whatever authentication folder you gave it:
2584
+
2585
+ ```
2586
+ ~/.waSession/919634847671/
2587
+ ├── 919634847671.pre-key-1.json
2588
+ ├── 919634847671.pre-key-2.json
2589
+ └── … 919634847671.pre-key-812.json
2590
+ ```
2591
+
2592
+ A companion link for the same number keeps its own pool beside it as
2593
+ `919634847671.web.pre-key-<id>.json`. The two are never shared: they are
2594
+ separate devices with separate identity keys, and a key offered under the wrong
2595
+ one cannot be answered.
2596
+
2597
+ The file is the same shape the reference client writes — the raw 32-byte public
2598
+ key and the private key, both BufferJSON:
2599
+
2600
+ ```json
2601
+ {"private":{"type":"Buffer","data":"…"},"public":{"type":"Buffer","data":"…"}}
2602
+ ```
2603
+
2604
+ **Keeping the server stocked.** Three things drive it, and all of them ask the
2605
+ server what it is actually holding rather than guessing from the local pool —
2606
+ the two drift apart, because the server spends a key on every bundle it hands
2607
+ out and none of that reaches this side.
2608
+
2609
+ | When | What it does |
2610
+ |---|---|
2611
+ | on login | asks the server for its count; **0** there means a full 812 goes up, anything low means a top-up of 5 |
2612
+ | when the server says it is running low | same check, same batch |
2613
+ | every 30 minutes | same check — for the session that stays up long enough to be drained without a word |
2614
+
2615
+ A batch is the next ids the server has not been sent, never the whole pool
2616
+ again, and only counts as sent once the server has acknowledged it — a rejected
2617
+ upload is retried four times with backoff and then left for the next check, with
2618
+ the same keys still queued. Ids are never reused, even after the key under one
2619
+ has been spent.
2620
+
2621
+ After the login upload the account also asks for the server's **key bundle
2622
+ digest** and compares the identity key and signed pre-key it is being served
2623
+ against the ones this session holds. A mismatch there is the failure that used
2624
+ to need the number registering again to clear.
2625
+
2626
+ None of this needs calling: `init()` and `connectWeb()` both do it, identically.
2627
+
2628
+ > [!NOTE]
2629
+ > **Sessions written before 5.14.19 keep every key they had.** Pre-keys used to
2630
+ > live inside `<phone>.signal.json`; on the first start they are moved out into
2631
+ > files of their own and the aggregate copy is dropped. The keys themselves do
2632
+ > not change, so the bundles the server has already handed out stay answerable.
2633
+
2569
2634
  ### Where the folder comes from
2570
2635
 
2571
2636
  | order | source |
@@ -2720,6 +2785,14 @@ Creates a fresh credential store for the given phone number — the key pairs, r
2720
2785
 
2721
2786
  It returns everything `createNewStore` does, plus a few fields kept for application code that expects them: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, `processedHistoryMessages` and `advSecretKey`. Those extras live on the object only — `saveStore` does not write them, so they are not there again after a reload. Nothing in the library reads them; treat them as a convenience, not as state.
2722
2787
 
2788
+ > [!NOTE]
2789
+ > The two pre-key counters the library actually runs on are the ones in the
2790
+ > `SignalStore` — `nextPreKeyId()` and `firstUnuploadedPreKeyId()`, persisted in
2791
+ > `<phone>.signal.json`. They live there rather than in the store because a
2792
+ > number's mobile half and its companion half have a `SignalStore` each and must
2793
+ > never share a pre-key id space. The fields above are the same names on a
2794
+ > different object and do not drive anything.
2795
+
2723
2796
  ```js
2724
2797
  const { initAuthCreds, saveStore } = require('whalibmob')
2725
2798
  const path = require('path')
package/lib/Client.js CHANGED
@@ -11,7 +11,7 @@ const { NoiseSocket } = require('./noise');
11
11
  const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
12
12
  const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assertRegistrationKeys, fetchIosVersion, fetchWaVersion } = require('./Registration');
13
13
  const { defaultBaseDir, sessionDirFor, isLegacyLayout,
14
- SESSION_SUFFIXES } = require('./SessionPaths');
14
+ preKeyFilesFor, SESSION_SUFFIXES } = require('./SessionPaths');
15
15
  const { getDeviceConfig } = require('./DeviceConfig');
16
16
  const { prepareProfilePicture, probeImageSize, canDecodeImage } = require('./MediaThumbnail');
17
17
  const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = require('./Store');
@@ -47,7 +47,8 @@ const { NodeCache } = require('@cacheable/node-cache');
47
47
 
48
48
  // Group metadata cache lifetime — matches DeviceManager's device cache.
49
49
  const GROUP_META_CACHE_TTL = '5m';
50
- const { SignalProtocol } = require('./signal/SignalProtocol');
50
+ const { SignalProtocol, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT } =
51
+ require('./signal/SignalProtocol');
51
52
  const { DeviceManager, jidStrToObj } = require('./DeviceManager');
52
53
  const {
53
54
  processHistorySyncNotification,
@@ -172,10 +173,10 @@ const PING_INTERVAL_MS = 25000;
172
173
  const KEEPALIVE_INTERVAL = 20000;
173
174
  const RECONNECT_BACKOFF = [1000, 2000, 4000, 8000, 15000, 30000];
174
175
 
175
- // When the server is down to this many one-time keys or fewer, send it more.
176
- // The number is small on purpose: the server tells us when it is running low,
177
- // and this is the floor for the case where that notice never arrives.
178
- const PRE_KEY_SERVER_MIN = 5;
176
+ // How many attempts an upload gets before it is left for the next check, and
177
+ // how long it waits between them.
178
+ const PRE_KEY_UPLOAD_TRIES = 4;
179
+ const PRE_KEY_UPLOAD_BACKOFF = (attempt) => Math.min(1000 * Math.pow(2, attempt), 10000);
179
180
 
180
181
  // How often to ask the server how many it has left. The drain is invisible from
181
182
  // here — every bundle handed out is a key gone, and none of that reaches this
@@ -1229,6 +1230,22 @@ class WhalibmobClient extends EventEmitter {
1229
1230
  fs.renameSync(from, to);
1230
1231
  moved.push(suffix);
1231
1232
  }
1233
+
1234
+ // The pre-key files carry the number in their names too, one per key.
1235
+ // preKeyFilesFor anchors on the digits, so that is what the new name
1236
+ // replaces — the `.web` half and the id after it are kept as they are.
1237
+ const currentDigits = current.replace(/\D/g, '');
1238
+ for (const name of preKeyFilesFor(fromDir, current)) {
1239
+ const renamed = canonical + name.slice(currentDigits.length);
1240
+ const to = path.join(fromDir, renamed);
1241
+ if (fs.existsSync(to)) {
1242
+ throw new Error('Cannot rename session: ' + renamed +
1243
+ ' already exists. Move it aside first.');
1244
+ }
1245
+ fs.renameSync(path.join(fromDir, name), to);
1246
+ moved.push(renamed);
1247
+ }
1248
+
1232
1249
  if (!moved.length) throw new Error('No session files found for ' + current);
1233
1250
 
1234
1251
  if (toDir !== fromDir) {
@@ -1306,7 +1323,10 @@ class WhalibmobClient extends EventEmitter {
1306
1323
  // bundle it is serving, which is the thing that goes quietly wrong and
1307
1324
  // takes the account's reachability with it.
1308
1325
  this._ensureServerPreKeys('login')
1309
- .then(() => this._verifyServerKeyBundle('login'));
1326
+ .then(() => this._verifyServerKeyBundle('login'))
1327
+ // Both swallow their own failures; this is only here so that a rejection
1328
+ // from somewhere neither expected cannot take the process down with it.
1329
+ .catch(err => _whaDbg('[DBG] PREKEY login check failed: ' + (err && err.message)));
1310
1330
 
1311
1331
  // Same telemetry ping the web client sends on every completed login.
1312
1332
  this._sendUnifiedSession();
@@ -3041,7 +3061,7 @@ class WhalibmobClient extends EventEmitter {
3041
3061
  // rotating index wraps back onto ids that are still in flight.
3042
3062
  let pkId = existing ? existing.pkId : null;
3043
3063
  if (pkId === null && this._signal) {
3044
- const allKeys = this._signal.getPreKeysForUpload(800);
3064
+ const allKeys = this._signal.getPreKeysForUpload(INITIAL_PREKEY_COUNT);
3045
3065
  const free = allKeys.find(k => !this._retryAdvertisedPreKeys.has(k.keyId));
3046
3066
  // If every key is already spoken for, fall back to rotating rather than
3047
3067
  // sending no bundle at all — a stale id beats no retry.
@@ -3069,7 +3089,7 @@ class WhalibmobClient extends EventEmitter {
3069
3089
  try {
3070
3090
  const spk = this._signal.getSignedPreKeyForUpload();
3071
3091
  const identKey = this._signal.getIdentityKey();
3072
- const allPreKeys = this._signal.getPreKeysForUpload(800);
3092
+ const allPreKeys = this._signal.getPreKeysForUpload(INITIAL_PREKEY_COUNT);
3073
3093
  const pk = allPreKeys.find(k => k.keyId === pkId) || allPreKeys[0];
3074
3094
 
3075
3095
  if (spk && identKey && pk) {
@@ -3527,39 +3547,81 @@ class WhalibmobClient extends EventEmitter {
3527
3547
 
3528
3548
  // ─── Pre-key upload ───────────────────────────────────────────────────────
3529
3549
 
3530
- _uploadPreKeys() {
3531
- if (!this._signal || !this._socket || !this._connected) return;
3550
+ // Send the server a batch of one-time keys, plus the identity and signed
3551
+ // pre-key that go with them.
3552
+ //
3553
+ // The batch is the next `count` ids the server has not been sent — not the
3554
+ // whole local pool, which is what this used to send. Eight hundred keys went
3555
+ // up on every login and every top-up, the same eight hundred each time, and
3556
+ // the ids the server had already been given were sent again while the ones
3557
+ // generated since were not.
3558
+ //
3559
+ // Awaited, and retried, because it is the one message whose loss is silent:
3560
+ // an upload that never lands leaves the server with nothing to hand out and
3561
+ // the account unreachable, with no error anywhere to say so.
3562
+ //
3563
+ // @returns {Promise<boolean>} whether the server acknowledged it
3564
+ async _uploadPreKeys(count) {
3565
+ if (!this._signal || !this._socket || !this._connected) return false;
3566
+ count = count > 0 ? count : MIN_PREKEY_COUNT;
3532
3567
 
3533
- const preKeys = this._signal.getPreKeysForUpload(800);
3568
+ const { preKeys, update } = this._signal.getNextPreKeys(count);
3534
3569
  const spk = this._signal.getSignedPreKeyForUpload();
3535
3570
  const identKey = this._signal.getIdentityKey();
3536
- if (!preKeys.length || !spk) return;
3537
-
3538
- const preKeyNodes = preKeys.map(pk => new BinaryNode('key', {}, [
3539
- new BinaryNode('id', {}, intToBytes(pk.keyId, 3)),
3540
- new BinaryNode('value', {}, stripKeyPrefix(pk.pubKey))
3541
- ]));
3571
+ if (!preKeys.length || !spk) return false;
3542
3572
 
3543
- const skNode = new BinaryNode('skey', {}, [
3544
- new BinaryNode('id', {}, intToBytes(spk.keyId, 3)),
3545
- new BinaryNode('value', {}, stripKeyPrefix(spk.keyPair.pubKey)),
3546
- new BinaryNode('signature', {}, spk.signature)
3547
- ]);
3548
-
3549
- const pkId = this._genMsgId();
3550
- _whaDbg('[DBG] SEND uploadPreKeys IQ id=' + pkId);
3551
- this._socket.sendNode(new BinaryNode('iq', {
3552
- id: pkId,
3553
- to: 's.whatsapp.net',
3554
- type: 'set',
3555
- xmlns: 'encrypt'
3556
- }, [
3573
+ const children = [
3557
3574
  new BinaryNode('registration', {}, intToBytes(this._store.registrationId, 4)),
3558
3575
  new BinaryNode('type', {}, Buffer.from([5])),
3559
3576
  new BinaryNode('identity', {}, stripKeyPrefix(identKey)),
3560
- new BinaryNode('list', {}, preKeyNodes),
3561
- skNode
3562
- ]));
3577
+ new BinaryNode('list', {}, preKeys.map(pk => new BinaryNode('key', {}, [
3578
+ new BinaryNode('id', {}, intToBytes(pk.keyId, 3)),
3579
+ new BinaryNode('value', {}, stripKeyPrefix(pk.pubKey))
3580
+ ]))),
3581
+ new BinaryNode('skey', {}, [
3582
+ new BinaryNode('id', {}, intToBytes(spk.keyId, 3)),
3583
+ new BinaryNode('value', {}, stripKeyPrefix(spk.keyPair.pubKey)),
3584
+ new BinaryNode('signature', {}, spk.signature)
3585
+ ])
3586
+ ];
3587
+
3588
+ for (let attempt = 0; attempt < PRE_KEY_UPLOAD_TRIES; attempt++) {
3589
+ if (!this._socket || !this._connected) break;
3590
+
3591
+ const pkId = this._genMsgId();
3592
+ _whaDbg('[DBG] SEND uploadPreKeys IQ id=' + pkId + ' count=' + preKeys.length +
3593
+ ' ids=' + preKeys[0].keyId + '..' + preKeys[preKeys.length - 1].keyId +
3594
+ (attempt ? ' (retry ' + attempt + ')' : ''));
3595
+
3596
+ let resp = null;
3597
+ try {
3598
+ resp = await this._sendIq(new BinaryNode('iq', {
3599
+ id: pkId,
3600
+ to: 's.whatsapp.net',
3601
+ type: 'set',
3602
+ xmlns: 'encrypt'
3603
+ }, children));
3604
+ } catch (err) {
3605
+ _whaDbg('[DBG] PREKEY upload send failed: ' + (err && err.message));
3606
+ }
3607
+
3608
+ if (resp && !(resp.attrs && resp.attrs.type === 'error')) {
3609
+ // Only now. The keys themselves were spent the moment they were
3610
+ // generated — an id is never handed out twice — but a batch the server
3611
+ // did not take is a batch that still has to go, so the range stays
3612
+ // unuploaded until it is acknowledged.
3613
+ this._signal.commitPreKeyUpload(update);
3614
+ _whaDbg('[DBG] PREKEY uploaded ' + preKeys.length + ' key(s)');
3615
+ return true;
3616
+ }
3617
+
3618
+ if (attempt < PRE_KEY_UPLOAD_TRIES - 1) {
3619
+ await new Promise(r => setTimeout(r, PRE_KEY_UPLOAD_BACKOFF(attempt)));
3620
+ }
3621
+ }
3622
+
3623
+ _whaDbg('[DBG] PREKEY upload gave up after ' + PRE_KEY_UPLOAD_TRIES + ' attempt(s)');
3624
+ return false;
3563
3625
  }
3564
3626
 
3565
3627
  // How many one-time keys the server is still holding for us.
@@ -3671,8 +3733,7 @@ class WhalibmobClient extends EventEmitter {
3671
3733
 
3672
3734
  if (digest.missing) {
3673
3735
  _whaDbg('[DBG] KEYBUNDLE server returned no digest (' + reason + ') — re-uploading');
3674
- this._replenishLocalPreKeys();
3675
- this._uploadPreKeys();
3736
+ await this._uploadPreKeys(INITIAL_PREKEY_COUNT);
3676
3737
  return;
3677
3738
  }
3678
3739
 
@@ -3705,8 +3766,10 @@ class WhalibmobClient extends EventEmitter {
3705
3766
 
3706
3767
  _whaDbg('[DBG] KEYBUNDLE server disagrees on ' + mismatches.join(' and ') +
3707
3768
  ' (' + reason + ') — re-uploading');
3708
- this._replenishLocalPreKeys();
3709
- this._uploadPreKeys();
3769
+ // What is wrong here is the identity or the signed pre-key, and both ride
3770
+ // along with any batch, however small. The one-time pool was settled a
3771
+ // moment ago by _ensureServerPreKeys.
3772
+ await this._uploadPreKeys(MIN_PREKEY_COUNT);
3710
3773
  } catch (err) {
3711
3774
  _whaDbg('[DBG] KEYBUNDLE check failed (' + reason + '): ' + (err && err.message));
3712
3775
  }
@@ -3726,33 +3789,48 @@ class WhalibmobClient extends EventEmitter {
3726
3789
  if (!this._signal) return;
3727
3790
  try {
3728
3791
  const serverCount = await this._queryServerPreKeyCount();
3792
+
3793
+ // How big a batch this warrants. A server holding none of ours has either
3794
+ // never had them or has handed every one out, and either way the account
3795
+ // is unreachable until a full pool is back up there; anything else is a
3796
+ // top-up. A session that has never uploaded counts as the first case even
3797
+ // when the server says it has some, because whatever it is holding did
3798
+ // not come from here.
3799
+ const neverUploaded = this._signal.firstUnuploadedPreKeyId() <= 1;
3800
+ const count = (serverCount === 0 || neverUploaded)
3801
+ ? INITIAL_PREKEY_COUNT : MIN_PREKEY_COUNT;
3802
+
3729
3803
  if (serverCount === null) {
3730
- // No usable answer. Fall back to the old behaviour — an upload we may
3731
- // not have needed costs nothing next to an account nobody can reach.
3804
+ // No usable answer. Upload anyway — one we may not have needed costs
3805
+ // nothing next to an account nobody can reach.
3732
3806
  _whaDbg('[DBG] PREKEY server count unavailable (' + reason + ') — uploading anyway');
3733
- this._replenishLocalPreKeys();
3734
- this._uploadPreKeys();
3807
+ await this._uploadPreKeys(count);
3735
3808
  return;
3736
3809
  }
3737
3810
 
3738
- _whaDbg('[DBG] PREKEY server holds ' + serverCount + ' (' + reason + ')');
3739
- if (serverCount > PRE_KEY_SERVER_MIN) return;
3811
+ // The newest key we generated. If it is not in the pool the store has
3812
+ // lost keys the server may still be serving, and the bundle needs
3813
+ // rebuilding whatever the count says.
3814
+ const currentId = this._signal.currentPreKeyId();
3815
+ const missingCurrent = currentId > 0 && !this._signal.hasPreKey(currentId);
3816
+ const lowServer = serverCount <= count;
3817
+
3818
+ _whaDbg('[DBG] PREKEY server holds ' + serverCount + ' (' + reason +
3819
+ '), current id ' + currentId + (missingCurrent ? ' MISSING' : ' present'));
3740
3820
 
3741
- _whaDbg('[DBG] PREKEY server low (' + serverCount + ' <= ' + PRE_KEY_SERVER_MIN +
3742
- ') — uploading');
3743
- this._replenishLocalPreKeys();
3744
- this._uploadPreKeys();
3821
+ if (!lowServer && !missingCurrent) return;
3822
+
3823
+ const why = [];
3824
+ if (lowServer) why.push('server count low (' + serverCount + ')');
3825
+ if (missingCurrent) why.push('current pre-key ' + currentId + ' missing from storage');
3826
+ _whaDbg('[DBG] PREKEY uploading ' + count + ' due to: ' + why.join(', '));
3827
+
3828
+ await this._uploadPreKeys(count);
3745
3829
  } catch (err) {
3746
3830
  _whaDbg('[DBG] PREKEY check failed (' + reason + '): ' + (err && err.message));
3747
3831
  }
3748
3832
  }
3749
3833
 
3750
- // Make sure there is something to upload before uploading.
3751
- _replenishLocalPreKeys() {
3752
- if (!this._signal) return;
3753
- if (this._signal.preKeyCount() < 20) this._signal.replenishPreKeys();
3754
- }
3755
-
3756
3834
  _checkAndReplenishPreKeys() {
3757
3835
  if (!this._signal) return;
3758
3836
  // The server said it is running low, so ask it how low and act on that
@@ -67,6 +67,28 @@ const SESSION_SUFFIXES = [
67
67
  '.web.appStateKeys.json'
68
68
  ];
69
69
 
70
+ // The one-time pre-keys are the exception to the list above. There are 812 of
71
+ // them and they are a file each — <phone>.pre-key-<id>.json for the number
72
+ // itself, <phone>.web.pre-key-<id>.json for its companion half — so they cannot
73
+ // be enumerated by suffix and have to be found by name instead.
74
+ const PRE_KEY_FILE_RE = (phone) =>
75
+ new RegExp('^' + String(phone).replace(/\D/g, '') + '(\\.web)?\\.pre-key-\\d+\\.json$');
76
+
77
+ /**
78
+ * Every pre-key file a number owns in a directory, by name.
79
+ *
80
+ * Anything that moves a session has to move these too: a number whose pre-keys
81
+ * are left behind still has its Signal sessions and looks perfectly healthy
82
+ * until somebody tries to open a new one with it, and then the pkmsg arrives
83
+ * naming a key that is not there any more.
84
+ */
85
+ function preKeyFilesFor(dir, phone) {
86
+ const re = PRE_KEY_FILE_RE(phone);
87
+ let entries;
88
+ try { entries = fs.readdirSync(dir); } catch (_) { return []; }
89
+ return entries.filter(name => re.test(name));
90
+ }
91
+
70
92
  // Files that belong to the installation rather than to any number, and stay at
71
93
  // the top of the base directory.
72
94
  const SHARED_FILES = [
@@ -202,6 +224,13 @@ function migrateSession(baseDir, phone) {
202
224
  moved.push(phone + suffix);
203
225
  }
204
226
 
227
+ for (const name of preKeyFilesFor(baseDir, phone)) {
228
+ const dest = path.join(to, name);
229
+ if (_exists(dest)) { skipped.push(name); continue; }
230
+ fs.renameSync(path.join(baseDir, name), dest);
231
+ moved.push(name);
232
+ }
233
+
205
234
  return { phone, from: baseDir, to, moved, skipped };
206
235
  }
207
236
 
@@ -213,6 +242,7 @@ module.exports = {
213
242
  webStoreFileFor,
214
243
  listSessions,
215
244
  migrateSession,
245
+ preKeyFilesFor,
216
246
  SESSION_SUFFIXES,
217
247
  SHARED_FILES
218
248
  };
@@ -8,9 +8,22 @@ const { GroupCipher, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, Sender
8
8
 
9
9
  const { SessionCipher, SessionBuilder, ProtocolAddress, keyhelper } = libsignal;
10
10
 
11
- const PRE_KEY_COUNT = 800;
12
- const PRE_KEY_MIN = 50;
13
- const PRE_KEY_START = 1;
11
+ // The pre-key pool, sized the way the reference client sizes it.
12
+ //
13
+ // INITIAL_PREKEY_COUNT what a fresh account puts on the server, and what it
14
+ // puts back when the server turns out to be holding
15
+ // none of ours
16
+ // MIN_PREKEY_COUNT the routine top-up, sent when the server says it is
17
+ // running low
18
+ //
19
+ // The pool is generated up front rather than on demand, so the authentication
20
+ // folder holds all 812 keys from the moment the session exists.
21
+ const INITIAL_PREKEY_COUNT = 812;
22
+ const MIN_PREKEY_COUNT = 5;
23
+
24
+ // Below this many keys left, a session that is starting up refills to the full
25
+ // pool rather than waiting for the server to ask.
26
+ const PRE_KEY_LOW_WATER = 50;
14
27
 
15
28
  // ─── Per-JID async mutex ────────────
16
29
  // Prevents race conditions when two messages are encrypted simultaneously
@@ -128,17 +141,22 @@ class SignalProtocol {
128
141
  }
129
142
 
130
143
  _ensurePreKeys() {
131
- if (this.store.preKeyCount() >= PRE_KEY_MIN) return;
144
+ if (this.store.preKeyCount() >= PRE_KEY_LOW_WATER) return;
132
145
  this.replenishPreKeys();
133
146
  }
134
147
 
148
+ /** Top the local pool back up to INITIAL_PREKEY_COUNT. */
135
149
  replenishPreKeys() {
136
- const have = this.store.preKeyCount();
137
- const need = PRE_KEY_COUNT - have;
150
+ const need = INITIAL_PREKEY_COUNT - this.store.preKeyCount();
138
151
  if (need <= 0) return [];
152
+ return this.generatePreKeys(need);
153
+ }
154
+
155
+ /** Generate `count` fresh pre-keys, continuing the id sequence. */
156
+ generatePreKeys(count) {
139
157
  const startId = this.store.nextPreKeyId();
140
158
  const newKeys = [];
141
- for (let i = 0; i < need; i++) {
159
+ for (let i = 0; i < count; i++) {
142
160
  const kp = keyhelper.generatePreKey(startId + i);
143
161
  this.store.storePreKey(kp.keyId, kp.keyPair);
144
162
  newKeys.push({ keyId: kp.keyId, pubKey: kp.keyPair.pubKey });
@@ -150,10 +168,78 @@ class SignalProtocol {
150
168
  return this.store.preKeyCount();
151
169
  }
152
170
 
171
+ /** The id of the newest pre-key generated, 0 when none ever was. */
172
+ currentPreKeyId() {
173
+ return this.store.nextPreKeyId() - 1;
174
+ }
175
+
176
+ hasPreKey(keyId) {
177
+ return this.store.hasPreKey(keyId);
178
+ }
179
+
180
+ firstUnuploadedPreKeyId() {
181
+ return this.store.firstUnuploadedPreKeyId();
182
+ }
183
+
184
+ /**
185
+ * The next batch to send the server, generating whatever is missing to make
186
+ * `count` of them — the same arithmetic the reference client does.
187
+ *
188
+ * `firstUnuploadedPreKeyId` is the lowest id the server has not been sent, so
189
+ * the batch is the `count` ids from there up. Whatever of those has already
190
+ * been generated is reused; the rest is made now, continuing from
191
+ * `nextPreKeyId`. That is what stops an account from re-sending its whole
192
+ * pool on every top-up — which is what this used to do, eight hundred keys at
193
+ * a time, several times a session.
194
+ *
195
+ * The returned `update` is not applied here. Ids are spent the moment they
196
+ * are generated (storePreKey advances nextPreKeyId), but a batch only counts
197
+ * as uploaded once the server has acknowledged it — see commitPreKeyUpload().
198
+ *
199
+ * @returns {{preKeys: Array<{keyId, pubKey}>, update: {firstUnuploadedPreKeyId: number}}}
200
+ */
201
+ getNextPreKeys(count) {
202
+ count = count > 0 ? Math.floor(count) : MIN_PREKEY_COUNT;
203
+
204
+ const first = this.store.firstUnuploadedPreKeyId();
205
+ const available = this.store.nextPreKeyId() - first;
206
+ const remaining = count - available;
207
+ if (remaining > 0) this.generatePreKeys(remaining);
208
+
209
+ const last = first + count - 1;
210
+ const preKeys = [];
211
+ for (let id = first; id <= last; id++) {
212
+ const raw = this.store._preKeys[String(id)];
213
+ // An id in the range whose key has already been consumed. The reference
214
+ // client would put an undefined key on the wire here; skip it instead.
215
+ if (!raw) continue;
216
+ preKeys.push({ keyId: id, pubKey: Buffer.from(raw.pubKey, 'base64') });
217
+ }
218
+
219
+ return { preKeys, update: { firstUnuploadedPreKeyId: last + 1 } };
220
+ }
221
+
222
+ /** Record that the batch getNextPreKeys() returned reached the server. */
223
+ commitPreKeyUpload(update) {
224
+ if (!update) return;
225
+ this.store.setFirstUnuploadedPreKeyId(update.firstUnuploadedPreKeyId);
226
+ }
227
+
228
+ /**
229
+ * Every pre-key still in the pool, oldest id first.
230
+ *
231
+ * Used where a single key has to be picked out by id — the bundle attached to
232
+ * a retry receipt — rather than for uploads, which go through
233
+ * getNextPreKeys().
234
+ */
153
235
  getPreKeysForUpload(max) {
154
- max = max || PRE_KEY_COUNT;
236
+ max = max || INITIAL_PREKEY_COUNT;
237
+ const ids = Object.keys(this.store._preKeys)
238
+ .map(Number)
239
+ .filter(n => Number.isFinite(n))
240
+ .sort((a, b) => a - b);
241
+
155
242
  const preKeys = [];
156
- const ids = Object.keys(this.store._preKeys).map(Number);
157
243
  for (const id of ids.slice(0, max)) {
158
244
  const raw = this.store._preKeys[String(id)];
159
245
  preKeys.push({
@@ -362,4 +448,7 @@ function jidToAddress(jid) {
362
448
  return new ProtocolAddress(user, deviceId);
363
449
  }
364
450
 
365
- module.exports = { SignalProtocol, jidToAddress, pad, unpad };
451
+ module.exports = {
452
+ SignalProtocol, jidToAddress, pad, unpad,
453
+ INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT
454
+ };
@@ -38,6 +38,91 @@ const { SessionRecord } = require('./libsignal');
38
38
 
39
39
  const DEBOUNCE_MS = 300;
40
40
 
41
+ // ─── Pre-key files ────────────────────────────────────────────────────────────
42
+ //
43
+ // One-time pre-keys do not live in the snapshot above. They get a file each,
44
+ // named the way Baileys names them:
45
+ //
46
+ // <base>/40756469325/40756469325.pre-key-1.json
47
+ // ...
48
+ // <base>/40756469325/40756469325.pre-key-812.json
49
+ //
50
+ // Everything else about a pre-key is small, but there are 812 of them
51
+ // and every one that is consumed rewrites the whole store — so keeping them in
52
+ // the aggregate file meant a ~95 KB write on each incoming pkmsg, and made the
53
+ // pool something you could only inspect by parsing the file. A file each is
54
+ // what the reference client does, it is what anyone comparing the two
55
+ // authentication folders expects to see, and a write now touches one key.
56
+ //
57
+ // The stem in front of `.pre-key-` is the one the .signal.json file already
58
+ // carries, so a number's mobile half (40756469325.pre-key-N.json) and its
59
+ // companion half (40756469325.web.pre-key-N.json) keep separate pools, as they
60
+ // must — and a session still sitting in the old flat layout, where every
61
+ // number shares one directory, does not collide with its neighbours either.
62
+ //
63
+ // A pre-key on disk is byte-for-byte what Baileys writes: BufferJSON, the raw
64
+ // 32-byte public key with no type prefix.
65
+ //
66
+ // {"private":{"type":"Buffer","data":"…"},"public":{"type":"Buffer","data":"…"}}
67
+ //
68
+ // In memory whalibmob carries the 33-byte 0x05-prefixed public key libsignal
69
+ // hands out, so the prefix comes off on the way to disk and goes back on on the
70
+ // way in.
71
+ //
72
+ // Unlike the snapshot these are written straight, without the temp-file dance.
73
+ // The blast radius of a torn write is one key: it fails to parse at load, is
74
+ // skipped, and the pool is topped back up. The snapshot cannot be treated that
75
+ // way because losing it loses every session the account has.
76
+
77
+ const PRE_KEY_INFIX = '.pre-key-';
78
+ const SIGNAL_KEY_PREFIX = 0x05;
79
+
80
+ // How many pre-key files may be in flight at once. Eight hundred parallel
81
+ // writes is a reliable way to meet EMFILE.
82
+ const PRE_KEY_WRITE_CONCURRENCY = 32;
83
+
84
+ function _escapeRe(s) {
85
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
+ }
87
+
88
+ // A BufferJSON field, in any of the shapes it is written in.
89
+ function _bufFrom(field) {
90
+ if (!field) return null;
91
+ if (Buffer.isBuffer(field)) return field;
92
+ if (typeof field === 'string') return Buffer.from(field, 'base64');
93
+ if (field.type === 'Buffer') {
94
+ return Array.isArray(field.data)
95
+ ? Buffer.from(field.data)
96
+ : Buffer.from(String(field.data), 'base64');
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function _encodePreKeyFile(entry) {
102
+ const pub = Buffer.from(entry.pubKey, 'base64');
103
+ const raw = (pub.length === 33 && pub[0] === SIGNAL_KEY_PREFIX) ? pub.slice(1) : pub;
104
+ return JSON.stringify({
105
+ private: { type: 'Buffer', data: entry.privKey },
106
+ public: { type: 'Buffer', data: raw.toString('base64') }
107
+ });
108
+ }
109
+
110
+ function _decodePreKeyFile(text) {
111
+ const obj = JSON.parse(text);
112
+ const priv = _bufFrom(obj && obj.private);
113
+ let pub = _bufFrom(obj && obj.public);
114
+ if (!priv || !pub || priv.length !== 32) return null;
115
+ if (pub.length === 32) pub = Buffer.concat([Buffer.from([SIGNAL_KEY_PREFIX]), pub]);
116
+ if (pub.length !== 33) return null;
117
+ return { pubKey: pub.toString('base64'), privKey: priv.toString('base64') };
118
+ }
119
+
120
+ // A stored counter, or 1 when the file predates it or holds nonsense.
121
+ function _counter(value) {
122
+ const n = Number(value);
123
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
124
+ }
125
+
41
126
  class SignalStore {
42
127
  constructor() {
43
128
  this._identityKeyPair = null;
@@ -68,6 +153,30 @@ class SignalStore {
68
153
  this._writing = false;
69
154
  this._writeAgain = false;
70
155
 
156
+ // Where the pre-key files go, and which ids are waiting to be written or
157
+ // unlinked. Empty until attachFile() — a store with nowhere to write keeps
158
+ // its pre-keys in memory and queues nothing.
159
+ this._pkDir = null;
160
+ this._pkPrefix = null;
161
+ this._pkPending = new Set();
162
+ this._pkTimer = null;
163
+ this._pkWriting = false;
164
+
165
+ // The two counters Baileys keeps in creds. They live here instead because a
166
+ // number's mobile and companion halves have a .signal.json each and must
167
+ // never share a pre-key id space.
168
+ //
169
+ // _nextPreKeyId the id the next generated key takes
170
+ // _firstUnuploadedPreKeyId the lowest id the server has not been sent
171
+ //
172
+ // Both only ever go up. An id that has been handed to the server is spent
173
+ // even after the key itself is consumed and deleted, so deriving the next
174
+ // id from what is still on disk — which is what this used to do — walks
175
+ // backwards over ids the server is still serving and hands two different
176
+ // keys out under one id.
177
+ this._nextPreKeyId = 1;
178
+ this._firstUnuploadedPreKeyId = 1;
179
+
71
180
  // Flush synchronously on exit so no state is ever lost.
72
181
  //
73
182
  // Kept as references so they can be taken off again: a store that has been
@@ -98,6 +207,12 @@ class SignalStore {
98
207
  const stale = this._filePath ? [this._tmpPath(true), this._tmpPath(false)] : [];
99
208
  this._filePath = null;
100
209
  this._writeAgain = false;
210
+ // Same for the pre-key files: _flushSync() above has written whatever was
211
+ // outstanding, so let the directory go before the caller moves it.
212
+ if (this._pkTimer) { clearTimeout(this._pkTimer); this._pkTimer = null; }
213
+ this._pkDir = null;
214
+ this._pkPrefix = null;
215
+ this._pkPending.clear();
101
216
  for (const f of stale) { try { fs.unlinkSync(f); } catch (_) {} }
102
217
  }
103
218
 
@@ -108,19 +223,90 @@ class SignalStore {
108
223
 
109
224
  attachFile(filePath) {
110
225
  this._filePath = filePath;
226
+ // The pre-key files sit beside the snapshot and share its stem, so
227
+ // 40756469325.signal.json gives 40756469325.pre-key-<id>.json and
228
+ // 40756469325.web.signal.json gives 40756469325.web.pre-key-<id>.json.
229
+ const base = path.basename(filePath);
230
+ this._pkDir = path.dirname(filePath);
231
+ this._pkPrefix = base.endsWith('.signal.json')
232
+ ? base.slice(0, -'.signal.json'.length)
233
+ : base.replace(/\.json$/, '');
111
234
  this._load();
112
235
  }
113
236
 
237
+ /** Where a pre-key of this id belongs on disk. */
238
+ _preKeyFile(id) {
239
+ return path.join(this._pkDir, this._pkPrefix + PRE_KEY_INFIX + id + '.json');
240
+ }
241
+
242
+ // Read every pre-key file this session owns. Returns how many were read.
243
+ //
244
+ // A file that will not parse is skipped rather than fatal: it costs one key
245
+ // out of 812, and the pool is topped back up on the next check.
246
+ _loadPreKeyFiles() {
247
+ if (!this._pkDir) return 0;
248
+ let entries;
249
+ try { entries = fs.readdirSync(this._pkDir); } catch (_) { return 0; }
250
+
251
+ const re = new RegExp('^' + _escapeRe(this._pkPrefix + PRE_KEY_INFIX) + '(\\d+)\\.json$');
252
+ let count = 0;
253
+ for (const name of entries) {
254
+ const m = re.exec(name);
255
+ if (!m) continue;
256
+ try {
257
+ const entry = _decodePreKeyFile(
258
+ fs.readFileSync(path.join(this._pkDir, name), 'utf8'));
259
+ if (entry) { this._preKeys[m[1]] = entry; count++; }
260
+ } catch (_) {}
261
+ }
262
+ return count;
263
+ }
264
+
114
265
  _load() {
115
- if (!this._filePath || !fs.existsSync(this._filePath)) return;
116
- try {
117
- const raw = JSON.parse(fs.readFileSync(this._filePath, 'utf8'));
266
+ this._preKeys = {};
267
+
268
+ let raw = null;
269
+ if (this._filePath && fs.existsSync(this._filePath)) {
270
+ try { raw = JSON.parse(fs.readFileSync(this._filePath, 'utf8')); } catch (_) {}
271
+ }
272
+ if (raw) {
118
273
  this._sessions = raw.sessions || {};
119
- this._preKeys = raw.preKeys || {};
120
274
  this._signedPreKeys = raw.signedPreKeys || {};
121
275
  this._identities = raw.identities || {};
122
276
  this._lidMappings = raw.lidMappings || {}; // phone → lid, persisted
123
- } catch (_) {}
277
+ this._nextPreKeyId = _counter(raw.nextPreKeyId);
278
+ this._firstUnuploadedPreKeyId = _counter(raw.firstUnuploadedPreKeyId);
279
+ }
280
+
281
+ // Files first — they are where a pre-key lives now.
282
+ this._loadPreKeyFiles();
283
+
284
+ // Anything still only in the old aggregate block moves out to a file of its
285
+ // own. A session written before this change has its whole pool in there and
286
+ // the server is serving those exact keys, so dropping them would leave
287
+ // every bundle already handed out unanswerable.
288
+ let migrated = 0;
289
+ const legacy = (raw && raw.preKeys) || {};
290
+ for (const id of Object.keys(legacy)) {
291
+ if (this._preKeys[id]) continue;
292
+ const entry = legacy[id];
293
+ if (!entry || !entry.pubKey || !entry.privKey) continue;
294
+ this._preKeys[id] = entry;
295
+ this._pkPending.add(id);
296
+ migrated++;
297
+ }
298
+ if (migrated) {
299
+ this._flushPreKeysSync();
300
+ this._save(); // rewrites the snapshot without the old block
301
+ }
302
+
303
+ // An id on disk is an id that has been used, whatever the counter says.
304
+ const ids = Object.keys(this._preKeys).map(Number).filter(n => Number.isFinite(n) && n > 0);
305
+ const maxId = ids.length ? Math.max(...ids) : 0;
306
+ if (this._nextPreKeyId <= maxId) this._nextPreKeyId = maxId + 1;
307
+ if (this._firstUnuploadedPreKeyId > this._nextPreKeyId) {
308
+ this._firstUnuploadedPreKeyId = this._nextPreKeyId;
309
+ }
124
310
  }
125
311
 
126
312
  // Persist a phone ↔ LID mapping. Called any time we learn a new mapping from
@@ -138,16 +324,96 @@ class SignalStore {
138
324
  }
139
325
 
140
326
  // Everything that is persisted, as it goes on disk.
327
+ //
328
+ // One-time pre-keys are not in here — they are a file each, see _preKeyFile().
329
+ // Their two counters are, because they have to survive a pool that has been
330
+ // drained to nothing and say what id comes next anyway.
141
331
  _snapshot() {
142
332
  return JSON.stringify({
143
333
  sessions: this._sessions,
144
- preKeys: this._preKeys,
145
334
  signedPreKeys: this._signedPreKeys,
146
335
  identities: this._identities,
147
- lidMappings: this._lidMappings
336
+ lidMappings: this._lidMappings,
337
+ nextPreKeyId: this._nextPreKeyId,
338
+ firstUnuploadedPreKeyId: this._firstUnuploadedPreKeyId
148
339
  });
149
340
  }
150
341
 
342
+ // ─── Pre-key writing ──────────────────────────────────────────────────────
343
+
344
+ // Mark one pre-key id as needing its file written (or removed, when the key
345
+ // is no longer in memory) and arm the debounce.
346
+ _savePreKey(id) {
347
+ if (!this._pkDir) return;
348
+ this._pkPending.add(String(id));
349
+ if (this._pkTimer) clearTimeout(this._pkTimer);
350
+ this._pkTimer = setTimeout(() => {
351
+ this._pkTimer = null;
352
+ this._flushPreKeysAsync();
353
+ }, DEBOUNCE_MS);
354
+ }
355
+
356
+ _flushPreKeysAsync() {
357
+ if (!this._pkDir || this._pkWriting || !this._pkPending.size) return;
358
+ try { fs.mkdirSync(this._pkDir, { recursive: true }); } catch (_) { return; }
359
+
360
+ this._pkWriting = true;
361
+ const ids = [...this._pkPending];
362
+ this._pkPending.clear();
363
+
364
+ let next = 0, active = 0, done = false;
365
+ const pump = () => {
366
+ while (active < PRE_KEY_WRITE_CONCURRENCY && next < ids.length) {
367
+ active++;
368
+ this._writePreKeyAsync(ids[next++], () => { active--; pump(); });
369
+ }
370
+ if (!active && next >= ids.length && !done) {
371
+ done = true;
372
+ this._pkWriting = false;
373
+ // Something came due while this batch was running — and cleared the
374
+ // debounce that would have picked it up, since _savePreKey only arms a
375
+ // timer. Arm one now.
376
+ if (this._pkPending.size) this._savePreKey([...this._pkPending][0]);
377
+ }
378
+ };
379
+ pump();
380
+ }
381
+
382
+ _writePreKeyAsync(id, cb) {
383
+ if (!this._pkDir) return cb();
384
+ const file = this._preKeyFile(id);
385
+ const entry = this._preKeys[id];
386
+ if (!entry) return fs.unlink(file, () => cb());
387
+
388
+ let data;
389
+ try { data = _encodePreKeyFile(entry); } catch (_) { return cb(); }
390
+ fs.writeFile(file, data, 'utf8', () => {
391
+ // Consumed while the write was in flight. A key that has been used is one
392
+ // the account must not offer again, so take it back off the disk rather
393
+ // than leave the write that overtook the unlink standing.
394
+ if (!this._preKeys[id]) return fs.unlink(file, () => cb());
395
+ cb();
396
+ });
397
+ }
398
+
399
+ _flushPreKeysSync() {
400
+ if (!this._pkDir || !this._pkPending.size) return;
401
+ if (this._pkTimer) { clearTimeout(this._pkTimer); this._pkTimer = null; }
402
+
403
+ const ids = [...this._pkPending];
404
+ this._pkPending.clear();
405
+ try { fs.mkdirSync(this._pkDir, { recursive: true }); } catch (_) { return; }
406
+
407
+ for (const id of ids) {
408
+ const file = this._preKeyFile(id);
409
+ const entry = this._preKeys[id];
410
+ try {
411
+ if (entry) fs.writeFileSync(file, _encodePreKeyFile(entry), 'utf8');
412
+ else fs.unlinkSync(file);
413
+ } catch (_) {}
414
+ }
415
+ }
416
+
151
417
  // Where a write assembles the file before it becomes the file. The sync and
152
418
  // async writers get separate names so the one can never find the other's
153
419
  // half-written temp file under it.
@@ -166,6 +432,9 @@ class SignalStore {
166
432
 
167
433
  // Immediate synchronous flush (process exit, and detach())
168
434
  _flushSync() {
435
+ // Pre-keys have their own dirty set — a pool that changed while the
436
+ // snapshot did not still has files owed to it.
437
+ this._flushPreKeysSync();
169
438
  if (!this._dirty || !this._filePath) return;
170
439
  if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
171
440
  const rev = this._rev;
@@ -378,16 +647,20 @@ class SignalStore {
378
647
  }
379
648
 
380
649
  async storePreKey(keyId, keyPair) {
381
- this._preKeys[String(keyId)] = {
650
+ const id = String(keyId);
651
+ this._preKeys[id] = {
382
652
  pubKey: Buffer.from(keyPair.pubKey).toString('base64'),
383
653
  privKey: Buffer.from(keyPair.privKey).toString('base64')
384
654
  };
385
- this._save();
655
+ this._savePreKey(id);
656
+ // Storing an id spends it, whether it was generated here or restored from
657
+ // somewhere else.
658
+ this.setNextPreKeyId(Number(keyId) + 1);
386
659
  }
387
660
 
388
661
  async removePreKey(keyId) {
389
662
  delete this._preKeys[String(keyId)];
390
- this._save();
663
+ this._savePreKey(keyId);
391
664
  }
392
665
 
393
666
  // ─── Signed PreKeys ───────────────────────────────────────────────────────
@@ -420,9 +693,36 @@ class SignalStore {
420
693
  return Object.keys(this._preKeys).length;
421
694
  }
422
695
 
696
+ /** Whether this id is still in the pool — i.e. nobody has spent it yet. */
697
+ hasPreKey(keyId) {
698
+ return Object.prototype.hasOwnProperty.call(this._preKeys, String(keyId));
699
+ }
700
+
701
+ /** The id the next generated pre-key takes. Never goes backwards. */
423
702
  nextPreKeyId() {
424
- const ids = Object.keys(this._preKeys).map(Number);
425
- return ids.length ? Math.max(...ids) + 1 : 1;
703
+ return this._nextPreKeyId;
704
+ }
705
+
706
+ setNextPreKeyId(id) {
707
+ const n = Number(id);
708
+ if (!Number.isFinite(n) || n <= this._nextPreKeyId) return;
709
+ this._nextPreKeyId = Math.floor(n);
710
+ if (this._firstUnuploadedPreKeyId > this._nextPreKeyId) {
711
+ this._firstUnuploadedPreKeyId = this._nextPreKeyId;
712
+ }
713
+ this._save();
714
+ }
715
+
716
+ /** The lowest id the server has not been sent. Never goes backwards. */
717
+ firstUnuploadedPreKeyId() {
718
+ return this._firstUnuploadedPreKeyId;
719
+ }
720
+
721
+ setFirstUnuploadedPreKeyId(id) {
722
+ const n = Number(id);
723
+ if (!Number.isFinite(n) || n <= this._firstUnuploadedPreKeyId) return;
724
+ this._firstUnuploadedPreKeyId = Math.min(Math.floor(n), this._nextPreKeyId);
725
+ this._save();
426
726
  }
427
727
  }
428
728
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.18",
3
+ "version": "5.14.19",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",