@aunucore/baileys 1.0.86 → 1.0.88

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.
@@ -1779,6 +1779,7 @@ message Message {
1779
1779
  optional StickerPackMessage stickerPackMessage = 86;
1780
1780
  optional FutureProofMessage statusMentionMessage = 87;
1781
1781
  optional PollResultSnapshotMessage pollResultSnapshotMessage = 88;
1782
+ optional PollResultSnapshotMessage pollResultSnapshotMessageV3 = 115;
1782
1783
  optional FutureProofMessage pollCreationOptionImageMessage = 90;
1783
1784
  optional FutureProofMessage associatedChildMessage = 91;
1784
1785
  optional FutureProofMessage groupStatusMentionMessage = 92;
@@ -18838,6 +18838,9 @@ export namespace proto {
18838
18838
  /** Message pollResultSnapshotMessage */
18839
18839
  pollResultSnapshotMessage?: (proto.Message.IPollResultSnapshotMessage|null);
18840
18840
 
18841
+ /** Message pollResultSnapshotMessageV3 */
18842
+ pollResultSnapshotMessageV3?: (proto.Message.IPollResultSnapshotMessage|null);
18843
+
18841
18844
  /** Message pollCreationOptionImageMessage */
18842
18845
  pollCreationOptionImageMessage?: (proto.Message.IFutureProofMessage|null);
18843
18846
 
@@ -19118,6 +19121,9 @@ export namespace proto {
19118
19121
  /** Message pollResultSnapshotMessage. */
19119
19122
  public pollResultSnapshotMessage?: (proto.Message.IPollResultSnapshotMessage|null);
19120
19123
 
19124
+ /** Message pollResultSnapshotMessageV3. */
19125
+ public pollResultSnapshotMessageV3?: (proto.Message.IPollResultSnapshotMessage|null);
19126
+
19121
19127
  /** Message pollCreationOptionImageMessage. */
19122
19128
  public pollCreationOptionImageMessage?: (proto.Message.IFutureProofMessage|null);
19123
19129
 
package/WAProto/index.js CHANGED
@@ -57492,6 +57492,7 @@ export const proto = $root.proto = (() => {
57492
57492
  * @property {proto.Message.IStickerPackMessage|null} [stickerPackMessage] Message stickerPackMessage
57493
57493
  * @property {proto.Message.IFutureProofMessage|null} [statusMentionMessage] Message statusMentionMessage
57494
57494
  * @property {proto.Message.IPollResultSnapshotMessage|null} [pollResultSnapshotMessage] Message pollResultSnapshotMessage
57495
+ * @property {proto.Message.IPollResultSnapshotMessage|null} [pollResultSnapshotMessageV3] Message pollResultSnapshotMessageV3
57495
57496
  * @property {proto.Message.IFutureProofMessage|null} [pollCreationOptionImageMessage] Message pollCreationOptionImageMessage
57496
57497
  * @property {proto.Message.IFutureProofMessage|null} [associatedChildMessage] Message associatedChildMessage
57497
57498
  * @property {proto.Message.IFutureProofMessage|null} [groupStatusMentionMessage] Message groupStatusMentionMessage
@@ -58113,7 +58114,13 @@ export const proto = $root.proto = (() => {
58113
58114
  * @memberof proto.Message
58114
58115
  * @instance
58115
58116
  */
58116
- Message.prototype.pollResultSnapshotMessage = null;
58117
+ Message.prototype.pollResultSnapshotMessage = null; /**
58118
+ * Message pollResultSnapshotMessageV3.
58119
+ * @member {proto.Message.IPollResultSnapshotMessage|null|undefined} pollResultSnapshotMessageV3
58120
+ * @memberof proto.Message
58121
+ * @instance
58122
+ */
58123
+ Message.prototype.pollResultSnapshotMessageV3 = null;
58117
58124
 
58118
58125
  /**
58119
58126
  * Message pollCreationOptionImageMessage.
@@ -58685,6 +58692,9 @@ export const proto = $root.proto = (() => {
58685
58692
  Object.defineProperty(Message.prototype, "_pollResultSnapshotMessage", {
58686
58693
  get: $util.oneOfGetter($oneOfFields = ["pollResultSnapshotMessage"]),
58687
58694
  set: $util.oneOfSetter($oneOfFields)
58695
+ }); Object.defineProperty(Message.prototype, "_pollResultSnapshotMessageV3", {
58696
+ get: $util.oneOfGetter($oneOfFields = ["pollResultSnapshotMessageV3"]),
58697
+ set: $util.oneOfSetter($oneOfFields)
58688
58698
  });
58689
58699
 
58690
58700
  // Virtual OneOf for proto3 optional field
@@ -58953,7 +58963,8 @@ export const proto = $root.proto = (() => {
58953
58963
  if (message.statusMentionMessage != null && Object.hasOwnProperty.call(message, "statusMentionMessage"))
58954
58964
  $root.proto.Message.FutureProofMessage.encode(message.statusMentionMessage, writer.uint32(/* id 87, wireType 2 =*/698).fork()).ldelim();
58955
58965
  if (message.pollResultSnapshotMessage != null && Object.hasOwnProperty.call(message, "pollResultSnapshotMessage"))
58956
- $root.proto.Message.PollResultSnapshotMessage.encode(message.pollResultSnapshotMessage, writer.uint32(/* id 88, wireType 2 =*/706).fork()).ldelim();
58966
+ $root.proto.Message.PollResultSnapshotMessage.encode(message.pollResultSnapshotMessage, writer.uint32(/* id 88, wireType 2 =*/706).fork()).ldelim(); if (message.pollResultSnapshotMessageV3 != null && Object.hasOwnProperty.call(message, "pollResultSnapshotMessageV3"))
58967
+ $root.proto.Message.PollResultSnapshotMessage.encode(message.pollResultSnapshotMessageV3, writer.uint32(/* id 115, wireType 2 =*/922).fork()).ldelim();
58957
58968
  if (message.pollCreationOptionImageMessage != null && Object.hasOwnProperty.call(message, "pollCreationOptionImageMessage"))
58958
58969
  $root.proto.Message.FutureProofMessage.encode(message.pollCreationOptionImageMessage, writer.uint32(/* id 90, wireType 2 =*/722).fork()).ldelim();
58959
58970
  if (message.associatedChildMessage != null && Object.hasOwnProperty.call(message, "associatedChildMessage"))
@@ -59314,6 +59325,9 @@ export const proto = $root.proto = (() => {
59314
59325
  case 88: {
59315
59326
  message.pollResultSnapshotMessage = $root.proto.Message.PollResultSnapshotMessage.decode(reader, reader.uint32());
59316
59327
  break;
59328
+ } case 115: {
59329
+ message.pollResultSnapshotMessageV3 = $root.proto.Message.PollResultSnapshotMessage.decode(reader, reader.uint32());
59330
+ break;
59317
59331
  }
59318
59332
  case 90: {
59319
59333
  message.pollCreationOptionImageMessage = $root.proto.Message.FutureProofMessage.decode(reader, reader.uint32());
@@ -60002,6 +60016,13 @@ export const proto = $root.proto = (() => {
60002
60016
  if (error)
60003
60017
  return "pollResultSnapshotMessage." + error;
60004
60018
  }
60019
+ } if (message.pollResultSnapshotMessageV3 != null && message.hasOwnProperty("pollResultSnapshotMessageV3")) {
60020
+ properties._pollResultSnapshotMessageV3 = 1;
60021
+ {
60022
+ let error = $root.proto.Message.PollResultSnapshotMessage.verify(message.pollResultSnapshotMessageV3);
60023
+ if (error)
60024
+ return "pollResultSnapshotMessageV3." + error;
60025
+ }
60005
60026
  }
60006
60027
  if (message.pollCreationOptionImageMessage != null && message.hasOwnProperty("pollCreationOptionImageMessage")) {
60007
60028
  properties._pollCreationOptionImageMessage = 1;
@@ -60511,6 +60532,10 @@ export const proto = $root.proto = (() => {
60511
60532
  if (typeof object.pollResultSnapshotMessage !== "object")
60512
60533
  throw TypeError(".proto.Message.pollResultSnapshotMessage: object expected");
60513
60534
  message.pollResultSnapshotMessage = $root.proto.Message.PollResultSnapshotMessage.fromObject(object.pollResultSnapshotMessage);
60535
+ } if (object.pollResultSnapshotMessageV3 != null) {
60536
+ if (typeof object.pollResultSnapshotMessageV3 !== "object")
60537
+ throw TypeError(".proto.Message.pollResultSnapshotMessageV3: object expected");
60538
+ message.pollResultSnapshotMessageV3 = $root.proto.Message.PollResultSnapshotMessage.fromObject(object.pollResultSnapshotMessageV3);
60514
60539
  }
60515
60540
  if (object.pollCreationOptionImageMessage != null) {
60516
60541
  if (typeof object.pollCreationOptionImageMessage !== "object")
@@ -60976,6 +61001,10 @@ export const proto = $root.proto = (() => {
60976
61001
  object.pollResultSnapshotMessage = $root.proto.Message.PollResultSnapshotMessage.toObject(message.pollResultSnapshotMessage, options);
60977
61002
  if (options.oneofs)
60978
61003
  object._pollResultSnapshotMessage = "pollResultSnapshotMessage";
61004
+ } if (message.pollResultSnapshotMessageV3 != null && message.hasOwnProperty("pollResultSnapshotMessageV3")) {
61005
+ object.pollResultSnapshotMessageV3 = $root.proto.Message.PollResultSnapshotMessage.toObject(message.pollResultSnapshotMessageV3, options);
61006
+ if (options.oneofs)
61007
+ object._pollResultSnapshotMessageV3 = "pollResultSnapshotMessageV3";
60979
61008
  }
60980
61009
  if (message.pollCreationOptionImageMessage != null && message.hasOwnProperty("pollCreationOptionImageMessage")) {
60981
61010
  object.pollCreationOptionImageMessage = $root.proto.Message.FutureProofMessage.toObject(message.pollCreationOptionImageMessage, options);
@@ -38,6 +38,28 @@ export class GroupSessionBuilder {
38
38
  senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey);
39
39
  await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
40
40
  }
41
+ else {
42
+ // AunuBaileys (1.0.88, HB-124 addendum): a stored OWN
43
+ // state whose signing key has no PRIVATE half can never
44
+ // encrypt — GroupCipher's calculateSignature(undefined)
45
+ // dies in libsignal's validatePrivKey ('Undefined
46
+ // private key') on EVERY group send. The load sanitiser
47
+ // deliberately KEEPS private-less states (a received
48
+ // AUTHOR state legitimately has no private half), and
49
+ // the branch above only regenerated an EMPTY record, so
50
+ // the brick was permanent until the record was cleared
51
+ // by hand. Replace a private-less current state with a
52
+ // fresh own state; the SKDM returned below distributes
53
+ // the new key, so peers heal on this very send.
54
+ const current = senderKeyRecord.getSenderKeyState();
55
+ if (current && !current.getSigningKeyPrivate()) {
56
+ const keyId = keyhelper.generateSenderKeyId();
57
+ const senderKey = keyhelper.generateSenderKey();
58
+ const signingKey = keyhelper.generateSenderSigningKey();
59
+ senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey);
60
+ await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
61
+ }
62
+ }
41
63
  const state = senderKeyRecord.getSenderKeyState();
42
64
  if (!state) {
43
65
  throw new Error('No session state available');
@@ -38,7 +38,13 @@ const sanitizeSenderKeyStructure = (structure) => {
38
38
  return null;
39
39
  }
40
40
  const iteration = storedCountToNumber(chainKey.iteration);
41
- if (iteration === null) {
41
+ // AunuBaileys (1.0.88, HB-202): a NEGATIVE iteration passes the
42
+ // integer check above, but protobufjs encodes the field as
43
+ // uint32 — it wraps (-5 -> 4294967291), so peers fail every
44
+ // message from this state until a 3-strike resync heals it.
45
+ // The finite/integer check alone cannot see this; reject
46
+ // negatives exactly like missing values.
47
+ if (iteration === null || iteration < 0) {
42
48
  return null;
43
49
  }
44
50
  const signingPublic = storedSeedToBuffer(signingKey.public);
@@ -68,7 +74,9 @@ const sanitizeSenderKeyStructure = (structure) => {
68
74
  }
69
75
  const mkIteration = storedCountToNumber(entry.iteration);
70
76
  const mkSeed = storedSeedToBuffer(entry.seed);
71
- if (mkIteration === null || !mkSeed || mkSeed.length !== 32) {
77
+ // (1.0.88, HB-202): negative message-key iterations wrap
78
+ // on the wire the same way — skip those entries too.
79
+ if (mkIteration === null || mkIteration < 0 || !mkSeed || mkSeed.length !== 32) {
72
80
  continue;
73
81
  }
74
82
  messageKeys.push({ iteration: mkIteration, seed: mkSeed });
@@ -92,7 +92,22 @@ export function makeLibSignalRepository(auth) {
92
92
  const chained = prev.then(run);
93
93
  // the stored chain must never stay rejected, or one failed
94
94
  // send would poison every later send to the group
95
- groupEncryptChains.set(group, chained.catch(() => { }));
95
+ const stored = chained.catch(() => { });
96
+ groupEncryptChains.set(group, stored);
97
+ // AunuBaileys (1.0.88, HB-256): the map used to keep one
98
+ // entry per group FOREVER (a settled promise + the group
99
+ // string, ~81 bytes each — small, but unbounded over the
100
+ // socket's life: 6000 groups measured +477 KiB retained
101
+ // after every store record was gone). Drop the entry
102
+ // once its chain settles; the identity check protects a
103
+ // NEWER chain for the same group from being deleted by
104
+ // an older one, and `stored` never rejects, so this
105
+ // cannot float an unhandled rejection.
106
+ stored.then(() => {
107
+ if (groupEncryptChains.get(group) === stored) {
108
+ groupEncryptChains.delete(group);
109
+ }
110
+ });
96
111
  return chained;
97
112
  },
98
113
  async deleteSenderKey(group, authorJid) {
@@ -82,8 +82,30 @@ export const makeChatsSocket = (config) => {
82
82
  // AunuBaileys (1.0.71): a privacy change is a settings change —
83
83
  // refetch the full set (the cached copy is now stale) and publish
84
84
  // it (donor event: violetix 'privacy.update' on account_sync).
85
- privacySettings = await fetchPrivacySettings(true);
86
- ev.emit('privacy.update', privacySettings);
85
+ // AunuBaileys (1.0.87, HB-163): the SET above has already
86
+ // been applied by the server when this refetch runs. A
87
+ // refetch failure used to reject the whole privacyQuery —
88
+ // the caller saw a failed update that had in fact taken
89
+ // effect, the cache kept the OLD value, and readMessages
90
+ // derived its receipt type from that stale cache (sending
91
+ // 'read' receipts against a receipts setting the user had
92
+ // just switched off). Make the refetch best-effort: on
93
+ // failure, fold the applied change into the cached copy so
94
+ // cache and event reflect what the server now holds for
95
+ // this key. Never fabricate a partial cache when none
96
+ // exists — that would suppress the next full fetch.
97
+ try {
98
+ privacySettings = await fetchPrivacySettings(true);
99
+ }
100
+ catch (err) {
101
+ logger.warn({ err, name, value }, 'failed to refetch privacy settings after a successful set; folding the change into the cached copy');
102
+ if (privacySettings) {
103
+ privacySettings = { ...privacySettings, [name]: value };
104
+ }
105
+ }
106
+ if (privacySettings) {
107
+ ev.emit('privacy.update', privacySettings);
108
+ }
87
109
  };
88
110
  const updateMessagesPrivacy = async (value) => {
89
111
  await privacyQuery('messages', value);
@@ -946,11 +968,35 @@ export const makeChatsSocket = (config) => {
946
968
  const doAppStateSync = async () => {
947
969
  if (syncState === SyncState.Syncing) {
948
970
  logger.info('Doing app state sync');
949
- await resyncAppState(ALL_WA_PATCH_NAMES, true);
950
- // Sync is complete, go online and flush everything
971
+ // AunuBaileys (1.0.87, HB-241): resyncAppState can
972
+ // throw OUTSIDE its per-collection handling — the
973
+ // sync IQ itself, a malformed blob in
974
+ // extractSyncdPatches, the resync-round cap. That
975
+ // throw used to reject upsertMessage and leave
976
+ // syncState at Syncing for the rest of the session:
977
+ // the full initial sync was never retried (nothing
978
+ // re-triggers it) and the state machine never
979
+ // reached Online. A failed sync must END the
980
+ // Syncing state, not freeze it — go Online and
981
+ // flush either way, and only count the sync as
982
+ // complete (the accountSyncCounter advance below)
983
+ // when it actually succeeded.
984
+ let syncFailed = false;
985
+ try {
986
+ await resyncAppState(ALL_WA_PATCH_NAMES, true);
987
+ }
988
+ catch (err) {
989
+ syncFailed = true;
990
+ logger.error({ err }, 'app state sync failed; transitioning to Online state anyway');
991
+ }
992
+ // Sync ran its course, go online and flush everything
951
993
  syncState = SyncState.Online;
952
994
  logger.info('App state sync complete, transitioning to Online state and flushing buffer');
953
995
  ev.flush();
996
+ // (HB-241) a failed sync is not a completed one:
997
+ // do not advance the counter that signals the
998
+ // initial sync finished.
999
+ if (!syncFailed) {
954
1000
  const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
955
1001
  // AunuBaileys (1.0.81, HB-257): same guard as the
956
1002
  // pushName emit above — a throwing 'creds.update'
@@ -963,6 +1009,7 @@ export const makeChatsSocket = (config) => {
963
1009
  catch (err) {
964
1010
  onUnexpectedError(err, 'creds.update (accountSyncCounter)');
965
1011
  }
1012
+ }
966
1013
  }
967
1014
  };
968
1015
  await Promise.all([
@@ -355,7 +355,13 @@ export const extractCommunityMetadata = (result) => {
355
355
  memberAddMode,
356
356
  participants: getBinaryNodeChildren(community, 'participant').map(({ attrs }) => {
357
357
  return {
358
- id: attrs.jid,
358
+ // AunuBaileys (1.0.88, HB-40 addendum): the id
359
+ // fallback extractGroupMetadata gained for groups
360
+ // was never mirrored here — a participant stanza
361
+ // carrying only phone_number (or only lid) produced
362
+ // id: undefined, a participant the host cannot
363
+ // address. Same fallback chain as groups.js.
364
+ id: attrs.jid || (attrs.phone_number ? jidNormalizedUser(attrs.phone_number.includes('@') ? attrs.phone_number : attrs.phone_number + '@s.whatsapp.net') : attrs.lid) || undefined,
359
365
  admin: (attrs.type || null)
360
366
  };
361
367
  }),
@@ -965,7 +965,24 @@ export const makeMessagesRecvSocket = (config) => {
965
965
  logger.warn({ msgId: key.id }, 'aunu: cannot request placeholder resend before pairing completes; skipping');
966
966
  }
967
967
  else {
968
- response = await requestPlaceholderResend(key);
968
+ // AunuBaileys (1.0.88, HB-260): this was the ONLY
969
+ // requestPlaceholderResend call site awaited WITHOUT
970
+ // a handler (the other two are fire-and-forget with
971
+ // catches). A transport failure inside it (the PDO
972
+ // send throws 'Not authenticated' / 'Connection
973
+ // Closed' / a timeout) escaped to the caller's
974
+ // logger — and since the ack above had already gone
975
+ // out, the placeholder vanished silently: no
976
+ // redelivery will ever come, and the main decrypt
977
+ // path below (which upserts the placeholder stub)
978
+ // never ran. Log the failure and fall through to
979
+ // that path, like the decode guard above does.
980
+ try {
981
+ response = await requestPlaceholderResend(key);
982
+ }
983
+ catch (resendErr) {
984
+ logger.warn({ err: resendErr, msgId: key.id }, 'aunu: placeholder resend request failed; continuing to the main message path');
985
+ }
969
986
  }
970
987
  if (response === 'RESOLVED') {
971
988
  return;
@@ -1771,7 +1788,14 @@ export const makeMessagesRecvSocket = (config) => {
1771
1788
  }
1772
1789
  const operation = data?.operation;
1773
1790
  const updates = data?.updates;
1774
- if (!updates || !operation) {
1791
+ // AunuBaileys (1.0.87, HB-121): this guard used to be a
1792
+ // truthiness check only — a MEX payload whose `updates`
1793
+ // is a non-array (object/number/boolean) passed it, and
1794
+ // the for...of loops in every switch branch below threw a
1795
+ // TypeError, silently dropping the whole notification
1796
+ // (handleNotification acks the stanza in its finally
1797
+ // either way, so nothing ever retried it).
1798
+ if (!Array.isArray(updates) || !operation) {
1775
1799
  logger.warn({ data }, 'Invalid mex newsletter notification content');
1776
1800
  return;
1777
1801
  }
@@ -142,9 +142,26 @@ export const makeMessagesSocket = (config) => {
142
142
  };
143
143
  /** Bulk read messages. Keys can be from different chats & participants */
144
144
  const readMessages = async (keys) => {
145
- const privacySettings = await fetchPrivacySettings();
145
+ // AunuBaileys (1.0.88, HB-261): the privacy fetch was awaited
146
+ // bare — on a COLD cache it runs a real IQ, and when that IQ
147
+ // failed (error stanza, timeout, closing socket)
148
+ // readMessages rejected BEFORE sendReceipts ran: zero
149
+ // receipts sent, the messages stayed unread, and the host
150
+ // could not tell "partially sent" from "nothing sent" (it
151
+ // was always nothing). The fetch is best-effort now: a warm
152
+ // cache answers without an IQ anyway, and on failure we fall
153
+ // back to the default 'read' type rather than dropping the
154
+ // receipts entirely. (Not HB-163: that was the SET-then-
155
+ // refetch path in chats.js; this is the read-side consumer.)
156
+ let privacySettings;
157
+ try {
158
+ privacySettings = await fetchPrivacySettings();
159
+ }
160
+ catch (err) {
161
+ logger.warn({ err }, 'aunu: privacy settings fetch failed in readMessages; sending plain read receipts');
162
+ }
146
163
  // based on privacy settings, we have to change the read type
147
- const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self';
164
+ const readType = !privacySettings || privacySettings.readreceipts === 'all' ? 'read' : 'read-self';
148
165
  await sendReceipts(keys, readType);
149
166
  };
150
167
  /** Fetch all the devices we've to send a message to */
@@ -268,6 +285,21 @@ export const makeMessagesSocket = (config) => {
268
285
  };
269
286
  const createParticipantNodes = async (jids, message, extraAttrs) => {
270
287
  let patched = await patchMessageBeforeSending(message, jids);
288
+ // AunuBaileys (1.0.88, HB-267): a patch hook returning an
289
+ // EMPTY ARRAY used to slip past the 'All encryptions failed'
290
+ // guard below (its condition requires patched.length), so
291
+ // the send produced zero nodes and relayMessage resolved —
292
+ // a stanza with no content went out as a success (the
293
+ // silent-empty class of HB-253; HB-136 closed the
294
+ // missing-recipientJid sibling in 1.0.86, HB-212 the
295
+ // newsletter-branch array). An empty patch for a NON-empty
296
+ // jid list is a host bug: fail loudly. (An empty JID list is
297
+ // the one legitimate empty case — a participant-targeted
298
+ // relay calls this for the side that has no recipients —
299
+ // and still returns zero nodes.)
300
+ if (Array.isArray(patched) && !patched.length && jids?.length) {
301
+ throw new Boom('patchMessageBeforeSending returned an empty array; refusing to send an empty message', { statusCode: 400 });
302
+ }
271
303
  if (!Array.isArray(patched)) {
272
304
  patched = jids ? jids.map(jid => ({ recipientJid: jid, ...patched })) : [patched];
273
305
  }
@@ -530,6 +562,19 @@ export const makeMessagesSocket = (config) => {
530
562
  }
531
563
  const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false);
532
564
  devices.push(...additionalDevices);
565
+ // AunuBaileys (1.0.88, HB-266): ZERO resolved
566
+ // devices used to fall through — the stanza
567
+ // went out carrying only the sender-key
568
+ // ciphertext (an empty participants list adds
569
+ // no <participants> node below), nobody received
570
+ // the sender key, nobody could decrypt, and
571
+ // relayMessage still resolved with a message id
572
+ // as if delivered. Mirror the HB-147 call-path
573
+ // guard: an undeliverable group/status/broadcast
574
+ // send fails loudly instead.
575
+ if (!devices.length) {
576
+ throw new Boom('relayMessage: no recipient devices resolved for this group/status message (empty participant list or empty status audience); refusing to send an undeliverable message', { statusCode: 400 });
577
+ }
533
578
  }
534
579
  const patched = await patchMessageBeforeSending(message);
535
580
  if (Array.isArray(patched)) {
package/lib/Socket/mex.js CHANGED
@@ -22,11 +22,29 @@ export const executeWMexQuery = async (variables, queryId, dataPath, query, gene
22
22
  const result = await wMexQuery(variables, queryId, query, generateMessageTag);
23
23
  const child = getBinaryNodeChild(result, 'result');
24
24
  if (child?.content) {
25
- const data = JSON.parse(child.content.toString());
25
+ // AunuBaileys (1.0.87, HB-211): a <result> that is not JSON
26
+ // (including a child-node array body, which stringifies to
27
+ // "[object Object]") used to reject with a raw SyntaxError
28
+ // the host could not classify.
29
+ let data;
30
+ try {
31
+ data = JSON.parse(child.content.toString());
32
+ }
33
+ catch (err) {
34
+ throw new Boom('GraphQL server error: unparseable response', { statusCode: 400, data: result });
35
+ }
26
36
  if (data.errors && data.errors.length > 0) {
27
37
  const errorMessages = data.errors.map((err) => err.message || 'Unknown error').join(', ');
28
38
  const firstError = data.errors[0];
29
- const errorCode = firstError.extensions?.error_code || 400;
39
+ // AunuBaileys (1.0.87, HB-211): the server's error_code
40
+ // went into the Boom constructor unsanitised — a string
41
+ // code ('RATE_LIMITED') or an out-of-range one (200)
42
+ // made `new Boom` itself throw a non-Boom TypeError,
43
+ // and the server's actual error message was lost with
44
+ // it. Same sanitation as HB-42: only a numeric HTTP
45
+ // error code survives, everything else becomes 400.
46
+ const rawErrorCode = firstError.extensions?.error_code;
47
+ const errorCode = typeof rawErrorCode === 'number' && rawErrorCode >= 400 && rawErrorCode <= 599 ? rawErrorCode : 400;
30
48
  throw new Boom(`GraphQL server error: ${errorMessages}`, { statusCode: errorCode, data: firstError });
31
49
  }
32
50
  const response = dataPath ? data?.data?.[dataPath] : data?.data;
@@ -194,7 +194,28 @@ export const makeSocket = (config) => {
194
194
  node.attrs.id = generateMessageTag();
195
195
  }
196
196
  const msgId = node.attrs.id;
197
- const [result] = await Promise.all([waitForMessage(msgId, timeoutMs), sendNode(node)]);
197
+ // AunuBaileys (1.0.88, HB-123): sendNode encodes SYNCHRONOUSLY
198
+ // — when encodeBinaryNode throws (a malformed node from any
199
+ // caller), the old Promise.all([...]) expression evaluated
200
+ // waitForMessage FIRST and sendNode second, so the throw
201
+ // escaped before Promise.all was ever invoked: the wait
202
+ // promise kept its ws listeners and rejected 'Timed Out'
203
+ // after the query timeout with no handler attached — an
204
+ // unhandled rejection crashing the process long after the
205
+ // caller had handled the real (encode) error. Detach the
206
+ // wait when the send throws synchronously; an ASYNC send
207
+ // rejection is already safe (Promise.all subscribes to both
208
+ // promises).
209
+ const waitPromise = waitForMessage(msgId, timeoutMs);
210
+ let sendPromise;
211
+ try {
212
+ sendPromise = sendNode(node);
213
+ }
214
+ catch (err) {
215
+ waitPromise.catch(() => { });
216
+ throw err;
217
+ }
218
+ const [result] = await Promise.all([waitPromise, sendPromise]);
198
219
  if ('tag' in result) {
199
220
  assertNodeErrorFree(result);
200
221
  }
@@ -268,7 +289,26 @@ export const makeSocket = (config) => {
268
289
  // transaction, so a crash between the send and the commit left
269
290
  // the server holding pre-key IDs whose private halves were
270
291
  // never persisted ("key used already or never filled").
271
- const { update, node } = await keys.transaction(async () => await getNextPreKeysNode({ creds, keys }, count));
292
+ // AunuBaileys (1.0.88, HB-235 + addendum): the counters
293
+ // advance INSIDE the transaction, and the creds.update emit
294
+ // fires BEFORE the upload IQ — not after it. The old order
295
+ // (commit the keys, await the IQ for up to 30s, only then
296
+ // emit/advance) let a second upload starting during the
297
+ // first IQ read the STALE counters and regenerate the SAME
298
+ // ids with NEW material, overwriting batch 1's private keys
299
+ // in the store; if IQ 2 (or IQ 1 itself, on a plain timeout
300
+ // — the addendum facet) then failed, the server held batch 1
301
+ // publics over batch 2 privates and the range was never
302
+ // regenerated. Advancing under the transaction mutex makes
303
+ // concurrent uploads take disjoint ranges; a failed or
304
+ // timed-out IQ now only wastes an id range (the server-count
305
+ // check uploads a fresh range next time).
306
+ const { update, node } = await keys.transaction(async () => {
307
+ const result = await getNextPreKeysNode({ creds, keys }, count);
308
+ Object.assign(creds, result.update);
309
+ return result;
310
+ });
311
+ ev.emit('creds.update', update);
272
312
  // AunuBaileys: timeout upload pre-keys agar koneksi tidak menggantung (dari MichelleBot/baileys)
273
313
  await promiseTimeout(30000, async (resolve, reject) => {
274
314
  try {
@@ -278,7 +318,6 @@ export const makeSocket = (config) => {
278
318
  reject(error);
279
319
  }
280
320
  });
281
- ev.emit('creds.update', update);
282
321
  logger.info({ count }, 'uploaded pre-keys');
283
322
  };
284
323
  const uploadPreKeysToServerIfRequired = async () => {
@@ -640,7 +679,17 @@ export const makeSocket = (config) => {
640
679
  id: jidEncode(phoneNumber, 's.whatsapp.net'),
641
680
  name: '~'
642
681
  };
643
- ev.emit('creds.update', authState.creds);
682
+ // AunuBaileys (1.0.88, HB-262): deliberately NO creds.update
683
+ // emit here. The HB-191 fix rolls the optimistic me back on
684
+ // an in-process rejection, but the built-in saver writes
685
+ // ATOMICALLY the moment the emit fires — a process death
686
+ // (kill, OOM, deploy restart) in the window between this
687
+ // emit and the IQ below persisted the fake `me` to disk, and
688
+ // the next boot took the LOGIN path into a 401 loggedOut
689
+ // loop that only wiping the auth state could end. The emit
690
+ // now happens after the IQ succeeds (below); the hello flow
691
+ // reads me/pairingCode from MEMORY, never from disk, so
692
+ // nothing needs the persisted values earlier.
644
693
  // AunuBaileys (1.0.77, HB-191): query(), not sendNode() — the
645
694
  // companion_hello result must be awaited and error-checked.
646
695
  // sendNode resolved when the bytes hit the wire, so a server
@@ -693,6 +742,11 @@ export const makeSocket = (config) => {
693
742
  }
694
743
  ]
695
744
  });
745
+ // AunuBaileys (1.0.88, HB-262): the server accepted the
746
+ // pairing request — only NOW may the optimistic me /
747
+ // pairingCode be persisted (see the note at the assignment
748
+ // above).
749
+ ev.emit('creds.update', authState.creds);
696
750
  return authState.creds.pairingCode;
697
751
  }
698
752
  catch (err) {
@@ -37,31 +37,37 @@ import { jidDecode } from '../WABinary/index.js';
37
37
  import { getKeyAuthor } from './generics.js';
38
38
  import { getAggregateVotesInPollMessage, normalizeMessageContent } from './messages.js';
39
39
 
40
+ // AunuBaileys (1.0.87, HB-218): every operand is optional-chained,
41
+ // exactly like the base getFutureProofMessage in messages.js. A
42
+ // wrapper whose `message` field is absent decodes with message ===
43
+ // null; the bare derefs used to throw a raw TypeError on the next
44
+ // loop iteration of normalizeAunuMessageContentExtended (and in
45
+ // getAggregateVotesInPollMessageExtended downstream of it).
40
46
  const getFutureProofMessageExtended = (message) => (
41
- message.ephemeralMessage ||
42
- message.viewOnceMessage ||
43
- message.documentWithCaptionMessage ||
44
- message.viewOnceMessageV2 ||
45
- message.viewOnceMessageV2Extension ||
46
- message.editedMessage ||
47
- message.groupMentionedMessage ||
48
- message.botInvokeMessage ||
49
- message.lottieStickerMessage ||
50
- message.eventCoverImage ||
51
- message.statusMentionMessage ||
52
- message.pollCreationOptionImageMessage ||
53
- message.associatedChildMessage ||
54
- message.groupStatusMentionMessage ||
55
- message.pollCreationMessageV4 ||
56
- message.pollCreationMessageV5 ||
57
- message.statusAddYours ||
58
- message.groupStatusMessage ||
59
- message.limitSharingMessage ||
60
- message.botTaskMessage ||
61
- message.questionMessage ||
62
- message.groupStatusMessageV2 ||
63
- message.botForwardedMessage ||
64
- message.spoilerMessage
47
+ message?.ephemeralMessage ||
48
+ message?.viewOnceMessage ||
49
+ message?.documentWithCaptionMessage ||
50
+ message?.viewOnceMessageV2 ||
51
+ message?.viewOnceMessageV2Extension ||
52
+ message?.editedMessage ||
53
+ message?.groupMentionedMessage ||
54
+ message?.botInvokeMessage ||
55
+ message?.lottieStickerMessage ||
56
+ message?.eventCoverImage ||
57
+ message?.statusMentionMessage ||
58
+ message?.pollCreationOptionImageMessage ||
59
+ message?.associatedChildMessage ||
60
+ message?.groupStatusMentionMessage ||
61
+ message?.pollCreationMessageV4 ||
62
+ message?.pollCreationMessageV5 ||
63
+ message?.statusAddYours ||
64
+ message?.groupStatusMessage ||
65
+ message?.limitSharingMessage ||
66
+ message?.botTaskMessage ||
67
+ message?.questionMessage ||
68
+ message?.groupStatusMessageV2 ||
69
+ message?.botForwardedMessage ||
70
+ message?.spoilerMessage
65
71
  );
66
72
 
67
73
  /**
@@ -286,6 +286,22 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
286
286
  };
287
287
  const mutationMap = {};
288
288
  for (const syncd of syncds) {
289
+ // AunuBaileys (1.0.87, HB-141 + HB-149): snapshot the state
290
+ // before this patch. The version assignment inside the try
291
+ // runs BEFORE the patch is decoded, so a patch that failed
292
+ // (bad content HMAC, missing key, snapshot-MAC mismatch)
293
+ // still advanced the persisted version past itself — its
294
+ // mutations were never re-sent by the server, and the LT
295
+ // hash diverged from the true state permanently: every
296
+ // later snapshot-MAC check failed while the log claimed the
297
+ // patch had merely been "skipped". decodeSyncdPatch never
298
+ // mutates newState in place (the LT-hash generator copies
299
+ // the index map and returns fresh state), so restoring
300
+ // these three references in the catch is an exact rollback.
301
+ const prevVersion = newState.version;
302
+ const prevHash = newState.hash;
303
+ const prevIndexValueMap = newState.indexValueMap;
304
+ const appliedMutations = [];
289
305
  try {
290
306
  const { version, keyId, snapshotMac } = syncd;
291
307
  if (syncd.externalMutations) {
@@ -300,6 +316,11 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
300
316
  const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, shouldMutate
301
317
  ? mutation => {
302
318
  const index = mutation.syncAction.index?.toString();
319
+ // (HB-141 + HB-149) record for rollback: a patch
320
+ // whose snapshot-MAC check fails below has already
321
+ // delivered its mutations here — they must not
322
+ // survive being "skipped" either.
323
+ appliedMutations.push({ index, had: index in mutationMap, prev: mutationMap[index] });
303
324
  mutationMap[index] = mutation;
304
325
  }
305
326
  : () => { }, true);
@@ -324,6 +345,28 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
324
345
  // AunuBaileys: satu patch rusak tidak membunuh seluruh sinkronisasi
325
346
  logger?.warn({ err, name, version: syncd.version }, 'skipping undecodable app state patch');
326
347
  syncd.mutations = [];
348
+ // AunuBaileys (1.0.87, HB-141 + HB-149): roll the failed
349
+ // patch back completely, then stop this batch — later
350
+ // patches build on this one's state, and letting one of
351
+ // them advance the persisted version past the hole would
352
+ // cement the loss. The version stays at the last good
353
+ // patch, so the next resync re-requests the failed patch
354
+ // instead of reporting a version that silently skips it;
355
+ // a transient cause (failed external download, a sync key
356
+ // that had not arrived yet) now self-heals.
357
+ newState.version = prevVersion;
358
+ newState.hash = prevHash;
359
+ newState.indexValueMap = prevIndexValueMap;
360
+ for (let i = appliedMutations.length - 1; i >= 0; i--) {
361
+ const { index, had, prev } = appliedMutations[i];
362
+ if (had) {
363
+ mutationMap[index] = prev;
364
+ }
365
+ else {
366
+ delete mutationMap[index];
367
+ }
368
+ }
369
+ break;
327
370
  }
328
371
  }
329
372
  return { state: newState, mutationMap };
@@ -671,7 +671,20 @@ export const getUrlFromDirectPath = (directPath, fallbackHost) => `https://${fal
671
671
  // the canonical media host (the path is globally valid there); without
672
672
  // one the download is refused.
673
673
  const WA_MEDIA_HOST_SUFFIXES = ['.whatsapp.net', '.fbcdn.net'];
674
+ // Farm delta (itsliaaa 0.5.6, adopted 1.0.88): some media messages
675
+ // (stickers, voice notes, images) carry their url on the host
676
+ // 'a.whatsapp.net' — a PLACEHOLDER that does not resolve in DNS
677
+ // (verified live, twice: name_not_resolved), while their directPath
678
+ // is perfectly valid on the real media host. Treating the
679
+ // placeholder as a WA host built https://a.whatsapp.net<directPath>
680
+ // and the download died on DNS with no fallback. The exact host is
681
+ // excluded from the allowlist below, so host extraction yields no
682
+ // host and the download falls back to mmg.whatsapp.net — every
683
+ // OTHER *.whatsapp.net / *.fbcdn.net host keeps passing exactly as
684
+ // before (the HB-114 SSRF allowlist is unchanged for real hosts).
685
+ const PLACEHOLDER_MEDIA_HOST = 'a.whatsapp.net';
674
686
  const isWAMediaHost = (hostname) => !!hostname &&
687
+ hostname !== PLACEHOLDER_MEDIA_HOST &&
675
688
  (hostname === 'whatsapp.net' || hostname === 'fbcdn.net' ||
676
689
  WA_MEDIA_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)));
677
690
  export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
@@ -1019,12 +1019,33 @@ export const generateWAMessageContent = async (message, options) => {
1019
1019
  }
1020
1020
  else if ('pollResult' in message) {
1021
1021
  // AunuBaileys: poll result snapshot (dari @itsliaaa/baileys)
1022
+ // AunuBaileys (1.0.88, HB-250): this branch validated NOTHING
1023
+ // while its siblings ('poll', 'imagePoll', 'contacts') all
1024
+ // Boom 400 on bad input — a missing/non-array votes died as
1025
+ // a raw TypeError, and a missing/non-numeric voteCount was
1026
+ // parseInt'd to NaN, which protobufjs silently coerces to 0
1027
+ // on the wire: the snapshot sent to the peer CLAIMED zero
1028
+ // votes for an option that actually had some. Mirror the
1029
+ // 'poll' branch: votes must be an array and every count a
1030
+ // finite integer (numeric strings stay accepted, as before).
1031
+ if (!Array.isArray(message.pollResult.votes)) {
1032
+ throw new Boom('pollResult.votes must be an array', { statusCode: 400 });
1033
+ }
1022
1034
  const pollResultSnapshotMessage = {
1023
1035
  name: message.pollResult.name,
1024
- pollVotes: message.pollResult.votes.map(vote => ({
1025
- optionName: vote.name,
1026
- optionVoteCount: parseInt(vote.voteCount)
1027
- }))
1036
+ pollVotes: message.pollResult.votes.map(vote => {
1037
+ if (!vote || typeof vote !== 'object') {
1038
+ throw new Boom('pollResult.votes entries must be objects', { statusCode: 400 });
1039
+ }
1040
+ const count = typeof vote.voteCount === 'string' ? parseInt(vote.voteCount, 10) : vote.voteCount;
1041
+ if (!Number.isInteger(count)) {
1042
+ throw new Boom('pollResult voteCount must be a finite integer', { statusCode: 400 });
1043
+ }
1044
+ return {
1045
+ optionName: vote.name,
1046
+ optionVoteCount: count
1047
+ };
1048
+ })
1028
1049
  };
1029
1050
  if (message.pollResult.pollType === 1) {
1030
1051
  pollResultSnapshotMessage.pollType = proto.Message.PollCreationMessage.PollType.QUIZ;
@@ -206,14 +206,39 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
206
206
  await keyStore.transaction(async () => {
207
207
  const newKeys = [];
208
208
  for (const { keyData, keyId } of keys) {
209
- const strKeyId = Buffer.from(keyId.keyId).toString('base64');
209
+ // AunuBaileys (1.0.88, HB-135 + keyData
210
+ // facet): an entry whose keyId message is
211
+ // absent decodes as keyId: null — the bare
212
+ // Buffer.from(keyId.keyId) below threw,
213
+ // and every key AFTER the bad entry in the
214
+ // same share was never injected. Worse,
215
+ // an entry with a keyId but NO keyData
216
+ // stored null AND was adopted as
217
+ // myAppStateKeyId, so getAppStateSyncKey
218
+ // returned null downstream and the
219
+ // collection stopped syncing after its
220
+ // retries. Skip a malformed entry per-item
221
+ // (the HB-1/HB-3 pattern); the rest of the
222
+ // share still injects.
223
+ const rawKeyId = keyId?.keyId;
224
+ if (!rawKeyId || !rawKeyId.length || !keyData || !keyData.length) {
225
+ logger?.warn({ keyId: keyId ?? null }, 'aunu: skipping a malformed app-state sync key in the share (missing keyId or keyData); the remaining keys still inject');
226
+ continue;
227
+ }
228
+ const strKeyId = Buffer.from(rawKeyId).toString('base64');
210
229
  newKeys.push(strKeyId);
211
230
  await keyStore.set({ 'app-state-sync-key': { [strKeyId]: keyData } });
212
231
  newAppStateSyncKeyId = strKeyId;
213
232
  }
214
233
  logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys');
215
234
  });
216
- ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
235
+ // AunuBaileys (1.0.88, HB-135): adopt a new
236
+ // myAppStateKeyId only when a valid key was
237
+ // actually injected — an all-malformed share
238
+ // used to emit the empty initial value here.
239
+ if (newAppStateSyncKeyId) {
240
+ ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
241
+ }
217
242
  }
218
243
  else {
219
244
  logger?.info({ protocolMsg }, 'recv app state sync with 0 keys');
@@ -111,8 +111,26 @@ export const parseRetryReceiptSession = (node) => {
111
111
  if (!value) {
112
112
  return undefined;
113
113
  }
114
+ // AunuBaileys (1.0.88, HB-263 + addendum): the <id> child
115
+ // was never validated — a bundle whose <id> is missing
116
+ // (keyId undefined) or shorter than 3 bytes (bufferToUInt
117
+ // reads past the end -> NaN) produced a key with a
118
+ // non-finite keyId, and SessionBuilder.initOutgoing does
119
+ // NOT throw on it: a poisoned session was stored, the
120
+ // server fetch was skipped as "already injected", and
121
+ // pkmsg went out with preKeyId/signedPreKeyId 0 until the
122
+ // peer's 'Missing SignedPreKey' errors piled up. A
123
+ // non-finite keyId makes the key unusable — skip it like a
124
+ // missing <value>; when the SIGNED key drops out, the
125
+ // bundle counts as absent and the caller falls back to a
126
+ // server fetch (whose initOutgoing DOES throw on a fully
127
+ // missing signed key, landing in the same fallback).
128
+ const keyId = getBinaryNodeChildUInt(key, 'id', 3);
129
+ if (!Number.isFinite(keyId)) {
130
+ return undefined;
131
+ }
114
132
  return {
115
- keyId: getBinaryNodeChildUInt(key, 'id', 3),
133
+ keyId,
116
134
  publicKey: generateSignalPubKey(value),
117
135
  signature: getBinaryNodeChildBuffer(key, 'signature')
118
136
  };
@@ -138,8 +156,26 @@ export const parseAndInjectE2ESessions = async (node, repository, logger) => {
138
156
  if (!value) {
139
157
  return undefined;
140
158
  }
159
+ // AunuBaileys (1.0.88, HB-263 + addendum): the <id> child
160
+ // was never validated — a bundle whose <id> is missing
161
+ // (keyId undefined) or shorter than 3 bytes (bufferToUInt
162
+ // reads past the end -> NaN) produced a key with a
163
+ // non-finite keyId, and SessionBuilder.initOutgoing does
164
+ // NOT throw on it: a poisoned session was stored, the
165
+ // server fetch was skipped as "already injected", and
166
+ // pkmsg went out with preKeyId/signedPreKeyId 0 until the
167
+ // peer's 'Missing SignedPreKey' errors piled up. A
168
+ // non-finite keyId makes the key unusable — skip it like a
169
+ // missing <value>; when the SIGNED key drops out, the
170
+ // bundle counts as absent and the caller falls back to a
171
+ // server fetch (whose initOutgoing DOES throw on a fully
172
+ // missing signed key, landing in the same fallback).
173
+ const keyId = getBinaryNodeChildUInt(key, 'id', 3);
174
+ if (!Number.isFinite(keyId)) {
175
+ return undefined;
176
+ }
141
177
  return {
142
- keyId: getBinaryNodeChildUInt(key, 'id', 3),
178
+ keyId,
143
179
  publicKey: generateSignalPubKey(value),
144
180
  signature: getBinaryNodeChildBuffer(key, 'signature')
145
181
  };
@@ -50,7 +50,17 @@ const isValidCredsShape = (creds) => !!creds &&
50
50
  isCredsKeyPair(creds.signedPreKey?.keyPair) &&
51
51
  (Buffer.isBuffer(creds.signedPreKey?.signature) || typeof creds.signedPreKey?.signature === 'string') &&
52
52
  Number.isFinite(creds.signedPreKey?.keyId) &&
53
- Number.isFinite(creds.registrationId);
53
+ Number.isFinite(creds.registrationId) &&
54
+ // AunuBaileys (1.0.88, HB-80 addendum): the HB-151 shape check
55
+ // missed the two fields pairing itself dereferences —
56
+ // advSecretKey (genPairQR joins it into the QR payload; a
57
+ // missing one silently produced an EMPTY fourth QR segment
58
+ // instead of an error) and pairingEphemeralKeyPair
59
+ // (generatePairingKey encrypts with its public half).
60
+ // initAuthCreds always fills both, so a stored creds missing
61
+ // either is the same partial restore HB-151 exists to reject.
62
+ (typeof creds.advSecretKey === 'string' && creds.advSecretKey.length > 0) &&
63
+ isCredsKeyPair(creds.pairingEphemeralKeyPair);
54
64
  export const useMultiFileAuthState = async (folder, logger) => {
55
65
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
66
  const writeData = async (data, file) => {
@@ -191,11 +201,38 @@ export const useMultiFileAuthState = async (folder, logger) => {
191
201
  return credsWriteInFlight;
192
202
  }
193
203
  credsWriteInFlight = (async () => {
204
+ // AunuBaileys (1.0.88, HB-34 addendum): the HB-34
205
+ // design decision stands — a persist failure THROWS
206
+ // (a swallowed error lets the session rot silently).
207
+ // But the coalescing used to discard a write that was
208
+ // QUEUED during a FAILED write: callers arriving
209
+ // mid-write shared the in-flight promise and set the
210
+ // queued flag, the first write threw, and the queued
211
+ // latest state was never attempted — disk stayed
212
+ // stale (registered:false, counter 0) while memory
213
+ // ran ahead, until some unrelated saveCreds call
214
+ // happened by. A failure with a queued write behind
215
+ // it now attempts that latest state once more; the
216
+ // throw surfaces only when a write fails with nothing
217
+ // newer queued behind it.
218
+ let lastError;
194
219
  try {
195
220
  do {
196
221
  credsWriteQueued = false;
197
- await writeData(creds, 'creds.json');
222
+ try {
223
+ await writeData(creds, 'creds.json');
224
+ lastError = undefined;
225
+ }
226
+ catch (err) {
227
+ lastError = err;
228
+ if (!credsWriteQueued) {
229
+ break;
230
+ }
231
+ }
198
232
  } while (credsWriteQueued);
233
+ if (lastError) {
234
+ throw lastError;
235
+ }
199
236
  }
200
237
  catch (err) {
201
238
  // writeData already logged it; this log covers callers
@@ -110,7 +110,22 @@ export class USyncQuery {
110
110
  if (errCode === 401 || errCode === 403 || errCode === 405) {
111
111
  return ['isBlockedByContact', true];
112
112
  }
113
- throw err;
113
+ // AunuBaileys (1.0.87, HB-175): any
114
+ // OTHER per-user parser error — the
115
+ // protocol node carried an <error>
116
+ // child with e.g. a 404 — used to be
117
+ // rethrown, aborting the parse of
118
+ // the ENTIRE batch: one user's error
119
+ // cost every other user's devices /
120
+ // contacts, and downstream sends that
121
+ // depend on the device list failed
122
+ // outright. Treat it the way the
123
+ // business parser treats an error
124
+ // child: this user simply has no data
125
+ // for this protocol (the null is
126
+ // dropped by the filter below); the
127
+ // rest of the batch continues.
128
+ return [protocol, null];
114
129
  }
115
130
  }
116
131
  else {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.86",
4
+ "version": "1.0.88",
5
5
  "description": "AunuBaileys — the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom — plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
6
6
  "keywords": [
7
7
  "whatsapp",