@ryuu-reinzz/baileys 3.5.0 → 5.0.0

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 (41) hide show
  1. package/README.md +30 -25
  2. package/WAProto/fix-imports.js +22 -18
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/index.js +10 -9
  5. package/lib/Signal/libsignal.js +46 -19
  6. package/lib/Signal/lid-mapping.js +6 -0
  7. package/lib/Socket/chats.js +241 -39
  8. package/lib/Socket/groups.js +20 -0
  9. package/lib/Socket/messages-recv.js +736 -314
  10. package/lib/Socket/messages-send.js +279 -129
  11. package/lib/Socket/newsletter.js +2 -2
  12. package/lib/Socket/socket.js +56 -25
  13. package/lib/Types/{Newsletter.js → Mex.js} +9 -3
  14. package/lib/Types/State.js +43 -0
  15. package/lib/Types/index.js +1 -1
  16. package/lib/Utils/auth-utils.js +12 -0
  17. package/lib/Utils/chat-utils.js +80 -20
  18. package/lib/Utils/companion-reg-client-utils.js +35 -0
  19. package/lib/Utils/decode-wa-message.js +34 -0
  20. package/lib/Utils/event-buffer.js +49 -1
  21. package/lib/Utils/generics.js +12 -3
  22. package/lib/Utils/history.js +12 -9
  23. package/lib/Utils/identity-change-handler.js +1 -0
  24. package/lib/Utils/index.js +3 -1
  25. package/lib/Utils/link-preview.js +2 -2
  26. package/lib/Utils/message-retry-manager.js +40 -0
  27. package/lib/Utils/messages-media.js +21 -7
  28. package/lib/Utils/messages.js +28 -5
  29. package/lib/Utils/offline-node-processor.js +40 -0
  30. package/lib/Utils/process-message.js +103 -1
  31. package/lib/Utils/signal.js +42 -0
  32. package/lib/Utils/stanza-ack.js +38 -0
  33. package/lib/Utils/sync-action-utils.js +1 -0
  34. package/lib/Utils/tc-token-utils.js +149 -4
  35. package/lib/Utils/validate-connection.js +3 -0
  36. package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
  37. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  38. package/lib/WAUSync/Protocols/index.js +1 -0
  39. package/lib/WAUSync/USyncQuery.js +6 -2
  40. package/lib/WAUSync/USyncUser.js +8 -0
  41. package/package.json +39 -12
@@ -3,14 +3,15 @@ import { randomBytes } from 'crypto';
3
3
  import { URL } from 'url';
4
4
  import { promisify } from 'util';
5
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';
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';
8
+ import { DisconnectReason, XWAPaths } from '../Types/index.js';
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';
10
10
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
11
11
  import { BinaryInfo } from '../WAM/BinaryInfo.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
13
  import { WebSocketClient } from './Client/index.js';
14
+ import { executeWMexQuery } from './mex.js';
14
15
  /**
15
16
  * Connects to WA servers and performs:
16
17
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -270,6 +271,7 @@ export const makeSocket = (config) => {
270
271
  let keepAliveReq;
271
272
  let qrTimer;
272
273
  let closed = false;
274
+ const socketEndHandlers = [];
273
275
  /** log & process any unexpected errors */
