@violetix/baileys 0.3.18 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ import { areJidsSameUser, jidDecode, jidEncode } from '../WABinary/index.js';
2
+ /**
3
+ * Stanza tags a filter may drop. Stream control, `iq` replies and the auth-critical
4
+ * `success` / `failure` tags are deliberately absent so the connection flow stays intact.
5
+ */
6
+ export const IGNORE_KEY_KINDS = ['message', 'receipt', 'notification', 'presence', 'chatstate', 'call'];
7
+ const IGNORE_KEY_KIND_SET = new Set(IGNORE_KEY_KINDS);
8
+ /**
9
+ * Kinds the server re-delivers until acked. Dropping one without an ack makes the server
10
+ * retry it forever, so the filter acks on the handler's behalf. Presence and chat-state are
11
+ * fire-and-forget and must not be acked.
12
+ */
13
+ const ACKED_KINDS = new Set(['message', 'receipt', 'notification', 'call']);
14
+ /**
15
+ * A JID arrives in whichever namespace the sender used, and the alt attributes carry the other
16
+ * form. Matching every one of these lets a filter written with a phone number also catch the
17
+ * LID-addressed copy of the same stanza (and vice versa).
18
+ */
19
+ const REMOTE_JID_ATTRS = ['from', 'sender_pn', 'sender_lid'];
20
+ const PARTICIPANT_ATTRS = ['participant', 'participant_pn', 'participant_lid'];
21
+ const DESCRIPTOR_FIELDS = ['remoteJid', 'fromMe', 'id', 'participant'];
22
+ /** Deviceless user-form JID. Userless values (e.g. `s.whatsapp.net`) are kept verbatim. */
23
+ const stripDevice = (jid) => {
24
+ if (!jid) {
25
+ return null;
26
+ }
27
+ const decoded = jidDecode(jid);
28
+ if (!decoded?.user) {
29
+ return jid;
30
+ }
31
+ return jidEncode(decoded.user, decoded.server);
32
+ };
33
+ /**
34
+ * Compares deviceless JIDs as whole strings rather than by user part. `areJidsSameUser` ignores
35
+ * the server, so it reports `5551234@s.whatsapp.net` and `5551234@lid` as the same user — two
36
+ * unrelated accounts that would then share a filter. Server equality is required here; the
37
+ * PN/LID crossover is handled by the alt attributes instead.
38
+ */
39
+ const sameJid = (a, b) => {
40
+ if (!a || !b) {
41
+ return false;
42
+ }
43
+ return stripDevice(a) === stripDevice(b);
44
+ };
45
+ export const isIgnorableKind = (tag) => IGNORE_KEY_KIND_SET.has(tag);
46
+ /** Validates a descriptor or predicate and freezes it into an internal entry. */
47
+ export const normalizeIgnoreKey = (input) => {
48
+ if (typeof input === 'function') {
49
+ return { predicate: input };
50
+ }
51
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
52
+ throw new Error('ignoreKey expects a descriptor object or a predicate function');
53
+ }
54
+ if (!DESCRIPTOR_FIELDS.some(field => input[field] !== undefined)) {
55
+ throw new Error('ignoreKey descriptor needs at least one of remoteJid, fromMe, id, participant');
56
+ }
57
+ const entry = {};
58
+ if (input.remoteJid !== undefined) {
59
+ const jids = (Array.isArray(input.remoteJid) ? input.remoteJid : [input.remoteJid]).filter(Boolean);
60
+ if (!jids.length) {
61
+ throw new Error('ignoreKey remoteJid cannot be empty');
62
+ }
63
+ entry.remoteJid = jids;
64
+ }
65
+ if (input.participant !== undefined) {
66
+ if (typeof input.participant !== 'string' || !input.participant) {
67
+ throw new Error('ignoreKey participant must be a non-empty jid');
68
+ }
69
+ entry.participant = input.participant;
70
+ }
71
+ if (input.fromMe !== undefined) {
72
+ entry.fromMe = !!input.fromMe;
73
+ }
74
+ if (input.id !== undefined) {
75
+ if (typeof input.id !== 'string' || !input.id) {
76
+ throw new Error('ignoreKey id must be a non-empty string');
77
+ }
78
+ entry.id = input.id;
79
+ }
80
+ if (input.only !== undefined) {
81
+ const only = Array.isArray(input.only) ? input.only : [input.only];
82
+ if (!only.length) {
83
+ throw new Error('ignoreKey only cannot be empty');
84
+ }
85
+ const unknown = only.filter(kind => !IGNORE_KEY_KIND_SET.has(kind));
86
+ if (unknown.length) {
87
+ throw new Error(`ignoreKey only has unknown kinds: ${unknown.join(', ')}`);
88
+ }
89
+ entry.only = only;
90
+ }
91
+ return entry;
92
+ };
93
+ /**
94
+ * Parses the wire node into the shape filters match on, so a predicate never has to walk a
95
+ * BinaryNode. `remoteJid` and `participant` are device-stripped to line up with the
96
+ * `event.key.remoteJid` a handler would see.
97
+ */
98
+ export const buildIgnoreContext = (node, meId, meLid) => {
99
+ const attrs = node.attrs || {};
100
+ const author = attrs.participant || attrs.participant_pn || attrs.participant_lid || attrs.from;
101
+ const fromMe = !!author && ((!!meId && areJidsSameUser(author, meId)) || (!!meLid && areJidsSameUser(author, meLid)));
102
+ return {
103
+ kind: node.tag,
104
+ // a 1:1 message sent from another of our devices arrives from us, with the chat in `recipient`
105
+ remoteJid: stripDevice(fromMe && attrs.recipient ? attrs.recipient : attrs.from),
106
+ fromMe,
107
+ id: attrs.id,
108
+ participant: stripDevice(attrs.participant || attrs.participant_pn || attrs.participant_lid)
109
+ };
110
+ };
111
+ export const matchesIgnoreKey = (entry, ctx, node) => {
112
+ if (entry.predicate) {
113
+ return !!entry.predicate(ctx);
114
+ }
115
+ if (entry.only && !entry.only.includes(ctx.kind)) {
116
+ return false;
117
+ }
118
+ if (entry.id !== undefined && entry.id !== ctx.id) {
119
+ return false;
120
+ }
121
+ if (entry.fromMe !== undefined && entry.fromMe !== ctx.fromMe) {
122
+ return false;
123
+ }
124
+ const attrs = node.attrs || {};
125
+ const remoteAttrs = ctx.fromMe && attrs.recipient ? ['recipient', ...REMOTE_JID_ATTRS] : REMOTE_JID_ATTRS;
126
+ if (entry.remoteJid && !entry.remoteJid.some(wanted => remoteAttrs.some(attr => sameJid(attrs[attr], wanted)))) {
127
+ return false;
128
+ }
129
+ if (entry.participant && !PARTICIPANT_ATTRS.some(attr => sameJid(attrs[attr], entry.participant))) {
130
+ return false;
131
+ }
132
+ return true;
133
+ };
134
+ export const ignoredStanzaNeedsAck = (tag) => ACKED_KINDS.has(tag);
135
+ /**
136
+ * Holds the registered filters and answers the one question the socket asks per stanza.
137
+ *
138
+ * `category='peer'` traffic from this account's own devices is never dropped: those stanzas
139
+ * carry app-state key shares, history sync and PDO responses between the account's devices, so
140
+ * the common `{ fromMe: true }` filter would otherwise swallow the app-state key share and leave
141
+ * the collections blocked on a key they re-request on every sync. The exemption only applies
142
+ * when the sender really is this account — resolved against both `meJid` and `meLid` — so a
143
+ * foreign stanza cannot dodge a filter by stamping the attribute.
144
+ */
145
+ export const makeIgnoreKeyRegistry = ({ getMeId, getMeLid, logger }) => {
146
+ const entries = new Set();
147
+ const register = (input) => {
148
+ const entry = normalizeIgnoreKey(input);
149
+ entries.add(entry);
150
+ return () => {
151
+ entries.delete(entry);
152
+ };
153
+ };
154
+ const shouldDrop = (node) => {
155
+ if (!entries.size || !isIgnorableKind(node.tag)) {
156
+ return false;
157
+ }
158
+ const ctx = buildIgnoreContext(node, getMeId(), getMeLid());
159
+ if (ctx.fromMe && node.attrs?.category === 'peer') {
160
+ return false;
161
+ }
162
+ for (const entry of entries) {
163
+ try {
164
+ if (matchesIgnoreKey(entry, ctx, node)) {
165
+ return true;
166
+ }
167
+ }
168
+ catch (err) {
169
+ // A throwing predicate must not take the stanza with it; the stanza stays.
170
+ logger?.error({ err, kind: ctx.kind, id: ctx.id }, 'ignoreKey filter threw, keeping stanza');
171
+ }
172
+ }
173
+ return false;
174
+ };
175
+ return { register, shouldDrop, get size() { return entries.size; } };
176
+ };
@@ -22,5 +22,8 @@ export * from "./browser-utils.js";
22
22
  export * from "./companion-reg-client-utils.js";
