whalibmob 5.14.4 → 5.14.6

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
@@ -294,6 +294,20 @@ wa registration --request-code 919634847671 --method voice
294
294
  wa registration --request-code 919634847671 --method wa_old
295
295
  ```
296
296
 
297
+ **Set the account's display name while registering** with `--name`. This is the
298
+ name people who have *not* saved your number see next to it — in group
299
+ participant lists, in notifications, and beside your messages. Quote it if it
300
+ contains spaces:
301
+
302
+ ```sh
303
+ wa registration --request-code 919634847671 --name "Ricardo Trade"
304
+ ```
305
+
306
+ The name is stored with the session and announced on every connection from then
307
+ on. Registration itself carries no name — the server learns it when the session
308
+ connects — so a number registered without `--name` has none until one is set.
309
+ It can be given at either registration step, and changed later with `/name`.
310
+
297
311
  **Registering as Android** is the same command with the platform named. Nothing
298
312
  else to prepare — see [what it does behind that one command](#registering-as-android).
299
313
 
@@ -1378,6 +1392,10 @@ wa> /reg code 919634847671
1378
1392
  wa> /reg code 919634847671 voice
1379
1393
  wa> /reg code 919634847671 wa_old
1380
1394
 
1395
+ # register with a display name — what people who have not saved
1396
+ # your number see. Quote it if it has spaces.
1397
+ wa> /reg code 919634847671 --name "Ricardo Trade"
1398
+
1381
1399
  # confirm the code received
1382
1400
  wa> /reg confirm 919634847671 123456
1383
1401
  registered session saved to /home/user/.waSession/919634847671/919634847671.json
@@ -1542,8 +1560,8 @@ wa> /quit
1542
1560
  | `/biz <phone\|jid>` | Query business profile of a WhatsApp Business account |
1543
1561
  | **Registration** | |
1544
1562
  | `/reg check <phone>` | Check if number has WhatsApp |
1545
- | `/reg code <phone> [method]` | Request verification code |
1546
- | `/reg confirm <phone> <code>` | Complete registration |
1563
+ | `/reg code <phone> [method] [--name "Name"]` | Request verification code, optionally naming the account |
1564
+ | `/reg confirm <phone> <code> [--name "Name"]` | Complete registration |
1547
1565
  | **Connection** | |
1548
1566
  | `/connect <phone> [sms\|pair]` | Connect to WhatsApp — asks which method when unset |
1549
1567
  | `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
@@ -1578,12 +1596,27 @@ const sessFile = path.join(sessDir, phone + '.json')
1578
1596
 
1579
1597
  fs.mkdirSync(sessDir, { recursive: true })
1580
1598
 
1581
- const store = createNewStore(phone)
1599
+ // `name` is the display name the account registers with — what people who have
1600
+ // not saved the number see. Omit it and the account has no name until one is
1601
+ // set later with client.changeName().
1602
+ const store = createNewStore(phone, { name: 'Ricardo Trade' })
1582
1603
  saveStore(store, sessFile)
1583
1604
 
1584
1605
  await requestSmsCode(store, 'sms') // 'sms' | 'voice' | 'wa_old'
1585
1606
  ```
1586
1607
 
1608
+ The name is written to the session, not sent to the registration endpoint — the
1609
+ server learns it from the first connection, and from every one after. It can
1610
+ also be passed as an option at either step, which is the way to name a session
1611
+ that already exists:
1612
+
1613
+ ```js
1614
+ await requestSmsCode(store, 'sms', { name: 'Ricardo Trade' })
1615
+ await verifyCode(store, '123456', { name: 'Ricardo Trade' })
1616
+ ```
1617
+
1618
+ Names are trimmed, collapsed to single spaces and capped at 25 characters.
1619
+
1587
1620
  **Step 2 — verify the code**
1588
1621
 
1589
1622
  ```js
package/cli.js CHANGED
@@ -202,7 +202,12 @@ function enableWireTrace() {
202
202
  return origWrite(chunk, ...rest);
203
203
  };
204
204
 