274
276
  const onUnexpectedError = (err, msg) => {
275
277
  logger.error({ err }, `unexpected error in '${msg}'`);
@@ -344,25 +346,16 @@ export const makeSocket = (config) => {
344
346
  const countChild = getBinaryNodeChild(result, 'count');
345
347
  return +countChild.attrs.value;
346
348
  };
347
- // Pre-key upload state management
349
+ // WAWeb has no time throttle here; the server drives uploads via PreKeyLow notifications.
348
350
  let uploadPreKeysPromise = null;
349
- let lastUploadTime = 0;
350
351
  /** 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)
353
- if (retryCount === 0) {
354
- const timeSinceLastUpload = Date.now() - lastUploadTime;
355
- if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
356
- logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
357
- return;
358
- }
359
- }
360
- // Prevent multiple concurrent uploads
352
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT) => {
361
353
  if (uploadPreKeysPromise) {
362
354
  logger.debug('Pre-key upload already in progress, waiting for completion');
363
355
  await uploadPreKeysPromise;
356
+ return;
364
357
  }
365
- const uploadLogic = async () => {
358
+ const uploadLogic = async (retryCount) => {
366
359
  logger.info({ count, retryCount }, 'uploading pre-keys');
367
360
  // Generate and save pre-keys atomically (prevents ID collisions on retry)
368
361
  const node = await keys.transaction(async () => {
@@ -370,29 +363,28 @@ export const makeSocket = (config) => {
370
363
  const { update, node } = await getNextPreKeysNode({ creds, keys }, count);
371
364
  // Update credentials immediately to prevent duplicate IDs on retry
372
365
  ev.emit('creds.update', update);
373
- return node; // Only return node since update is already used
366
+ return node;
374
367
  }, creds?.me?.id || 'upload-pre-keys');
375
368
  // Upload to server (outside transaction, can fail without affecting local keys)
376
369
  try {
377
370
  await query(node);
378
371
  logger.info({ count }, 'uploaded pre-keys successfully');
379
- lastUploadTime = Date.now();
380
372
  }
381
373
  catch (uploadError) {
382
374
  logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
383
- // Exponential backoff retry (max 3 retries)
375
+ // Recurse into uploadLogic; calling uploadPreKeys would await its own in-flight promise.
384
376
  if (retryCount < 3) {
385
377
  const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
386
378
  logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
387
379
  await new Promise(resolve => setTimeout(resolve, backoffDelay));
388
- return uploadPreKeys(count, retryCount + 1);
380
+ return uploadLogic(retryCount + 1);
389
381
  }
390
382
  throw uploadError;
391
383
  }
392
384
  };
393
385
  // Add timeout protection
394
386
  uploadPreKeysPromise = Promise.race([
395
- uploadLogic(),
387
+ uploadLogic(0),
396
388
  new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
397
389
  ]);
398
390
  try {
@@ -486,12 +478,21 @@ export const makeSocket = (config) => {
486
478
  ws.removeAllListeners('close');
487
479
  ws.removeAllListeners('open');
488
480
  ws.removeAllListeners('message');
481
+ signalRepository.close?.();
489
482
  if (!ws.isClosed && !ws.isClosing) {
490
483
  try {
491
484
  await ws.close();
492
485
  }
493
486
  catch { }
494
487
  }
488
+ for (const handler of socketEndHandlers) {
489
+ try {
490
+ await handler(error);
491
+ }
492
+ catch (err) {
493
+ logger.error({ err }, 'error in socket end handler');
494
+ }
495
+ }
495
496
  ev.emit('connection.update', {
496
497
  connection: 'close',
497
498
  lastDisconnect: {
@@ -500,6 +501,7 @@ export const makeSocket = (config) => {
500
501
  }
501
502
  });
502
503
  ev.removeAllListeners('connection.update');
504
+ ev.destroy();
503
505
  };
504
506
  const waitForSocketOpen = async () => {
505
507
  if (ws.isOpen) {
@@ -629,7 +631,7 @@ export const makeSocket = (config) => {
629
631
  {
630
632
  tag: 'companion_platform_id',
631
633
  attrs: {},
632
- content: getPlatformId(browser[1])
634
+ content: getCompanionPlatformId(browser)
633
635
  },
634
636
  {
635
637
  tag: 'companion_platform_display',
@@ -712,7 +714,7 @@ export const makeSocket = (config) => {
712
714
  return;
713
715
  }
714
716
  const ref = refNode.content.toString('utf-8');
715
- const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',');
717
+ const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser);
716
718
  ev.emit('connection.update', { qr });
717
719
  qrTimer = setTimeout(genPairQR, qrMs);
718
720
  qrMs = qrTimeout || 20000; // shorter subsequent qrs
@@ -891,6 +893,32 @@ export const makeSocket = (config) => {
891
893
  logger.debug({ error }, 'failed to send unified_session telemetry');
892
894
  }
893
895
  };
896
+ const registerSocketEndHandler = (handler) => {
897
+ socketEndHandlers.push(handler);
898
+ };
899
+ /**
900
+ * Fetches your account's standing when it comes to restrictions.
901
+ * @returns Returns the state of the restrictions.
902
+ */
903
+ const fetchAccountReachoutTimelock = async () => {
904
+ const queryResult = await executeWMexQuery({}, QueryIds.REACHOUT_TIMELOCK, XWAPaths.xwa2_fetch_account_reachout_timelock, query, generateMessageTag);
905
+ const result = {
906
+ isActive: !!queryResult?.is_active,
907
+ timeEnforcementEnds: queryResult?.time_enforcement_ends && queryResult?.time_enforcement_ends !== '0'
908
+ ? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
909
+ : undefined,
910
+ enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
911
+ };
912
+ ev.emit('connection.update', { reachoutTimeLock: result });
913
+ return result;
914
+ };
915
+ /**
916
+ * Fetches your account's new chat limits.
917
+ * @returns Returns the quota and the usage.
918
+ */
919
+ const fetchNewChatMessageCap = async () => {
920
+ return executeWMexQuery({ input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } }, QueryIds.MESSAGE_CAPPING_INFO, XWAPaths.xwa2_message_capping_info, query, generateMessageTag);
921
+ };
894
922
  return {
895
923
  type: 'md',
896
924
  ws,
@@ -908,6 +936,7 @@ export const makeSocket = (config) => {
908
936
  sendNode,
909
937
  logout,
910
938
  end,
939
+ registerSocketEndHandler,
911
940
  onUnexpectedError,
912
941
  uploadPreKeys,
913
942
  uploadPreKeysToServerIfRequired,
@@ -921,7 +950,9 @@ export const makeSocket = (config) => {
921
950
  waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
922
951
  sendWAMBuffer,
923
952
  executeUSyncQuery,
924
- onWhatsApp
953
+ onWhatsApp,
954
+ fetchAccountReachoutTimelock,
955
+ fetchNewChatMessageCap
925
956
  };
926
957
  };
927
958
  /**
@@ -9,9 +9,13 @@ export var XWAPaths;
9
9
  XWAPaths["xwa2_newsletter_unmute_v2"] = "xwa2_newsletter_unmute_v2";
10
10
  XWAPaths["xwa2_newsletter_follow"] = "xwa2_newsletter_follow";
11
11
  XWAPaths["xwa2_newsletter_unfollow"] = "xwa2_newsletter_unfollow";
12
+ XWAPaths["xwa2_newsletter_join_v2"] = "xwa2_newsletter_join_v2";
13
+ XWAPaths["xwa2_newsletter_leave_v2"] = "xwa2_newsletter_leave_v2";
12
14
  XWAPaths["xwa2_newsletter_change_owner"] = "xwa2_newsletter_change_owner";
13
15
  XWAPaths["xwa2_newsletter_demote"] = "xwa2_newsletter_demote";
14
16
  XWAPaths["xwa2_newsletter_delete_v2"] = "xwa2_newsletter_delete_v2";
17
+ XWAPaths["xwa2_fetch_account_reachout_timelock"] = "xwa2_fetch_account_reachout_timelock";
18
+ XWAPaths["xwa2_message_capping_info"] = "xwa2_message_capping_info";
15
19
  })(XWAPaths || (XWAPaths = {}));
16
20
  export var QueryIds;
17
21
  (function (QueryIds) {
@@ -19,13 +23,15 @@ export var QueryIds;
19
23
  QueryIds["UPDATE_METADATA"] = "24250201037901610";
20
24
  QueryIds["METADATA"] = "6563316087068696";
21
25
  QueryIds["SUBSCRIBERS"] = "9783111038412085";
22
- QueryIds["FOLLOW"] = "7871414976211147";
23
- QueryIds["UNFOLLOW"] = "7238632346214362";
26
+ QueryIds["FOLLOW"] = "24404358912487870";
27
+ QueryIds["UNFOLLOW"] = "9767147403369991";
24
28
  QueryIds["MUTE"] = "29766401636284406";
25
29
  QueryIds["UNMUTE"] = "9864994326891137";
26
30
  QueryIds["ADMIN_COUNT"] = "7130823597031706";
27
31
  QueryIds["CHANGE_OWNER"] = "7341777602580933";
28
32
  QueryIds["DEMOTE"] = "6551828931592903";
29
33
  QueryIds["DELETE"] = "30062808666639665";
34
+ QueryIds["REACHOUT_TIMELOCK"] = "23983697327930364";
35
+ QueryIds["MESSAGE_CAPPING_INFO"] = "24503548349331633";
30
36
  })(QueryIds || (QueryIds = {}));
31
- //# sourceMappingURL=Newsletter.js.map
37
+ //# sourceMappingURL=Mex.js.map
@@ -10,4 +10,47 @@ export var SyncState;
10
10
  /** Initial sync is complete, or was skipped. The socket is fully operational and events are processed in real-time. */
11
11
  SyncState[SyncState["Online"] = 3] = "Online";
12
12
  })(SyncState || (SyncState = {}));