23
23
  export * from "./identity-change-handler.js";
24
24
  export * from "./stanza-ack.js";
25
+ export * from "./ignore-key.js";
26
+ export * from "./anchor-guard.js";
27
+ export * from "./a2ui.js";
25
28
  export * from "./meta-ai-msmsg.js";
26
29
  //# sourceMappingURL=index.d.ts.map
@@ -22,5 +22,8 @@ export * from './browser-utils.js';
22
22
  export * from './companion-reg-client-utils.js';
23
23
  export * from './identity-change-handler.js';
24
24
  export * from './stanza-ack.js';
25
+ export * from './ignore-key.js';
26
+ export * from './anchor-guard.js';
27
+ export * from './a2ui.js';
25
28
  export * from './meta-ai-msmsg.js';
26
29
  //# sourceMappingURL=index.js.map
@@ -57,6 +57,8 @@ export interface MediaEncryptionOptions {
57
57
  };
58
58
  saveOriginalFileIfRequired?: boolean;
59
59
  opts?: MediaFetchOptions;
60
+ /** Reuse an existing 32-byte media key instead of generating one (e.g. sticker-pack thumbnails). */
61
+ mediaKey?: Uint8Array;
60
62
  }
61
63
  export interface MediaDownloadOptions {
62
64
  startByte?: number;
@@ -303,9 +303,31 @@ export const getStream = async (item, opts) => {
303
303
  if (Buffer.isBuffer(item)) {
304
304
  return { stream: toReadable(item), type: 'buffer' };
305
305
  }
306
+ // A bare path or URL string is accepted, and the reason is the error it used to produce. The
307
+ // contract below is `Buffer | { stream } | { url }`, so `'stream' in item` throws on a string —
308
+ // and it throws with the path interpolated:
309
+ //
310
+ // TypeError: Cannot use 'in' operator to search for 'stream' in /tmp/8cktb7o7x.webp
311
+ //
312
+ // Measured on a production fleet, 2026-10-10: five of those under [SEND_STICKER]. The message
313
+ // names a file, so it reads as a missing or unreadable one, while the real fault is the SHAPE of
314
+ // the argument — and the file was perfectly readable: `createReadStream(item.url)` at the end of
315
+ // this function handles exactly that string. Widening here costs nothing and removes the whole
316
+ // class, for every caller rather than the one that was caught.
317
+ if (typeof item === 'string') {
318
+ item = { url: item };
319
+ }
320
+ if (!item || typeof item !== 'object') {
321
+ throw new Boom(`media source must be a Buffer, a string, { stream } or { url }, got ${item === null ? 'null' : typeof item}`);
322
+ }
306
323
  if ('stream' in item) {
307
324
  return { stream: item.stream, type: 'readable' };
308
325
  }
326
+ // Named rather than left to throw "Cannot read properties of undefined (reading 'toString')",
327
+ // which says nothing about which field is missing.
328
+ if (item.url === undefined || item.url === null) {
329
+ throw new Boom('media source object has neither `stream` nor `url`');
330
+ }
309
331
  const urlStr = item.url.toString();
310
332
  if (urlStr.startsWith('data:')) {
311
333
  const buffer = Buffer.from(urlStr.split(',')[1], 'base64');
@@ -459,10 +481,11 @@ class SidecarAccumulator {
459
481
  this.windowOffset = SIDECAR_IV_SIZE;
460
482
  }
461
483
  }
462
- export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
484
+ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, mediaKey: givenMediaKey } = {}) => {
463
485
  const { stream, type } = await getStream(media, opts);
464
486
  logger?.debug('fetched media stream');
465
- const mediaKey = Crypto.randomBytes(32);
487
+ // sticker-pack thumbnails must reuse the pack's key, the message carries only that one
488
+ const mediaKey = givenMediaKey ?? Crypto.randomBytes(32);
466
489
  const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
467
490
  const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
468
491
  const encFileWriteStream = createWriteStream(encFilePath);
@@ -19,6 +19,7 @@ export function getAggregateResponsesInEventMessage({ eventResponses }: {
19
19
  }, meId: any): any[];
20
20
  export function extractUrlFromText(text: any): any;
21
21
  export function generateLinkPreviewIfRequired(text: any, getUrlInfo: any, logger: any): Promise<any>;
22
+ export function hashImagePollOption(optionName: any, fileSha256?: Uint8Array | null): string;
22
23
  export function prepareWAMessageMedia(message: any, options: any): Promise<proto.Message>;
23
24
  export function prepareDisappearingMessageSettingContent(ephemeralExpiration: any): proto.Message;
24
25
  export function generateForwardMessageContent(message: any, forceForward: any): any;
@@ -6,6 +6,7 @@ import { proto } from '../../WAProto/index.js';
6
6
  import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, DONATE_URL, LIBRARY_NAME, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
7
7
  import { AssociationType, ButtonHeaderType, ButtonType, CarouselCardType, ListType, ProtocolType, WAMessageStatus, WAProto } from '../Types/index.js';
8
8
  import { isLidUser, isPnUser, isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
+ import { buildA2UIWidget } from './a2ui.js';
9
10
  import { sha256 } from './crypto.js';
10
11
  import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
12
  import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
@@ -60,6 +61,16 @@ const assertColor = async (color) => {
60
61
  }
61
62
  return assertedColor;
62
63
  };
64
+ /**
65
+ * Option identifier for an image poll: WhatsApp hashes the option name, appends the base64
66
+ * media hash, and hashes that — `sha256(hex(sha256(name)) + base64(fileSha256))`. A vote carries
67
+ * this hash, not the name, so it has to match byte for byte.
68
+ */
69
+ export const hashImagePollOption = (optionName, fileSha256) => {
70
+ const nameHash = sha256(Buffer.from(String(optionName), 'utf-8')).toString('hex');
71
+ const mediaHashB64 = fileSha256 ? Buffer.from(fileSha256).toString('base64') : '';
72
+ return sha256(Buffer.from(nameHash + mediaHashB64, 'utf-8')).toString('hex');
73
+ };
63
74
  export const prepareWAMessageMedia = async (message, options) => {
64
75
  const logger = options.logger;
65
76
  let mediaType;
@@ -94,7 +105,8 @@ export const prepareWAMessageMedia = async (message, options) => {
94
105
  if (mediaBuff) {
95
106
  logger?.debug({ cacheableKey }, 'got media cache hit');
96
107
  const obj = proto.Message.decode(mediaBuff);
97
- const key = `${mediaType}Message`;
108
+ // ptv entries are cached after videoMessage was renamed to ptvMessage
109
+ const key = obj.ptvMessage ? 'ptvMessage' : `${mediaType}Message`;
98
110
  Object.assign(obj[key], { ...uploadData, media: undefined });
99
111
  return obj;
100
112
  }
@@ -891,6 +903,36 @@ export const generateWAMessageContent = async (message, options) => {
891
903
  messageSecret: message.poll.messageSecret || randomBytes(32)
892
904
  };
893
905
  }
906
+ // Image poll: the parent carries only the option names and their hashes. The images
907
+ // themselves travel as separate pollCreationOptionImageMessage children, associated back
908
+ // to this message by MEDIA_POLL — sendMessage sends those after the parent.
909
+ else if (hasNonNullishProperty(message, 'imagePoll')) {
910
+ if (!Array.isArray(message.imagePoll.options) || !message.imagePoll.options.length) {
911
+ throw new Boom('Invalid imagePoll options', { statusCode: 400 });
912
+ }
913
+ const selectableOptionsCount = message.imagePoll.selectableCount ?? 1;
914
+ if (selectableOptionsCount < 0 || selectableOptionsCount > message.imagePoll.options.length) {
915
+ throw new Boom(`imagePoll.selectableCount should be >= 0 and <= ${message.imagePoll.options.length}`, {
916
+ statusCode: 400
917
+ });
918
+ }
919
+ const missingHash = message.imagePoll.options.find(option => !option.optionHash);
920
+ if (missingHash) {
921
+ throw new Boom('imagePoll options must be prepared with hashImagePollOption; send via sendMessage instead of building the content by hand', { statusCode: 400 });
922
+ }
923
+ m.pollCreationMessageV3 = {
924
+ name: message.imagePoll.name,
925
+ selectableOptionsCount,
926
+ options: message.imagePoll.options.map(option => ({
927
+ optionName: option.optionName,
928
+ optionHash: option.optionHash
929
+ })),
930
+ pollContentType: proto.Message.PollContentType.IMAGE
931
+ };
932
+ m.messageContextInfo = {
933
+ messageSecret: message.imagePoll.messageSecret || randomBytes(32)
934
+ };
935
+ }
894
936
  // Lia@Changes 08-02-26 --- Add poll result snapshot message
895
937
  else if (hasNonNullishProperty(message, 'pollResult')) {
896
938
  const pollResultSnapshotMessage = {
@@ -994,6 +1036,12 @@ export const generateWAMessageContent = async (message, options) => {
994
1036
  }
995
1037
  };
996
1038
  }
1039
+ // An A2UI widget has no base content of its own — the interactive chain below replaces `m`
1040
+ // wholesale. Without this it falls through to the media branch and is rejected as a media
1041
+ // message, so a widget would need a dummy `text` to send at all.
1042
+ else if (hasNonNullishProperty(message, 'a2ui')) {
1043
+ m = {};
1044
+ }
997
1045
  else {
998
1046
  m = await prepareWAMessageMedia(message, options);
999
1047
  }
@@ -1117,6 +1165,31 @@ export const generateWAMessageContent = async (message, options) => {
1117
1165
  }
1118
1166
  };
1119
1167
  }
