violetics 7.0.1-alpha → 7.0.2-alpha

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.
Files changed (114) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +1001 -232
  3. package/WAProto/index.js +75379 -142631
  4. package/engine-requirements.js +11 -8
  5. package/lib/Defaults/index.js +132 -146
  6. package/lib/Signal/Group/ciphertext-message.js +2 -6
  7. package/lib/Signal/Group/group-session-builder.js +7 -42
  8. package/lib/Signal/Group/group_cipher.js +37 -52
  9. package/lib/Signal/Group/index.js +11 -57
  10. package/lib/Signal/Group/keyhelper.js +7 -45
  11. package/lib/Signal/Group/sender-chain-key.js +7 -16
  12. package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
  13. package/lib/Signal/Group/sender-key-message.js +9 -13
  14. package/lib/Signal/Group/sender-key-name.js +2 -6
  15. package/lib/Signal/Group/sender-key-record.js +9 -22
  16. package/lib/Signal/Group/sender-key-state.js +27 -43
  17. package/lib/Signal/Group/sender-message-key.js +4 -8
  18. package/lib/Signal/libsignal.js +319 -94
  19. package/lib/Signal/lid-mapping.js +224 -139
  20. package/lib/Socket/Client/index.js +2 -19
  21. package/lib/Socket/Client/types.js +10 -0
  22. package/lib/Socket/Client/websocket.js +53 -0
  23. package/lib/Socket/business.js +162 -44
  24. package/lib/Socket/chats.js +477 -418
  25. package/lib/Socket/communities.js +430 -0
  26. package/lib/Socket/groups.js +110 -99
  27. package/lib/Socket/index.js +10 -10
  28. package/lib/Socket/messages-recv.js +884 -561
  29. package/lib/Socket/messages-send.js +859 -428
  30. package/lib/Socket/mex.js +41 -0
  31. package/lib/Socket/newsletter.js +195 -390
  32. package/lib/Socket/socket.js +465 -315
  33. package/lib/Store/index.js +3 -10
  34. package/lib/Store/make-in-memory-store.js +73 -79
  35. package/lib/Store/make-ordered-dictionary.js +4 -7
  36. package/lib/Store/object-repository.js +2 -6
  37. package/lib/Types/Auth.js +1 -2
  38. package/lib/Types/Bussines.js +1 -0
  39. package/lib/Types/Call.js +1 -2
  40. package/lib/Types/Chat.js +7 -4
  41. package/lib/Types/Contact.js +1 -2
  42. package/lib/Types/Events.js +1 -2
  43. package/lib/Types/GroupMetadata.js +1 -2
  44. package/lib/Types/Label.js +2 -5
  45. package/lib/Types/LabelAssociation.js +2 -5
  46. package/lib/Types/Message.js +17 -9
  47. package/lib/Types/Newsletter.js +33 -38
  48. package/lib/Types/Product.js +1 -2
  49. package/lib/Types/Signal.js +1 -2
  50. package/lib/Types/Socket.js +2 -2
  51. package/lib/Types/State.js +12 -2
  52. package/lib/Types/USync.js +1 -2
  53. package/lib/Types/index.js +14 -31
  54. package/lib/Utils/auth-utils.js +228 -152
  55. package/lib/Utils/browser-utils.js +28 -0
  56. package/lib/Utils/business.js +66 -70
  57. package/lib/Utils/chat-utils.js +331 -249
  58. package/lib/Utils/crypto.js +57 -91
  59. package/lib/Utils/decode-wa-message.js +168 -84
  60. package/lib/Utils/event-buffer.js +138 -80
  61. package/lib/Utils/generics.js +180 -297
  62. package/lib/Utils/history.js +83 -49
  63. package/lib/Utils/identity-change-handler.js +48 -0
  64. package/lib/Utils/index.js +19 -33
  65. package/lib/Utils/link-preview.js +14 -23
  66. package/lib/Utils/logger.js +2 -7
  67. package/lib/Utils/lt-hash.js +2 -46
  68. package/lib/Utils/make-mutex.js +24 -47
  69. package/lib/Utils/message-retry-manager.js +224 -0
  70. package/lib/Utils/messages-media.js +501 -496
  71. package/lib/Utils/messages.js +1428 -362
  72. package/lib/Utils/noise-handler.js +145 -100
  73. package/lib/Utils/pre-key-manager.js +105 -0
  74. package/lib/Utils/process-message.js +356 -150
  75. package/lib/Utils/reporting-utils.js +257 -0
  76. package/lib/Utils/signal.js +78 -73
  77. package/lib/Utils/sync-action-utils.js +47 -0
  78. package/lib/Utils/tc-token-utils.js +17 -0
  79. package/lib/Utils/use-multi-file-auth-state.js +35 -45
  80. package/lib/Utils/validate-connection.js +91 -107
  81. package/lib/WABinary/constants.js +1300 -1304
  82. package/lib/WABinary/decode.js +26 -48
  83. package/lib/WABinary/encode.js +109 -155
  84. package/lib/WABinary/generic-utils.js +161 -149
  85. package/lib/WABinary/index.js +5 -21
  86. package/lib/WABinary/jid-utils.js +73 -40
  87. package/lib/WABinary/types.js +1 -2
  88. package/lib/WAM/BinaryInfo.js +2 -6
  89. package/lib/WAM/constants.js +19070 -11568
  90. package/lib/WAM/encode.js +17 -23
  91. package/lib/WAM/index.js +3 -19
  92. package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
  93. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
  94. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
  95. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
  96. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
  97. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
  98. package/lib/WAUSync/Protocols/index.js +4 -20
  99. package/lib/WAUSync/USyncQuery.js +40 -36
  100. package/lib/WAUSync/USyncUser.js +2 -6
  101. package/lib/WAUSync/index.js +3 -19
  102. package/lib/index.js +11 -44
  103. package/package.json +74 -107
  104. package/lib/Defaults/baileys-version.json +0 -3
  105. package/lib/Defaults/phonenumber-mcc.json +0 -223
  106. package/lib/Signal/Group/queue-job.js +0 -57
  107. package/lib/Socket/Client/abstract-socket-client.js +0 -13
  108. package/lib/Socket/Client/mobile-socket-client.js +0 -65
  109. package/lib/Socket/Client/web-socket-client.js +0 -118
  110. package/lib/Socket/groupStatus.js +0 -637
  111. package/lib/Socket/registration.js +0 -166
  112. package/lib/Socket/usync.js +0 -70
  113. package/lib/Store/make-cache-manager-store.js +0 -83
  114. package/lib/Utils/baileys-event-stream.js +0 -63