13
+ export var ReachoutTimelockEnforcementType;
14
+ (function (ReachoutTimelockEnforcementType) {
15
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ALCOHOL"] = "BIZ_COMMERCE_VIOLATION_ALCOHOL";
16
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ADULT"] = "BIZ_COMMERCE_VIOLATION_ADULT";
17
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_ANIMALS"] = "BIZ_COMMERCE_VIOLATION_ANIMALS";
18
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS"] = "BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS";
19
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DATING"] = "BIZ_COMMERCE_VIOLATION_DATING";
20
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS"] = "BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS";
21
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DRUGS"] = "BIZ_COMMERCE_VIOLATION_DRUGS";
22
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC"] = "BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC";
23
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_GAMBLING"] = "BIZ_COMMERCE_VIOLATION_GAMBLING";
24
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_HEALTHCARE"] = "BIZ_COMMERCE_VIOLATION_HEALTHCARE";
25
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY"] = "BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY";
26
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_SUPPLEMENTS"] = "BIZ_COMMERCE_VIOLATION_SUPPLEMENTS";
27
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_TOBACCO"] = "BIZ_COMMERCE_VIOLATION_TOBACCO";
28
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT"] = "BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT";
29
+ ReachoutTimelockEnforcementType["BIZ_COMMERCE_VIOLATION_WEAPONS"] = "BIZ_COMMERCE_VIOLATION_WEAPONS";
30
+ ReachoutTimelockEnforcementType["BIZ_QUALITY"] = "BIZ_QUALITY";
31
+ /** This means there is no restriction */
32
+ ReachoutTimelockEnforcementType["DEFAULT"] = "DEFAULT";
33
+ ReachoutTimelockEnforcementType["WEB_COMPANION_ONLY"] = "WEB_COMPANION_ONLY";
34
+ })(ReachoutTimelockEnforcementType || (ReachoutTimelockEnforcementType = {}));
35
+ export var NewChatMessageCappingStatusType;
36
+ (function (NewChatMessageCappingStatusType) {
37
+ NewChatMessageCappingStatusType["NONE"] = "NONE";
38
+ NewChatMessageCappingStatusType["FIRST_WARNING"] = "FIRST_WARNING";
39
+ NewChatMessageCappingStatusType["SECOND_WARNING"] = "SECOND_WARNING";
40
+ NewChatMessageCappingStatusType["CAPPED"] = "CAPPED";
41
+ })(NewChatMessageCappingStatusType || (NewChatMessageCappingStatusType = {}));
42
+ export var NewChatMessageCappingMVStatusType;
43
+ (function (NewChatMessageCappingMVStatusType) {
44
+ NewChatMessageCappingMVStatusType["NOT_ELIGIBLE"] = "NOT_ELIGIBLE";
45
+ NewChatMessageCappingMVStatusType["NOT_ACTIVE"] = "NOT_ACTIVE";
46
+ NewChatMessageCappingMVStatusType["ACTIVE"] = "ACTIVE";
47
+ NewChatMessageCappingMVStatusType["ACTIVE_UPGRADE_AVAILABLE"] = "ACTIVE_UPGRADE_AVAILABLE";
48
+ })(NewChatMessageCappingMVStatusType || (NewChatMessageCappingMVStatusType = {}));
49
+ export var NewChatMessageCappingOTEStatusType;
50
+ (function (NewChatMessageCappingOTEStatusType) {
51
+ NewChatMessageCappingOTEStatusType["NOT_ELIGIBLE"] = "NOT_ELIGIBLE";
52
+ NewChatMessageCappingOTEStatusType["ELIGIBLE"] = "ELIGIBLE";
53
+ NewChatMessageCappingOTEStatusType["ACTIVE_IN_CURRENT_CYCLE"] = "ACTIVE_IN_CURRENT_CYCLE";
54
+ NewChatMessageCappingOTEStatusType["EXHAUSTED"] = "EXHAUSTED";
55
+ })(NewChatMessageCappingOTEStatusType || (NewChatMessageCappingOTEStatusType = {}));
13
56
  //# sourceMappingURL=State.js.map
