whalibmob 5.14.8 → 5.14.11

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
  /**
@@ -3280,7 +3304,11 @@ class WhalibmobClient extends EventEmitter {
3280
3304
  this.emit('auth_failure', { reason, location: loc, loggedOut: spent, node });
3281
3305
  this._fatal = spent;
3282
3306
  this._reconnecting = false;
3283
- 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 });
3284
3312
  }
3285
3313
 
3286
3314
  // ─── Media connection ─────────────────────────────────────────────────────
@@ -482,7 +482,7 @@ const COUNTRY_META = {
482
482
  '370': { mcc: '246', mnc: '01', lg: 'lt', lc: 'LT' }, // Lithuania (Telia)
483
483
  '371': { mcc: '247', mnc: '01', lg: 'lv', lc: 'LV' }, // Latvia (LMT)
484
484
  '372': { mcc: '248', mnc: '01', lg: 'et', lc: 'EE' }, // Estonia (Telia)
485
- '373': { mcc: '255', mnc: '00', lg: 'ro', lc: 'MD' }, // Moldova (IDC)
485
+ '373': { mcc: '259', mnc: '01', lg: 'ro', lc: 'MD' }, // Moldova (Orange)
486
486
  '374': { mcc: '283', mnc: '01', lg: 'hy', lc: 'AM' }, // Armenia (Beeline)
487
487
  '375': { mcc: '257', mnc: '01', lg: 'be', lc: 'BY' }, // Belarus (A1)
488
488
  '376': { mcc: '213', mnc: '03', lg: 'ca', lc: 'AD' }, // Andorra (Som, Mobiland)
@@ -499,7 +499,7 @@ const COUNTRY_META = {
499
499
  '423': { mcc: '295', mnc: '01', lg: 'de', lc: 'LI' }, // Liechtenstein (Swisscom)
500
500
  '500': { mcc: '750', mnc: '001', lg: 'en', lc: 'FK' }, // Falkland Islands (Sure)
501
501
  '501': { mcc: '702', mnc: '67', lg: 'en', lc: 'BZ' }, // Belize (DigiCell)
502
- '502': { mcc: '704', mnc: '?', lg: 'es', lc: 'GT' }, // Guatemala (RED/INTELFON)
502
+ '502': { mcc: '704', mnc: '01', lg: 'es', lc: 'GT' }, // Guatemala (Claro)
503
503
  '503': { mcc: '706', mnc: '01', lg: 'es', lc: 'SV' }, // El Salvador (Claro)
504
504
  '504': { mcc: '708', mnc: '001', lg: 'es', lc: 'HN' }, // Honduras (Claro)
505
505
  '505': { mcc: '710', mnc: '21', lg: 'es', lc: 'NI' }, // Nicaragua (Claro)
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.8",
3
+ "version": "5.14.11",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",