whalibmob 5.5.60 → 5.5.62
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/.env.example +78 -0
- package/cli.js +6 -1
- package/lib/Client.js +46 -7
- package/lib/messages/MessageSender.js +43 -7
- package/lib/signal/WaSignalGroup/group_cipher.js +1 -1
- package/lib/signal/WaSignalGroup/keyhelper.js +1 -1
- package/lib/signal/WaSignalGroup/sender-chain-key.js +1 -1
- package/lib/signal/WaSignalGroup/sender-key-message.js +1 -1
- package/lib/signal/WaSignalGroup/sender-message-key.js +1 -1
- package/package.json +1 -1
package/.env.example
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# whalibmob — example environment configuration
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to `.env` in your project root and uncomment what you need:
|
|
4
|
+
#
|
|
5
|
+
# cp node_modules/whalibmob/.env.example .env
|
|
6
|
+
#
|
|
7
|
+
# The CLI loads `.env` from the current working directory automatically.
|
|
8
|
+
# If you use the library programmatically, call `require('dotenv').config()`
|
|
9
|
+
# yourself before creating a client.
|
|
10
|
+
#
|
|
11
|
+
# Every variable below is OPTIONAL — with an empty .env the library runs with
|
|
12
|
+
# the defaults shown next to each entry.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ─── Device profile ──────────────────────────────────────────────────────────
|
|
16
|
+
# Which platform to identify as during registration and connection.
|
|
17
|
+
# Values: ios | android
|
|
18
|
+
# WA_OS=ios
|
|
19
|
+
|
|
20
|
+
# Named predefined device profile. Takes priority over the individual
|
|
21
|
+
# WA_DEVICE_* variables below.
|
|
22
|
+
#
|
|
23
|
+
# ios iphone15pro, iphone15, iphone14, iphone14pro, iphone13, iphone12
|
|
24
|
+
# android samsung-s24-ultra, samsung-s24, samsung-s23, samsung-s23-ultra,
|
|
25
|
+
# samsung-a55, pixel8pro, pixel8, pixel7, pixel7a, xiaomi14,
|
|
26
|
+
# xiaomi13, oneplus12, oneplus11, oppo-find-x7, realme-gt5
|
|
27
|
+
#
|
|
28
|
+
# Default: iphone15pro when WA_OS=ios, samsungs24ultra when WA_OS=android
|
|
29
|
+
# WA_DEVICE=iphone15pro
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─── Custom device profile ───────────────────────────────────────────────────
|
|
33
|
+
# Only used when WA_DEVICE does not match a predefined profile. Any value left
|
|
34
|
+
# unset falls back to the default profile for the selected WA_OS.
|
|
35
|
+
# The defaults shown here are the iOS ones (iPhone 15 Pro).
|
|
36
|
+
|
|
37
|
+
# Device display name — e.g. "iPhone 15 Pro", "Samsung Galaxy S24 Ultra"
|
|
38
|
+
# WA_DEVICE_MODEL=iPhone 15 Pro
|
|
39
|
+
|
|
40
|
+
# Apple | Samsung | Google | Xiaomi | OnePlus …
|
|
41
|
+
# WA_DEVICE_MANUFACTURER=Apple
|
|
42
|
+
|
|
43
|
+
# OS version string — e.g. 17.4.1 for iOS, 14 for Android
|
|
44
|
+
# WA_DEVICE_OS_VERSION=17.4.1
|
|
45
|
+
|
|
46
|
+
# OS build number — e.g. 21E236 for iOS, UP1A.231005.007 for Android
|
|
47
|
+
# WA_DEVICE_BUILD=21E236
|
|
48
|
+
|
|
49
|
+
# Internal model identifier — e.g. iPhone16,1 for iOS, SM-S928B for Android
|
|
50
|
+
# WA_DEVICE_MODEL_ID=iPhone16,1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ─── WhatsApp version ────────────────────────────────────────────────────────
|
|
54
|
+
# Pin the WhatsApp client version. When set, the live version lookup is skipped
|
|
55
|
+
# entirely. Leave unset to fetch the current version automatically (recommended).
|
|
56
|
+
# WA_VERSION=2.24.10.75
|
|
57
|
+
|
|
58
|
+
# Override the static registration token. Leave unset to use the built-in token
|
|
59
|
+
# for the selected platform — only change this if you know what you are doing.
|
|
60
|
+
# WA_STATIC_TOKEN=
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ─── Proxy ───────────────────────────────────────────────────────────────────
|
|
64
|
+
# Route registration traffic through a SOCKS5 proxy, given as host:port.
|
|
65
|
+
# TOR_PROXY takes precedence over SOCKS_PROXY when both are set.
|
|
66
|
+
# Default Tor SOCKS port is 9050.
|
|
67
|
+
# TOR_PROXY=127.0.0.1:9050
|
|
68
|
+
# SOCKS_PROXY=127.0.0.1:1080
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─── Frida attestation bridge ────────────────────────────────────────────────
|
|
72
|
+
# Host running the Frida attestation helper (see the frida/ directory).
|
|
73
|
+
# Leave unset to disable the bridge.
|
|
74
|
+
# WA_FRIDA_HOST=127.0.0.1
|
|
75
|
+
|
|
76
|
+
# Port the helper listens on.
|
|
77
|
+
# Default: 1119 (WhatsApp consumer; the Business build listens on 1120)
|
|
78
|
+
# WA_FRIDA_PORT=1119
|
package/cli.js
CHANGED
|
@@ -37,7 +37,12 @@ const {
|
|
|
37
37
|
|
|
38
38
|
const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Read the version straight from package.json so it can never drift out of sync
|
|
41
|
+
// with the published package. npm always ships package.json in the tarball,
|
|
42
|
+
// regardless of the "files" list, so this resolves for installed users too.
|
|
43
|
+
const VERSION = (() => {
|
|
44
|
+
try { return require('./package.json').version; } catch (_) { return 'unknown'; }
|
|
45
|
+
})();
|
|
41
46
|
|
|
42
47
|
// ─── output helpers ───────────────────────────────────────────────────────────
|
|
43
48
|
|
package/lib/Client.js
CHANGED
|
@@ -762,7 +762,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
762
762
|
this._sendReadReceipt(id, fromRaw, partRaw);
|
|
763
763
|
})
|
|
764
764
|
.catch(err => {
|
|
765
|
+
_whaDbg('[DBG] GROUP_ERR id=' + id + ' from=' + from +
|
|
766
|
+
' participant=' + participant + ' err=' + (err && err.message));
|
|
765
767
|
this.emit('decrypt_error', { id, from, participant, err });
|
|
768
|
+
// Ask the sender to re-send with a fresh SenderKey distribution.
|
|
769
|
+
// Without this a group we joined after the sender distributed its key
|
|
770
|
+
// — or any group whose SKDM we missed — stays permanently
|
|
771
|
+
// undecryptable, failing every message with "No session found to
|
|
772
|
+
// decrypt message". The DM path already does this; groups need it
|
|
773
|
+
// just as much, and the retry must be addressed to the participant
|
|
774
|
+
// that actually encrypted the message, not to the group JID.
|
|
775
|
+
this._sendRetryRequest(id, node);
|
|
766
776
|
});
|
|
767
777
|
}
|
|
768
778
|
|
|
@@ -1244,6 +1254,15 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1244
1254
|
if (!this._socket || !this._connected) return;
|
|
1245
1255
|
const mcId = this._genMsgId();
|
|
1246
1256
|
_whaDbg('[DBG] SEND media_conn IQ id=' + mcId);
|
|
1257
|
+
// Match the reply by request id. The server echoes back only `from`, `id`
|
|
1258
|
+
// and `type='result'` — it does NOT repeat the `xmlns` of the request, so
|
|
1259
|
+
// keying off xmlns alone would never fire and every upload would stall
|
|
1260
|
+
// until the 15 s "Timeout waiting for media connection".
|
|
1261
|
+
this._pendingIqs.set(mcId, (resp) => {
|
|
1262
|
+
this._onMediaConnectionResult(resp);
|
|
1263
|
+
});
|
|
1264
|
+
// Don't leak the handler if the server never answers.
|
|
1265
|
+
setTimeout(() => this._pendingIqs.delete(mcId), 20000);
|
|
1247
1266
|
this._socket.sendNode(new BinaryNode('iq', {
|
|
1248
1267
|
id: mcId,
|
|
1249
1268
|
to: 's.whatsapp.net',
|
|
@@ -1254,7 +1273,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1254
1273
|
|
|
1255
1274
|
_onMediaConnectionResult(node) {
|
|
1256
1275
|
const connNode = findChild(node, 'media_conn');
|
|
1257
|
-
if (!connNode)
|
|
1276
|
+
if (!connNode) {
|
|
1277
|
+
// Server answered but refused (or the IQ handler timed out and passed
|
|
1278
|
+
// nothing). Surface it so the waiter fails fast with a real reason
|
|
1279
|
+
// instead of sitting out the full media-connection timeout.
|
|
1280
|
+
const reason = (node && node.attrs && node.attrs.type === 'error')
|
|
1281
|
+
? ('server returned IQ error' + (node.attrs.code ? ' ' + node.attrs.code : ''))
|
|
1282
|
+
: 'no media_conn node in server reply';
|
|
1283
|
+
_whaDbg('[DBG] media_conn failed: ' + reason);
|
|
1284
|
+
this.emit('media_conn_error', new Error('Media connection refused: ' + reason));
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1258
1287
|
const auth = (connNode.attrs && connNode.attrs.auth) || '';
|
|
1259
1288
|
const ttl = connNode.attrs && connNode.attrs.ttl ? parseInt(connNode.attrs.ttl, 10) : 300;
|
|
1260
1289
|
const hosts = [];
|
|
@@ -1473,15 +1502,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1473
1502
|
]);
|
|
1474
1503
|
|
|
1475
1504
|
const response = await this._sendIq(node);
|
|
1476
|
-
if (!response)
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1505
|
+
if (!response) {
|
|
1506
|
+
throw new Error('fetchAllGroups: no reply from server (IQ timed out)');
|
|
1507
|
+
}
|
|
1508
|
+
if (response.attrs && response.attrs.type === 'error') {
|
|
1509
|
+
const errNode = findChild(response, 'error');
|
|
1510
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
1511
|
+
throw new Error('fetchAllGroups: server returned error' + (code ? ' ' + code : ''));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// The server wraps the list in a <groups> node: iq > groups > group[].
|
|
1515
|
+
// Fall back to scanning the iq body directly in case a server build ever
|
|
1516
|
+
// returns them unwrapped.
|
|
1517
|
+
const groupsNode = findChild(response, 'groups');
|
|
1518
|
+
const container = groupsNode || response;
|
|
1519
|
+
const children = Array.isArray(container.content) ? container.content : [];
|
|
1520
|
+
return children
|
|
1481
1521
|
.filter(n => n && n.description === 'group')
|
|
1482
1522
|
.map(n => this._parseGroupNode(n))
|
|
1483
1523
|
.filter(Boolean);
|
|
1484
|
-
return groups;
|
|
1485
1524
|
}
|
|
1486
1525
|
|
|
1487
1526
|
// ─── Group member helpers ─────────────────────────────────────────────────
|
|
@@ -813,11 +813,28 @@ class MessageSender {
|
|
|
813
813
|
// Auto-fetch group metadata if member cache is empty (first send to this group)
|
|
814
814
|
let members = this._client._getGroupMembers(groupJid);
|
|
815
815
|
if (members.length === 0 && typeof this._client.getGroupMetadata === 'function') {
|
|
816
|
+
_whaDbg('[DBG] GROUP_SEND auto-fetch metadata for ' + groupJid);
|
|
817
|
+
let metaErr = null;
|
|
816
818
|
try {
|
|
817
|
-
_whaDbg('[DBG] GROUP_SEND auto-fetch metadata for ' + groupJid);
|
|
818
819
|
await this._client.getGroupMetadata(groupJid);
|
|
819
820
|
members = this._client._getGroupMembers(groupJid);
|
|
820
|
-
} catch (
|
|
821
|
+
} catch (err) {
|
|
822
|
+
metaErr = err;
|
|
823
|
+
_whaDbg('[DBG] GROUP_SEND metadata fetch failed: ' + (err && err.message));
|
|
824
|
+
}
|
|
825
|
+
// Without the participant list we cannot establish Signal sessions with
|
|
826
|
+
// the members, so the SenderKey distribution would reach nobody and the
|
|
827
|
+
// stanza would carry an <enc type='skmsg'> that no one can decrypt. The
|
|
828
|
+
// server rejects exactly that shape with ack error 479. Fail here with a
|
|
829
|
+
// reason the caller can act on instead of emitting a doomed stanza.
|
|
830
|
+
if (members.length === 0) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
'Cannot send to ' + groupJid + ': group metadata unavailable' +
|
|
833
|
+
(metaErr ? ' (' + metaErr.message + ')' : '') +
|
|
834
|
+
' — no Signal session can be established with the members. ' +
|
|
835
|
+
'Check that the account is still a participant of this group.'
|
|
836
|
+
);
|
|
837
|
+
}
|
|
821
838
|
}
|
|
822
839
|
|
|
823
840
|
// Determine addressing mode for this group (lid = modern groups, pn = legacy)
|
|
@@ -1153,20 +1170,39 @@ class MessageSender {
|
|
|
1153
1170
|
}
|
|
1154
1171
|
|
|
1155
1172
|
async _fetchMediaConn() {
|
|
1173
|
+
if (!this._client || !this._client._connected) {
|
|
1174
|
+
throw new Error('Cannot fetch media connection: not connected');
|
|
1175
|
+
}
|
|
1156
1176
|
return new Promise((resolve, reject) => {
|
|
1157
|
-
const
|
|
1177
|
+
const cleanup = () => {
|
|
1178
|
+
clearTimeout(timeout);
|
|
1158
1179
|
this._client.removeListener('media_conn', onConn);
|
|
1180
|
+
this._client.removeListener('media_conn_error', onErr);
|
|
1181
|
+
};
|
|
1182
|
+
const timeout = setTimeout(() => {
|
|
1183
|
+
cleanup();
|
|
1159
1184
|
reject(new Error('Timeout waiting for media connection'));
|
|
1160
1185
|
}, 15000);
|
|
1161
1186
|
const onConn = ({ hosts, auth, ttl }) => {
|
|
1162
|
-
|
|
1163
|
-
if (hosts
|
|
1164
|
-
|
|
1187
|
+
cleanup();
|
|
1188
|
+
if (!hosts || !hosts.length) {
|
|
1189
|
+
return reject(new Error('Media connection returned no upload hosts'));
|
|
1165
1190
|
}
|
|
1191
|
+
this.setMediaConnection(hosts, auth, ttl, Date.now());
|
|
1166
1192
|
resolve();
|
|
1167
1193
|
};
|
|
1194
|
+
const onErr = (err) => {
|
|
1195
|
+
cleanup();
|
|
1196
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1197
|
+
};
|
|
1168
1198
|
this._client.once('media_conn', onConn);
|
|
1169
|
-
|
|
1199
|
+
this._client.once('media_conn_error', onErr);
|
|
1200
|
+
try {
|
|
1201
|
+
this._client._requestMediaConnection();
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
cleanup();
|
|
1204
|
+
reject(err);
|
|
1205
|
+
}
|
|
1170
1206
|
});
|
|
1171
1207
|
}
|
|
1172
1208
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { decrypt, encrypt } = require('libsignal/
|
|
3
|
+
const { decrypt, encrypt } = require('../libsignal/crypto');
|
|
4
4
|
const { SenderKeyMessage } = require('./sender-key-message');
|
|
5
5
|
const { SenderKeyRecord } = require('./sender-key-record');
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { calculateSignature, verifySignature } = require('libsignal/
|
|
3
|
+
const { calculateSignature, verifySignature } = require('../libsignal/curve');
|
|
4
4
|
const { CiphertextMessage } = require('./ciphertext-message');
|
|
5
5
|
|
|
6
6
|
// ─── Minimal protobuf encode/decode for SenderKeyMessage ─────────────────────
|