205
- sock.on('data', (d) => {
205
+ // Prepended, not appended: the socket already carries the handshake's own
206
+ // data listener, registered while connect() was building the socket. An
207
+ // appended one runs after the frame has been read, so a frame the server
208
+ // sent would print *after* the error it caused — or not at all, once the
209
+ // failure tears the connection down. The trace has to come first.
210
+ sock.prependListener('data', (d) => {
206
211
  if (this.secured) return;
207
212
  // Label positionally, but say so when the reply is plainly not a Noise
208
213
  // frame — a proxy or captive portal answering in ASCII is worth reading
@@ -1147,6 +1152,17 @@ function requireConn() {
1147
1152
 
1148
1153
  // ─── tokenizer ────────────────────────────────────────────────────────────────
1149
1154
 
1155
+ // Pull `--flag value` out of a token list, in place, and hand back the value.
1156
+ // Removing it keeps every positional argument at the index the usage line
1157
+ // promises, whichever end of the command the flag was typed at.
1158
+ function takeFlag(toks, flag) {
1159
+ const i = toks.indexOf(flag);
1160
+ if (i === -1) return null;
1161
+ const value = (i + 1 < toks.length && !toks[i + 1].startsWith('--')) ? toks[i + 1] : null;
1162
+ toks.splice(i, value === null ? 1 : 2);
1163
+ return value;
1164
+ }
1165
+
1150
1166
  function tokens(line) {
1151
1167
  const s = String(line || '');
1152
1168
  const t = []; let b = '';
@@ -2162,6 +2178,10 @@ async function handleLine(line) {
2162
2178
  // ── registration ───────────────────────────────────────────────────────
2163
2179
 
2164
2180
  case '/reg': {
2181
+ // --name is pulled out before anything reads a position, so the phone
2182
+ // and the code stay where the usage lines say they are no matter where
2183
+ // the flag was typed. A name with spaces needs quoting: --name "A B".
2184
+ const regName = takeFlag(p, '--name');
2165
2185
  const sub = p[1] && p[1].toLowerCase();
2166
2186
 
2167
2187
  if (sub === 'check') {
@@ -2177,7 +2197,12 @@ async function handleLine(line) {
2177
2197
  const method = (p[3] || 'sms').toLowerCase();
2178
2198
  // email method: /reg code <phone> email <address>
2179
2199
  const emailAddr = method === 'email' ? (p[4] || '') : '';
2180
- if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>]'); break; }
2200
+ if (!ph) {
2201
+ fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>] [--name "Your Name"]');
2202
+ out(' --name sets the display name the account registers with — what people');
2203
+ out(' who have not saved your number see. It can be changed later with /name.');
2204
+ break;
2205
+ }
2181
2206
  if (method === 'email' && !emailAddr) {
2182
2207
  fail('email method requires an address — usage: /reg code <phone> email <address>');
2183
2208
  break;
@@ -2186,7 +2211,7 @@ async function handleLine(line) {
2186
2211
  const sessFile = storeFileFor(_sessDir, ph);
2187
2212
  let store = loadStore(sessFile);
2188
2213
  if (!store) {
2189
- store = initAuthCreds(ph);
2214
+ store = initAuthCreds(ph, { name: regName });
2190
2215
  saveStore(store, sessFile);
2191
2216
  } else if (!store.codePending && !store.registered) {
2192
2217
  // Only check /exist when keys were never used to request a code.
@@ -2195,14 +2220,16 @@ async function handleLine(line) {
2195
2220
  const fresh = await assertRegistrationKeys(store, waVersion);
2196
2221
  if (!fresh) {
2197
2222
  out(' device keys already registered — generating new keys...');
2198
- store = initAuthCreds(ph);
2223
+ store = initAuthCreds(ph, { name: regName });
2199
2224
  saveStore(store, sessFile);
2200
2225
  out(' new keys saved — proceed with code below');
2201
2226
  }
2202
2227
  }
2203
2228
  const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
2204
2229
  out('requesting ' + methodLabel + ' code for +' + ph + '...');
2205
- const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {}, { onProgress: out });
2230
+ const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
2231
+ { onProgress: out, name: regName });
2232
+ if (regName) out(' registering as "' + (store.name || regName) + '"');
2206
2233
  const r = await requestSmsCode(store, method, codeOpts);
2207
2234
  store.codePending = true;
2208
2235
  saveStore(store, sessFile);
@@ -2213,11 +2240,12 @@ async function handleLine(line) {
2213
2240
  else if (sub === 'confirm') {
2214
2241
  const ph = normalizePhone(p[2]);
2215
2242
  const code = p[3];
2216
- if (!ph || !code) { fail('usage: /reg confirm <phone> <code>'); break; }
2243
+ if (!ph || !code) { fail('usage: /reg confirm <phone> <code> [--name "Your Name"]'); break; }
2217
2244
  const file = storeFileFor(_sessDir, ph);
2218
- const store = loadStore(file) || initAuthCreds(ph);
2245
+ const store = loadStore(file) || initAuthCreds(ph, { name: regName });
2219
2246
  out('verifying...');
2220
- const r = await verifyCode(store, code, Object.assign(registrationPrompts(), { onProgress: out }));
2247
+ const r = await verifyCode(store, code,
2248
+ Object.assign(registrationPrompts(), { onProgress: out, name: regName }));
2221
2249
  if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
2222
2250
  sessionDirFor(_sessDir, ph, { create: true });
2223
2251
  const finalStore = r.store || store;
@@ -2234,6 +2262,11 @@ async function handleLine(line) {
2234
2262
  out(' the session is saved under that number');
2235
2263
  }
2236
2264
  out('registered session saved to ' + savedFile);
2265
+ if (finalStore.name && finalStore.name !== 'User') {
2266
+ out(' name ' + finalStore.name + ' (announced on every connect)');
2267
+ } else {
2268
+ out(' name not set — run /name <text> after connecting');
2269
+ }
2237
2270
  out('now run: /connect ' + savedPhone);
2238
2271
  } else {
2239
2272
  fail('verification failed ' + JSON.stringify(r));
@@ -2459,8 +2492,8 @@ usage:
2459
2492
  wa connect <phone> connect and open interactive shell
2460
2493
  wa pair <phone> [code] link to an existing account (8-digit code)
2461
2494
  wa listen <phone> connect and listen (stay-alive)
2462
- wa registration --request-code <phone> request SMS code
2463
- wa registration --register <phone> --code <code>
2495
+ wa registration --request-code <phone> [--name "Your Name"]
2496
+ wa registration --register <phone> --code <code> [--name "Your Name"]
2464
2497
  wa registration --check <phone>
2465
2498
  wa apk-material <base.apk> [split.apk ...] read the Android token material
2466
2499
  wa apk-material --download fetch that APK from Google Play
@@ -2485,6 +2518,7 @@ debug:
2485
2518
 
2486
2519
  --debug trace without asking (same as WA_DEBUG=1)
2487
2520
  --no-debug, -q stay quiet without asking (same as WA_DEBUG=0)
2521
+ --name <text> display name to register with (registration commands only)
2488
2522
  --trace-bytes also dump the raw encoded bytes of every stanza
2489
2523
 
2490
2524
  after connecting, type /help for all available commands.
@@ -2816,10 +2850,11 @@ async function main() {
2816
2850
  // the base directory stays there, a new one gets a directory of its own.
2817
2851
  sessionDirFor(_sessDir, ph, { create: true });
2818
2852
  const sessFile = storeFileFor(_sessDir, ph);
2853
+ const regName = typeof flags.name === 'string' ? flags.name : null;
2819
2854
  let store = loadStore(sessFile);
2820
2855
  if (!store) {
2821
2856
  // Brand new — generate fresh keys, save immediately, no need to check /exist
2822
- store = initAuthCreds(ph);
2857
+ store = initAuthCreds(ph, { name: regName });
2823
2858
  saveStore(store, sessFile);
2824
2859
  } else if (!store.codePending && !store.registered) {
2825
2860
  // Existing store but code was never sent and not registered — check if
@@ -2830,7 +2865,7 @@ async function main() {
2830
2865
  const fresh = await assertRegistrationKeys(store, waVersion);
2831
2866
  if (!fresh) {
2832
2867
  out(' device keys already registered — generating new keys...');
2833
- store = initAuthCreds(ph);
2868
+ store = initAuthCreds(ph, { name: regName });
2834
2869
  saveStore(store, sessFile);
2835
2870
  }
2836
2871
  }
@@ -2839,7 +2874,9 @@ async function main() {
2839
2874
  const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
2840
2875
  out('requesting ' + methodLabel + ' code for +' + ph + '...');
2841
2876
  try {
2842
- const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {}, { onProgress: out });
2877
+ const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
2878
+ { onProgress: out, name: regName });
2879
+ if (regName) out(' registering as "' + (store.name || regName) + '"');
2843
2880
  const r = await requestSmsCode(store, method, codeOpts);
2844
2881
  store.codePending = true;
2845
2882
  saveStore(store, sessFile);
@@ -2861,11 +2898,13 @@ async function main() {
2861
2898
  const code = flags.code;
2862
2899
  if (!ph) { fail('phone number required'); process.exit(1); }
2863
2900
  if (!code) { fail('--code is required'); process.exit(1); }
2901
+ const regName = typeof flags.name === 'string' ? flags.name : null;
2864
2902
  const file = storeFileFor(_sessDir, ph);
2865
- const store = loadStore(file) || initAuthCreds(ph);
2903
+ const store = loadStore(file) || initAuthCreds(ph, { name: regName });
2866
2904
  out('verifying code for +' + ph + '...');
2867
2905
  try {
2868
- const r = await verifyCode(store, code, Object.assign(registrationPrompts(), { onProgress: out }));
2906
+ const r = await verifyCode(store, code,
2907
+ Object.assign(registrationPrompts(), { onProgress: out, name: regName }));
2869
2908
  if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
2870
2909
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
2871
2910
  const finalStore = r.store || store;
package/lib/Client.js CHANGED
@@ -676,6 +676,19 @@ class WhalibmobClient extends EventEmitter {
676
676
  saveWebStore(this._store, this._webSessionFile);
677
677
  }
678
678
 
679
+ // Write the session back to disk, whichever kind it is. A change that only
680
+ // lives in memory is a change that is gone at the next start.
681
+ _persistStore() {
682
+ try {
683
+ if (this._mode === 'web') { this._saveWebStore(); return; }
684
+ if (!this._sessionDir || !this._store || !this._store.phoneNumber) return;
685
+ const { saveStore } = require('./Store');
686
+ saveStore(this._store, path.join(this._sessionDir, `${this._store.phoneNumber}.json`));
687
+ } catch (err) {
688
+ _whaDbg('[DBG] STORE persist failed: ' + (err && err.message));
689
+ }
690
+ }
691
+
679
692
  async _connectWebSocket() {
680
693
  const { encodeCompanionRegisterPayload, encodeCompanionLoginPayload } =
681
694
  require('./webproto');
@@ -1279,6 +1292,26 @@ class WhalibmobClient extends EventEmitter {
1279
1292
  const attrs = node.attrs || {};
1280
1293
  // Debug: log the full success node structure
1281
1294
  _whaDbg('[DBG] SUCCESS node attrs=' + JSON.stringify(attrs));
1295
+
1296
+ // The one place the server ever tells us what it thinks this account is
1297
+ // called. Nothing else answers that question — there is no request that
1298
+ // reads a push name back — so this is both how the local name is kept
1299
+ // honest and the only way to find out whether a rename was accepted.
1300
+ if (attrs.display_name) {
1301
+ const serverName = String(attrs.display_name);
1302
+ if (this._store && this._store.name !== serverName) {
1303
+ _whaDbg('[DBG] SUCCESS display_name="' + serverName + '" — server disagrees with the ' +
1304
+ 'stored name "' + this._store.name + '"; taking the server\'s');
1305
+ this._store.name = serverName;
1306
+ if (!this._store.pushName) this._store.pushName = serverName;
1307
+ this._persistStore();
1308
+ } else {
1309
+ _whaDbg('[DBG] SUCCESS display_name="' + serverName + '" — matches the stored name');
1310
+ }
1311
+ } else {
1312
+ _whaDbg('[DBG] SUCCESS carried no display_name — the server did not say what this ' +
1313
+ 'account is called');
1314
+ }
1282
1315
  if (Array.isArray(node.content)) {
1283
1316
  for (const child of node.content) {
1284
1317
  if (child && child.description) {
@@ -1725,7 +1758,15 @@ class WhalibmobClient extends EventEmitter {
1725
1758
  }
1726
1759
  case 'pushName': {
1727
1760
  const name = (a.pushNameSetting && a.pushNameSetting.name) || null;
1728
- if (name && this._store) this._store.pushName = name;
1761
+ // Both fields, and to disk. Only `pushName` was set here, and it is
1762
+ // the one the store does not carry — so a rename made on another
1763
+ // device took effect until the process restarted, and never reached
1764
+ // the handshake, which announces `name`.
1765
+ if (name && this._store) {
1766
+ this._store.name = name;
1767
+ this._store.pushName = name;
1768
+ this._persistStore();
1769
+ }
1729
1770
  this.emit('push_name_update', { name, remote: true });
1730
1771
  break;
1731
1772
  }
@@ -4546,13 +4587,71 @@ class WhalibmobClient extends EventEmitter {
4546
4587
 
4547
4588
  // Change display name
4548
4589
  // Sends presence node with new name — immediate, no IQ needed.
4590
+ // The push name lives in two places, and only one of them survives.
4591
+ //
4592
+ // The presence node announces it to whoever is looking right now — it is how
4593
+ // a contact who has not saved the number sees a name at all. But it says
4594
+ // nothing lasting: the server treats it as the state of this connection, so a
4595
+ // name sent only that way is gone by the next login, and the account keeps
4596
+ // whatever the profile actually holds.
4597
+ //
4598
+ // What persists is an app state write, filed under 'setting_pushName' in the
4599
+ // critical_block collection. That is the record the phone and every other
4600
+ // linked device read, and the one the profile is served from.
4601
+ //
4602
+ // Both are sent: the presence so the change shows immediately, the patch so
4603
+ // it is still there tomorrow.
4604
+ //
4605
+ // Returns a promise for the patch that resolves true when it was written and
4606
+ // false when app state was not writable (no sync key shared yet) or the
4607
+ // server refused it. It never rejects, so a caller that ignores the return —
4608
+ // as every caller did when this only sent presence — cannot end up with an
4609
+ // unhandled rejection. The connection check still throws synchronously.
4549
4610
  changeName(name) {
4550
4611
  if (!this._socket || !this._connected) throw new Error('Not connected');
4612
+ // A presence node carrying only a name is a name republish: it says what
4613
+ // this account is called without also describing whether it is online.
4614
+ // Carrying a type turns it into an availability update that happens to
4615
+ // mention a name, which is a different thing to ask for and is not what a
4616
+ // rename is. The bare form goes first, on its own.
4617
+ this._socket.sendNode(new BinaryNode('presence', { name }, null));
4618
+
4619
+ // And the same name again on an availability node, unconditionally. The
4620
+ // bare form above is what a browser client sends, but the implementation
4621
+ // with the longest production record only ever sends this one, and ties the
4622
+ // server learning your name to it: announce it at least once after
4623
+ // connecting or other people see a placeholder instead. Making it
4624
+ // conditional on already being marked available would leave a session that
4625
+ // renames before going online with only the form that may be ignored.
4551
4626
  this._socket.sendNode(new BinaryNode('presence', {
4552
4627
  name,
4553
4628
  type: 'available'
4554
4629
  }, null));
4555
- if (this._store) this._store.pushName = name;
4630
+
4631
+ // `name` is the field the handshake reads when it announces this account,
4632
+ // and it is the one the session file carries. Writing only `pushName` left
4633
+ // the store's `name` at its default, so every login announced that default
4634
+ // again and undid the change — the account went on being called "User" no
4635
+ // matter how often the presence went out. Both are set: `name` because the
4636
+ // handshake and the file are keyed off it, `pushName` because the presence
4637
+ // helper reads that one first.
4638
+ if (this._store) {
4639
+ this._store.name = name;
4640
+ this._store.pushName = name;
4641
+ this._persistStore();
4642
+ }
4643
+
4644
+ return this._appStatePatchOrFallback(AppMutations.pushNamePatch(name))
4645
+ .then(synced => {
4646
+ _whaDbg(synced
4647
+ ? '[DBG] PUSHNAME persisted in app state: ' + name
4648
+ : '[DBG] PUSHNAME announced but NOT persisted — no app state sync key yet');
4649
+ return synced;
4650
+ })
4651
+ .catch(err => {
4652
+ _whaDbg('[DBG] PUSHNAME app state write failed: ' + (err && err.message));
4653
+ return false;
4654
+ });
4556
4655
  }
4557
4656
 
4558
4657
  // Change bio/about text
@@ -1653,9 +1653,21 @@ async function assertRegistrationKeys(store, waVersion) {
1653
1653
  // bites when the server keeps answering with a different reason each time.
1654
1654
  const MAX_CODE_REQUEST_ATTEMPTS = 5;
1655
1655
 
1656
+ // The display name a registration is carrying, if the caller gave one. It is
1657
+ // never sent to the registration endpoints — those take no name, and adding one
1658
+ // would be inventing a field. It is written to the session, and the handshake
1659
+ // on the first connection is what announces it.
1660
+ function applyRegistrationName(store, opts) {
1661
+ if (!store || !opts || opts.name === undefined || opts.name === null) return;
1662
+ const { normalizePushName } = require('./Store');
1663
+ const clean = normalizePushName(opts.name);
1664
+ if (clean) store.name = clean;
1665
+ }
1666
+
1656
1667
  async function requestSmsCode(store, method, opts) {
1657
1668
  method = method || 'sms';
1658
1669
  opts = opts || {};
1670
+ applyRegistrationName(store, opts);
1659
1671
  // Before the version is read, since on Android the version comes out of the
1660
1672
  // APK this fetches.
1661
1673
  const _device = deviceForRegistration(store, opts);
@@ -1792,6 +1804,7 @@ async function requestSmsCode(store, method, opts) {
1792
1804
 
1793
1805
  async function verifyCode(store, code, opts) {
1794
1806
  opts = opts || {};
1807
+ applyRegistrationName(store, opts);
1795
1808
  // Normally already there from the code request, but a confirmation can be run
1796
1809
  // from a fresh shell that never made one.
1797
1810
  const _device = deviceForRegistration(store, opts);
package/lib/Store.js CHANGED
@@ -49,7 +49,30 @@ function sign(privKey32, message) {
49
49
  // Store creation
50
50
  // ─────────────────────────────────────────────────────────
51
51
 
52
- function createNewStore(phoneNumber) {
52
+ // What is safe to announce as a display name. The server takes the value
53
+ // verbatim, so a stray newline or a run of spaces goes out as typed; and 'User'
54
+ // is the placeholder this file writes when no name was given, so accepting it
55
+ // back as a real name would make it indistinguishable from having none.
56
+ const MAX_PUSH_NAME_LENGTH = 25;
57
+
58
+ function normalizePushName(name) {
59
+ if (name === null || name === undefined) return null;
60
+ const clean = String(name).replace(/[\r\n\t]+/g, ' ').trim().replace(/\s{2,}/g, ' ');
61
+ if (!clean || clean === 'User') return null;
62
+ return clean.length > MAX_PUSH_NAME_LENGTH ? clean.slice(0, MAX_PUSH_NAME_LENGTH).trim() : clean;
63
+ }
64
+
65
+ /**
66
+ * @param {string} phoneNumber digits, no +
67
+ * @param {object} [opts]
68
+ * name the display name this account announces to people who have not saved
69
+ * the number. Setting it here is what makes the first connection carry
70
+ * it: the handshake reads this field, and it only announces a name that
71
+ * is not the placeholder below. Left unset, the account stays nameless
72
+ * until something calls changeName().
73
+ */
74
+ function createNewStore(phoneNumber, opts) {
75
+ opts = opts || {};
53
76
  const noiseKeyPair = generateKeyPair();
54
77
  const identityKeyPair = generateKeyPair();
55
78
  const signedPreKeyPair = generateKeyPair();
@@ -89,7 +112,7 @@ function createNewStore(phoneNumber) {
89
112
  backupToken,
90
113
  registered: false,
91
114
  codePending: false, // true after /code request, cleared on successful /register
92
- name: 'User',
115
+ name: normalizePushName(opts.name) || 'User',
93
116
  version,
94
117
  device,
95
118
  // ADVSignedDeviceIdentity bytes received from server <success> node.
@@ -307,6 +330,8 @@ function fromSixParts(sixParts) {
307
330
 
308
331
  module.exports = {
309
332
  createNewStore,
333
+ normalizePushName,
334
+ MAX_PUSH_NAME_LENGTH,
310
335
  saveStore,
311
336
  loadStore,
312
337
  toSixParts,
package/lib/auth-utils.js CHANGED
@@ -666,7 +666,9 @@ function initAuthCreds(phoneNumber, options) {
666
666
  options = options || {};
667
667
 
668
668
  // The library's own key generation — curve25519, with the right prefixes.
669
- const store = createNewStore(phoneNumber);
669
+ // options.name, when given, is the display name the account registers with:
670
+ // it is stored here and announced by the first handshake.
671
+ const store = createNewStore(phoneNumber, { name: options.name });
670
672
 
671
673
  // ── Extra credential fields ──────────────────────────────────────────────────
672
674
  // Not required internally, but commonly expected by application code.
package/lib/noise.js CHANGED
@@ -301,6 +301,25 @@ function readFrameLength(data) {
301
301
  return (data[0] << 16) | (data[1] << 8) | data[2];
302
302
  }
303
303
 
304
+ // ─── Handshake trace ─────────────────────────────────────────────────────────
305
+ //
306
+ // The handshake runs before there are stanzas to log, so without this the debug
307
+ // output jumps from "connecting" to either a secured channel or an error with
308
+ // nothing in between — and when it fails, nothing to say which step it got to.
309
+ //
310
+ // Only public material is rendered: ephemeral and static public keys, frame and
311
+ // field sizes, certificate outcome. Derived session keys and anything private
312
+ // are named but never printed, because a debug trace is the thing people paste
313
+ // into a bug report.
314
+ function _hsKey(buf) {
315
+ if (!Buffer.isBuffer(buf)) return String(buf);
316
+ return buf.toString('hex') + ' (' + buf.length + 'B)';
317
+ }
318
+
319
+ function _hsSize(buf) {
320
+ return Buffer.isBuffer(buf) ? buf.length + 'B' : 'absent';
321
+ }
322
+
304
323
  // ─── NoiseSocket ─────────────────────────────────────────────────────────────
305
324
  //
306
325
  // Events emitted:
@@ -395,9 +414,18 @@ class NoiseSocket extends EventEmitter {
395
414
  };
396
415
  this.noiseState.hash(this._ephemeralKeyPair.public);
397
416
 
417
+ _whaDbg('[DBG] NOISE handshake begin transport=' + this.transport +
418
+ ' prologue=' + this._prologue.toString('hex') +
419
+ ' pattern=Noise_XX_25519_AESGCM_SHA256');
420
+ _whaDbg('[DBG] NOISE ──▶ ClientHello ephemeral=' + _hsKey(this._ephemeralKeyPair.public));
421
+
398
422
  const helloPayload = encodeHandshakeClientHello(this._ephemeralKeyPair.public);
399
423
  const frame = Buffer.concat([this._prologue, makeFrame(helloPayload)]);
424
+ _whaDbg('[DBG] NOISE ──▶ ClientHello ' + frame.length + 'B on the wire = prologue ' +
425
+ this._prologue.length + 'B + header ' + FRAME_HEADER_SIZE + 'B + payload ' +
426
+ helloPayload.length + 'B');
400
427
  this.socket.write(frame);
428
+ _whaDbg('[DBG] NOISE waiting for ServerHello');
401
429
  }
402
430
 
403
431
  _onData(data) {
@@ -424,16 +452,24 @@ class NoiseSocket extends EventEmitter {
424
452
 
425
453
  _handleHandshakeFrame(frame) {
426
454
  try {
455
+ _whaDbg('[DBG] NOISE ◀── ServerHello ' + frame.length + 'B frame');
427
456
  const serverHello = decodeServerHello(frame);
457
+ _whaDbg('[DBG] NOISE ◀── ServerHello ephemeral=' + _hsSize(serverHello.ephemeral) +
458
+ ' static=' + _hsSize(serverHello.staticEnc) + ' encrypted' +
459
+ ' certificate=' + _hsSize(serverHello.payload) + ' encrypted');
428
460
  if (!serverHello.ephemeral) throw new Error('No server ephemeral in ServerHello');
461
+ _whaDbg('[DBG] NOISE ◀── server ephemeral=' + _hsKey(serverHello.ephemeral));
429
462
 
430
463
  this.noiseState.hash(serverHello.ephemeral);
431
464
  const sharedEE = dhShared(this._ephemeralKeyPair.private, serverHello.ephemeral);
432
465
  this.noiseState.mixKey(sharedEE);
466
+ _whaDbg('[DBG] NOISE mixed DH(our ephemeral, server ephemeral)');
433
467
 
434
468
  const serverStaticDec = this.noiseState.decryptWithAd(serverHello.staticEnc);
469
+ _whaDbg('[DBG] NOISE ◀── server static key decrypted=' + _hsKey(serverStaticDec));
435
470
  const sharedSE = dhShared(this._ephemeralKeyPair.private, serverStaticDec);
436
471
  this.noiseState.mixKey(sharedSE);
472
+ _whaDbg('[DBG] NOISE mixed DH(our ephemeral, server static)');
437
473
 
438
474
  // The server is authenticated here, before ClientFinish hands it our
439
475
  // static key and payload. A ServerHello without a certificate leaves
@@ -442,14 +478,20 @@ class NoiseSocket extends EventEmitter {
442
478
  throw new Error('WA server cert validation failed: the server sent no certificate');
443
479
  }
444
480
  const certPlain = this.noiseState.decryptWithAd(serverHello.payload);
481
+ _whaDbg('[DBG] NOISE ◀── certificate decrypted=' + certPlain.length + 'B — verifying');
445
482
  validateServerCert(certPlain, serverStaticDec);
483
+ _whaDbg('[DBG] NOISE ◀── certificate verified: signed by the root CA and ' +
484
+ 'certifying the static key above — server authenticated');
446
485
 
447
486
  const noisePriv = this.store.noiseKeyPair.private;
448
487
  // Send the raw 32-byte noise public key (no 0x05 Signal prefix)
449
488
  const noisePub = stripKeyPrefix(this.store.noiseKeyPair.public);
450
489
  const encStatic = this.noiseState.encryptWithAd(noisePub);
490
+ _whaDbg('[DBG] NOISE ──▶ our static key=' + _hsKey(noisePub) +
491
+ ' encrypted to ' + encStatic.length + 'B');
451
492
  const sharedSS = dhShared(noisePriv, serverHello.ephemeral);
452
493
  this.noiseState.mixKey(sharedSS);
494
+ _whaDbg('[DBG] NOISE mixed DH(our static, server ephemeral)');
453
495
 
454
496
  // Web mode hands us a finished companion payload; there is no phone
455
497
  // identity to derive anything from, and the fields below do not exist on
@@ -457,6 +499,9 @@ class NoiseSocket extends EventEmitter {
457
499
  if (this._buildPayload) {
458
500
  const encPayload = this.noiseState.encryptWithAd(this._buildPayload());
459
501
  const finish = encodeHandshakeClientFinish(encStatic, encPayload);
502
+ _whaDbg('[DBG] NOISE ──▶ ClientFinish static=' + encStatic.length + 'B payload=' +
503
+ encPayload.length + 'B (companion) ' + (finish.length + FRAME_HEADER_SIZE) +
504
+ 'B on the wire');
460
505
  this.socket.write(makeFrame(finish));
461
506
 
462
507
  const webKeys = this.noiseState.split();
@@ -465,6 +510,7 @@ class NoiseSocket extends EventEmitter {
465
510
  this.writeCounter = 0;
466
511
  this.readCounter = 0;
467
512
  this.secured = true;
513
+ _whaDbg('[DBG] NOISE split() derived the send and receive keys — channel secured');
468
514
 
469
515
  if (this._expectSuccess) {
470
516
  this._awaitingAuth = true;
@@ -500,7 +546,14 @@ class NoiseSocket extends EventEmitter {
500
546
  const payload = encodeClientPayload({
501
547
  username: BigInt(this.store.phoneNumber),
502
548
  passive: false,
503
- pushName: this.store.registered ? (this.store.name || null) : null,
549
+ // A session starts out named "User" because the store needs something
550
+ // there, not because the account is called that. Announcing it says the
551
+ // account IS called "User", and the handshake runs on every connect, so
552
+ // a name set while online was overwritten by the placeholder the moment
553
+ // the connection came back. Send nothing until there is a real name —
554
+ // the companion payload has always guarded this the same way.
555
+ pushName: (this.store.registered && this.store.name &&
556
+ this.store.name !== 'User') ? this.store.name : null,
504
557
  shortConnect: _reconnect,
505
558
  // ConnectType is a closed set — 0 CELLULAR_UNKNOWN, 1 WIFI_UNKNOWN,
506
559
  // and 100-112 for the named cellular radios. There is no 3, which is
@@ -560,6 +613,9 @@ class NoiseSocket extends EventEmitter {
560
613
 
561
614
  const encPayload = this.noiseState.encryptWithAd(payload);
562
615
  const finish = encodeHandshakeClientFinish(encStatic, encPayload);
616
+ _whaDbg('[DBG] NOISE ──▶ ClientFinish static=' + encStatic.length + 'B payload=' +
617
+ encPayload.length + 'B (username=' + this.store.phoneNumber + ' version=' +
618
+ _version + ') ' + (finish.length + FRAME_HEADER_SIZE) + 'B on the wire');
563
619
  this.socket.write(makeFrame(finish));
564
620
 
565
621
  const keys = this.noiseState.split();
@@ -567,6 +623,8 @@ class NoiseSocket extends EventEmitter {
567
623
  this.readKey = keys.readKey;
568
624
  this.writeCounter = 0;
569
625
  this.readCounter = 0;
626
+ _whaDbg('[DBG] NOISE split() derived the send and receive keys — channel secured');
627
+ _whaDbg('[DBG] NOISE waiting for the server to accept the payload');
570
628
  this.secured = true;
571
629
 
572
630
  // Wait for the server's <success> or <failure> confirmation before
@@ -575,6 +633,8 @@ class NoiseSocket extends EventEmitter {
575
633
  this._awaitingAuth = true;
576
634
 
577
635
  } catch (err) {
636
+ // The step that failed is the one after the last line traced above.
637
+ _whaDbg('[DBG] NOISE ✗ handshake failed: ' + (err && err.message));
578
638
  if (this._connectReject) {
579
639
  this._connectReject(err);
580
640
  this._connectResolve = null;
@@ -596,6 +656,8 @@ class NoiseSocket extends EventEmitter {
596
656
  if (this._awaitingAuth) {
597
657
  this._awaitingAuth = false;
598
658
  const tag = node && node.description;
659
+ _whaDbg('[DBG] NOISE ◀── <' + tag + '> — the first frame over the secured channel, ' +
660
+ 'and the server\'s answer on the payload');
599
661
 
600
662
  if (tag === 'success') {
601
663
  if (this._connectResolve) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.4",
3
+ "version": "5.14.6",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",