1168
+ // A2UI widget. `nativeFlowMessage` is always set, even empty: it is what the client keys the
1169
+ // interactive render off, and it is also what makes the socket attach the <biz> node.
1170
+ else if (hasNonNullishProperty(message, 'a2ui')) {
1171
+ const interactiveMessage = {
1172
+ bloksWidget: buildA2UIWidget(message.a2ui, {
1173
+ type: message.a2uiType,
1174
+ wrapped: message.a2uiWrapped !== false
1175
+ }),
1176
+ nativeFlowMessage: hasNonNullishProperty(message, 'nativeFlow')
1177
+ ? prepareNativeFlowButtons(message)
1178
+ : { messageParamsJson: '' }
1179
+ };
1180
+ // singleScreen drops the surrounding bubble so the widget is the whole message
1181
+ if (!message.singleScreen) {
1182
+ interactiveMessage.header = { hasMediaAttachment: false };
1183
+ interactiveMessage.body = { text: message.text || '' };
1184
+ interactiveMessage.footer = { text: message.footer || '' };
1185
+ }
1186
+ m = {
1187
+ interactiveMessage,
1188
+ messageContextInfo: {
1189
+ messageSecret: message.messageSecret || randomBytes(32)
1190
+ }
1191
+ };
1192
+ }
1120
1193
  else if (hasNonNullishProperty(message, 'nativeFlow')) {
1121
1194
  const interactiveMessage = {
1122
1195
  nativeFlowMessage: prepareNativeFlowButtons(message)
@@ -114,7 +114,10 @@ export const isRealMessage = (message) => {
114
114
  hasSomeContent &&
115
115
  !normalizedContent?.protocolMessage &&
116
116
  !normalizedContent?.reactionMessage &&
117
- !normalizedContent?.pollUpdateMessage);
117
+ !normalizedContent?.pollUpdateMessage &&
118
+ // edit envelopes and event responses are not new chat messages (unread/timestamp bump)
119
+ !normalizedContent?.secretEncryptedMessage &&
120
+ !normalizedContent?.encEventResponseMessage);
118
121
  };
