@mrlegendbot/baileys 1.2.5 → 1.2.7

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