@@ -9,7 +9,7 @@ export * from './Events.js';
9
9
  export * from './Product.js';
10
10
  export * from './Call.js';
11
11
  export * from './Signal.js';
12
- export * from './Newsletter.js';
12
+ export * from './Mex.js';
13
13
  export var DisconnectReason;
14
14
  (function (DisconnectReason) {
15
15
  DisconnectReason[DisconnectReason["connectionClosed"] = 428] = "connectionClosed";
@@ -1,4 +1,5 @@
1
1
  import NodeCache from '@cacheable/node-cache';
2
+ import { Boom } from '@hapi/boom';
2
3
  import { AsyncLocalStorage } from 'async_hooks';
3
4
  import { Mutex } from 'async-mutex';
4
5
  import { randomBytes } from 'crypto';
@@ -264,6 +265,17 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
264
265
  }
265
266
  };
266
267
  };
268
+ /**
269
+ * Returns the authenticated user's JID, or throws a Boom-401 if creds are not yet authenticated.
270
+ * Use this anywhere we'd otherwise reach for `creds.me!.id` to fail fast with a descriptive error.
271
+ */
272
+ export const assertMeId = (creds) => {
273
+ const id = creds.me?.id;
274
+ if (!id) {
275
+ throw new Boom('Cannot proceed: socket is not authenticated yet (creds.me.id is missing)', { statusCode: 401 });
276
+ }
277
+ return id;
278
+ };
267
279
  export const initAuthCreds = () => {
268
280
  const identityKey = Curve.generateKeyPair();
269
281
  return {
@@ -38,7 +38,7 @@ const to64BitNetworkOrder = (e) => {
38
38
  buff.writeUint32BE(e, 4);
39
39
  return buff;
40
40
  };
41
- const makeLtHashGenerator = ({ indexValueMap, hash }) => {
41
+ export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
42
42
  indexValueMap = { ...indexValueMap };
43
43
  const addBuffs = [];
44
44
  const subBuffs = [];
@@ -48,7 +48,10 @@ const makeLtHashGenerator = ({ indexValueMap, hash }) => {
48
48
  const prevOp = indexValueMap[indexMacBase64];
49
49
  if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
50
50
  if (!prevOp) {
51
- throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } });
51
+ // WA Web does not throw here it logs a warning and skips the subtract.
52
+ // The missing REMOVE will cause an LTHash mismatch, which is handled
53
+ // by the MAC validation layer (snapshot recovery or retry).
54
+ return;
52
55
  }
53
56
  // remove from index value mac, since this mutation is erased
54
57
  delete indexValueMap[indexMacBase64];
@@ -80,10 +83,33 @@ const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => {
80
83
  return hmacSign(total, key);
81
84
  };
82
85
  export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} });