119
122
  export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
120
123
  /**
@@ -236,7 +236,7 @@ export const prepareRichResponseMessage = (content) => {
236
236
  submessages.push({
237
237
  messageType: RichSubMessageType.LATEX,
238
238
  latexMetadata: {
239
- text,
239
+ text: contentText,
240
240
  expressions: latex
241
241
  }
242
242
  });
@@ -1,4 +1,4 @@
1
- export function useMultiFileAuthState(folder: any): Promise<{
1
+ export function useMultiFileAuthState(folder: any, logger?: any): Promise<{
2
2
  state: {
3
3
  creds: any;
4
4
  keys: {
@@ -8,5 +8,6 @@ export function useMultiFileAuthState(folder: any): Promise<{
8
8
  };
9
9
  saveCreds: () => void;
10
10
  _flushCreds: () => Promise<void>;
11
+ _destroy: () => void;
11
12
  }>;
12
13
  //# sourceMappingURL=use-multi-file-auth-state.d.ts.map
@@ -51,7 +51,11 @@ const makeDebouncedSaver = (filePath, getData, logger, delayMs = 500) => {
51
51
  clearTimeout(timer);
52
52
  try {
53
53
  writeFileSync(filePath, JSON.stringify(getData(), BufferJSON.replacer));
54
- } catch { }
54
+ } catch (err) {
55
+ // Last-chance save on the way out. Nothing left to retry with, but a silent
56
+ // failure here loses the session and looks like a random logout next boot.
57
+ logger?.error?.({ err, filePath }, 'Failed to save credentials on exit');
58
+ }
55
59
  }
56
60
  };
57
61
 
@@ -82,7 +86,7 @@ const makeDebouncedSaver = (filePath, getData, logger, delayMs = 500) => {
82
86
  return { save, flush: flushNow, destroy };
83
87
  };
84
88
 
85
- export const useMultiFileAuthState = async (folder) => {
89
+ export const useMultiFileAuthState = async (folder, logger) => {
86
90
  const writeData = async (data, file) => {
87
91
  const filePath = join(folder, fixFileName(file));
88
92
  const mutex = getFileLock(filePath);
@@ -107,7 +111,13 @@ export const useMultiFileAuthState = async (folder) => {
107
111
  release();
108
112
  }
109
113
  });
110
- } catch {
114
+ } catch (err) {
115
+ // A missing file is the normal "no such key yet" answer. Anything else — corrupt
116
+ // JSON, bad permissions — also yields null, which silently starts a fresh pairing,
117
+ // so it is logged rather than swallowed.
118
+ if (err?.code !== 'ENOENT') {
119
+ logger?.warn?.({ err, file }, 'Failed to read auth state file');
120
+ }
111
121
  return null;
112
122
  }
113
123
  };
@@ -119,7 +129,12 @@ export const useMultiFileAuthState = async (folder) => {
119
129
  return mutex.acquire().then(async (release) => {
120
130
  try {
121
131
  await unlink(filePath);
122
- } catch { }
132
+ } catch (err) {
133
+ // Deleting a key that was never written is normal.
134
+ if (err?.code !== 'ENOENT') {
135
+ logger?.warn?.({ err, file }, 'Failed to remove auth state file');
136
+ }
137
+ }
123
138
  finally {
124
139
  release();
125
140
  }
@@ -139,7 +154,7 @@ export const useMultiFileAuthState = async (folder) => {
139
154
  const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
140
155
  const creds = (await readData('creds.json')) || initAuthCreds();
141
156
  const credsPath = join(folder, 'creds.json');
142
- const debouncer = makeDebouncedSaver(credsPath, () => creds, null, 500);
157
+ const debouncer = makeDebouncedSaver(credsPath, () => creds, logger, 500);
143
158
 
144
159
  return {
145
160
  state: {
@@ -170,6 +185,12 @@ export const useMultiFileAuthState = async (folder) => {
170
185
  }
171
186
  },
172
187
  saveCreds: debouncer.save,
173
- _flushCreds: debouncer.flush
188
+ _flushCreds: debouncer.flush,
189
+ /**
190
+ * Drops the process exit/SIGINT/SIGTERM listeners this auth state installed. A long-lived
191
+ * host that opens and closes many sessions leaks one set per call without it, and Node
192
+ * starts warning about listener counts after eleven.
193
+ */
194
+ _destroy: debouncer.destroy
174
195
  };
