violetics 7.0.0-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 -144
  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 -442
  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 +878 -552
  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 +463 -289
  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 -145
  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 -35
  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 +32 -34
  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 +75 -108
  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 -111
  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,65 +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, } = 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
- ws.connect();
34
- const ev = (0, Utils_1.makeEventBuffer)(logger);
35
41
  /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
36
- const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();
42
+ const ephemeralKeyPair = Curve.generateKeyPair();
37
43
  /** WA noise protocol wrapper */
38
- const noise = (0, Utils_1.makeNoiseHandler)({
44
+ const noise = makeNoiseHandler({
39
45
  keyPair: ephemeralKeyPair,
40
- NOISE_HEADER: Defaults_1.NOISE_WA_HEADER,
46
+ NOISE_HEADER: NOISE_WA_HEADER,
41
47
  logger,
42
- routingInfo: (_b = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _b === void 0 ? void 0 : _b.routingInfo
48
+ routingInfo: authState?.creds?.routingInfo
43
49
  });
44
- const { creds } = authState;
45
- // add transaction capability
46
- const keys = (0, Utils_1.addTransactionCapability)(authState.keys, logger, transactionOpts);
47
- const signalRepository = makeSignalRepository({ creds, keys });
48
- let lastDateRecv;
49
- let epoch = 1;
50
- let keepAliveReq;
51
- let qrTimer;
52
- let closed = false;
53
- const uqTagId = (0, Utils_1.generateMdTagPrefix)();
54
- const generateMessageTag = () => `${uqTagId}${epoch++}`;
55
- const sendPromise = (0, util_1.promisify)(ws.send);
50
+ const ws = new WebSocketClient(url, config);
51
+ ws.connect();
52
+ const sendPromise = promisify(ws.send);
56
53
  /** send a raw buffer */
57
54
  const sendRawMessage = async (data) => {
58
55
  if (!ws.isOpen) {
59
- throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
56
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
60
57
  }
61
58
  const bytes = noise.encodeFrame(data);
62
- await (0, Utils_1.promiseTimeout)(connectTimeoutMs, async (resolve, reject) => {
59
+ await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
63
60
  try {
64
61
  await sendPromise.call(ws, bytes);
65
62
  resolve();
@@ -72,50 +69,11 @@ const makeSocket = (config) => {
72
69
  /** send a binary node */
73
70
  const sendNode = (frame) => {
74
71
  if (logger.level === 'trace') {
75
- logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'xml send' });
72
+ logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' });
76
73
  }
77
- const buff = (0, WABinary_1.encodeBinaryNode)(frame);
74
+ const buff = encodeBinaryNode(frame);
78
75
  return sendRawMessage(buff);
79
76
  };
80
- /** log & process any unexpected errors */
81
- const onUnexpectedError = (err, msg) => {
82
- logger.error({ err }, `unexpected error in '${msg}'`);
83
- const message = (err && ((err.stack || err.message) || String(err))).toLowerCase();
84
- if (message.includes('bad mac') || (message.includes('mac') && message.includes('invalid'))) {
85
- try {
86
- uploadPreKeys()
87
- .catch(e => logger.warn({ e }, 'failed to re-upload prekeys after bad mac'));
88
- }
89
- catch (_e) {
90
- }
91
- }
92
- };
93
- /** await the next incoming message */
94
- const awaitNextMessage = async (sendMsg) => {
95
- if (!ws.isOpen) {
96
- throw new boom_1.Boom('Connection Closed', {
97
- statusCode: Types_1.DisconnectReason.connectionClosed
98
- });
99
- }
100
- let onOpen;
101
- let onClose;
102
- const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {
103
- onOpen = resolve;
104
- onClose = mapWebSocketError(reject);
105
- ws.on('frame', onOpen);
106
- ws.on('close', onClose);
107
- ws.on('error', onClose);
108
- })
109
- .finally(() => {
110
- ws.off('frame', onOpen);
111
- ws.off('close', onClose);
112
- ws.off('error', onClose);
113
- });
114
- if (sendMsg) {
115
- sendRawMessage(sendMsg).catch(onClose);
116
- }
117
- return result;
118
- };
119
77
  /**
120
78
  * Wait for a message with a certain tag to be received
121
79
  * @param msgId the message tag to await
@@ -125,21 +83,38 @@ const makeSocket = (config) => {
125
83
  let onRecv;
126
84
  let onErr;
127
85
  try {
128
- const result = await (0, Utils_1.promiseTimeout)(timeoutMs, (resolve, reject) => {
129
- onRecv = resolve;
86
+ const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
87
+ onRecv = data => {
88
+ resolve(data);
89
+ };
130
90
  onErr = err => {
131
- 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
+ }));
132
95
  };
133
96
  ws.on(`TAG:${msgId}`, onRecv);
134
97
  ws.on('close', onErr);
135
98
  ws.on('error', onErr);
99
+ return () => reject(new Boom('Query Cancelled'));
136
100
  });
137
101
  return result;
138
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
+ }
139
111
  finally {
140
- ws.off(`TAG:${msgId}`, onRecv);
141
- ws.off('close', onErr);
142
- 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
+ }
143
118
  }
144
119
  };
145
120
  /** send a query, and wait for its response. auto-generates message ID if not provided */
@@ -148,12 +123,179 @@ const makeSocket = (config) => {
148
123
  node.attrs.id = generateMessageTag();
149
124
  }
150
125
  const msgId = node.attrs.id;
151
- const [result] = await Promise.all([
152
- waitForMessage(msgId, timeoutMs),
126
+ const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
127
+ const result = waitForMessage(msgId, timeoutMs).catch(reject);
153
128
  sendNode(node)
154
- ]);
155
- if ('tag' in result) {
156
- (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);
157
299
  }
158
300
  return result;
159
301
  };
@@ -162,30 +304,30 @@ const makeSocket = (config) => {
162
304
  let helloMsg = {
163
305
  clientHello: { ephemeral: ephemeralKeyPair.public }
164
306
  };
165
- helloMsg = WAProto_1.proto.HandshakeMessage.fromObject(helloMsg);
307
+ helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
166
308
  logger.info({ browser, helloMsg }, 'connected to WA');
167
- const init = WAProto_1.proto.HandshakeMessage.encode(helloMsg).finish();
309
+ const init = proto.HandshakeMessage.encode(helloMsg).finish();
168
310
  const result = await awaitNextMessage(init);
169
- const handshake = WAProto_1.proto.HandshakeMessage.decode(result);
311
+ const handshake = proto.HandshakeMessage.decode(result);
170
312
  logger.trace({ handshake }, 'handshake recv from WA');
171
- const keyEnc = await noise.processHandshake(handshake, creds.noiseKey);
313
+ const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
172
314
  let node;
173
315
  if (!creds.me) {
174
- node = (0, Utils_1.generateRegistrationNode)(creds, config);
316
+ node = generateRegistrationNode(creds, config);
175
317
  logger.info({ node }, 'not logged in, attempting registration...');
176
318
  }
177
319
  else {
178
- node = (0, Utils_1.generateLoginNode)(creds.me.id, config);
320
+ node = generateLoginNode(creds.me.id, config);
179
321
  logger.info({ node }, 'logging in...');
180
322
  }
181
- const payloadEnc = noise.encrypt(WAProto_1.proto.ClientPayload.encode(node).finish());
182
- await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({
323
+ const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
324
+ await sendRawMessage(proto.HandshakeMessage.encode({
183
325
  clientFinish: {
184
326
  static: keyEnc,
185
- payload: payloadEnc,
186
- },
327
+ payload: payloadEnc
328
+ }
187
329
  }).finish());
188
- noise.finishInit();
330
+ await noise.finishInit();
189
331
  startKeepAliveRequest();
190
332
  };
191
333
  const getAvailablePreKeysOnServer = async () => {
@@ -195,42 +337,50 @@ const makeSocket = (config) => {
195
337
  id: generateMessageTag(),
196
338
  xmlns: 'encrypt',
197
339
  type: 'get',
198
- to: WABinary_1.S_WHATSAPP_NET
340
+ to: S_WHATSAPP_NET
199
341
  },
200
- content: [
201
- { tag: 'count', attrs: {} }
202
- ]
342
+ content: [{ tag: 'count', attrs: {} }]
203
343
  });
204
- const countChild = (0, WABinary_1.getBinaryNodeChild)(result, 'count');
344
+ const countChild = getBinaryNodeChild(result, 'count');
205
345
  return +countChild.attrs.value;
206
346
  };
347
+ // Pre-key upload state management
207
348
  let uploadPreKeysPromise = null;
208
349
  let lastUploadTime = 0;
209
- 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)
210
353
  if (retryCount === 0) {
211
354
  const timeSinceLastUpload = Date.now() - lastUploadTime;
212
- if (timeSinceLastUpload < Defaults_1.MIN_UPLOAD_INTERVAL) {
355
+ if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
213
356
  logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
214
357
  return;
215
358
  }
216
359
  }
360
+ // Prevent multiple concurrent uploads
217
361
  if (uploadPreKeysPromise) {
218
362
  logger.debug('Pre-key upload already in progress, waiting for completion');
219
363
  await uploadPreKeysPromise;
220
364
  }
221
365
  const uploadLogic = async () => {
222
366
  logger.info({ count, retryCount }, 'uploading pre-keys');
367
+ // Generate and save pre-keys atomically (prevents ID collisions on retry)
223
368
  const node = await keys.transaction(async () => {
224
- 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
225
372
  ev.emit('creds.update', update);
226
- return node;
227
- });
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)
228
376
  try {
229
377
  await query(node);
230
- logger.info({ count }, 'uploaded pre-keys');
378
+ logger.info({ count }, 'uploaded pre-keys successfully');
231
379
  lastUploadTime = Date.now();
232
- } catch (uploadError) {
380
+ }
381
+ catch (uploadError) {
233
382
  logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
383
+ // Exponential backoff retry (max 3 retries)
234
384
  if (retryCount < 3) {
235
385
  const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
236
386
  logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
@@ -240,33 +390,61 @@ const makeSocket = (config) => {
240
390
  throw uploadError;
241
391
  }
242
392
  };
393
+ // Add timeout protection
243
394
  uploadPreKeysPromise = Promise.race([
244
395
  uploadLogic(),
245
- new Promise((_, reject) =>
246
- setTimeout(() => reject(new boom_1.Boom('Pre-key upload timeout', { statusCode: 408 })), Defaults_1.UPLOAD_TIMEOUT)
247
- )
396
+ new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
248
397
  ]);
249
398
  try {
250
399
  await uploadPreKeysPromise;
251
- } finally {
400
+ }
401
+ finally {
252
402
  uploadPreKeysPromise = null;
253
403
  }
254
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
+ };
255
414
  const uploadPreKeysToServerIfRequired = async () => {
256
415
  try {
416
+ let count = 0;
257
417
  const preKeyCount = await getAvailablePreKeysOnServer();
258
- 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();
259
423
  logger.info(`${preKeyCount} pre-keys found on server`);
260
- 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(', ')}`);
261
435
  await uploadPreKeys(count);
262
436
  }
263
- } catch (error) {
437
+ else {
438
+ logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
439
+ }
440
+ }
441
+ catch (error) {
264
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
265
444
  }
266
445
  };
267
- const onMessageReceived = (data) => {
268
- noise.decodeFrame(data, frame => {
269
- var _a;
446
+ const onMessageReceived = async (data) => {
447
+ await noise.decodeFrame(data, frame => {
270
448
  // reset ping timeout
271
449
  lastDateRecv = new Date();
272
450
  let anyTriggered = false;
@@ -275,21 +453,21 @@ const makeSocket = (config) => {
275
453
  if (!(frame instanceof Uint8Array)) {
276
454
  const msgId = frame.attrs.id;
277
455
  if (logger.level === 'trace') {
278
- logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'recv xml' });
456
+ logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
279
457
  }
280
458
  /* Check if this is a response to a message we sent */
281
- anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
459
+ anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
282
460
  /* Check if this is a response to a message we are expecting */
283
461
  const l0 = frame.tag;
284
462
  const l1 = frame.attrs || {};
285
- 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 : '';
286
464
  for (const key of Object.keys(l1)) {
287
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
288
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
289
- 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;
290
468
  }
291
- anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
292
- 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;
293
471
  if (!anyTriggered && logger.level === 'debug') {
294
472
  logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
295
473
  }
@@ -298,39 +476,28 @@ const makeSocket = (config) => {
298
476
  };
299
477
  const end = async (error) => {
300
478
  if (closed) {
301
- logger.trace({ trace: error === null || error === void 0 ? void 0 : error.stack }, 'connection already closed');
479
+ logger.trace({ trace: error?.stack }, 'connection already closed');
302
480
  return;
303
481
  }
304
482
  closed = true;
305
- 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');
306
484
  clearInterval(keepAliveReq);
307
485
  clearTimeout(qrTimer);
308
486
  ws.removeAllListeners('close');
309
- ws.removeAllListeners('error');
310
487
  ws.removeAllListeners('open');
311
488
  ws.removeAllListeners('message');
312
489
  if (!ws.isClosed && !ws.isClosing) {
313
490
  try {
314
491
  await ws.close();
315
492
  }
316
- catch (_a) { }
317
- }
318
- // Determine what the consumer should do next based on the disconnect reason
319
- const statusCode = error?.output?.statusCode;
320
- // loggedOut (401) and badSession (500) require deleting the session folder and re-scanning QR
321
- const shouldDeleteSession = statusCode === Types_1.DisconnectReason.loggedOut ||
322
- statusCode === Types_1.DisconnectReason.badSession;
323
- // For all other closures (connectionClosed, connectionLost, restartRequired, etc.) just reconnect
324
- const shouldReconnect = !shouldDeleteSession &&
325
- statusCode !== Types_1.DisconnectReason.timedOut;
493
+ catch { }
494
+ }
326
495
  ev.emit('connection.update', {
327
496
  connection: 'close',
328
497
  lastDisconnect: {
329
498
  error,
330
499
  date: new Date()
331
- },
332
- shouldDeleteSession,
333
- shouldReconnect
500
+ }
334
501
  });
335
502
  ev.removeAllListeners('connection.update');
336
503
  };
@@ -339,7 +506,7 @@ const makeSocket = (config) => {
339
506
  return;
340
507
  }
341
508
  if (ws.isClosed || ws.isClosing) {
342
- throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
509
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
343
510
  }
344
511
  let onOpen;
345
512
  let onClose;
@@ -349,8 +516,7 @@ const makeSocket = (config) => {
349
516
  ws.on('open', onOpen);
350
517
  ws.on('close', onClose);
351
518
  ws.on('error', onClose);
352
- })
353
- .finally(() => {
519
+ }).finally(() => {
354
520
  ws.off('open', onOpen);
355
521
  ws.off('close', onClose);
356
522
  ws.off('error', onClose);
@@ -365,8 +531,8 @@ const makeSocket = (config) => {
365
531
  check if it's been a suspicious amount of time since the server responded with our last seen
366
532
  it could be that the network is down
367
533
  */
368
- if (diff > keepAliveIntervalMs + 10000) {
369
- 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 }));
370
536
  }
371
537
  else if (ws.isOpen) {
372
538
  // if its all good, send a keep alive request
@@ -374,13 +540,12 @@ const makeSocket = (config) => {
374
540
  tag: 'iq',
375
541
  attrs: {
376
542
  id: generateMessageTag(),
377
- to: WABinary_1.S_WHATSAPP_NET,
543
+ to: S_WHATSAPP_NET,
378
544
  type: 'get',
379
- xmlns: 'w:p',
545
+ xmlns: 'w:p'
380
546
  },
381
547
  content: [{ tag: 'ping', attrs: {} }]
382
- })
383
- .catch(err => {
548
+ }).catch(err => {
384
549
  logger.error({ trace: err.stack }, 'error in sending keep alive');
385
550
  });
386
551
  }
@@ -389,26 +554,23 @@ const makeSocket = (config) => {
389
554
  }
390
555
  }, keepAliveIntervalMs));
391
556
  /** i have no idea why this exists. pls enlighten me */
392
- const sendPassiveIq = (tag) => (query({
557
+ const sendPassiveIq = (tag) => query({
393
558
  tag: 'iq',
394
559
  attrs: {
395
- to: WABinary_1.S_WHATSAPP_NET,
560
+ to: S_WHATSAPP_NET,
396
561
  xmlns: 'passive',
397
- type: 'set',
562
+ type: 'set'
398
563
  },
399
- content: [
400
- { tag, attrs: {} }
401
- ]
402
- }));
564
+ content: [{ tag, attrs: {} }]
565
+ });
403
566
  /** logout & invalidate connection */
404
567
  const logout = async (msg) => {
405
- var _a;
406
- const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;
568
+ const jid = authState.creds.me?.id;
407
569
  if (jid) {
408
570
  await sendNode({
409
571
  tag: 'iq',
410
572
  attrs: {
411
- to: WABinary_1.S_WHATSAPP_NET,
573
+ to: S_WHATSAPP_NET,
412
574
  type: 'set',
413
575
  id: generateMessageTag(),
414
576
  xmlns: 'md'
@@ -424,27 +586,23 @@ const makeSocket = (config) => {
424
586
  ]
425
587
  });
426
588
  }
427
- end(new boom_1.Boom(msg || 'Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut }));
589
+ void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
428
590
  };
429
-
430
- const requestPairingCode = async (phoneNumber, pairKey) => {
431
- if (pairKey) {
432
- authState.creds.pairingCode = pairKey.toUpperCase();
433
- } else {
434
- authState.creds.pairingCode = (0, Utils_1.bytesToCrockford)((0, crypto_1.randomBytes)(5));
435
- }
436
-
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;
437
597
  authState.creds.me = {
438
- id: (0, WABinary_1.jidEncode)(phoneNumber, 's.whatsapp.net'),
598
+ id: jidEncode(phoneNumber, 's.whatsapp.net'),
439
599
  name: '~'
440
600
  };
441
-
442
601
  ev.emit('creds.update', authState.creds);
443
-
444
602
  await sendNode({
445
603
  tag: 'iq',
446
604
  attrs: {
447
- to: WABinary_1.S_WHATSAPP_NET,
605
+ to: S_WHATSAPP_NET,
448
606
  type: 'set',
449
607
  id: generateMessageTag(),
450
608
  xmlns: 'md'
@@ -471,7 +629,7 @@ const makeSocket = (config) => {
471
629
  {
472
630
  tag: 'companion_platform_id',
473
631
  attrs: {},
474
- content: (0, Utils_1.getPlatformId)(browser[1])
632
+ content: getPlatformId(browser[1])
475
633
  },
476
634
  {
477
635
  tag: 'companion_platform_display',
@@ -481,68 +639,38 @@ const makeSocket = (config) => {
481
639
  {
482
640
  tag: 'link_code_pairing_nonce',
483
641
  attrs: {},
484
- content: "0"
642
+ content: '0'
485
643
  }
486
644
  ]
487
645
  }
488
646
  ]
489
647
  });
490
-
491
648
  return authState.creds.pairingCode;
492
- }
649
+ };
493
650
  async function generatePairingKey() {
494
- const salt = (0, crypto_1.randomBytes)(32);
495
- const randomIv = (0, crypto_1.randomBytes)(16);
496
- const key = await (0, Utils_1.derivePairingCodeKey)(authState.creds.pairingCode, salt);
497
- 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);
498
655
  return Buffer.concat([salt, randomIv, ciphered]);
499
656
  }
500
657
  const sendWAMBuffer = (wamBuffer) => {
501
658
  return query({
502
659
  tag: 'iq',
503
660
  attrs: {
504
- to: WABinary_1.S_WHATSAPP_NET,
661
+ to: S_WHATSAPP_NET,
505
662
  id: generateMessageTag(),
506
663
  xmlns: 'w:stats'
507
664
  },
508
665
  content: [
509
666
  {
510
667
  tag: 'add',
511
- attrs: {},
668
+ attrs: { t: Math.round(Date.now() / 1000) + '' },
512
669
  content: wamBuffer
513
670
  }
514
671
  ]
515
672
  });
516
673
  };
517
- let serverTimeOffsetMs = 0;
518
- const updateServerTimeOffset = ({ attrs }) => {
519
- const tValue = attrs && attrs.t;
520
- if (!tValue) return;
521
- const parsed = Number(tValue);
522
- if (Number.isNaN(parsed) || parsed <= 0) return;
523
- const localMs = Date.now();
524
- serverTimeOffsetMs = parsed * 1000 - localMs;
525
- logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
526
- };
527
- const getUnifiedSessionId = () => {
528
- const offsetMs = 3 * Defaults_1.TimeMs.Day;
529
- const now = Date.now() + serverTimeOffsetMs;
530
- const id = (now + offsetMs) % Defaults_1.TimeMs.Week;
531
- return id.toString();
532
- };
533
- const sendUnifiedSession = async () => {
534
- if (!ws.isOpen) return;
535
- const node = {
536
- tag: 'ib',
537
- attrs: {},
538
- content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
539
- };
540
- try {
541
- await sendNode(node);
542
- } catch (error) {
543
- logger.debug({ error }, 'failed to send unified_session telemetry');
544
- }
545
- };
546
674
  ws.on('message', onMessageReceived);
547
675
  ws.on('open', async () => {
548
676
  try {
@@ -550,25 +678,26 @@ const makeSocket = (config) => {
550
678
  }
551
679
  catch (err) {
552
680
  logger.error({ err }, 'error in validating connection');
553
- end(err);
681
+ void end(err);
554
682
  }
555
683
  });
556
684
  ws.on('error', mapWebSocketError(end));
557
- ws.on('close', () => void end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed })));
558
- 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 })));
559
688
  // QR gen
560
689
  ws.on('CB:iq,type:set,pair-device', async (stanza) => {
561
690
  const iq = {
562
691
  tag: 'iq',
563
692
  attrs: {
564
- to: WABinary_1.S_WHATSAPP_NET,
693
+ to: S_WHATSAPP_NET,
565
694
  type: 'result',
566
- id: stanza.attrs.id,
695
+ id: stanza.attrs.id
567
696
  }
568
697
  };
569
698
  await sendNode(iq);
570
- const pairDeviceNode = (0, WABinary_1.getBinaryNodeChild)(stanza, 'pair-device');
571
- const refNodes = (0, WABinary_1.getBinaryNodeChildren)(pairDeviceNode, 'ref');
699
+ const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
700
+ const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
572
701
  const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
573
702
  const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
574
703
  const advB64 = creds.advSecretKey;
@@ -579,7 +708,7 @@ const makeSocket = (config) => {
579
708
  }
580
709
  const refNode = refNodes.shift();
581
710
  if (!refNode) {
582
- 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 }));
583
712
  return;
584
713
  }
585
714
  const ref = refNode.content.toString('utf-8');
@@ -596,7 +725,7 @@ const makeSocket = (config) => {
596
725
  logger.debug('pair success recv');
597
726
  try {
598
727
  updateServerTimeOffset(stanza);
599
- const { reply, creds: updatedCreds } = (0, Utils_1.configureSuccessfulPairing)(stanza, creds);
728
+ const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
600
729
  logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
601
730
  ev.emit('creds.update', updatedCreds);
602
731
  ev.emit('connection.update', { isNewLogin: true, qr: undefined });
@@ -608,84 +737,97 @@ const makeSocket = (config) => {
608
737
  void end(error);
609
738
  }
610
739
  });
740
+ // login complete
611
741
  ws.on('CB:success', async (node) => {
612
742
  try {
613
743
  updateServerTimeOffset(node);
614
744
  await uploadPreKeysToServerIfRequired();
615
745
  await sendPassiveIq('active');
616
- } 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) {
617
755
  logger.warn({ err }, 'failed to send initial passive iq');
618
756
  }
619
757
  logger.info('opened connection to WA');
620
- clearTimeout(qrTimer);
758
+ clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
621
759
  ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
622
760
  ev.emit('connection.update', { connection: 'open' });
623
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
+ }
624
785
  });
625
786
  ws.on('CB:stream:error', (node) => {
626
- const [reasonNode] = (0, WABinary_1.getAllBinaryNodeChildren)(node);
787
+ const [reasonNode] = getAllBinaryNodeChildren(node);
627
788
  logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
628
- const { reason, statusCode } = (0, Utils_1.getErrorCodeFromStreamError)(node);
629
- 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 }));
630
791
  });
792
+ // stream fail, possible logout
631
793
  ws.on('CB:failure', (node) => {
632
794
  const reason = +(node.attrs.reason || 500);
633
- // Try to extract a human-readable description from child nodes or attrs
634
- const description = (Array.isArray(node.content) && node.content[0]?.attrs?.description)
635
- || node.attrs.reason
636
- || 'unknown';
637
- logger.warn({ reason, description, attrs: node.attrs }, 'WA connection failure received');
638
- 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 }));
639
796
  });
640
797
  ws.on('CB:ib,,downgrade_webclient', () => {
641
- 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 }));
642
799
  });
643
- ws.on('CB:ib,,offline_preview', (node) => {
800
+ ws.on('CB:ib,,offline_preview', async (node) => {
644
801
  logger.info('offline preview received', JSON.stringify(node));
645
- sendNode({
802
+ await sendNode({
646
803
  tag: 'ib',
647
804
  attrs: {},
648
805
  content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
649
806
  });
650
807
  });
651
808
  ws.on('CB:ib,,edge_routing', (node) => {
652
- const edgeRoutingNode = (0, WABinary_1.getBinaryNodeChild)(node, 'edge_routing');
653
- const routingInfo = (0, WABinary_1.getBinaryNodeChild)(edgeRoutingNode, 'routing_info');
654
- if (routingInfo === null || routingInfo === void 0 ? void 0 : routingInfo.content) {
655
- 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);
656
813
  ev.emit('creds.update', authState.creds);
657
814
  }
658
815
  });
659
816
  let didStartBuffer = false;
660
- let offlineFlushTimer = null;
661
817
  process.nextTick(() => {
662
- var _a;
663
- if ((_a = creds.me) === null || _a === void 0 ? void 0 : _a.id) {
818
+ if (creds.me?.id) {
664
819
  // start buffering important events
665
820
  // if we're logged in
666
821
  ev.buffer();
667
822
  didStartBuffer = true;
668
- // Safety net: if server never sends CB:ib,,offline (e.g. slow server, no pending msgs),
669
- // force-flush the buffer after 10s so messages/commands are never stuck
670
- offlineFlushTimer = setTimeout(() => {
671
- if (didStartBuffer && ev.isBuffering()) {
672
- logger.warn('offline event not received within 10s — force-flushing event buffer to prevent message lag');
673
- ev.flush(true);
674
- ev.emit('connection.update', { receivedPendingNotifications: true });
675
- }
676
- }, 10000);
677
823
  }
678
824
  ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
679
825
  });
680
826
  // called when all offline notifs are handled
681
827
  ws.on('CB:ib,,offline', (node) => {
682
- const child = (0, WABinary_1.getBinaryNodeChild)(node, 'offline');
683
- 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);
684
830
  logger.info(`handled ${offlineNotifs} offline messages/notifications`);
685
- if (offlineFlushTimer) {
686
- clearTimeout(offlineFlushTimer);
687
- offlineFlushTimer = null;
688
- }
689
831
  if (didStartBuffer) {
690
832
  ev.flush();
691
833
  logger.trace('flushed events for initial buffer');
@@ -694,39 +836,66 @@ const makeSocket = (config) => {
694
836
  });
695
837
  // update credentials when required
696
838
  ev.on('creds.update', update => {
697
- var _a, _b;
698
- const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;
839
+ const name = update.me?.name;
699
840
  // if name has just been received
700
- if (((_b = creds.me) === null || _b === void 0 ? void 0 : _b.name) !== name) {
841
+ if (creds.me?.name !== name) {
701
842
  logger.debug({ name }, 'updated pushName');
702
843
  sendNode({
703
844
  tag: 'presence',
704
845
  attrs: { name: name }
705
- })
706
- .catch(err => {
846
+ }).catch(err => {
707
847
  logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
708
848
  });
709
849
  }
710
850
  Object.assign(creds, update);
711
851
  });
712
- 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
+ };
713
887
  try {
714
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]);
715
- } catch (error) {
716
- logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
888
+ await sendNode(node);
717
889
  }
718
- });
719
- if (printQRInTerminal) {
720
- (0, Utils_1.printQRIfNecessaryListener)(ev, logger);
721
- }
890
+ catch (error) {
891
+ logger.debug({ error }, 'failed to send unified_session telemetry');
892
+ }
893
+ };
722
894
  return {
723
895
  type: 'md',
724
896
  ws,
725
897
  ev,
726
- authState: {
727
- creds,
728
- keys
729
- },
898
+ authState: { creds, keys },
730
899
  signalRepository,
731
900
  get user() {
732
901
  return authState.creds.me;
@@ -742,20 +911,25 @@ const makeSocket = (config) => {
742
911
  onUnexpectedError,
743
912
  uploadPreKeys,
744
913
  uploadPreKeysToServerIfRequired,
914
+ digestKeyBundle,
915
+ rotateSignedPreKey,
745
916
  requestPairingCode,
746
917
  updateServerTimeOffset,
747
918
  sendUnifiedSession,
748
- waitForConnectionUpdate: (0, Utils_1.bindWaitForConnectionUpdate)(ev),
919
+ wamBuffer: publicWAMBuffer,
920
+ /** Waits for the connection to WA to reach a state */
921
+ waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
749
922
  sendWAMBuffer,
923
+ executeUSyncQuery,
924
+ onWhatsApp
750
925
  };
751
926
  };
752
- exports.makeSocket = makeSocket;
753
927
  /**
754
928
  * map the websocket error to the right type
755
929
  * so it can be retried by the caller
756
930
  * */
757
931
  function mapWebSocketError(handler) {
758
932
  return (error) => {
759
- 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 }));
760
934
  };
761
- }
935
+ }