86
+ export const ensureLTHashStateVersion = (state) => {
87
+ if (typeof state.version !== 'number' || isNaN(state.version)) {
88
+ state.version = 0;
89
+ }
90
+ return state;
91
+ };
92
+ export const MAX_SYNC_ATTEMPTS = 2;
93
+ /**
94
+ * Check if an error is a missing app state sync key.
95
+ * WA Web treats these as "Blocked" (waits for key arrival), not fatal.
96
+ * In Baileys we retry with a snapshot which may use a different key.
97
+ */
98
+ export const isMissingKeyError = (error) => {
99
+ return error?.data?.isMissingKey === true;
100
+ };
101
+ /**
102
+ * Determines if an app state sync error is unrecoverable.
103
+ * TypeError indicates a WASM crash; otherwise we give up after MAX_SYNC_ATTEMPTS.
104
+ * Missing keys are NOT checked here — they are handled separately as "Blocked".
105
+ */
106
+ export const isAppStateSyncIrrecoverable = (error, attempts) => {
107
+ return attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError';
108
+ };
83
109
  export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
84
110
  const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;
85
111
  if (!key) {
86
- throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 });
112
+ throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } });
87
113
  }
88
114
  const encKeyId = Buffer.from(myAppStateKeyId, 'base64');