175
196
  };
@@ -1,4 +1,4 @@
1
- export function useSingleFileAuthState(fileName: any): Promise<{
1
+ export function useSingleFileAuthState(fileName: any, logger?: any): Promise<{
2
2
  state: {
3
3
  creds: any;
4
4
  keys: {
@@ -8,7 +8,7 @@ import { Mutex } from 'async-mutex';
8
8
  // Lia@Changes 25-03-26 --- Add useSingleFileAuthState with integrated cache
9
9
  const FLUSH_TIMEOUT_MS = 3000;
10
10
  // Lia@Changes 22-04-26 --- Enhanced useSingleFileAuthState with LRUCache
11
- export const useSingleFileAuthState = async (fileName) => {
11
+ export const useSingleFileAuthState = async (fileName, logger) => {
12
12
  const cache = new LRUCache({
13
13
  max: 20000,
14
14
  ttl: 1000 * DEFAULT_CACHE_TTLS.SIGNAL_STORE,
@@ -32,7 +32,12 @@ export const useSingleFileAuthState = async (fileName) => {
32
32
  cache.set(keyName, value);
33
33
  }
34
34
  }
35
- catch {
35
+ catch (err) {
36
+ // A missing file is a fresh session. Corrupt JSON or bad permissions also land
37
+ // here and silently discard a working session, so they are logged.
38
+ if (err?.code !== 'ENOENT') {
39
+ logger?.warn?.({ err, fileName }, 'Failed to load auth state file, starting empty');
40
+ }
36
41
  fileData = {};
37
42
  }
38
43
  isLoaded = true;
@@ -49,7 +54,11 @@ export const useSingleFileAuthState = async (fileName) => {
49
54
  await writeFile(tempFile, JSON.stringify(fileData, BufferJSON.replacer));
50
55
  await rename(tempFile, fileName);
51
56
  }
52
- catch { }
57
+ catch (err) {
58
+ // Swallowing this lost every key written since the last successful flush with
59
+ // no trace, which surfaces later as a session that cannot decrypt.
60
+ logger?.error?.({ err, fileName }, 'Failed to flush auth state file');
61
+ }
53
62
  });
54
63
  }, FLUSH_TIMEOUT_MS);
55
64
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@violetix/baileys",
3
- "version": "0.3.18",
3
+ "version": "0.5.0",
4
4
  "description": "Enhanced Baileys v7 with fixes for newsletter media uploads, plus support for interactive messages, albums, and additional message types.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -67,7 +67,7 @@
67
67
  "better-sqlite3": "^11.0.0",
68
68
  "jimp": "^1.6.1",
69
69
  "link-preview-js": "^3.0.0",
70
- "sharp": "^0.35.5"
70
+ "sharp": ">=0.33.0"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "@napi-rs/image": {