@@ -1,75 +1,62 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.makeSocket = void 0;
4
- const boom_1 = require("@hapi/boom");
5
- const crypto_1 = require("crypto");
6
- const url_1 = require("url");
7
- const util_1 = require("util");
8
- const WAProto_1 = require("../../WAProto");
9
- const Defaults_1 = require("../Defaults");
10
- const Types_1 = require("../Types");
11
- const Utils_1 = require("../Utils");
12
- const WABinary_1 = require("../WABinary");
13
- const Client_1 = require("./Client");
1
+ import { Boom } from '@hapi/boom';
2
+ import { randomBytes } from 'crypto';
3
+ import { URL } from 'url';
4
+ import { promisify } from 'util';
5
+ import { proto } from '../../WAProto/index.js';
6
+ import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
7
+ import { DisconnectReason } from '../Types/index.js';
8
+ import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
9
+ import { getPlatformId } from '../Utils/browser-utils.js';
10
+ import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { BinaryInfo } from '../WAM/BinaryInfo.js';
12
+ import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
+ import { WebSocketClient } from './Client/index.js';
14
14
  /**
15
15
  * Connects to WA servers and performs:
16
16
  * - simple queries (no retry mechanism, wait for connection establishment)
17
17
  * - listen to messages and emit events
18
18
  * - query phone connection
19
- */
20
- const makeSocket = (config) => {
21
- var _a, _b;
22
- const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository, connectJitterMs, } = config;
23
- const url = typeof waWebSocketUrl === 'string' ? new url_1.URL(waWebSocketUrl) : waWebSocketUrl;
19
+ */
20
+ export const makeSocket = (config) => {
21
+ const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
22
+ const publicWAMBuffer = new BinaryInfo();
23
+ let serverTimeOffsetMs = 0;
24
+ const uqTagId = generateMdTagPrefix();
25
+ const generateMessageTag = () => `${uqTagId}${epoch++}`;
26
+ if (printQRInTerminal) {
27
+ logger.warn({}, '⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.');
28
+ }
29
+ const syncDisabled = PROCESSABLE_HISTORY_TYPES.map(syncType => config.shouldSyncHistoryMessage({ syncType })).filter(x => x === false)
30
+ .length === PROCESSABLE_HISTORY_TYPES.length;
31
+ if (syncDisabled) {
32
+ logger.warn('⚠️ DANGER: DISABLING ALL SYNC BY shouldSyncHistoryMsg PREVENTS BAILEYS FROM ACCESSING INITIAL LID MAPPINGS, LEADING TO INSTABILIY AND SESSION ERRORS');
33
+ }
34
+ const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl;
24
35
  if (config.mobile || url.protocol === 'tcp:') {
25
- throw new boom_1.Boom('Mobile API is not supported anymore', {
26
- statusCode: Types_1.DisconnectReason.loggedOut
27
- });
36
+ throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut });
28
37
  }