89
115
  state = { ...state, indexValueMap: { ...state.indexValueMap } };
@@ -139,17 +165,36 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
139
165
  // otherwise, if it's only a record -- it'll be a SET mutation
140
166
  const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET;
141
167
  const record = 'record' in msgMutation && !!msgMutation.record ? msgMutation.record : msgMutation;
142
- const key = await getKey(record.keyId.id);
168
+ let key;
169
+ try {
170
+ key = await getKey(record.keyId.id);
171
+ }
172
+ catch (err) {
173
+ // Missing-key errors must propagate so the orchestrator can park the
174
+ // collection (Blocked) and retry when APP_STATE_SYNC_KEY_SHARE arrives.
175
+ // Other errors → individual record corruption, skip and keep going.
176
+ if (isMissingKeyError(err))
177
+ throw err;
178
+ continue;
179
+ }
143
180
  const content = record.value.blob;
144
181
  const encContent = content.subarray(0, -32);
145
182
  const ogValueMac = content.subarray(-32);
146
183
  if (validateMacs) {
147
184
  const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey);
148
185
  if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
149
- throw new Boom('HMAC content verification failed');
186
+ // HMAC verification failed — skip this record
187
+ continue;
150
188
  }
151
189
  }
152
- const result = aesDecrypt(encContent, key.valueEncryptionKey);
190
+ let result;
191
+ try {
192
+ result = aesDecrypt(encContent, key.valueEncryptionKey);
193
+ }
194
+ catch {
195
+ // decrypt failed — skip this record instead of aborting
196
+ continue;
197
+ }
153
198
  const syncAction = proto.SyncActionData.decode(result);
154
199
  if (validateMacs) {
155
200
  const hmac = hmacSign(syncAction.index, key.indexKey);
@@ -175,8 +220,7 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
175
220
  const keyEnc = await getAppStateSyncKey(base64Key);
176
221
  if (!keyEnc) {
177
222
  throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
178
- statusCode: 404,
179
- data: { msgMutations }
223
+ data: { isMissingKey: true, msgMutations }
180
224
  });
181
225
  }
182
226
  const keys = mutationKeys(keyEnc.keyData);
@@ -189,7 +233,7 @@ export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncK
189
233
  const base64Key = Buffer.from(msg.keyId.id).toString('base64');
190
234
  const mainKeyObj = await getAppStateSyncKey(base64Key);
191
235
  if (!mainKeyObj) {
192
- throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } });
236
+ throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } });
193
237
  }
194
238
  const mainKey = mutationKeys(mainKeyObj.keyData);
195
239
  const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));
@@ -250,7 +294,7 @@ export const downloadExternalPatch = async (blob, options) => {
250
294
  const syncData = proto.SyncdMutations.decode(buffer);
251
295
  return syncData;
252
296
  };
