whalibmob 5.5.66 → 5.5.68

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/cli.js CHANGED
@@ -7,15 +7,39 @@
7
7
  // process.env is fully populated when modules read it at load time.
8
8
  try { require('dotenv').config(); } catch (_) {}
9
9
 
10
- // Suppress internal [DBG] lines — keep console clean for users
10
+ // ─── Wire trace ───────────────────────────────────────────────────────────────
11
+ // Full protocol tracing prints every stanza this client sends and receives.
12
+ // It is opt-in: an interactive session asks once at startup. `--debug` turns it
13
+ // on without asking (for scripts and pipes), `--no-debug` / `--quiet` / `-q` /
14
+ // WA_DEBUG=0 turn it off without asking.
15
+ const TRACE_FORCE_ON = process.argv.includes('--debug') || process.env.WA_DEBUG === '1';
16
+ const TRACE_FORCE_OFF = (
17
+ process.argv.includes('--no-debug') ||
18
+ process.argv.includes('--quiet') ||
19
+ process.argv.includes('-q') ||
20
+ process.env.WA_DEBUG === '0'
21
+ );
22
+ // `--trace-bytes` additionally dumps the raw encoded bytes of every stanza.
23
+ const TRACE_BYTES = process.argv.includes('--trace-bytes');
24
+
25
+ // Consume the trace flags so the command parser below never sees them and
26
+ // reports e.g. `--no-debug` as an unknown command.
27
+ const _TRACE_FLAGS = ['--debug', '--no-debug', '--quiet', '-q', '--trace-bytes'];
28
+ process.argv = process.argv.filter(a => !_TRACE_FLAGS.includes(a));
29
+
30
+ // Console starts clean: internal [DBG] lines are swallowed unless tracing is
31
+ // switched on, which restores this stream.
11
32
  const _origStderrWrite = process.stderr.write.bind(process.stderr);
12
- process.stderr.write = function(chunk, enc, cb) {
13
- if (typeof chunk === 'string' && chunk.startsWith('[DBG]')) {
14
- if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb();
15
- return true;
16
- }
17
- return _origStderrWrite(chunk, enc, cb);
18
- };
33
+ function installDbgSuppression() {
34
+ process.stderr.write = function(chunk, enc, cb) {
35
+ if (typeof chunk === 'string' && chunk.startsWith('[DBG]')) {
36
+ if (typeof enc === 'function') enc(); else if (typeof cb === 'function') cb();
37
+ return true;
38
+ }
39
+ return _origStderrWrite(chunk, enc, cb);
40
+ };
41
+ }
42
+ installDbgSuppression();
19
43
 
20
44
  const path = require('path');
21
45
  const fs = require('fs');