29
- if (url.protocol === 'wss' && ((_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.routingInfo)) {
38
+ if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
30
39
  url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
31
40
  }
32
- const ws = new Client_1.WebSocketClient(url, config);
33
- // If connectJitterMs is set, stagger the initial connection by a random delay.
34
- // This prevents thundering-herd when many bots start simultaneously.
35
- // Uses setTimeout (not await) because makeSocket is a sync arrow function.
36
- if (connectJitterMs && connectJitterMs > 0) {
37
- const jitter = Math.floor(Math.random() * connectJitterMs);
38
- logger.debug({ jitter }, 'delaying connect by jitter ms');
39
- setTimeout(() => ws.connect(), jitter);
40
- } else {
41
- ws.connect();
42
- }
43
-
44
- const ev = (0, Utils_1.makeEventBuffer)(logger);
45
41
  /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
46
- const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();
42
+ const ephemeralKeyPair = Curve.generateKeyPair();
47
43
  /** WA noise protocol wrapper */
48
- const noise = (0, Utils_1.makeNoiseHandler)({
44
+ const noise = makeNoiseHandler({
49
45
  keyPair: ephemeralKeyPair,
50
- NOISE_HEADER: Defaults_1.NOISE_WA_HEADER,
46
+ NOISE_HEADER: NOISE_WA_HEADER,
51
47
  logger,
52
- routingInfo: (_b = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _b === void 0 ? void 0 : _b.routingInfo
48
+ routingInfo: authState?.creds?.routingInfo
53
49
  });
54
- const { creds } = authState;
55
- // add transaction capability
56
- const keys = (0, Utils_1.addTransactionCapability)(authState.keys, logger, transactionOpts);
57
- const signalRepository = makeSignalRepository({ creds, keys });
58
- let lastDateRecv;
59
- let epoch = 1;
60
- let keepAliveReq;
61
- let qrTimer;
62
- let closed = false;
63
- const uqTagId = (0, Utils_1.generateMdTagPrefix)();
64
- const generateMessageTag = () => `${uqTagId}${epoch++}`;
65
- const sendPromise = (0, util_1.promisify)(ws.send);
50
+ const ws = new WebSocketClient(url, config);
51
+ ws.connect();
52
+ const sendPromise = promisify(ws.send);
66
53
  /** send a raw buffer */
67
54
  const sendRawMessage = async (data) => {
68
55
  if (!ws.isOpen) {
69
- throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
56
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
70
57
  }
71
58
  const bytes = noise.encodeFrame(data);
72
- await (0, Utils_1.promiseTimeout)(connectTimeoutMs, async (resolve, reject) => {
59
+ await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
73
60
  try {
74
61
  await sendPromise.call(ws, bytes);
75
62
  resolve();
@@ -82,50 +69,11 @@ const makeSocket = (config) => {
82
69
  /** send a binary node */
83
70
  const sendNode = (frame) => {
84
71
  if (logger.level === 'trace') {
85
- logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'xml send' });
72
+ logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' });
86
73
  }
87
- const buff = (0, WABinary_1.encodeBinaryNode)(frame);
74
+ const buff = encodeBinaryNode(frame);
88
75
  return sendRawMessage(buff);
89
76
  };
90
- /** log & process any unexpected errors */
91
- const onUnexpectedError = (err, msg) => {
92
- logger.error({ err }, `unexpected error in '${msg}'`);
93
- const message = (err && ((err.stack || err.message) || String(err))).toLowerCase();
94
- if (message.includes('bad mac') || (message.includes('mac') && message.includes('invalid'))) {
95
- try {
96
- uploadPreKeys()
97
- .catch(e => logger.warn({ e }, 'failed to re-upload prekeys after bad mac'));
98
- }
99
- catch (_e) {
100
- }
101
- }
102
- };
103
- /** await the next incoming message */
104
- const awaitNextMessage = async (sendMsg) => {
105
- if (!ws.isOpen) {
106
- throw new boom_1.Boom('Connection Closed', {
107
- statusCode: Types_1.DisconnectReason.connectionClosed
108
- });
109
- }
110
- let onOpen;
111
- let onClose;
112
- const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {
113
- onOpen = resolve;
114
- onClose = mapWebSocketError(reject);
115
- ws.on('frame', onOpen);
116
- ws.on('close', onClose);
117
- ws.on('error', onClose);
118
- })
119
- .finally(() => {
120
- ws.off('frame', onOpen);
121
- ws.off('close', onClose);
122
- ws.off('error', onClose);
123
- });
124
- if (sendMsg) {
125
- sendRawMessage(sendMsg).catch(onClose);
126
- }
127
- return result;
128
- };
129
77
  /**
130
78
  * Wait for a message with a certain tag to be received
131
79
  * @param msgId the message tag to await
@@ -135,21 +83,38 @@ const makeSocket = (config) => {
135
83
  let onRecv;
136
84
  let onErr;
137
85
  try {
138
- const result = await (0, Utils_1.promiseTimeout)(timeoutMs, (resolve, reject) => {
139
- onRecv = resolve;
86
+ const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
87
+ onRecv = data => {
88
+ resolve(data);
89
+ };
140
90
  onErr = err => {
141
- reject(err || new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }));
91
+ reject(err ||
92
+ new Boom('Connection Closed', {
93
+ statusCode: DisconnectReason.connectionClosed
94
+ }));
142
95
  };
143
96
  ws.on(`TAG:${msgId}`, onRecv);
144
97
  ws.on('close', onErr);
145
98
  ws.on('error', onErr);
99
+ return () => reject(new Boom('Query Cancelled'));
146
100
  });
147
101
  return result;
148
102
  }
103
+ catch (error) {
104
+ // Catch timeout and return undefined instead of throwing
105
+ if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
106
+ logger?.warn?.({ msgId }, 'timed out waiting for message');
107
+ return undefined;
108
+ }
109
+ throw error;
110
+ }
149
111
  finally {
150
- ws.off(`TAG:${msgId}`, onRecv);
151
- ws.off('close', onErr);
152
- ws.off('error', onErr);
112
+ if (onRecv)
113
+ ws.off(`TAG:${msgId}`, onRecv);
114
+ if (onErr) {
115
+ ws.off('close', onErr);
116
+ ws.off('error', onErr);
117
+ }
153
118
  }
154
119
  };
155
120
  /** send a query, and wait for its response. auto-generates message ID if not provided */
@@ -158,12 +123,179 @@ const makeSocket = (config) => {
158
123
  node.attrs.id = generateMessageTag();
159
124
  }
160
125
  const msgId = node.attrs.id;
161
- const [result] = await Promise.all([
162
- waitForMessage(msgId, timeoutMs),
126
+ const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
127
+ const result = waitForMessage(msgId, timeoutMs).catch(reject);
163
128
  sendNode(node)
164
- ]);
165
- if ('tag' in result) {
166
- (0, WABinary_1.assertNodeErrorFree)(result);
129
+ .then(async () => resolve(await result))
130
+ .catch(reject);
131
+ });
132
+ if (result && 'tag' in result) {
133
+ assertNodeErrorFree(result);
134
+ }
135
+ return result;
136
+ };
137
+ // Validate current key-bundle on server; on failure, trigger pre-key upload and rethrow
138
+ const digestKeyBundle = async () => {
139
+ const res = await query({
140
+ tag: 'iq',
141
+ attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
142
+ content: [{ tag: 'digest', attrs: {} }]
143
+ });
144
+ const digestNode = getBinaryNodeChild(res, 'digest');
145
+ if (!digestNode) {
146
+ await uploadPreKeys();
147
+ throw new Error('encrypt/get digest returned no digest node');
148
+ }
149
+ };
150
+ // Rotate our signed pre-key on server; on failure, run digest as fallback and rethrow
151
+ const rotateSignedPreKey = async () => {
152
+ const newId = (creds.signedPreKey.keyId || 0) + 1;
153
+ const skey = await signedKeyPair(creds.signedIdentityKey, newId);
154
+ await query({
155
+ tag: 'iq',
156
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
157
+ content: [
158
+ {
159
+ tag: 'rotate',
160
+ attrs: {},
161
+ content: [xmppSignedPreKey(skey)]
162
+ }
163
+ ]
164
+ });
165
+ // Persist new signed pre-key in creds
166
+ ev.emit('creds.update', { signedPreKey: skey });
167
+ };
168
+ const executeUSyncQuery = async (usyncQuery) => {
169
+ if (usyncQuery.protocols.length === 0) {
170
+ throw new Boom('USyncQuery must have at least one protocol');
171
+ }
172
+ // todo: validate users, throw WARNING on no valid users
173
+ // variable below has only validated users
174
+ const validUsers = usyncQuery.users;
175
+ const userNodes = validUsers.map(user => {
176
+ return {
177
+ tag: 'user',
178
+ attrs: {
179
+ jid: !user.phone ? user.id : undefined
180
+ },
181
+ content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
182
+ };
183
+ });
184
+ const listNode = {
185
+ tag: 'list',
186
+ attrs: {},
187
+ content: userNodes
188
+ };
189
+ const queryNode = {
190
+ tag: 'query',
191
+ attrs: {},
192
+ content: usyncQuery.protocols.map(a => a.getQueryElement())
193
+ };
194
+ const iq = {
195
+ tag: 'iq',
196
+ attrs: {
197
+ to: S_WHATSAPP_NET,
198
+ type: 'get',
199
+ xmlns: 'usync'
200
+ },
201
+ content: [
202
+ {
203
+ tag: 'usync',
204
+ attrs: {
205
+ context: usyncQuery.context,
206
+ mode: usyncQuery.mode,
207
+ sid: generateMessageTag(),
208
+ last: 'true',
209
+ index: '0'
210
+ },
211
+ content: [queryNode, listNode]
212
+ }
213
+ ]
214
+ };
215
+ const result = await query(iq);
216
+ return usyncQuery.parseUSyncQueryResult(result);
217
+ };
218
+ const onWhatsApp = async (...phoneNumber) => {
219
+ let usyncQuery = new USyncQuery();
220
+ let contactEnabled = false;
221
+ for (const jid of phoneNumber) {
222
+ if (isLidUser(jid)) {
223
+ logger?.warn('LIDs are not supported with onWhatsApp');
224
+ continue;
225
+ }
226
+ else {
227
+ if (!contactEnabled) {
228
+ contactEnabled = true;
229
+ usyncQuery = usyncQuery.withContactProtocol();
230
+ }
231
+ const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`;
232
+ usyncQuery.withUser(new USyncUser().withPhone(phone));
233
+ }
234
+ }
235
+ if (usyncQuery.users.length === 0) {
236
+ return []; // return early without forcing an empty query
237
+ }
238
+ const results = await executeUSyncQuery(usyncQuery);
239
+ if (results) {
240
+ return results.list.filter(a => !!a.contact).map(({ contact, id }) => ({ jid: id, exists: contact }));
241
+ }
242
+ };
243
+ const pnFromLIDUSync = async (jids) => {
244
+ const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background');
245
+ for (const jid of jids) {
246
+ if (isLidUser(jid)) {
247
+ logger?.warn('LID user found in LID fetch call');
248
+ continue;
249
+ }
250
+ else {
251
+ usyncQuery.withUser(new USyncUser().withId(jid));
252
+ }
253
+ }
254
+ if (usyncQuery.users.length === 0) {
255
+ return []; // return early without forcing an empty query
256
+ }
257
+ const results = await executeUSyncQuery(usyncQuery);
258
+ if (results) {
259
+ return results.list.filter(a => !!a.lid).map(({ lid, id }) => ({ pn: id, lid: lid }));
260
+ }
261
+ return [];
262
+ };
263
+ const ev = makeEventBuffer(logger);
264
+ const { creds } = authState;
265
+ // add transaction capability
266
+ const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
267
+ const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
268
+ let lastDateRecv;
269
+ let epoch = 1;
270
+ let keepAliveReq;
271
+ let qrTimer;
272
+ let closed = false;
273
+ /** log & process any unexpected errors */
274
+ const onUnexpectedError = (err, msg) => {
275
+ logger.error({ err }, `unexpected error in '${msg}'`);
276
+ };
277
+ /** await the next incoming message */
278
+ const awaitNextMessage = async (sendMsg) => {
279
+ if (!ws.isOpen) {
280
+ throw new Boom('Connection Closed', {
281
+ statusCode: DisconnectReason.connectionClosed
282
+ });
283
+ }
284
+ let onOpen;
285
+ let onClose;
286
+ const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
287
+ onOpen = resolve;
288
+ onClose = mapWebSocketError(reject);
289
+ ws.on('frame', onOpen);
290
+ ws.on('close', onClose);
291
+ ws.on('error', onClose);
292
+ }).finally(() => {
293
+ ws.off('frame', onOpen);
294
+ ws.off('close', onClose);
295
+ ws.off('error', onClose);
296
+ });
297
+ if (sendMsg) {
298
+ sendRawMessage(sendMsg).catch(onClose);
167
299
  }
168
300
  return result;
169
301
  };
@@ -172,30 +304,30 @@ const makeSocket = (config) => {
172
304
  let helloMsg = {
173
305
  clientHello: { ephemeral: ephemeralKeyPair.public }
174
306
  };
175
- helloMsg = WAProto_1.proto.HandshakeMessage.fromObject(helloMsg);
307
+ helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
176
308
  logger.info({ browser, helloMsg }, 'connected to WA');
177
- const init = WAProto_1.proto.HandshakeMessage.encode(helloMsg).finish();
309
+ const init = proto.HandshakeMessage.encode(helloMsg).finish();
178
310
  const result = await awaitNextMessage(init);
179
- const handshake = WAProto_1.proto.HandshakeMessage.decode(result);
311
+ const handshake = proto.HandshakeMessage.decode(result);
180
312
  logger.trace({ handshake }, 'handshake recv from WA');
181
- const keyEnc = await noise.processHandshake(handshake, creds.noiseKey);
313
+ const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
182
314
  let node;
183
315
  if (!creds.me) {
184
- node = (0, Utils_1.generateRegistrationNode)(creds, config);
316
+ node = generateRegistrationNode(creds, config);
185
317
  logger.info({ node }, 'not logged in, attempting registration...');
186
318
  }
187
319
  else {
188
- node = (0, Utils_1.generateLoginNode)(creds.me.id, config);
320
+ node = generateLoginNode(creds.me.id, config);
189
321
  logger.info({ node }, 'logging in...');
190
322
  }
191
- const payloadEnc = noise.encrypt(WAProto_1.proto.ClientPayload.encode(node).finish());
192
- await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({
323
+ const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
324
+ await sendRawMessage(proto.HandshakeMessage.encode({
193
325
  clientFinish: {
194
326
  static: keyEnc,
195
- payload: payloadEnc,
196
- },
327
+ payload: payloadEnc
328
+ }
197
329
  }).finish());
198
- noise.finishInit();
330
+ await noise.finishInit();
199
331
  startKeepAliveRequest();
200
332
  };
201
333
  const getAvailablePreKeysOnServer = async () => {
@@ -205,49 +337,50 @@ const makeSocket = (config) => {
205
337
  id: generateMessageTag(),
206
338
  xmlns: 'encrypt',
207
339
  type: 'get',
208
- to: WABinary_1.S_WHATSAPP_NET
340
+ to: S_WHATSAPP_NET
209
341
  },
210
- content: [
211
- { tag: 'count', attrs: {} }
212
- ]
342
+ content: [{ tag: 'count', attrs: {} }]
213
343
  });
214
- const countChild = (0, WABinary_1.getBinaryNodeChild)(result, 'count');
344
+ const countChild = getBinaryNodeChild(result, 'count');
215
345
  return +countChild.attrs.value;
216
346
  };
347
+ // Pre-key upload state management
217
348
  let uploadPreKeysPromise = null;
218
349
  let lastUploadTime = 0;
219
- const uploadPreKeys = async (count = Defaults_1.MIN_PREKEY_COUNT, retryCount = 0) => {
350
+ /** generates and uploads a set of pre-keys to the server */
351
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
352
+ // Check minimum interval (except for retries)
220
353
  if (retryCount === 0) {
221
354
  const timeSinceLastUpload = Date.now() - lastUploadTime;
222
- if (timeSinceLastUpload < Defaults_1.MIN_UPLOAD_INTERVAL) {
355
+ if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
223
356
  logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
224
357
  return;
225
358
  }
226
359
  }
360
+ // Prevent multiple concurrent uploads
227
361
  if (uploadPreKeysPromise) {
228
362
  logger.debug('Pre-key upload already in progress, waiting for completion');
229
363
  await uploadPreKeysPromise;
230
364
  }
231
- // Spread prekey uploads across a random window to avoid thundering-herd
232
- // when many bots start simultaneously and all need to upload prekeys.
233
- const uploadJitter = Math.floor(Math.random() * Defaults_1.PREKEY_UPLOAD_JITTER_MS);
234
- if (uploadJitter > 0) {
235
- await new Promise(resolve => setTimeout(resolve, uploadJitter));
236
- }
237
-
238
365
  const uploadLogic = async () => {
239
366
  logger.info({ count, retryCount }, 'uploading pre-keys');
367
+ // Generate and save pre-keys atomically (prevents ID collisions on retry)
240
368
  const node = await keys.transaction(async () => {
241
- const { update, node } = await (0, Utils_1.getNextPreKeysNode)({ creds, keys }, count);
369
+ logger.debug({ requestedCount: count }, 'generating pre-keys with requested count');
370
+ const { update, node } = await getNextPreKeysNode({ creds, keys }, count);
371
+ // Update credentials immediately to prevent duplicate IDs on retry
242
372
  ev.emit('creds.update', update);
243
- return node;
244
- });
373
+ return node; // Only return node since update is already used
374
+ }, creds?.me?.id || 'upload-pre-keys');
375
+ // Upload to server (outside transaction, can fail without affecting local keys)
245
376
  try {
246
377
  await query(node);
247
- logger.info({ count }, 'uploaded pre-keys');
378
+ logger.info({ count }, 'uploaded pre-keys successfully');
248
379
  lastUploadTime = Date.now();
249
- } catch (uploadError) {
380
+ }
381
+ catch (uploadError) {
250
382
  logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
383
+ // Exponential backoff retry (max 3 retries)
251
384
  if (retryCount < 3) {
252
385
  const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
253
386
  logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
@@ -257,33 +390,61 @@ const makeSocket = (config) => {
257
390
  throw uploadError;
258
391
  }
259
392
  };
393
+ // Add timeout protection
260
394
  uploadPreKeysPromise = Promise.race([
261
395
  uploadLogic(),
262
- new Promise((_, reject) =>
263
- setTimeout(() => reject(new boom_1.Boom('Pre-key upload timeout', { statusCode: 408 })), Defaults_1.UPLOAD_TIMEOUT)
264
- )
396
+ new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
265
397
  ]);
266
398
  try {
267
399
  await uploadPreKeysPromise;
268
- } finally {
400
+ }
401
+ finally {
269
402
  uploadPreKeysPromise = null;
270
403
  }
271
404
  };
405
+ const verifyCurrentPreKeyExists = async () => {
406
+ const currentPreKeyId = creds.nextPreKeyId - 1;
407
+ if (currentPreKeyId <= 0) {
408
+ return { exists: false, currentPreKeyId: 0 };
409
+ }
410
+ const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()]);
411
+ const exists = !!preKeys[currentPreKeyId.toString()];
412
+ return { exists, currentPreKeyId };
413
+ };
272
414
  const uploadPreKeysToServerIfRequired = async () => {
273
415
  try {
416
+ let count = 0;
274
417
  const preKeyCount = await getAvailablePreKeysOnServer();
275
- const count = preKeyCount === 0 ? Defaults_1.INITIAL_PREKEY_COUNT : Defaults_1.MIN_PREKEY_COUNT;
418
+ if (preKeyCount === 0)
419
+ count = INITIAL_PREKEY_COUNT;
420
+ else
421
+ count = MIN_PREKEY_COUNT;
422
+ const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists();
276
423
  logger.info(`${preKeyCount} pre-keys found on server`);
277
- if (preKeyCount <= count) {
424
+ logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`);
425
+ const lowServerCount = preKeyCount <= count;
426
+ const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0;
427
+ const shouldUpload = lowServerCount || missingCurrentPreKey;
428
+ if (shouldUpload) {
429
+ const reasons = [];
430
+ if (lowServerCount)
431
+ reasons.push(`server count low (${preKeyCount})`);
432
+ if (missingCurrentPreKey)
433
+ reasons.push(`current prekey ${currentPreKeyId} missing from storage`);
434
+ logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`);
278
435
  await uploadPreKeys(count);
279
436
  }
280
- } catch (error) {
437
+ else {
438
+ logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
439
+ }
440
+ }
441
+ catch (error) {
281
442
  logger.error({ error }, 'Failed to check/upload pre-keys during initialization');
443
+ // Don't throw - allow connection to continue even if pre-key check fails
282
444
  }
283
445
  };
284
- const onMessageReceived = (data) => {
285
- noise.decodeFrame(data, frame => {
286
- var _a;
446
+ const onMessageReceived = async (data) => {
447
+ await noise.decodeFrame(data, frame => {
287
448
  // reset ping timeout
288
449
  lastDateRecv = new Date();
289
450
  let anyTriggered = false;
@@ -292,21 +453,21 @@ const makeSocket = (config) => {
292
453
  if (!(frame instanceof Uint8Array)) {
293
454
  const msgId = frame.attrs.id;
294
455
  if (logger.level === 'trace') {
295
- logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'recv xml' });
456
+ logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
296
457
  }
297
458
  /* Check if this is a response to a message we sent */
298
- anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
459
+ anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
299
460
  /* Check if this is a response to a message we are expecting */
300
461
  const l0 = frame.tag;
301
462
  const l1 = frame.attrs || {};
302
- const l2 = Array.isArray(frame.content) ? (_a = frame.content[0]) === null || _a === void 0 ? void 0 : _a.tag : '';
463
+ const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '';
303
464
  for (const key of Object.keys(l1)) {
304
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
305
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
306
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
465
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
466
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
467
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
307
468
  }
308
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
309
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
469
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
470
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
310
471
  if (!anyTriggered && logger.level === 'debug') {
311
472
  logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
312
473
  }
@@ -315,39 +476,28 @@ const makeSocket = (config) => {
315
476
  };
316
477
  const end = async (error) => {
317
478
  if (closed) {
318
- logger.trace({ trace: error === null || error === void 0 ? void 0 : error.stack }, 'connection already closed');
479
+ logger.trace({ trace: error?.stack }, 'connection already closed');
319
480
  return;
320
481
  }
321
482
  closed = true;
322
- logger.info({ trace: error === null || error === void 0 ? void 0 : error.stack }, error ? 'connection errored' : 'connection closed');
483
+ logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
323
484
  clearInterval(keepAliveReq);
324
485
  clearTimeout(qrTimer);
325
486
  ws.removeAllListeners('close');
326
- ws.removeAllListeners('error');
327
487
  ws.removeAllListeners('open');
328
488
  ws.removeAllListeners('message');
329
489
  if (!ws.isClosed && !ws.isClosing) {
330
490
  try {
331
491
  await ws.close();
332
492
  }
333
- catch (_a) { }
334
- }
335
- // Determine what the consumer should do next based on the disconnect reason
336
- const statusCode = error?.output?.statusCode;
337
- // loggedOut (401) and badSession (500) require deleting the session folder and re-scanning QR
338
- const shouldDeleteSession = statusCode === Types_1.DisconnectReason.loggedOut ||
339
- statusCode === Types_1.DisconnectReason.badSession;
340
- // For all other closures (connectionClosed, connectionLost, restartRequired, etc.) just reconnect
341
- const shouldReconnect = !shouldDeleteSession &&
342
- statusCode !== Types_1.DisconnectReason.timedOut;
493
+ catch { }
494
+ }
343
495
  ev.emit('connection.update', {
344
496
  connection: 'close',
345
497
  lastDisconnect: {
346
498
  error,
347
499
  date: new Date()
348
- },
349
- shouldDeleteSession,
350
- shouldReconnect
500
+ }
351
501
  });
352
502
  ev.removeAllListeners('connection.update');
353
503
  };
@@ -356,7 +506,7 @@ const makeSocket = (config) => {
356
506
  return;
357
507
  }
358
508
  if (ws.isClosed || ws.isClosing) {
359
- throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
509
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
360
510
  }
361
511
  let onOpen;
362
512
  let onClose;
@@ -366,20 +516,13 @@ const makeSocket = (config) => {
366
516
  ws.on('open', onOpen);
367
517
  ws.on('close', onClose);
368
518
  ws.on('error', onClose);
369
- })
370
- .finally(() => {
519
+ }).finally(() => {
371
520
  ws.off('open', onOpen);
372
521
  ws.off('close', onClose);
373
522
  ws.off('error', onClose);
374
523
  });
375
524
  };
376
- const startKeepAliveRequest = () => {
377
- // Add a random initial offset (±20% of keepAliveIntervalMs) so bots started
378
- // at the same time do NOT send keep-alive pings simultaneously, which would
379
- // create a thundering-herd of IQ requests to WA servers.
380
- const jitter = Math.floor(Math.random() * keepAliveIntervalMs * 0.4) - Math.floor(keepAliveIntervalMs * 0.2);
381
- const effectiveInterval = Math.max(keepAliveIntervalMs + jitter, 5000);
382
- return (keepAliveReq = setInterval(() => {
525
+ const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {
383
526
  if (!lastDateRecv) {
384
527
  lastDateRecv = new Date();
385
528
  }
@@ -388,8 +531,8 @@ const makeSocket = (config) => {
388
531
  check if it's been a suspicious amount of time since the server responded with our last seen
389
532
  it could be that the network is down
390
533
  */
391
- if (diff > keepAliveIntervalMs + 10000) {
392
- void end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost }));
534
+ if (diff > keepAliveIntervalMs + 5000) {
535
+ void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
393
536
  }
394
537
  else if (ws.isOpen) {
395
538
  // if its all good, send a keep alive request
@@ -397,42 +540,37 @@ const makeSocket = (config) => {
397
540
  tag: 'iq',
398
541
  attrs: {
399
542
  id: generateMessageTag(),
400
- to: WABinary_1.S_WHATSAPP_NET,
543
+ to: S_WHATSAPP_NET,
401
544
  type: 'get',
402
- xmlns: 'w:p',
545
+ xmlns: 'w:p'
403
546
  },
404
547
  content: [{ tag: 'ping', attrs: {} }]
405
- })
406
- .catch(err => {
548
+ }).catch(err => {
407
549
  logger.error({ trace: err.stack }, 'error in sending keep alive');
408
550
  });
409
551
  }
410
552
  else {
411
553
  logger.warn('keep alive called when WS not open');
412
554
  }
413
- }, effectiveInterval));
414
- };
555
+ }, keepAliveIntervalMs));
415
556
  /** i have no idea why this exists. pls enlighten me */
416
- const sendPassiveIq = (tag) => (query({
557
+ const sendPassiveIq = (tag) => query({
417
558
  tag: 'iq',
418
559
  attrs: {
419
- to: WABinary_1.S_WHATSAPP_NET,
560
+ to: S_WHATSAPP_NET,
420
561
  xmlns: 'passive',
421
- type: 'set',
562
+ type: 'set'
422
563
  },
423
- content: [
424
- { tag, attrs: {} }
425
- ]
426
- }));
564
+ content: [{ tag, attrs: {} }]
565
+ });
427
566
  /** logout & invalidate connection */
428
567
  const logout = async (msg) => {
429
- var _a;
430
- const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;
568
+ const jid = authState.creds.me?.id;
431
569
  if (jid) {
432
570
  await sendNode({
433
571
  tag: 'iq',
434
572
  attrs: {
435
- to: WABinary_1.S_WHATSAPP_NET,
573
+ to: S_WHATSAPP_NET,
436
574
  type: 'set',
437
575
  id: generateMessageTag(),
438
576
  xmlns: 'md'
@@ -448,27 +586,23 @@ const makeSocket = (config) => {
448
586
  ]
449
587
  });
450
588
  }
451
- end(new boom_1.Boom(msg || 'Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut }));
589
+ void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
452
590
  };
453
-
454
- const requestPairingCode = async (phoneNumber, pairKey) => {
455
- if (pairKey) {
456
- authState.creds.pairingCode = pairKey.toUpperCase();
457
- } else {
458
- authState.creds.pairingCode = (0, Utils_1.bytesToCrockford)((0, crypto_1.randomBytes)(5));
459
- }
460
-
591
+ const requestPairingCode = async (phoneNumber, customPairingCode) => {
592
+ const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
593
+ if (customPairingCode && customPairingCode?.length !== 8) {
594
+ throw new Error('Custom pairing code must be exactly 8 chars');
595
+ }
596
+ authState.creds.pairingCode = pairingCode;
461
597
  authState.creds.me = {
462
- id: (0, WABinary_1.jidEncode)(phoneNumber, 's.whatsapp.net'),
598
+ id: jidEncode(phoneNumber, 's.whatsapp.net'),
463
599
  name: '~'
464
600
  };
465
-
466
601
  ev.emit('creds.update', authState.creds);
467
-
468
602
  await sendNode({
469
603
  tag: 'iq',
470
604
  attrs: {
471
- to: WABinary_1.S_WHATSAPP_NET,
605
+ to: S_WHATSAPP_NET,
472
606
  type: 'set',
473
607
  id: generateMessageTag(),
474
608
  xmlns: 'md'
@@ -495,7 +629,7 @@ const makeSocket = (config) => {
495
629
  {
496
630
  tag: 'companion_platform_id',
497
631
  attrs: {},
498
- content: (0, Utils_1.getPlatformId)(browser[1])
632
+ content: getPlatformId(browser[1])
499
633
  },
500
634
  {
501
635
  tag: 'companion_platform_display',
@@ -505,68 +639,38 @@ const makeSocket = (config) => {
505
639
  {
506
640
  tag: 'link_code_pairing_nonce',
507
641
  attrs: {},
508
- content: "0"
642
+ content: '0'
509
643
  }
510
644
  ]
511
645
  }
512
646
  ]
513
647
  });
514
-
515
648
  return authState.creds.pairingCode;
516
- }
649
+ };
517
650
  async function generatePairingKey() {
518
- const salt = (0, crypto_1.randomBytes)(32);
519
- const randomIv = (0, crypto_1.randomBytes)(16);
520
- const key = await (0, Utils_1.derivePairingCodeKey)(authState.creds.pairingCode, salt);
521
- const ciphered = (0, Utils_1.aesEncryptCTR)(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
651
+ const salt = randomBytes(32);
652
+ const randomIv = randomBytes(16);
653
+ const key = await derivePairingCodeKey(authState.creds.pairingCode, salt);
654
+ const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
522
655
  return Buffer.concat([salt, randomIv, ciphered]);
523
656
  }
524
657
  const sendWAMBuffer = (wamBuffer) => {
525
658
  return query({
526
659
  tag: 'iq',
527
660
  attrs: {
528
- to: WABinary_1.S_WHATSAPP_NET,
661
+ to: S_WHATSAPP_NET,
529
662
  id: generateMessageTag(),
530
663
  xmlns: 'w:stats'
531
664
  },
532
665
  content: [
533
666
  {
534
667
  tag: 'add',
535
- attrs: {},
668
+ attrs: { t: Math.round(Date.now() / 1000) + '' },
536
669
  content: wamBuffer
537
670
  }
538
671
  ]
539
672
  });
540
673
  };
541
- let serverTimeOffsetMs = 0;
542
- const updateServerTimeOffset = ({ attrs }) => {
543
- const tValue = attrs && attrs.t;
544
- if (!tValue) return;
545
- const parsed = Number(tValue);
546
- if (Number.isNaN(parsed) || parsed <= 0) return;
547
- const localMs = Date.now();
548
- serverTimeOffsetMs = parsed * 1000 - localMs;
549
- logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
550
- };
551
- const getUnifiedSessionId = () => {
552
- const offsetMs = 3 * Defaults_1.TimeMs.Day;
553
- const now = Date.now() + serverTimeOffsetMs;
554
- const id = (now + offsetMs) % Defaults_1.TimeMs.Week;
555
- return id.toString();
556
- };
557
- const sendUnifiedSession = async () => {
558
- if (!ws.isOpen) return;
559
- const node = {
560
- tag: 'ib',
561
- attrs: {},
562
- content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
563
- };
564
- try {
565
- await sendNode(node);
566
- } catch (error) {
567
- logger.debug({ error }, 'failed to send unified_session telemetry');
568
- }
569
- };
570
674
  ws.on('message', onMessageReceived);
571
675
  ws.on('open', async () => {
572
676
  try {
@@ -574,25 +678,26 @@ const makeSocket = (config) => {
574
678
  }
575
679
  catch (err) {
576
680
  logger.error({ err }, 'error in validating connection');
577
- end(err);
681
+ void end(err);
578
682
  }
579
683
  });
580
684
  ws.on('error', mapWebSocketError(end));
581
- ws.on('close', () => void end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed })));
582
- ws.on('CB:xmlstreamend', () => void end(new boom_1.Boom('Connection Terminated by Server', { statusCode: Types_1.DisconnectReason.connectionClosed })));
685
+ ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
686
+ // the server terminated the connection
687
+ ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
583
688
  // QR gen
584
689
  ws.on('CB:iq,type:set,pair-device', async (stanza) => {
585
690
  const iq = {
586
691
  tag: 'iq',
587
692
  attrs: {
588
- to: WABinary_1.S_WHATSAPP_NET,
693
+ to: S_WHATSAPP_NET,
589
694
  type: 'result',
590
- id: stanza.attrs.id,
695
+ id: stanza.attrs.id
591
696
  }
592
697
  };
593
698
  await sendNode(iq);
594
- const pairDeviceNode = (0, WABinary_1.getBinaryNodeChild)(stanza, 'pair-device');
595
- const refNodes = (0, WABinary_1.getBinaryNodeChildren)(pairDeviceNode, 'ref');
699
+ const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
700
+ const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
596
701
  const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
597
702
  const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
598
703
  const advB64 = creds.advSecretKey;
@@ -603,7 +708,7 @@ const makeSocket = (config) => {
603
708
  }
604
709
  const refNode = refNodes.shift();
605
710
  if (!refNode) {
606
- void end(new boom_1.Boom('QR refs attempts ended', { statusCode: Types_1.DisconnectReason.timedOut }));
711
+ void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
607
712
  return;
608
713
  }
609
714
  const ref = refNode.content.toString('utf-8');
@@ -620,7 +725,7 @@ const makeSocket = (config) => {
620
725
  logger.debug('pair success recv');
621
726
  try {
622
727
  updateServerTimeOffset(stanza);
623
- const { reply, creds: updatedCreds } = (0, Utils_1.configureSuccessfulPairing)(stanza, creds);
728
+ const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
624
729
  logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
625
730
  ev.emit('creds.update', updatedCreds);
626
731
  ev.emit('connection.update', { isNewLogin: true, qr: undefined });
@@ -632,84 +737,97 @@ const makeSocket = (config) => {
632
737
  void end(error);
633
738
  }
634
739
  });
740
+ // login complete
635
741
  ws.on('CB:success', async (node) => {
636
742
  try {
637
743
  updateServerTimeOffset(node);
638
744
  await uploadPreKeysToServerIfRequired();
639
745
  await sendPassiveIq('active');
640
- } catch (err) {
746
+ // After successful login, validate our key-bundle against server
747
+ try {
748
+ await digestKeyBundle();
749
+ }
750
+ catch (e) {
751
+ logger.warn({ e }, 'failed to run digest after login');
752
+ }
753
+ }
754
+ catch (err) {
641
755
  logger.warn({ err }, 'failed to send initial passive iq');
642
756
  }
643
757
  logger.info('opened connection to WA');
644
- clearTimeout(qrTimer);
758
+ clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
645
759
  ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
646
760
  ev.emit('connection.update', { connection: 'open' });
647
761
  void sendUnifiedSession();
762
+ if (node.attrs.lid && authState.creds.me?.id) {
763
+ const myLID = node.attrs.lid;
764
+ process.nextTick(async () => {
765
+ try {
766
+ const myPN = authState.creds.me.id;
767
+ // Store our own LID-PN mapping
768
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }]);
769
+ // Create device list for our own user (needed for bulk migration)
770
+ const { user, device } = jidDecode(myPN);
771
+ await authState.keys.set({
772
+ 'device-list': {
773
+ [user]: [device?.toString() || '0']
774
+ }
775
+ });
776
+ // migrate our own session
777
+ await signalRepository.migrateSession(myPN, myLID);
778
+ logger.info({ myPN, myLID }, 'Own LID session created successfully');
779
+ }
780
+ catch (error) {
781
+ logger.error({ error, lid: myLID }, 'Failed to create own LID session');
782
+ }
783
+ });
784
+ }
648
785
  });
649
786
  ws.on('CB:stream:error', (node) => {
650
- const [reasonNode] = (0, WABinary_1.getAllBinaryNodeChildren)(node);
787
+ const [reasonNode] = getAllBinaryNodeChildren(node);
651
788
  logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
652
- const { reason, statusCode } = (0, Utils_1.getErrorCodeFromStreamError)(node);
653
- void end(new boom_1.Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
789
+ const { reason, statusCode } = getErrorCodeFromStreamError(node);
790
+ void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
654
791
  });
792
+ // stream fail, possible logout
655
793
  ws.on('CB:failure', (node) => {
656
794
  const reason = +(node.attrs.reason || 500);
657
- // Try to extract a human-readable description from child nodes or attrs
658
- const description = (Array.isArray(node.content) && node.content[0]?.attrs?.description)
659
- || node.attrs.reason
660
- || 'unknown';
661
- logger.warn({ reason, description, attrs: node.attrs }, 'WA connection failure received');
662
- void end(new boom_1.Boom(`Connection Failure (${description})`, { statusCode: reason, data: node.attrs }));
795
+ void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
663
796
  });
664
797
  ws.on('CB:ib,,downgrade_webclient', () => {
665
- void end(new boom_1.Boom('Multi-device beta not joined', { statusCode: Types_1.DisconnectReason.multideviceMismatch }));
798
+ void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
666
799
  });
667
- ws.on('CB:ib,,offline_preview', (node) => {
800
+ ws.on('CB:ib,,offline_preview', async (node) => {
668
801
  logger.info('offline preview received', JSON.stringify(node));
669
- sendNode({
802
+ await sendNode({
670
803
  tag: 'ib',
671
804
  attrs: {},
672
805
  content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
673
806
  });
674
807
  });
675
808
  ws.on('CB:ib,,edge_routing', (node) => {
676
- const edgeRoutingNode = (0, WABinary_1.getBinaryNodeChild)(node, 'edge_routing');
677
- const routingInfo = (0, WABinary_1.getBinaryNodeChild)(edgeRoutingNode, 'routing_info');
678
- if (routingInfo === null || routingInfo === void 0 ? void 0 : routingInfo.content) {
679
- authState.creds.routingInfo = Buffer.from(routingInfo === null || routingInfo === void 0 ? void 0 : routingInfo.content);
809
+ const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
810
+ const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info');
811
+ if (routingInfo?.content) {
812
+ authState.creds.routingInfo = Buffer.from(routingInfo?.content);
680
813
  ev.emit('creds.update', authState.creds);
681
814
  }
682
815
  });
683
816
  let didStartBuffer = false;
684
- let offlineFlushTimer = null;
685
817
  process.nextTick(() => {
686
- var _a;
687
- if ((_a = creds.me) === null || _a === void 0 ? void 0 : _a.id) {
818
+ if (creds.me?.id) {
688
819
  // start buffering important events
689
820
  // if we're logged in
690
821
  ev.buffer();
691
822
  didStartBuffer = true;
692
- // Safety net: if server never sends CB:ib,,offline (e.g. slow server, no pending msgs),
693
- // force-flush the buffer after 10s so messages/commands are never stuck
694
- offlineFlushTimer = setTimeout(() => {
695
- if (didStartBuffer && ev.isBuffering()) {
696
- logger.warn('offline event not received within 10s — force-flushing event buffer to prevent message lag');
697
- ev.flush(true);
698
- ev.emit('connection.update', { receivedPendingNotifications: true });
699
- }
700
- }, 10000);
701
823
  }
702
824
  ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
703
825
  });
704
826
  // called when all offline notifs are handled
705
827
  ws.on('CB:ib,,offline', (node) => {
706
- const child = (0, WABinary_1.getBinaryNodeChild)(node, 'offline');
707
- const offlineNotifs = +((child === null || child === void 0 ? void 0 : child.attrs.count) || 0);
828
+ const child = getBinaryNodeChild(node, 'offline');
829
+ const offlineNotifs = +(child?.attrs.count || 0);
708
830
  logger.info(`handled ${offlineNotifs} offline messages/notifications`);
709
- if (offlineFlushTimer) {
710
- clearTimeout(offlineFlushTimer);
711
- offlineFlushTimer = null;
712
- }
713
831
  if (didStartBuffer) {
714
832
  ev.flush();
715
833
  logger.trace('flushed events for initial buffer');
@@ -718,39 +836,66 @@ const makeSocket = (config) => {
718
836
  });
719
837
  // update credentials when required
720
838
  ev.on('creds.update', update => {
721
- var _a, _b;
722
- const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;
839
+ const name = update.me?.name;
723
840
  // if name has just been received
724
- if (((_b = creds.me) === null || _b === void 0 ? void 0 : _b.name) !== name) {
841
+ if (creds.me?.name !== name) {
725
842
  logger.debug({ name }, 'updated pushName');
726
843
  sendNode({
727
844
  tag: 'presence',
728
845
  attrs: { name: name }
729
- })
730
- .catch(err => {
846
+ }).catch(err => {
731
847
  logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
732
848
  });
733
849
  }
734
850
  Object.assign(creds, update);
735
851
  });
736
- ev.on('lid-mapping.update', async ({ lid, pn }) => {
852
+ const updateServerTimeOffset = ({ attrs }) => {
853
+ const tValue = attrs?.t;
854
+ if (!tValue) {
855
+ return;
856
+ }
857
+ const parsed = Number(tValue);
858
+ if (Number.isNaN(parsed) || parsed <= 0) {
859
+ return;
860
+ }
861
+ const localMs = Date.now();
862
+ serverTimeOffsetMs = parsed * 1000 - localMs;
863
+ logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
864
+ };
865
+ const getUnifiedSessionId = () => {
866
+ const offsetMs = 3 * TimeMs.Day;
867
+ const now = Date.now() + serverTimeOffsetMs;
868
+ const id = (now + offsetMs) % TimeMs.Week;
869
+ return id.toString();
870
+ };
871
+ const sendUnifiedSession = async () => {
872
+ if (!ws.isOpen) {
873
+ return;
874
+ }
875
+ const node = {
876
+ tag: 'ib',
877
+ attrs: {},
878
+ content: [
879
+ {
880
+ tag: 'unified_session',
881
+ attrs: {
882
+ id: getUnifiedSessionId()
883
+ }
884
+ }
885
+ ]
886
+ };
737
887
  try {
738
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
739
- } catch (error) {
740
- logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
888
+ await sendNode(node);
741
889
  }
742
- });
743
- if (printQRInTerminal) {
744
- (0, Utils_1.printQRIfNecessaryListener)(ev, logger);
745
- }
890
+ catch (error) {
891
+ logger.debug({ error }, 'failed to send unified_session telemetry');
892
+ }
893
+ };
746
894
  return {
747
895
  type: 'md',
748
896
  ws,
749
897
  ev,
750
- authState: {
751
- creds,
752
- keys
753
- },
898
+ authState: { creds, keys },
754
899
  signalRepository,
755
900
  get user() {
756
901
  return authState.creds.me;
@@ -766,20 +911,25 @@ const makeSocket = (config) => {
766
911
  onUnexpectedError,
767
912
  uploadPreKeys,
768
913
  uploadPreKeysToServerIfRequired,
914
+ digestKeyBundle,
915
+ rotateSignedPreKey,
769
916
  requestPairingCode,
770
917
  updateServerTimeOffset,
771
918
  sendUnifiedSession,
772
- waitForConnectionUpdate: (0, Utils_1.bindWaitForConnectionUpdate)(ev),
919
+ wamBuffer: publicWAMBuffer,
920
+ /** Waits for the connection to WA to reach a state */
921
+ waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
773
922
  sendWAMBuffer,
923
+ executeUSyncQuery,
924
+ onWhatsApp
774
925
  };
775
926
  };
776
- exports.makeSocket = makeSocket;
777
927
  /**
778
928
  * map the websocket error to the right type
779
929
  * so it can be retried by the caller
780
930
  * */
781
931
  function mapWebSocketError(handler) {
782
932
  return (error) => {
783
- handler(new boom_1.Boom(`WebSocket Error (${error === null || error === void 0 ? void 0 : error.message})`, { statusCode: (0, Utils_1.getCodeFromWSError)(error), data: error }));
933
+ handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }));
784
934
  };
785
- }
935
+ }