253
- export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true) => {
297
+ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true, logger) => {
254
298
  const newState = newLTHashState();
255
299
  newState.version = toNumber(snapshot.version.version);
256
300
  const mutationMap = {};
@@ -267,12 +311,17 @@ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, mi
267
311
  const base64Key = Buffer.from(snapshot.keyId.id).toString('base64');
268
312
  const keyEnc = await getAppStateSyncKey(base64Key);
269
313
  if (!keyEnc) {
270
- throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
314
+ throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
271
315
  }
272
316
  const result = mutationKeys(keyEnc.keyData);
273
317
  const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
274
318
  if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {
275
- throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`);
319
+ // LTHash verification may fail when decodeSyncdMutations skipped undecryptable
320
+ // records (poisoned server-side snapshot); the aggregate client hash diverges
321
+ // from the server-computed mac. Fall through with a warning so the session stays
322
+ // alive with partial state, symmetric to how decodePatches handles its own
323
+ // LTHash mismatch a few lines below.
324
+ logger?.warn({ name, version: newState.version }, 'LTHash verification failed on snapshot, continuing with partial state');
276
325
  }
277
326
  }
278
327
  return {
@@ -297,24 +346,34 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
297
346
  const patchVersion = toNumber(version.version);
298
347
  newState.version = patchVersion;
299
348
  const shouldMutate = typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber;
300
- const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, shouldMutate
301
- ? mutation => {
302
- const index = mutation.syncAction.index?.toString();
303
- mutationMap[index] = mutation;
304
- }
305
- : () => { }, true);
349
+ let decodeResult;
350
+ try {
351
+ decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, shouldMutate
352
+ ? mutation => {
353
+ const index = mutation.syncAction.index?.toString();
354
+ mutationMap[index] = mutation;
355
+ }
356
+ : () => { }, validateMacs);
357
+ }
358
+ catch (err) {
359
+ if (isMissingKeyError(err))
360
+ throw err;
361
+ logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping');
362
+ continue;
363
+ }
306
364
  newState.hash = decodeResult.hash;
307
365
  newState.indexValueMap = decodeResult.indexValueMap;
308
366
  if (validateMacs) {
309
367
  const base64Key = Buffer.from(keyId.id).toString('base64');
310
368
  const keyEnc = await getAppStateSyncKey(base64Key);
311
369
  if (!keyEnc) {
312
- throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
370
+ throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
313
371
  }
314
372
  const result = mutationKeys(keyEnc.keyData);
315
373
  const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
316
374
  if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
317
- throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`);
375
+ logger?.warn({ name, version: newState.version }, 'LTHash verification failed, skipping remaining patches');
376
+ break;
318
377
  }
319
378
  }
320
379
  // clear memory used up by the mutations
@@ -779,6 +838,7 @@ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) =
779
838
  action.lidContactAction.firstName ||
780
839
  action.lidContactAction.username ||
781
840
  undefined,
841
+ username: action.lidContactAction.username || undefined,
782
842
  lid: id,
783
843
  phoneNumber: undefined
784
844
  }
