@aunucore/baileys 1.0.82 → 1.0.84

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.
@@ -1,4 +1,3 @@
1
- import NodeCache from '@cacheable/node-cache';
2
1
  import { Boom } from '@hapi/boom';
3
2
  import { proto } from '../../WAProto/index.js';
4
3
  import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
@@ -6,6 +5,7 @@ import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
6
5
  import { SyncState } from '../Types/State.js';
7
6
  import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
8
7
  import { makeMutex } from '../Utils/make-mutex.js';
8
+ import { makeBoundedTTLCache } from '../Utils/bounded-cache.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
10
  import { getBinaryNodeChild, getBinaryNodeChildren, isJidUser, isLidUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
@@ -24,10 +24,16 @@ export const makeChatsSocket = (config) => {
24
24
  // Timeout for AwaitingInitialSync state
25
25
  let awaitingSyncTimeout;
26
26
  const placeholderResendCache = config.placeholderResendCache ||
27
- new NodeCache({
28
- stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
29
- useClones: false
30
- });
27
+ // AunuBaileys (1.0.84, HB-231): in the standard
28
+ // makeWASocket composition this factory runs BEFORE
29
+ // messages-recv creates its own default, and the
30
+ // write-back below means the instance created HERE is
31
+ // the one the whole receive path actually uses. It used
32
+ // to be a NodeCache with no size cap, silently shadowing
33
+ // the bounded default messages-recv intended ("bounded
34
+ // by default — a TTL alone lets these grow without
35
+ // limit"). Build the same bounded cache here.
36
+ makeBoundedTTLCache({ ttlSeconds: DEFAULT_CACHE_TTLS.MSG_RETRY, maxEntries: 1000 });
31
37
  if (!config.placeholderResendCache) {
32
38
  config.placeholderResendCache = placeholderResendCache;
33
39
  }
@@ -188,7 +188,11 @@ export const makeMessagesSocket = (config) => {
188
188
  }
189
189
  const result = await sock.executeUSyncQuery(query);
190
190
  if (result) {
191
- const extracted = extractDeviceJids(result?.list, authState.creds.me?.id, ignoreZeroDevices);
191
+ // AunuBaileys (1.0.83, HB-258): pass our LID as well —
192
+ // comparing only the PN user left our own LID devices in
193
+ // the extracted list, so group sends encrypted a copy to
194
+ // our own LID device (upstream v7 made the same fix).
195
+ const extracted = extractDeviceJids(result?.list, authState.creds.me?.id, authState.creds.me?.lid, ignoreZeroDevices);
192
196
  const deviceMap = {};
193
197
  for (const item of extracted) {
194
198
  deviceMap[item.user] = deviceMap[item.user] || [];
@@ -327,6 +331,16 @@ export const makeMessagesSocket = (config) => {
327
331
  return { nodes: filteredNodes, shouldIncludeDeviceIdentity };
328
332
  };
329
333
  const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList, statusPrivacy }) => {
334
+ // AunuBaileys (1.0.83, HB-253): a non-object message — or an
335
+ // empty object — proto-encodes to ZERO content bytes without
336
+ // throwing, so the relay used to report success while the
337
+ // peer received random padding only: the 1.0.71 silent-empty
338
+ // class at the one public entry point that never got a guard
339
+ // (sendMessage fails loudly for the same class of input).
340
+ // Fail fast, before any wait or network work.
341
+ if (!message || typeof message !== 'object' || Array.isArray(message) || !Object.keys(message).length) {
342
+ throw new Boom('relayMessage requires a non-empty message object', { statusCode: 400 });
343
+ }
330
344
  if (!ws.isOpen) {
331
345
  // AunuBaileys: a send attempted during a reconnect window used to die
332
346
  // instantly with Boom 428 from the first internal query — the message
@@ -412,6 +426,10 @@ export const makeMessagesSocket = (config) => {
412
426
  const { user, device } = decodedParticipant;
413
427
  devices.push({ user, device });
414
428
  }
429
+ // AunuBaileys (1.0.83, HB-178): the stanza built inside the
430
+ // transaction is captured here and sent only AFTER the
431
+ // transaction commits — see the tail of this function.
432
+ let stanzaToSend;
415
433
  await authState.keys.transaction(async () => {
416
434
  const mediaType = getMediaType(message);
417
435
  if (mediaType) {
@@ -420,6 +438,15 @@ export const makeMessagesSocket = (config) => {
420
438
  if (isNewsletter) {
421
439
  // Patch message if needed, then encode as plaintext
422
440
  const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
441
+ // AunuBaileys (1.0.83, HB-212): a per-jid patch ARRAY
442
+ // is in-contract for patchMessageBeforeSending, but
443
+ // encodeNewsletterMessage is a bare proto encode —
444
+ // an array encodes to 0 bytes, so the channel got an
445
+ // empty post while sendMessage resolved as success.
446
+ // Mirror the group branch and refuse it loudly.
447
+ if (Array.isArray(patched)) {
448
+ throw new Boom('Per-jid patching is not supported in newsletters', { statusCode: 400 });
449
+ }
423
450
  const bytes = encodeNewsletterMessage(patched);
424
451
  binaryNodeContent.push({
425
452
  tag: 'plaintext',
@@ -447,8 +474,14 @@ export const makeMessagesSocket = (config) => {
447
474
  content: binaryNodeContent
448
475
  };
449
476
  logger.debug({ msgId }, `sending newsletter message to ${jid}`);
450
- await sendNode(stanza);
451
- sentMessageCacheFor(config).add(jid, msgId, message);
477
+ // AunuBaileys (1.0.83, HB-178): captured, not sent —
478
+ // the send happens after the commit at the tail of
479
+ // relayMessage, and the sent-message cache add lives
480
+ // there too (it used to sit here, inside the
481
+ // transaction; with send-after-commit it would
482
+ // register a message the commit failure prevented
483
+ // from ever being sent).
484
+ stanzaToSend = stanza;
452
485
  return;
453
486
  }
454
487
  if (normalizeMessageContent(message)?.pinInChatMessage) {
@@ -586,10 +619,43 @@ export const makeMessagesSocket = (config) => {
586
619
  allJids.push(jid);
587
620
  }
588
621
  await assertSessions(allJids, false);
589
- const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
622
+ // AunuBaileys (1.0.83, HB-161): the echo to our own
623
+ // devices and the send to the peer used to share one
624
+ // Promise.all — when encrypting for ALL of our own
625
+ // devices failed (a rotten self-session the one-shot
626
+ // heal inside createParticipantNodes could not save),
627
+ // the peer's already-encrypted nodes were thrown away
628
+ // with it and the DM never left, even though the
629
+ // peer's sessions were healthy; symmetrically, a
630
+ // total peer failure took the echo down too. The echo
631
+ // is best-effort: settle both sides independently and
632
+ // fail only when the side that must deliver produced
633
+ // nothing.
634
+ const [meSettled, otherSettled] = await Promise.allSettled([
590
635
  createParticipantNodes(meJids, meMsg, extraAttrs),
591
636
  createParticipantNodes(otherJids, message, extraAttrs)
592
637
  ]);
638
+ if (otherSettled.status === 'rejected') {
639
+ throw otherSettled.reason;
640
+ }
641
+ let meNodes = [];
642
+ let s1 = false;
643
+ if (meSettled.status === 'fulfilled') {
644
+ meNodes = meSettled.value.nodes;
645
+ s1 = meSettled.value.shouldIncludeDeviceIdentity;
646
+ }
647
+ else if (!otherSettled.value.nodes.length && meJids.length) {
648
+ // Our own devices were the ONLY recipients (a
649
+ // retry resend aimed at our own device, a
650
+ // self-chat): with the echo gone there is nothing
651
+ // to send — keep the original failure instead of
652
+ // emitting an empty stanza.
653
+ throw meSettled.reason;
654
+ }
655
+ else {
656
+ logger.warn({ err: meSettled.reason }, 'aunu: encrypting the echo to our own devices failed; sending to the peer without the device-sent echo');
657
+ }
658
+ const { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } = otherSettled.value;
593
659
  participants.push(...meNodes);
594
660
  participants.push(...otherNodes);
595
661
  shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2;
@@ -658,8 +724,24 @@ export const makeMessagesSocket = (config) => {
658
724
  stanza.content.push(getBizBinaryNode(message));
659
725
  }
660
726
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
661
- await sendNode(stanza);
727
+ stanzaToSend = stanza;
662
728
  });
729
+ // AunuBaileys (1.0.83, HB-178): the stanza goes on the wire
730
+ // only now — AFTER the transaction above has committed the
731
+ // ratchet state it was encrypted under. The send used to be
732
+ // the last await INSIDE the transaction, so a commit that
733
+ // failed through every retry threw after the message was
734
+ // already delivered: the caller saw an error for a delivered
735
+ // message (and a host retry then produced a duplicate the
736
+ // peer could not decrypt, re-using the un-persisted chain
737
+ // key), while the cache add and the message-secret
738
+ // registration below were skipped for that delivered
739
+ // message. Persist first, then send: a commit failure now
740
+ // fails a message that was never sent, and a sent message
741
+ // is always registered.
742
+ if (stanzaToSend) {
743
+ await sendNode(stanzaToSend);
744
+ }
663
745
  sentMessageCacheFor(config).add(jid, msgId, message);
664
746
  // AunuBaileys (1.0.71): register the messageSecret at send time
665
747
  // so an msmsg reply can be decrypted even if it arrives before
@@ -759,7 +841,19 @@ export const makeMessagesSocket = (config) => {
759
841
  }
760
842
  const callId = newCallId();
761
843
  const offerContent = buildCallOfferContent(isVideo);
762
- const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
844
+ // AunuBaileys (1.0.83, HB-184): the callee's devices take
845
+ // the server of the DESTINATION jid — a LID callee's devices
846
+ // live on 'lid'; encoding them on 's.whatsapp.net' addressed
847
+ // a phone number that does not exist (LID digits are not a
848
+ // number), so the offer could never ring.
849
+ const callServer = jidDecode(toJid)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
850
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, callServer, device));
851
+ // AunuBaileys (1.0.83, HB-147): an empty device list built
852
+ // an offer with ZERO <destination> nodes — a call that could
853
+ // ring nobody, with no client-side complaint. Fail fast.
854
+ if (!devices.length) {
855
+ throw new Boom(`offerCall: no devices found for ${toJid}; the call would ring nobody`, { statusCode: 400 });
856
+ }
763
857
  await assertSessions(devices, true);
764
858
  const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
765
859
  call: { callKey: createCallKey() }
@@ -800,7 +894,25 @@ export const makeMessagesSocket = (config) => {
800
894
  }
801
895
  const callId = newCallId();
802
896
  const offerContent = buildCallOfferContent(isVideo);
803
- const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
897
+ // AunuBaileys (1.0.83, HB-184): each participant's devices
898
+ // take the server of that participant's OWN jid — in a
899
+ // LID-addressed group the participants are LID jids and
900
+ // their devices live on 'lid', not 's.whatsapp.net'.
901
+ const serverByUser = new Map();
902
+ for (const participantJid of participants) {
903
+ const decodedParticipant = jidDecode(jidNormalizedUser(participantJid) || '');
904
+ if (decodedParticipant?.user) {
905
+ serverByUser.set(decodedParticipant.user, decodedParticipant.server === 'lid' ? 'lid' : 's.whatsapp.net');
906
+ }
907
+ }
908
+ const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, serverByUser.get(user) ?? 's.whatsapp.net', device));
909
+ // AunuBaileys (1.0.83, HB-147): the default participants=[]
910
+ // (or a usync answer with no devices) built an offer with
911
+ // ZERO <destination> nodes — a call that could ring nobody,
912
+ // with no client-side complaint. Fail fast.
913
+ if (!devices.length) {
914
+ throw new Boom('groupCall: no participant devices to call; the call would ring nobody', { statusCode: 400 });
915
+ }
804
916
  await assertSessions(devices, true);
805
917
  const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
806
918
  call: { callKey: createCallKey() }
@@ -869,19 +981,44 @@ export const makeMessagesSocket = (config) => {
869
981
  throw new Boom(`exclusive target is not a valid JID: ${target}`, { statusCode: 400 });
870
982
  }
871
983
  const devices = await getUSyncDevices([targetUser], relayOpts.useUserDevicesCache !== false, false);
872
- const deviceJids = devices.map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device)).filter(Boolean);
984
+ // AunuBaileys (1.0.83, HB-188): the target's devices take
985
+ // the server of the TARGET jid — for a LID member they live
986
+ // on 'lid'; encoding them on 's.whatsapp.net' addressed a
987
+ // phone number that does not exist (the LID digits are not
988
+ // a number), so the exclusive message could never arrive.
989
+ const targetServer = jidDecode(targetUser)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
990
+ const deviceJids = devices.map(({ user, device }) => jidEncode(user, targetServer, device)).filter(Boolean);
873
991
  // No device list means the member is unknown to usync; addressing
874
992
  // the bare jid still reaches their primary device rather than
875
993
  // failing the send outright.
876
994
  const targets = deviceJids.length ? deviceJids : [targetUser];
877
995
  logger.debug({ jid, target: targetUser, devices: targets.length }, 'sending exclusive group message');
996
+ // AunuBaileys (1.0.83, HB-108): per-device isolation — one
997
+ // device's failure (a rotten session, a rejected stanza)
998
+ // used to abort the loop, so the remaining devices never
999
+ // received the message and the error did not say the
1000
+ // delivery was partial. Deliver to every device that works
1001
+ // and fail only when NONE did; the return value now lists
1002
+ // the devices actually delivered to.
1003
+ const delivered = [];
1004
+ let firstDeviceError;
878
1005
  for (const deviceJid of targets) {
879
- await relayMessage(jid, message, {
880
- ...relayOpts,
881
- participant: { jid: deviceJid, count: 0 }
882
- });
1006
+ try {
1007
+ await relayMessage(jid, message, {
1008
+ ...relayOpts,
1009
+ participant: { jid: deviceJid, count: 0 }
1010
+ });
1011
+ delivered.push(deviceJid);
1012
+ }
1013
+ catch (err) {
1014
+ firstDeviceError = firstDeviceError ?? err;
1015
+ logger.warn({ err, jid, deviceJid }, 'aunu: exclusive send failed for one device; continuing with the remaining devices');
1016
+ }
883
1017
  }
884
- return targets;
1018
+ if (!delivered.length) {
1019
+ throw firstDeviceError ?? new Boom('exclusive send failed for every target device', { statusCode: 500 });
1020
+ }
1021
+ return delivered;
885
1022
  };
886
1023
  // AunuBaileys (1.0.71): named so the wrappers below can call the
887
1024
  // socket's own sendMessage — the `sock` closure variable is the
@@ -1094,60 +1231,99 @@ export const makeMessagesSocket = (config) => {
1094
1231
  // AunuBaileys (1.0.71): the option images follow the parent,
1095
1232
  // each pointing back at it via messageAssociation.
1096
1233
  if (imagePollOptionMedia?.length) {
1234
+ // AunuBaileys (1.0.83, HB-173): per-child
1235
+ // isolation — one failing option image used to
1236
+ // abort the loop, leaving the already-sent poll
1237
+ // parent with only some (or none) of its images
1238
+ // while the caller saw a total failure. Deliver
1239
+ // every child that works; fail only when NONE
1240
+ // did.
1241
+ let deliveredChildren = 0;
1242
+ let firstChildError;
1097
1243
  for (const media of imagePollOptionMedia) {
1098
- const optionMsg = generateWAMessageFromContent(jid, {
1099
- pollCreationOptionImageMessage: { message: media },
1100
- messageContextInfo: {
1101
- messageAssociation: {
1102
- parentMessageKey: fullMsg.key,
1103
- associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL
1244
+ try {
1245
+ const optionMsg = generateWAMessageFromContent(jid, {
1246
+ pollCreationOptionImageMessage: { message: media },
1247
+ messageContextInfo: {
1248
+ messageAssociation: {
1249
+ parentMessageKey: fullMsg.key,
1250
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL
1251
+ }
1104
1252
  }
1105
- }
1106
- }, {
1107
- userJid,
1108
- messageId: generateMessageIDV2(sock.user?.id)
1109
- });
1110
- await relay(jid, optionMsg.message, {
1111
- messageId: optionMsg.key.id,
1112
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1113
- additionalAttributes,
1114
- additionalNodes: []
1115
- });
1253
+ }, {
1254
+ userJid,
1255
+ messageId: generateMessageIDV2(sock.user?.id)
1256
+ });
1257
+ await relay(jid, optionMsg.message, {
1258
+ messageId: optionMsg.key.id,
1259
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1260
+ additionalAttributes,
1261
+ additionalNodes: []
1262
+ });
1263
+ deliveredChildren += 1;
1264
+ }
1265
+ catch (err) {
1266
+ firstChildError = firstChildError ?? err;
1267
+ logger.warn({ err, jid }, 'aunu: one image-poll option image failed to send; continuing with the rest');
1268
+ }
1269
+ }
1270
+ if (!deliveredChildren) {
1271
+ throw firstChildError ?? new Boom('all image-poll option images failed to send', { statusCode: 500 });
1116
1272
  }
1117
1273
  }
1118
1274
  // AunuBaileys: kirim isi album sebagai pesan terasosiasi (dari @itsliaaa/baileys)
1119
1275
  if ('album' in content) {
1120
1276
  const { delayMs = 1500 } = options;
1277
+ // AunuBaileys (1.0.83, HB-173): per-item
1278
+ // isolation — one failing item (upload timeout,
1279
+ // total encryption failure) used to abort the
1280
+ // loop, leaving the already-sent album parent
1281
+ // with only some (or none) of its items while
1282
+ // the caller saw a total failure (and a retry
1283
+ // duplicated the parent). Deliver every item
1284
+ // that works; fail only when NONE did.
1285
+ let deliveredItems = 0;
1286
+ let firstItemError;
1121
1287
  for (const albumMedia of content.album) {
1122
- const albumMsg = await generateWAMessage(jid, albumMedia, {
1123
- logger,
1124
- userJid,
1125
- upload: (fp, opts) => waUploadToServer(fp, { ...opts, newsletter: isJidNewsletter(jid) }),
1126
- mediaCache: config.mediaCache,
1127
- options: config.options,
1128
- ...options,
1129
- messageId: generateMessageIDV2(sock.user?.id)
1130
- });
1131
- const normalized = normalizeMessageContent(albumMsg.message);
1132
- if (!normalized || !('imageMessage' in normalized || 'videoMessage' in normalized)) {
1133
- throw new Boom('Invalid message type for album', { statusCode: 400 });
1288
+ try {
1289
+ const albumMsg = await generateWAMessage(jid, albumMedia, {
1290
+ logger,
1291
+ userJid,
1292
+ upload: (fp, opts) => waUploadToServer(fp, { ...opts, newsletter: isJidNewsletter(jid) }),
1293
+ mediaCache: config.mediaCache,
1294
+ options: config.options,
1295
+ ...options,
1296
+ messageId: generateMessageIDV2(sock.user?.id)
1297
+ });
1298
+ const normalized = normalizeMessageContent(albumMsg.message);
1299
+ if (!normalized || !('imageMessage' in normalized || 'videoMessage' in normalized)) {
1300
+ throw new Boom('Invalid message type for album', { statusCode: 400 });
1301
+ }
1302
+ albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
1303
+ albumMsg.message.messageContextInfo.messageAssociation = {
1304
+ parentMessageKey: fullMsg.key,
1305
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
1306
+ };
1307
+ await relay(jid, albumMsg.message, {
1308
+ messageId: albumMsg.key.id,
1309
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1310
+ statusJidList: options.statusJidList,
1311
+ statusPrivacy: options.statusPrivacy,
1312
+ statusPrivacy: options.statusPrivacy,
1313
+ additionalAttributes,
1314
+ additionalNodes
1315
+ });
1316
+ deliveredItems += 1;
1317
+ }
1318
+ catch (err) {
1319
+ firstItemError = firstItemError ?? err;
1320
+ logger.warn({ err, jid }, 'aunu: one album item failed to send; continuing with the rest of the album');
1134
1321
  }
1135
- albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
1136
- albumMsg.message.messageContextInfo.messageAssociation = {
1137
- parentMessageKey: fullMsg.key,
1138
- associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
1139
- };
1140
- await relay(jid, albumMsg.message, {
1141
- messageId: albumMsg.key.id,
1142
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1143
- statusJidList: options.statusJidList,
1144
- statusPrivacy: options.statusPrivacy,
1145
- statusPrivacy: options.statusPrivacy,
1146
- additionalAttributes,
1147
- additionalNodes
1148
- });
1149
1322
  await delay(delayMs);
1150
1323
  }
1324
+ if (content.album.length && !deliveredItems) {
1325
+ throw firstItemError ?? new Boom('all album items failed to send', { statusCode: 500 });
1326
+ }
1151
1327
  }
1152
1328
  if (config.emitOwnEvents) {
1153
1329
  process.nextTick(() => {
@@ -1,7 +1,7 @@
1
1
  // AunuCore fork backport: in-memory store dari Baileys 6.6.0, dikonversi ke ESM untuk basis 6.7.x
2
2
  import KeyedDBModule from '@adiwajshing/keyed-db';
3
3
  const KeyedDB = KeyedDBModule.default || KeyedDBModule;
4
- import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
7
7
  import { LabelAssociationType } from '../Types/LabelAssociation.js';
@@ -131,6 +131,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
131
131
  for (const update of updates) {
132
132
  if (contacts[update.id]) {
133
133
  Object.assign(contacts[update.id], update);
134
+ // AunuBaileys (1.0.84, HB-245): a picture
135
+ // notification arrives as imgUrl 'changed' /
136
+ // 'removed' — a sentinel, not a URL. Drop the
137
+ // cached URL instead of storing the sentinel:
138
+ // fetchImageUrl used to hand the literal string
139
+ // 'changed' to the host as the avatar URL, and
140
+ // writeToFile persisted the poison to disk.
141
+ if (update.imgUrl === 'changed' || update.imgUrl === 'removed') {
142
+ delete contacts[update.id].imgUrl;
143
+ }
134
144
  }
135
145
  else {
136
146
  logger.debug({ update }, 'got update for non-existant contact');
@@ -258,7 +268,18 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
258
268
  if (metadata) {
259
269
  switch (action) {
260
270
  case 'add':
261
- metadata.participants.push(...participants.map(id => ({ id, isAdmin: false, isSuperAdmin: false })));
271
+ // AunuBaileys (1.0.84, HB-237): skip ids
272
+ // that are already listed. The metadata is
273
+ // usually populated by a fetch that already
274
+ // contains the member, and a redelivery
275
+ // replays the notification — both used to
276
+ // push a duplicate entry every single time,
277
+ // without bound.
278
+ for (const participantId of participants) {
279
+ if (!metadata.participants.some(p => p.id === participantId)) {
280
+ metadata.participants.push({ id: participantId, isAdmin: false, isSuperAdmin: false });
281
+ }
282
+ }
262
283
  break;
263
284
  case 'demote':
264
285
  case 'promote':
@@ -301,14 +322,43 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
301
322
  labelAssociations
302
323
  });
303
324
  const fromJSON = (json) => {
304
- chats.upsert(...json.chats);
305
- labelAssociations.upsert(...json.labelAssociations || []);
306
- contactsUpsert(Object.values(json.contacts));
307
- labelsUpsert(Object.values(json.labels || {}));
308
- for (const jid in json.messages) {
325
+ // AunuBaileys (1.0.84, HB-134 + HB-142 + addenda): every
326
+ // stage — and every entry — is isolated. One poisoned
327
+ // message entry used to throw out of fromJSON mid-loop:
328
+ // that jid's remaining messages and EVERY later jid were
329
+ // silently lost on every boot, while the stages before
330
+ // the throw stayed applied and readFromFile logged
331
+ // "starting with an empty store" — a claim the state did
332
+ // not match. A missing/null contacts stage threw the
333
+ // same way (Object.values of undefined), and a poisoned
334
+ // contacts/labels entry aborted every stage after it.
335
+ // Wrong-typed stages now read as empty, unusable entries
336
+ // are skipped, and a bad message entry is caught per
337
+ // entry with a warning — the healthy remainder of the
338
+ // file always loads.
339
+ if (!json || typeof json !== 'object') {
340
+ return;
341
+ }
342
+ const entriesOf = (value) => (Array.isArray(value) ? value : []).filter(e => !!e && typeof e === 'object');
343
+ const valuesOf = (value) => (value && typeof value === 'object' ? Object.values(value) : []);
344
+ chats.upsert(...entriesOf(json.chats));
345
+ labelAssociations.upsert(...entriesOf(json.labelAssociations));
346
+ contactsUpsert(valuesOf(json.contacts).filter(c => !!c && typeof c === 'object' && typeof c.id === 'string'));
347
+ labelsUpsert(valuesOf(json.labels).filter(l => !!l && typeof l === 'object' && typeof l.id === 'string'));
348
+ const messageSets = json.messages && typeof json.messages === 'object' ? json.messages : {};
349
+ for (const jid in messageSets) {
350
+ const entries = messageSets[jid];
351
+ if (!Array.isArray(entries)) {
352
+ continue;
353
+ }
309
354
  const list = assertMessageList(jid);
310
- for (const msg of json.messages[jid]) {
311
- list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
355
+ for (const msg of entries) {
356
+ try {
357
+ list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
358
+ }
359
+ catch (err) {
360
+ logger.warn({ jid, err }, 'aunu: skipped an unreadable message entry while loading the store');
361
+ }
312
362
  }
313
363
  }
314
364
  };
@@ -329,6 +379,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
329
379
  const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
330
380
  const cursorValue = cursorKey ? list.get(cursorKey.id) : undefined;
331
381
  let messages;
382
+ // AunuBaileys (1.0.84, HB-177): a count of 0 means
383
+ // "no messages" — the slicing below used
384
+ // slice(-count), and slice(-0) is slice(0), so
385
+ // loadMessages(jid, 0) returned the chat's ENTIRE
386
+ // history (and negative counts mis-sliced as well).
387
+ // Every other path in this function already resolves
388
+ // to []. An absent count keeps the legacy behaviour.
389
+ if (typeof count === 'number' && count <= 0) {
390
+ return [];
391
+ }
332
392
  if (list && mode === 'before' && (!cursorKey || cursorValue)) {
333
393
  if (cursorValue) {
334
394
  const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
@@ -386,7 +446,13 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
386
446
  if (!contact) {
387
447
  return sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid);
388
448
  }
389
- if (typeof contact.imgUrl === 'undefined') {
449
+ // AunuBaileys (1.0.84, HB-245): a store file written
450
+ // before this fix can still carry the 'changed' /
451
+ // 'removed' sentinel as imgUrl — treat it as no
452
+ // cached URL and fetch fresh; never hand a sentinel
453
+ // back as an avatar URL. A real cached URL is still
454
+ // returned as-is (and a fresh fetch re-caches it).
455
+ if (typeof contact.imgUrl === 'undefined' || contact.imgUrl === 'changed' || contact.imgUrl === 'removed') {
390
456
  contact.imgUrl = await (sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid));
391
457
  }
392
458
  return contact.imgUrl;
@@ -419,7 +485,17 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
419
485
  writeToFile: (path) => {
420
486
  // require fs here so that in case "fs" is not available -- the app does not crash
421
487
 
422
- writeFileSync(path, JSON.stringify(toJSON()));
488
+ // AunuBaileys (1.0.84, HB-120 + addendum): every
489
+ // other persistence writer in the lib is atomic and
490
+ // 0600 (auth files HB-116, the lid store); this one
491
+ // wrote the full chat/contact/message history
492
+ // world-readable AND non-atomically — a crash
493
+ // mid-write left a truncated file the next boot read
494
+ // as corrupt and discarded wholesale. Write a
495
+ // per-pid temp file, then rename it over the target.
496
+ const tmpPath = `${path}.tmp-${process.pid}`;
497
+ writeFileSync(tmpPath, JSON.stringify(toJSON()), { mode: 0o600 });
498
+ renameSync(tmpPath, path);
423
499
  },
424
500
  readFromFile: (path) => {
425
501
  // require fs here so that in case "fs" is not available -- the app does not crash
@@ -16,11 +16,35 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
16
16
  new NodeCache({
17
17
  stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
18
18
  useClones: false,
19
- deleteOnExpire: true
19
+ deleteOnExpire: true,
20
+ // AunuBaileys (1.0.84, HB-199): a TTL bounds an
21
+ // entry's AGE, not the entry COUNT — a burst of
22
+ // distinct sender addresses inside the 5-minute
23
+ // window grew this cache without limit (the exact
24
+ // warning written in bounded-cache.js). Cap the
25
+ // number of keys as well. NOTE: NodeCache's maxKeys
26
+ // does not evict — set() THROWS once the cap is hit —
27
+ // so every cache write below goes through cacheSet,
28
+ // which swallows that one error: the underlying store
29
+ // is authoritative, a skipped cache entry only costs
30
+ // a re-read, and the cap can never break a session
31
+ // operation.
32
+ maxKeys: 1000
20
33
  });
21
34
  function getUniqueId(type, id) {
22
35
  return `${type}.${id}`;
23
36
  }
37
+ // AunuBaileys (1.0.84, HB-199): see the maxKeys note above —
38
+ // a full cache must degrade to "not cached", never to a throw
39
+ // escaping into get()/set().
40
+ function cacheSet(key, value) {
41
+ try {
42
+ cache.set(key, value);
43
+ }
44
+ catch {
45
+ // cache is at its key cap; the store stays authoritative
46
+ }
47
+ }
24
48
  return {
25
49
  async get(type, ids) {
26
50
  const data = {};
@@ -41,7 +65,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
41
65
  const item = fetched[id];
42
66
  if (item) {
43
67
  data[id] = item;
44
- cache.set(getUniqueId(type, id), item);
68
+ cacheSet(getUniqueId(type, id), item);
45
69
  }
46
70
  }
47
71
  }
@@ -67,7 +91,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
67
91
  cache.del(getUniqueId(type, id));
68
92
  }
69
93
  else {
70
- cache.set(getUniqueId(type, id), data[type][id]);
94
+ cacheSet(getUniqueId(type, id), data[type][id]);
71
95
  }
72
96
  keys += 1;
73
97
  }
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from 'events';
2
2
  import { createReadStream } from 'fs';
3
- import { writeFile } from 'fs/promises';
3
+ import { rename, writeFile } from 'fs/promises';
4
4
  import { createInterface } from 'readline';
5
5
  import { delay } from './generics.js';
6
6
  import { makeMutex } from './make-mutex.js';
@@ -13,13 +13,48 @@ export const captureEventStream = (ev, filename) => {
13
13
  const oldEmit = ev.emit;
14
14
  // write mutex so data is appended in order
15
15
  const writeMutex = makeMutex();
16
+ // AunuBaileys (1.0.84, HB-247): a creds.update payload is the
17
+ // full credential material (private noise / identity /
18
+ // signed-pre-key bytes, the adv secret). The capture below
19
+ // redacts it to the NAMES of the fields that changed, creates
20
+ // the file 0600, and rotates it to `<filename>.1` once per
21
+ // 10 MiB so a long-running debug capture cannot grow (or
22
+ // leak) without bound.
23
+ let capturedBytes = 0;
24
+ const MAX_CAPTURE_BYTES = 10 * 1024 * 1024;
16
25
  // monkey patch eventemitter to capture all events
17
26
  ev.emit = function (...args) {
18
- const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n';
19
27
  const result = oldEmit.apply(ev, args);
28
+ let content;
29
+ try {
30
+ const data = args[0] === 'creds.update' && args[1] && typeof args[1] === 'object'
31
+ ? { redacted: 'creds.update payload withheld (credential material)', updatedFields: Object.keys(args[1]) }
32
+ : args[1];
33
+ content = JSON.stringify({ timestamp: Date.now(), event: args[0], data }) + '\n';
34
+ }
35
+ catch {
36
+ // AunuBaileys (1.0.84, HB-129 addendum): an
37
+ // unserialisable (circular) payload used to throw
38
+ // synchronously out of the patched emit — into
39
+ // whatever lib code had emitted the event — because
40
+ // the stringify ran before oldEmit. Serialisation now
41
+ // happens after the real emit, and a payload that
42
+ // cannot be serialised is simply not captured.
43
+ return result;
44
+ }
45
+ // AunuBaileys (1.0.84, HB-129): the mutex promise used to
46
+ // be discarded — a failed write (deleted directory, full
47
+ // disk, permissions) rejected with no handler attached
48
+ // and crashed the host process (Node >= 15 default). A
49
+ // debug capture must never take the host down with it.
20
50
  writeMutex.mutex(async () => {
21
- await writeFile(filename, content, { flag: 'a' });
22
- });
51
+ capturedBytes += Buffer.byteLength(content);
52
+ if (capturedBytes > MAX_CAPTURE_BYTES) {
53
+ capturedBytes = Buffer.byteLength(content);
54
+ await rename(filename, `${filename}.1`).catch(() => { });
55
+ }
56
+ await writeFile(filename, content, { flag: 'a', mode: 0o600 });
57
+ }).catch(() => { });
23
58
  return result;
24
59
  };
25
60
  };
@@ -45,11 +45,25 @@ export const makeEventBuffer = (logger) => {
45
45
  // had to run. Log the error and keep dispatching the
46
46
  // remaining events of the batch; the event itself
47
47
  // still fired, with its payload unchanged.
48
- try {
49
- ev.emit(event, map[event]);
50
- }
51
- catch (err) {
52
- logger.error({ err, event }, 'error in event listener — dispatch continues');
48
+ //
49
+ // AunuBaileys (1.0.84, HB-264): dispatch PER LISTENER.
50
+ // A single ev.emit stops at the first listener that
51
+ // throws, so every listener registered AFTER the
52
+ // thrower silently missed that emit — a throwing
53
+ // logging listener registered before the host's
54
+ // saveCreds made that credential update vanish.
55
+ // rawListeners returns the once-wrappers themselves,
56
+ // so once semantics are preserved, and each listener
57
+ // gets its own guard: a throw now costs only the
58
+ // listener that threw.
59
+ const listeners = ev.rawListeners(event);
60
+ for (const listener of listeners) {
61
+ try {
62
+ listener.call(ev, map[event]);
63
+ }
64
+ catch (err) {
65
+ logger.error({ err, event }, 'error in event listener — dispatch continues');
66
+ }
53
67
  }
54
68
  }
55
69
  });
@@ -65,6 +79,17 @@ export const makeEventBuffer = (logger) => {
65
79
  }
66
80
  logger.debug('Flushing event buffer');
67
81
  isBuffering = false;
82
+ // AunuBaileys (1.0.84, HB-130 + addendum): the history
83
+ // dedup set only has to live for ONE buffering session.
84
+ // It used to survive every flush (cleared only in
85
+ // destroy()), so a second history sync inside the same
86
+ // socket lifetime silently dropped every already-known
87
+ // chat — and every already-known message key, even when
88
+ // that message's content had changed: fresh unread
89
+ // counts and edits never reached the store. Dedup for
90
+ // THIS session's appends already happened above; the
91
+ // next session starts empty.
92
+ historyCache.clear();
68
93
  const newData = makeBufferData();
69
94
  const chatUpdates = Object.values(data.chatUpdates);
70
95
  let conditionalChatUpdatesLeft = 0;
@@ -305,7 +330,14 @@ eventData, logger) {
305
330
  data.contactUpserts[contact.id] = upsert;
306
331
  }
307
332
  if (data.contactUpdates[contact.id]) {
308
- upsert = Object.assign(data.contactUpdates[contact.id], trimUndefined(contact));
333
+ // AunuBaileys (1.0.84, HB-182): the pending
334
+ // update merges INTO the upsert. The old
335
+ // direction merged the contact into the pending
336
+ // update object and then deleted that object —
337
+ // the merged result went nowhere, and fields
338
+ // only the update carried (notify, verifiedName)
339
+ // silently vanished from the flushed upsert.
340
+ Object.assign(upsert, data.contactUpdates[contact.id]);
309
341
  delete data.contactUpdates[contact.id];
310
342
  }
311
343
  }
@@ -46,7 +46,26 @@ export const BufferJSON = {
46
46
  reviver: (_, value) => {
47
47
  if (typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
48
48
  const val = value.data || value.value;
49
- return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val || []);
49
+ if (typeof val === 'string') {
50
+ // AunuBaileys (1.0.84, HB-143): Node's base64
51
+ // decoder is lenient — it silently DROPS
52
+ // characters outside the alphabet, so a garbage
53
+ // or hand-mangled string in a creds/key file
54
+ // decoded to wrong-length, wrong-value key bytes
55
+ // with no error anywhere, and the corruption only
56
+ // surfaced later as baffling Curve / handshake
57
+ // failures. The replacer above only ever produces
58
+ // canonical base64; reject anything else loudly.
59
+ // Both in-tree readers contain the throw: the
60
+ // auth-state reader logs a corrupt file and starts
61
+ // clean, and the sender-key reader falls back to
62
+ // an empty (self-healing) record.
63
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(val)) {
64
+ throw new Error('BufferJSON: invalid base64 in serialized Buffer data');
65
+ }
66
+ return Buffer.from(val, 'base64');
67
+ }
68
+ return Buffer.from(val || []);
50
69
  }
51
70
  return value;
52
71
  }
@@ -169,7 +169,10 @@ export const makeLIDStore = (options = {}) => {
169
169
  // AunuBaileys (1.0.70): atomic tmp+rename — a crash mid-write
170
170
  // used to leave a truncated store file behind.
171
171
  const tmpPath = `${path}.tmp-${process.pid}`;
172
- writeFileSync(tmpPath, JSON.stringify(Object.fromEntries(pnToLid)));
172
+ // AunuBaileys (1.0.84, HB-120): 0600, like the auth
173
+ // files (HB-116) — this file maps phone numbers to
174
+ // LIDs and must not land world-readable.
175
+ writeFileSync(tmpPath, JSON.stringify(Object.fromEntries(pnToLid)), { mode: 0o600 });
173
176
  renameSync(tmpPath, path);
174
177
  },
175
178
  loadFromFile: (path) => {
@@ -603,6 +603,18 @@ const withAunuButtonIcon = (button) => {
603
603
  buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params ?? {})
604
604
  };
605
605
  };
606
+ // AunuBaileys (1.0.83, HB-254): delete / pin / edit each carry a
607
+ // message KEY, and WAProto.Message.fromObject silently DROPS a key
608
+ // whose value is undefined — so { delete: undefined } used to relay
609
+ // a key-less REVOKE (pin and edit likewise) that "succeeded" but
610
+ // could never take effect, because the server had no key to act
611
+ // on. Mirror the pollUpdate branch's discipline: the argument
612
+ // must be a real key object carrying a message id.
613
+ const assertMessageKeyArg = (value, field) => {
614
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.id !== 'string' || !value.id) {
615
+ throw new Boom(`"${field}" requires a valid message key (an object with a message id)`, { statusCode: 400 });
616
+ }
617
+ };
606
618
  export const generateWAMessageContent = async (message, options) => {
607
619
  var _a, _b;
608
620
  let m = {};
@@ -610,8 +622,13 @@ export const generateWAMessageContent = async (message, options) => {
610
622
  // straight through to generateWAMessage() unprocessed (donor:
611
623
  // @violetix/baileys). The caller owns the proto shape entirely.
612
624
  if (message && typeof message === 'object' && 'raw' in message && !!message.raw) {
613
- delete message.raw;
614
- return message;
625
+ // AunuBaileys (1.0.83, HB-165): work on a copy — the object
626
+ // belongs to the caller, and deleting `raw` from it made a
627
+ // second send of the same object silently lose the raw
628
+ // passthrough entirely.
629
+ const rawContent = { ...message };
630
+ delete rawContent.raw;
631
+ return rawContent;
615
632
  }
616
633
  // AunuBaileys: rich response — blok kode / tabel / tautan (dari @itsliaaa/baileys)
617
634
  if ('code' in message || 'links' in message || 'table' in message || 'richResponse' in message || 'html' in message || 'richHtml' in message) {
@@ -664,12 +681,19 @@ export const generateWAMessageContent = async (message, options) => {
664
681
  m.locationMessage = WAProto.Message.LocationMessage.fromObject(message.location);
665
682
  }
666
683
  else if ('react' in message) {
667
- if (!message.react.senderTimestampMs) {
668
- message.react.senderTimestampMs = Date.now();
684
+ // AunuBaileys (1.0.83, HB-165): stamp the timestamp on a
685
+ // copy — the caller's react object used to keep it, so a
686
+ // re-send of the same object carried the first send's
687
+ // (stale) timestamp. A non-object react keeps the original
688
+ // behaviour (the deref below throws, caller-visible).
689
+ const react = message.react && typeof message.react === 'object' ? { ...message.react } : message.react;
690
+ if (!react.senderTimestampMs) {
691
+ react.senderTimestampMs = Date.now();
669
692
  }
670
- m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(message.react);
693
+ m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(react);
671
694
  }
672
695
  else if ('delete' in message) {
696
+ assertMessageKeyArg(message.delete, 'delete');
673
697
  m.protocolMessage = {
674
698
  key: message.delete,
675
699
  type: WAProto.Message.ProtocolMessage.Type.REVOKE
@@ -706,6 +730,7 @@ export const generateWAMessageContent = async (message, options) => {
706
730
  }
707
731
  }
708
732
  else if ('pin' in message) {
733
+ assertMessageKeyArg(message.pin, 'pin');
709
734
  m.pinInChatMessage = {};
710
735
  m.messageContextInfo = {};
711
736
  m.pinInChatMessage.key = message.pin;
@@ -729,6 +754,13 @@ export const generateWAMessageContent = async (message, options) => {
729
754
  type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
730
755
  };
731
756
  break;
757
+ default:
758
+ // AunuBaileys (1.0.83, HB-253 family): an unknown
759
+ // reply type used to fall through the switch,
760
+ // leaving m empty — a content-less message was
761
+ // relayed as if the reply had been sent. Fail
762
+ // loudly instead.
763
+ throw new Boom(`Unknown buttonReply type: ${message.type} (expected 'template' or 'plain')`, { statusCode: 400 });
732
764
  }
733
765
  }
734
766
  else if ('ptv' in message && message.ptv) {
@@ -814,6 +846,10 @@ export const generateWAMessageContent = async (message, options) => {
814
846
  throw new Boom('Event responses (RSVPs) are sent as encEventResponseMessage (Message field 76), encrypted with the event creation key — not yet implemented in AunuBaileys', { statusCode: 501 });
815
847
  }
816
848
  else if ('poll' in message) {
849
+ // AunuBaileys (1.0.83, HB-165): the default stamps below
850
+ // (selectableCount / toAnnouncementGroup) land on a copy of
851
+ // the caller's content, not on the caller's poll object.
852
+ message = { ...message, poll: { ...message.poll } };
817
853
  (_a = message.poll).selectableCount || (_a.selectableCount = 0);
818
854
  (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
819
855
  if (!Array.isArray(message.poll.values)) {
@@ -1190,7 +1226,10 @@ export const generateWAMessageContent = async (message, options) => {
1190
1226
  };
1191
1227
  }
1192
1228
  m = { groupStatusMessageV2: { message: m } };
1193
- delete message.groupStatus;
1229
+ // AunuBaileys (1.0.83, HB-165): the flag used to be deleted
1230
+ // from the CALLER's object here — a re-send of the same
1231
+ // content object silently lost its group-status wrapping.
1232
+ // The flag is already consumed into m; leave it in place.
1194
1233
  }
1195
1234
  // AunuBaileys (1.0.71): spoiler — set contextInfo.isSpoiler and wrap
1196
1235
  // the message into spoilerMessage (Message field 118).
@@ -1206,7 +1245,9 @@ export const generateWAMessageContent = async (message, options) => {
1206
1245
  };
1207
1246
  }
1208
1247
  m = { spoilerMessage: { message: m } };
1209
- delete message.spoiler;
1248
+ // AunuBaileys (1.0.83, HB-165): same caller-object mutation
1249
+ // as the groupStatus block above — the flag stays on the
1250
+ // caller's object, so a re-send wraps identically.
1210
1251
  }
1211
1252
  // AunuBaileys: interactiveAsTemplate (dari @itsliaaa/baileys) — bungkus interactiveMessage
1212
1253
  // ke templateMessage; messageContextInfo (deviceListMetadata) dipertahankan di sisinya
@@ -1223,6 +1264,7 @@ export const generateWAMessageContent = async (message, options) => {
1223
1264
  };
1224
1265
  }
1225
1266
  if ('edit' in message) {
1267
+ assertMessageKeyArg(message.edit, 'edit');
1226
1268
  m = {
1227
1269
  protocolMessage: {
1228
1270
  key: message.edit,
@@ -18,7 +18,7 @@ export declare const xmppPreKey: (pair: KeyPair, id: number) => BinaryNode;
18
18
  export declare const parseAndInjectE2ESessions: (node: BinaryNode, repository: SignalRepository) => Promise<void>;
19
19
  /** AunuBaileys: session parsed from a retry receipt's embedded key bundle, or undefined when absent. */
20
20
  export declare const parseRetryReceiptSession: (node: BinaryNode) => { registrationId: number; identityKey: Buffer; signedPreKey?: { keyId: number; publicKey: Buffer; signature?: Buffer }; preKey?: { keyId: number; publicKey: Buffer } } | undefined;
21
- export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => JidWithDevice[];
21
+ export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, myLid: string | undefined, excludeZeroDevices: boolean) => JidWithDevice[];
22
22
  /**
23
23
  * get the next N keys for upload or processing
24
24
  * @param count number of pre-keys to get or generate
@@ -184,8 +184,19 @@ export const parseAndInjectE2ESessions = async (node, repository, logger) => {
184
184
  }));
185
185
  }
186
186
  };
187
- export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
187
+ export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
188
188
  const { user: myUser, device: myDevice } = jidDecode(myJid) || {};
189
+ // AunuBaileys (1.0.83, HB-258): our own devices exist under TWO
190
+ // identities — the PN user and the LID user. Comparing only the
191
+ // PN user left our own LID devices in the extracted list, so a
192
+ // group send encrypted and delivered a copy to our own LID
193
+ // device (self-delivery plus wasted encryption). Upstream v7
194
+ // added the same myLid parameter; the comparison below decodes
195
+ // it to its user first, so the current device is excluded
196
+ // under BOTH identities while our other devices stay listed
197
+ // (group sender-key distribution and the DM echo still need
198
+ // them, exactly like the PN side).
199
+ const myLidUser = myLid ? jidDecode(myLid)?.user : undefined;
189
200
  const extracted = [];
190
201
  for (const userResult of result) {
191
202
  const { devices, id } = userResult;
@@ -197,7 +208,7 @@ export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
197
208
  if (Array.isArray(deviceList)) {
198
209
  for (const { id: device, keyIndex } of deviceList) {
199
210
  if ((!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
200
- (myUser !== user || myDevice !== device) && // either different user or if me user, not this device
211
+ ((myUser !== user && myLidUser !== user) || myDevice !== device) && // either a different user (neither our PN nor our LID) or if me user, not this device
201
212
  device !== 99 && // AunuBaileys: hosted / Cloud-API companion devices (:99) are not E2E targets — libsignal rejects them ("Unexpected non-hosted device JID with device 99"); skip them when building the device list
202
213
  (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
203
214
  ) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.82",
4
+ "version": "1.0.84",
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",