@@ -37,6 +61,165 @@ const {
37
61
 
38
62
  const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
39
63
 
64
+ // ─── Wire trace implementation ────────────────────────────────────────────────
65
+ // Everything below is CLI-only instrumentation; the library is untouched.
66
+ //
67
+ // Two hooks cover the whole protocol surface:
68
+ // * NoiseSocket.prototype.sendNode — every stanza leaving this client
69
+ // * NoiseSocket.prototype.emit — the 'node' event, every stanza arriving
70
+ // They are patched on the prototype rather than on an instance so the trace
71
+ // survives reconnects and covers frames sent during the handshake, before any
72
+ // client-level event has fired. https.request is wrapped as well, because SMS
73
+ // registration runs over HTTP and never touches the socket.
74
+ let _wireTraceOn = false;
75
+ function enableWireTrace() {
76
+ if (_wireTraceOn) return;
77
+ _wireTraceOn = true;
78
+ // Tracing is the point now — let [DBG] through again.
79
+ process.stderr.write = _origStderrWrite;
80
+ const { NoiseSocket } = require('./lib/noise');
81
+ const { configureLogger } = require('./lib/logger');
82
+ const https = require('https');
83
+
84
+ const C = process.stdout.isTTY
85
+ ? { out:'\x1b[36m', in:'\x1b[32m', http:'\x1b[35m', dim:'\x1b[2m', tag:'\x1b[33m', off:'\x1b[0m' }
86
+ : { out:'', in:'', http:'', dim:'', tag:'', off:'' };
87
+
88
+ // pino writes newline-delimited JSON to stdout. That is the right shape for a
89
+ // log collector and the wrong one for someone watching a live session, so
90
+ // reformat its lines on the way out. Anything that is not a pino record is
91
+ // passed through byte-for-byte.
92
+ const stamp = () => new Date().toISOString().slice(11, 23);
93
+ // Traffic arrives while readline owns the bottom line, so writing straight
94
+ // out overwrites the prompt and leaves the two interleaved. Clear the line
95
+ // first and redraw the prompt after, the same way the CLI's own event
96
+ // handlers do.
97
+ const trace = (s) => {
98
+ const live = _rl && process.stdout.isTTY;
99
+ if (live) {
100
+ readline.cursorTo(process.stdout, 0);
101
+ readline.clearLine(process.stdout, 0);
102
+ }
103
+ _origStderrWrite(s + '\n');
104
+ if (live) _rl.prompt(true);
105
+ };
106
+
107
+ // Route the library's internal [DBG] stream through pino at full verbosity.
108
+ // pino writes newline-delimited JSON straight to fd 1 via sonic-boom, which
109
+ // bypasses process.stdout — so rather than trying to reformat its output we
110
+ // intercept at pino's own logMethod hook, render the record ourselves and let
111
+ // pino emit nothing. The library still logs through pino; only the rendering
112
+ // is ours.
113
+ const LEVEL_NAME = { 10: 'TRACE', 20: 'DEBUG', 30: 'INFO ', 40: 'WARN ', 50: 'ERROR', 60: 'FATAL' };
114
+ configureLogger({
115
+ level: 'trace',
116
+ hooks: {
117
+ logMethod(args, _method, level) {
118
+ const msg = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
119
+ trace(`${C.dim}${stamp()} ${LEVEL_NAME[level] || level}${C.off} ${msg}`);
120
+ }
121
+ }
122
+ });
123
+
124
+ const isPrintable = (buf) => buf.length > 0 && buf.every(b => b === 9 || b === 10 || b === 13 || (b >= 32 && b < 127));
125
+
126
+ // JIDs decode to objects, byte fields to Buffers — render both readably.
127
+ function attrValue(v) {
128
+ if (v === null || v === undefined) return '';
129
+ if (Buffer.isBuffer(v)) return isPrintable(v) ? v.toString('utf8') : '0x' + v.toString('hex');
130
+ if (typeof v === 'object') {
131
+ if (v.user !== undefined) {
132
+ const dev = v.device ? ':' + v.device : '';
133
+ return `${v.user || ''}${dev}@${v.server || ''}`;
134
+ }
135
+ return JSON.stringify(v);
136
+ }
137
+ return String(v);
138
+ }
139
+
140
+ function renderContent(content, pad) {
141
+ if (content === null || content === undefined) return null;
142
+ if (Array.isArray(content)) return content.map(n => nodeToXml(n, pad)).join('\n');
143
+ if (Buffer.isBuffer(content)) {
144
+ if (isPrintable(content)) return pad + content.toString('utf8');
145
+ const hex = content.toString('hex');
146
+ const shown = hex.length > 512 ? hex.slice(0, 512) + '…' : hex;
147
+ return `${pad}${C.dim}[${content.length} bytes] ${shown}${C.off}`;
148
+ }
149
+ return pad + String(content);
150
+ }
151
+
152
+ function nodeToXml(node, pad) {
153
+ pad = pad || '';
154
+ if (!node || !node.description) return pad + String(node);
155
+ const attrs = Object.entries(node.attrs || {})
156
+ .map(([k, v]) => ` ${C.tag}${k}${C.off}="${attrValue(v)}"`).join('');
157
+ const tag = node.description;
158
+ const body = renderContent(node.content, pad + ' ');
159
+ if (body === null) return `${pad}<${tag}${attrs}/>`;
160
+ return `${pad}<${tag}${attrs}>\n${body}\n${pad}</${tag}>`;
161
+ }
162
+
163
+ function logStanza(dir, node) {
164
+ const colour = dir === 'OUT' ? C.out : C.in;
165
+ const arrow = dir === 'OUT' ? '──▶ SENT' : '◀── RECV';
166
+ trace(`\n${colour}${stamp()} ${arrow}${C.off}`);
167
+ trace(nodeToXml(node, ' '));
168
+ if (TRACE_BYTES) {
169
+ try {
170
+ const { encodeNode } = require('./lib/BinaryNode');
171
+ const raw = encodeNode(node);
172
+ trace(` ${C.dim}raw ${raw.length}B: ${raw.toString('hex')}${C.off}`);
173
+ } catch (_) {}
174
+ }
175
+ }
176
+
177
+ const origSendNode = NoiseSocket.prototype.sendNode;
178
+ NoiseSocket.prototype.sendNode = function (node) {
179
+ try { logStanza('OUT', node); } catch (_) {}
180
+ return origSendNode.call(this, node);
181
+ };
182
+
183
+ const origEmit = NoiseSocket.prototype.emit;
184
+ NoiseSocket.prototype.emit = function (event, ...args) {
185
+ try {
186
+ if (event === 'node') logStanza('IN', args[0]);
187
+ else if (event === 'open') trace(`\n${C.in}${stamp()} ◀── HANDSHAKE COMPLETE — channel secured${C.off}`);
188
+ else if (event === 'error') trace(`\n${C.in}${stamp()} ◀── SOCKET ERROR: ${args[0] && args[0].message}${C.off}`);
189
+ else if (event === 'close') trace(`\n${C.in}${stamp()} ◀── SOCKET CLOSED${C.off}`);
190
+ } catch (_) {}
191
+ return origEmit.apply(this, [event, ...args]);
192
+ };
193
+
194
+ // Registration (SMS request / code verify) goes over HTTPS, not the socket.
195
+ const origRequest = https.request;
196
+ https.request = function (...args) {
197
+ const opts = typeof args[0] === 'string' ? { href: args[0] } : (args[0] || {});
198
+ const host = opts.hostname || opts.host || opts.href || '?';
199
+ const target = `${opts.method || 'GET'} ${host}${opts.path || ''}`;
200
+ trace(`\n${C.http}${stamp()} ──▶ HTTP ${target}${C.off}`);
201
+ if (opts.headers) {
202
+ for (const [k, v] of Object.entries(opts.headers)) {
203
+ trace(` ${C.tag}${k}${C.off}: ${v}`);
204
+ }
205
+ }
206
+ const req = origRequest.apply(this, args);
207
+ const origWrite = req.write.bind(req);
208
+ req.write = function (chunk, ...rest) {
209
+ try { trace(` ${C.dim}body: ${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk}${C.off}`); } catch (_) {}
210
+ return origWrite(chunk, ...rest);
211
+ };
212
+ req.on('response', (res) => {
213
+ trace(`\n${C.http}${stamp()} ◀── HTTP ${res.statusCode} from ${host}${C.off}`);
214
+ let body = '';
215
+ res.on('data', d => { if (body.length < 8192) body += d.toString('utf8'); });
216
+ res.on('end', () => { if (body) trace(` ${C.dim}${body}${C.off}`); });
217
+ });
218
+ return req;
219
+ };
220
+
221
+ }
222
+
40
223
  // Read the version straight from package.json so it can never drift out of sync
41
224
  // with the published package. npm always ships package.json in the tarball,
42
225
  // regardless of the "files" list, so this resolves for installed users too.
@@ -1444,12 +1627,51 @@ options:
1444
1627
  --method sms | voice | wa_old | email (default: sms)
1445
1628
  --email <address> email address (required when --method email)
1446
1629
 
1630
+ debug:
1631
+ an interactive session asks once whether to trace the protocol.
1632
+ answer y to print every stanza sent and received, n to keep it quiet.
1633
+
1634
+ --debug trace without asking (same as WA_DEBUG=1)
1635
+ --no-debug, -q stay quiet without asking (same as WA_DEBUG=0)
1636
+ --trace-bytes also dump the raw encoded bytes of every stanza
1637
+
1447
1638
  after connecting, type /help for all available commands.
1448
1639
  `.trim();
1449
1640
 
1641
+ function announceTrace() {
1642
+ out('debug full ON — every stanza sent and received will be printed' +
1643
+ (TRACE_BYTES ? ', with raw frame bytes' : '') + '.\n');
1644
+ }
1645
+
1646
+ // Ask once, at startup, whether to trace the protocol. Commands that never
1647
+ // touch the network skip the question entirely. A non-interactive stdin (a
1648
+ // pipe, a script) is treated as "no" rather than hanging on a prompt that
1649
+ // nobody is there to answer.
1650
+ function askDebugMode(cmd) {
1651
+ if (TRACE_FORCE_ON) { enableWireTrace(); announceTrace(); return Promise.resolve(); }
1652
+ if (TRACE_FORCE_OFF) return Promise.resolve();
1653
+ const OFFLINE = ['version', '--version', '-v', 'help', '--help', '-h'];
1654
+ if (cmd && OFFLINE.includes(cmd)) return Promise.resolve();
1655
+ if (!process.stdin.isTTY) return Promise.resolve();
1656
+
1657
+ return new Promise(resolve => {
1658
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1659
+ rl.question('do you want debug full? [y/N] ', (answer) => {
1660
+ rl.close();
1661
+ if (/^y(es)?$/i.test(String(answer).trim())) {
1662
+ enableWireTrace();
1663
+ announceTrace();
1664
+ }
1665
+ resolve();
1666
+ });
1667
+ });
1668
+ }
1669
+
1450
1670
  async function main() {
1451
1671
  const { cmd, sub, flags, pos } = parseArgs(process.argv);
1452
1672
 
1673
+ await askDebugMode(cmd);
1674
+
1453
1675
  _sessDir = flags.session || defaultSessionDir();
1454
1676
 
1455
1677
  if (!cmd) {
@@ -424,7 +424,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
424
424
  'manage_call_permission', 'false',
425
425
  'clicked_education_link', 'false',
426
426
  'aid', '',
427
- 'push_code', ''
427
+ 'push_code', store.pushCode || ''
428
428
  ];
429
429
  }
430
430
 
@@ -434,7 +434,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
434
434
  'sim_mcc', meta.mcc,
435
435
  'sim_mnc', meta.mnc,
436
436
  'jailbroken', '0',
437
- 'push_code', '',
437
+ 'push_code', store.pushCode || '',
438
438
  'cellular_strength', '1'
439
439
  ];
440
440
  }
package/lib/Store.js CHANGED
@@ -64,6 +64,11 @@ function createNewStore(phoneNumber) {
64
64
  // Registration.js): advertising id (UUID) and the 20-byte backup token.
65
65
  const advertisingId = uuidv4();
66
66
  const backupToken = crypto.randomBytes(20);
67
+ // Push-notification token sent as `push_code` on /code. The server validates
68
+ // its shape and rejects an empty value with bad_param/bad_format, so it has
69
+ // to be a real 32-hex-character token. Generated once and persisted, because
70
+ // the value must stay the same across the /code and /register pair.
71
+ const pushCode = crypto.randomBytes(16).toString('hex');
67
72
 
68
73
  const device = getDeviceConfig();
69
74
  const version = device.os === 'android' ? ANDROID_VERSION_FALLBACK : IOS_VERSION_FALLBACK;
@@ -86,6 +91,7 @@ function createNewStore(phoneNumber) {
86
91
  identityId,
87
92
  advertisingId,
88
93
  backupToken,
94
+ pushCode,
89
95
  registered: false,
90
96
  codePending: false, // true after /code request, cleared on successful /register
91
97
  name: 'User',
@@ -140,6 +146,7 @@ function storeToJson(store) {
140
146
  identityId: store.identityId.toString('base64'),
141
147
  advertisingId: store.advertisingId || null,
142
148
  backupToken: store.backupToken ? store.backupToken.toString('base64') : null,
149
+ pushCode: store.pushCode || null,
143
150
  registered: !!store.registered,
144
151
  codePending: store.codePending || false,
145
152
  name,
@@ -182,6 +189,7 @@ function storeFromJson(obj) {
182
189
  // enriched /code request always has stable values.
183
190
  advertisingId: obj.advertisingId || uuidv4(),
184
191
  backupToken: obj.backupToken ? Buffer.from(obj.backupToken, 'base64') : crypto.randomBytes(20),
192
+ pushCode: obj.pushCode || crypto.randomBytes(16).toString('hex'),
185
193
  registered: !!obj.registered,
186
194
  codePending: obj.codePending || false,
187
195
  name,
@@ -862,14 +862,6 @@ class MessageSender {
862
862
  // Resolve member phones for session/device queries.
863
863
  // For LID members without a PN mapping, keep the raw LID user part so
864
864
  // bulkEnsureSessionsForLid can still acquire sessions for them.
865
- // "40778226909:2@s.whatsapp.net" → "<ourLid>:2@lid" — keep the device
866
- // suffix, swap identity and domain.
867
- const ownJidToLid = (jid) => {
868
- const base = String(jid).replace(/@(?:s\.whatsapp\.net|lid)$/, '');
869
- const colon = base.indexOf(':');
870
- return myLidUser + (colon >= 0 ? base.slice(colon) : '') + '@lid';
871
- };
872
-
873
865
  const lidMembersWithoutPn = [];
874
866
  const memberPhones = [...new Set(
875
867
  members.map(jid => {
@@ -883,10 +875,10 @@ class MessageSender {
883
875
  const pn = this._client._lidToPn && this._client._lidToPn.get(raw);
884
876
  if (pn) return pn;
885
877
  }
886
- // Our own LID is a member too ensureOwnDeviceSessions already
887
- // covers those devices, so listing it here would encrypt the SKDM
888
- // for ourselves twice and emit duplicate <to> entries.
889
- if (raw !== myLidUser) lidMembersWithoutPn.push(jid);
878
+ // We are a member of the group ourselves, so our own devices come out
879
+ // of this same walk Baileys likewise derives every group target
880
+ // from the participant list and never appends own devices separately.
881
+ lidMembersWithoutPn.push(jid);
890
882
  return null;
891
883
  }
892
884
  return raw;
@@ -907,20 +899,30 @@ class MessageSender {
907
899
  ]);
908
900
 
909
901
  // Own devices come back as @s.whatsapp.net — re-express them in LID space
910
- // so every target in a LID stanza shares one address family.
911
- const ownTargets = useLid ? ownDevices.map(ownJidToLid) : ownDevices;
902
+ // so every target in a LID stanza shares one address family. In LID mode
903
+ // the participant walk above already covered us, so this only fills the gap
904
+ // for legacy pn-addressed groups.
905
+ const ownTargets = useLid ? [] : ownDevices;
912
906
 
913
907
  // Dedupe: a device reached through more than one path must still appear
914
908
  // exactly once in <participants>.
915
909
  const allTargets = [...new Set([...memberDevices, ...lidDevices, ...ownTargets])];
916
910
 
917
- const phashTargets = allTargets.includes(selfJid)
918
- ? allTargets
919
- : [selfJid, ...allTargets];
920
-
921
911
  const skStore = this._signal.senderKeyStore;
922
912
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
923
- const skdmRecipients = allTargets.filter(jid => !existingSkdmMap[jid]);
913
+ // Same exclusions Baileys applies when choosing SenderKey recipients:
914
+ // hosted (bot-side) users and device 99, which is a server placeholder
915
+ // rather than a real endpoint, must never receive a distribution.
916
+ const isSkdmEligible = (jid) => {
917
+ const s = String(jid);
918
+ if (s.endsWith('@hosted') || s.endsWith('@hosted.lid')) return false;
919
+ const base = s.replace(/@.*$/, '');
920
+ const colon = base.indexOf(':');
921
+ return !(colon >= 0 && parseInt(base.slice(colon + 1), 10) === 99);
922
+ };
923
+ const skdmRecipients = allTargets.filter(
924
+ jid => !existingSkdmMap[jid] && isSkdmEligible(jid)
925
+ );
924
926
 
925
927
  // Acquire mutex for all group encryption — SKDM fanout and senderKey encrypt
926
928
  // share the same Signal session state and must not run concurrently.
@@ -934,22 +936,34 @@ class MessageSender {
934
936
  skmsgCiphertext = await this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
935
937
  });
936
938
 
937
- const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
938
-
939
939
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
940
940
  const advBytes = skdmHasPkmsg ? _buildOrGetAdvIdentity(this._client._store) : null;
941
+ // No device-identity is expected here. ADVSignedDeviceIdentity is issued
942
+ // when a companion device is paired, to prove the companion's identity key
943
+ // was authorised by the primary. This client registers over SMS as the
944
+ // primary device, so its identity key IS the account identity and there is
945
+ // nothing to attach — advBytes stays null and that is correct.
941
946
 
942
947
  const msgContent = [];
943
948
  if (advBytes) {
944
949
  msgContent.push(new BinaryNode('device-identity', {}, advBytes));
945
950
  }
946
951
  if (skdmEncrypted.length > 0) {
947
- msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, null));
952
+ // Baileys applies the same extra attrs — mediatype included — to the
953
+ // SenderKey participant nodes as to the skmsg, so an image sent to a
954
+ // group carries mediatype on both. Passing null here left the SKDM
955
+ // <enc> nodes unlabelled.
956
+ msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, mediaSubtype));
948
957
  }
949
958
  const skmsgAttrs = { type: 'skmsg', v: '2' };
950
959
  if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
951
960
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
952
961
 
962
+ // No phash on group stanzas. Baileys computes a participant hash only on
963
+ // the 1:1 path; a group message carries id/to/type/addressing_mode and
964
+ // nothing else. Sending a hash the server did not ask for — over a
965
+ // participant set it derives itself from the group — is a way to have the
966
+ // stanza rejected outright.
953
967
  const stanzaAttrs = {
954
968
  to: jidStrToObj(groupJid),
955
969
  id: msgId,
@@ -957,7 +971,6 @@ class MessageSender {
957
971
  addressing_mode: groupAddressingMode,
958
972
  t: String(msNow())
959
973
  };
960
- if (phash) stanzaAttrs.phash = phash;
961
974
  if (options.edit) stanzaAttrs.edit = String(options.edit);
962
975
 
963
976
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.66",
3
+ "version": "5.5.68",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",