@@ -0,0 +1,35 @@
1
+ export var CompanionWebClientType;
2
+ (function (CompanionWebClientType) {
3
+ CompanionWebClientType[CompanionWebClientType["UNKNOWN"] = 0] = "UNKNOWN";
4
+ CompanionWebClientType[CompanionWebClientType["CHROME"] = 1] = "CHROME";
5
+ CompanionWebClientType[CompanionWebClientType["EDGE"] = 2] = "EDGE";
6
+ CompanionWebClientType[CompanionWebClientType["FIREFOX"] = 3] = "FIREFOX";
7
+ CompanionWebClientType[CompanionWebClientType["IE"] = 4] = "IE";
8
+ CompanionWebClientType[CompanionWebClientType["OPERA"] = 5] = "OPERA";
9
+ CompanionWebClientType[CompanionWebClientType["SAFARI"] = 6] = "SAFARI";
10
+ CompanionWebClientType[CompanionWebClientType["ELECTRON"] = 7] = "ELECTRON";
11
+ CompanionWebClientType[CompanionWebClientType["UWP"] = 8] = "UWP";
12
+ CompanionWebClientType[CompanionWebClientType["OTHER_WEB_CLIENT"] = 9] = "OTHER_WEB_CLIENT";
13
+ })(CompanionWebClientType || (CompanionWebClientType = {}));
14
+ const BROWSER_TO_COMPANION_WEB_CLIENT = {
15
+ Chrome: CompanionWebClientType.CHROME,
16
+ Edge: CompanionWebClientType.EDGE,
17
+ Firefox: CompanionWebClientType.FIREFOX,
18
+ IE: CompanionWebClientType.IE,
19
+ Opera: CompanionWebClientType.OPERA,
20
+ Safari: CompanionWebClientType.SAFARI
21
+ };
22
+ export const getCompanionWebClientType = ([os, browserName]) => {
23
+ if (browserName === 'Desktop') {
24
+ return os === 'Windows' ? CompanionWebClientType.UWP : CompanionWebClientType.ELECTRON;
25
+ }
26
+ return BROWSER_TO_COMPANION_WEB_CLIENT[browserName] || CompanionWebClientType.OTHER_WEB_CLIENT;
27
+ };
28
+ export const getCompanionPlatformId = (browser) => {
29
+ return getCompanionWebClientType(browser).toString();
30
+ };
31
+ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, browser) => {
32
+ return ('https://wa.me/settings/linked_devices#' +
33
+ [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(','));
34
+ };
35
+ //# sourceMappingURL=companion-reg-client-utils.js.map
@@ -27,13 +27,16 @@ const storeMappingFromEnvelope = async (stanza, sender, repository, decryptionJi
27
27
  };
28
28
  export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node';
29
29
  export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled';
30
+ export const ACCOUNT_RESTRICTED_TEXT = 'Your account has been restricted';
30
31
  // Retry configuration for failed decryption
31
32
  export const DECRYPTION_RETRY_CONFIG = {
32
33
  maxRetries: 3,
33
34
  baseDelayMs: 100,
34
35
  sessionRecordErrors: ['No session record', 'SessionError: No session record']
35
36
  };
37
+ /** NACK reason codes we send to the server (client → server) */
36
38
  export const NACK_REASONS = {
39
+ SenderReachoutTimelocked: 463,
37
40
  ParsingError: 487,
38
41
  UnrecognizedStanza: 488,
39
42
  UnrecognizedStanzaClass: 489,
@@ -48,6 +51,21 @@ export const NACK_REASONS = {
48
51
  UnsupportedLIDGroup: 551,
49
52
  DBOperationFailed: 552
50
53
  };
54
+ /**
55
+ * Server-side error codes returned in ack stanzas (server → client) that we
56
+ * currently have dedicated handlers for. Extend as more handlers are added.
57
+ * Distinct from the client-side NackReason enum (WAWebCreateNackFromStanza).
58
+ */
59
+ export const SERVER_ERROR_CODES = {
60
+ /**
61
+ * 1:1 message missing privacy token (tctoken). Usually means the account is
62
+ * restricted: WhatsApp blocks starting new chats but preserves existing ones,
63
+ * since established chats already carry a tctoken.
64
+ */
65
+ MessageAccountRestriction: '463',
66
+ /** Stanza validation failure (SMAX_INVALID) — likely stale device session */
67
+ SmaxInvalid: '479'
68
+ };
51
69
  export const extractAddressingContext = (stanza) => {
52
70
  let senderAlt;
53
71
  let recipientAlt;
@@ -88,6 +106,12 @@ export function decodeMessageNode(stanza, meId, meLid) {
88
106
  const from = stanza.attrs.from;
89
107
  const participant = stanza.attrs.participant;
90
108
  const recipient = stanza.attrs.recipient;
109
+ if (!msgId) {
110
+ throw new Boom('Invalid message stanza: missing id attribute', { data: stanza });
111
+ }
112
+ if (!from) {
113
+ throw new Boom('Invalid message stanza: missing from attribute', { data: stanza });
114
+ }
91
115
  const addressingContext = extractAddressingContext(stanza);
92
116
  const isMe = (jid) => areJidsSameUser(jid, meId);
93
117
  const isMeLid = (jid) => areJidsSameUser(jid, meLid);
@@ -102,6 +126,12 @@ export function decodeMessageNode(stanza, meId, meLid) {
102
126
  chatId = recipient;
103
127
  }
104
128
  else {
129
+ // Peer-routed self stanzas (history sync, app-state sync, etc.) arrive
130
+ // with `from` set to our own device but no `recipient` attribute —
131
+ // still mark as fromMe so self-only protocolMessage handlers run.
132
+ if (isMe(from) || isMeLid(from)) {
133
+ fromMe = true;
134
+ }
105
135
  chatId = from;
106
136
  }
107
137
  msgType = 'chat';
@@ -148,10 +178,14 @@ export function decodeMessageNode(stanza, meId, meLid) {
148
178
  const key = {
149
179
  remoteJid: chatId,
150
180
  remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
181
+ remoteJidUsername: !isJidGroup(chatId)
182
+ ? stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username
183
+ : undefined,
151
184
  fromMe,
152
185
  id: msgId,
153
186
  participant,
154
187
  participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
188
+ participantUsername: stanza.attrs.participant ? stanza.attrs.participant_username : undefined,
155
189
  addressingMode: addressingContext.addressingMode,
156
190
  ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
157
191
  };