@aunucore/baileys 1.0.78 → 1.0.79

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.
@@ -189,7 +189,15 @@ export const makeChatsSocket = (config) => {
189
189
  if (!jid) {
190
190
  throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update');
191
191
  }
192
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) {
192
+ // AunuBaileys (1.0.79, HB-125): creds.me is undefined until
193
+ // pairing completes (and again after a pairing rollback) —
194
+ // the bare deref below threw a raw TypeError for a host
195
+ // calling too early. Fail like the guarded send paths.
196
+ const meId = authState.creds.me?.id;
197
+ if (!meId) {
198
+ throw new Boom('Not authenticated');
199
+ }
200
+ if (jidNormalizedUser(jid) !== jidNormalizedUser(meId)) {
193
201
  targetJid = jidNormalizedUser(jid); // in case it is someone other than us
194
202
  }
195
203
  else {
@@ -219,7 +227,13 @@ export const makeChatsSocket = (config) => {
219
227
  if (!jid) {
220
228
  throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update');
221
229
  }
222
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) {
230
+ // AunuBaileys (1.0.79, HB-125): same bare creds.me.id deref
231
+ // as updateProfilePicture — same guard.
232
+ const meId = authState.creds.me?.id;
233
+ if (!meId) {
234
+ throw new Boom('Not authenticated');
235
+ }
236
+ if (jidNormalizedUser(jid) !== jidNormalizedUser(meId)) {
223
237
  targetJid = jidNormalizedUser(jid); // in case it is someone other than us
224
238
  }
225
239
  else {
@@ -529,6 +543,13 @@ export const makeChatsSocket = (config) => {
529
543
  };
530
544
  const sendPresenceUpdate = async (type, toJid) => {
531
545
  const me = authState.creds.me;
546
+ // AunuBaileys (1.0.79, HB-125): me is undefined until pairing
547
+ // completes; both branches below deref it bare (me.name in
548
+ // this one, me.lid/me.id in the chatstate branch) and threw
549
+ // a raw TypeError for an early host call.
550
+ if (!me?.id) {
551
+ throw new Boom('Not authenticated');
552
+ }
532
553
  if (type === 'available' || type === 'unavailable') {
533
554
  if (!me.name) {
534
555
  logger.warn('no name present, ignoring presence update request...');
@@ -851,7 +872,12 @@ export const makeChatsSocket = (config) => {
851
872
  }
852
873
  ev.emit('messages.upsert', { messages: [msg], type });
853
874
  if (!!msg.pushName) {
854
- let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid;
875
+ // AunuBaileys (1.0.79, HB-125): me?.id — a fromMe message
876
+ // with a pushName processed before pairing completes
877
+ // threw a raw TypeError here; jidNormalizedUser maps an
878
+ // undefined jid to '' and the fromMe path does not use
879
+ // the computed jid for anything but normalisation.
880
+ let jid = msg.key.fromMe ? authState.creds.me?.id : msg.key.participant || msg.key.remoteJid;
855
881
  jid = jidNormalizedUser(jid);
856
882
  if (!msg.key.fromMe) {
857
883
  ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }]);
@@ -246,6 +246,11 @@ export const makeCommunitiesSocket = (config) => {
246
246
  */
247
247
  communityAcceptInviteV4: ev.createBufferedFunction(async (key, inviteMessage) => {
248
248
  key = typeof key === 'string' ? { remoteJid: key } : key;
249
+ // AunuBaileys (1.0.79, HB-125): same bare creds.me.id
250
+ // deref as groupAcceptInviteV4 — same guard.
251
+ if (!authState.creds.me?.id) {
252
+ throw new Boom('Not authenticated');
253
+ }
249
254
  const results = await communityQuery(inviteMessage.groupJid, 'set', [
250
255
  {
251
256
  tag: 'accept',
@@ -207,6 +207,13 @@ export const makeGroupsSocket = (config) => {
207
207
  */
208
208
  groupAcceptInviteV4: ev.createBufferedFunction(async (key, inviteMessage) => {
209
209
  key = typeof key === 'string' ? { remoteJid: key } : key;
210
+ // AunuBaileys (1.0.79, HB-125): creds.me.id is
211
+ // dereferenced for the stub message below (and inside
212
+ // processMessage); before pairing completes it threw a
213
+ // raw TypeError. Fail before any network work.
214
+ if (!authState.creds.me?.id) {
215
+ throw new Boom('Not authenticated');
216
+ }
210
217
  const results = await groupQuery(inviteMessage.groupJid, 'set', [
211
218
  {
212
219
  tag: 'accept',
@@ -12,6 +12,10 @@ import { createOfflineNodeQueue } from '../Utils/offline-queue.js';
12
12
  import { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
13
13
  import { extractGroupMetadata } from './groups.js';
14
14
  import { makeMessagesSocket } from './messages-send.js';
15
+ /** AunuBaileys (1.0.79, HB-201): the HB-98 key-material test — key
16
+ * material is a Buffer (or a base64 string in a hand-edited creds
17
+ * file); anything else is a wrong-typed corrupt value. */
18
+ const isKeyMaterial = (value) => Buffer.isBuffer(value) || typeof value === 'string';
15
19
  /** AunuBaileys: classify an <unavailable/> placeholder — 'bot' when a
16
20
  * <bot/> child rides along, 'hosted' when the node says so, otherwise
17
21
  * the unavailable type attribute (e.g. 'view_once'). */
@@ -82,6 +86,13 @@ export const makeMessagesRecvSocket = (config) => {
82
86
  // AunuBaileys: terima panggilan masuk (node kontrol <accept>) — pasangan
83
87
  // rejectCall bawaan; media VOIP-nya sendiri di luar cakupan protokol chat.
84
88
  const acceptCall = async (callId, callFrom) => {
89
+ // AunuBaileys (1.0.79, HB-172): creds.me.id was dereferenced
90
+ // bare in the stanza literal — a host auto-accepting a call
91
+ // before pairing completes got a raw TypeError instead of
92
+ // the guarded failure the neighbouring helpers give.
93
+ if (!authState.creds.me?.id) {
94
+ throw new Boom('Not authenticated');
95
+ }
85
96
  await query(buildCallControlStanza({
86
97
  from: authState.creds.me.id,
87
98
  to: callFrom,
@@ -92,6 +103,11 @@ export const makeMessagesRecvSocket = (config) => {
92
103
  };
93
104
  // AunuBaileys: akhiri panggilan yang sedang berjalan (<terminate>).
94
105
  const terminateCall = async (callId, callFrom, reason = 'user-terminated') => {
106
+ // AunuBaileys (1.0.79, HB-172): same bare creds.me.id deref
107
+ // as acceptCall — guard first.
108
+ if (!authState.creds.me?.id) {
109
+ throw new Boom('Not authenticated');
110
+ }
95
111
  await query(buildCallControlStanza({
96
112
  from: authState.creds.me.id,
97
113
  to: callFrom,
@@ -102,6 +118,11 @@ export const makeMessagesRecvSocket = (config) => {
102
118
  }));
103
119
  };
104
120
  const rejectCall = async (callId, callFrom) => {
121
+ // AunuBaileys (1.0.79, HB-172): same bare creds.me.id deref
122
+ // as acceptCall — guard first.
123
+ if (!authState.creds.me?.id) {
124
+ throw new Boom('Not authenticated');
125
+ }
105
126
  const stanza = {
106
127
  tag: 'call',
107
128
  attrs: {
@@ -144,9 +165,28 @@ export const makeMessagesRecvSocket = (config) => {
144
165
  logger.debug({ retryCount, msgId }, 'reached retry limit');
145
166
  return;
146
167
  }
168
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
169
+ // AunuBaileys (1.0.79, HB-201 + addendum HB-80): the receipt
170
+ // built below always embeds creds.registrationId, and from
171
+ // the second retry on it also embeds the identity and
172
+ // signed-pre-key material — all of it dereferenced bare
173
+ // inside the transaction callback. With a partial creds
174
+ // restore those derefs threw TypeError AFTER the retry
175
+ // counter above had already been consumed: the receipt was
176
+ // never sent, the counter was burned, and every later retry
177
+ // failed at the same spot until the cap left the message a
178
+ // stub forever. (A missing registrationId was worse — it
179
+ // silently encoded as 00000000.) Validate BEFORE the
180
+ // increment and fail descriptively: the counter is only
181
+ // spent on a receipt that can actually be built.
182
+ if (!Number.isFinite(authState.creds.registrationId)) {
183
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — cannot send a retry receipt; restore a full backup or pair again');
184
+ }
185
+ if ((retryCount >= 1 || forceIncludeKeys) && (!isKeyMaterial(identityKey?.public) || !isKeyMaterial(signedPreKey?.keyPair?.public) || !isKeyMaterial(signedPreKey?.signature) || typeof signedPreKey?.keyId === 'undefined')) {
186
+ throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — cannot include keys in the retry receipt; restore a full backup or pair again');
187
+ }
147
188
  retryCount += 1;
148
189
  msgRetryCache.set(key, retryCount);
149
- const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
150
190
  if (retryCount === 1) {
151
191
  // AunuBaileys (1.0.70): requestPlaceholderResend waits 5s
152
192
  // for the message to arrive by other means — awaiting it
@@ -206,7 +246,7 @@ export const makeMessagesRecvSocket = (config) => {
206
246
  attrs: {},
207
247
  content: [
208
248
  { tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
209
- { tag: 'identity', attrs: {}, content: identityKey.public },
249
+ { tag: 'identity', attrs: {}, content: keyMaterialToBuffer(identityKey.public) },
210
250
  xmppPreKey(key, +keyId),
211
251
  xmppSignedPreKey(signedPreKey),
212
252
  { tag: 'device-identity', attrs: {}, content: deviceIdentity }
@@ -1042,7 +1082,14 @@ export const makeMessagesRecvSocket = (config) => {
1042
1082
  // vanished silently if the retry also failed
1043
1083
  // (HB-90; upstream v7 falls through to the
1044
1084
  // upsert for exactly this reason).
1045
- cleanMessage(msg, authState.creds.me.id);
1085
+ // AunuBaileys (1.0.79, HB-131): me?.id — in
1086
+ // the pre-success window creds.me is still
1087
+ // undefined and the bare deref threw AFTER the
1088
+ // retry receipt had already been fired: the
1089
+ // stub was never upserted and the stanza was
1090
+ // NACKed 500 (delivered + failed). cleanMessage
1091
+ // tolerates an undefined meId.
1092
+ cleanMessage(msg, authState.creds.me?.id);
1046
1093
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1047
1094
  }
1048
1095
  else {
@@ -1080,7 +1127,12 @@ export const makeMessagesRecvSocket = (config) => {
1080
1127
  await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
1081
1128
  }
1082
1129
  }
1083
- cleanMessage(msg, authState.creds.me.id);
1130
+ // AunuBaileys (1.0.79, HB-131): me?.id — the
1131
+ // bare deref threw here AFTER sendReceipt had
1132
+ // already gone out, NACKing a delivered message
1133
+ // 500 and bursting redeliveries until CB:success
1134
+ // landed. cleanMessage tolerates undefined.
1135
+ cleanMessage(msg, authState.creds.me?.id);
1084
1136
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1085
1137
  })
1086
1138
  ]);
@@ -96,8 +96,16 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
96
96
  // commit attempt. Fall back per-field to the Defaults values, and
97
97
  // clamp retries to at least 1 so an explicit 0 means exactly one
98
98
  // attempt, matching host intent.
99
- const commitRetries = Math.max(1, maxCommitRetries ?? 10);
100
- const commitDelayMs = delayBetweenTriesMs ?? 3000;
99
+ // AunuBaileys (1.0.79, addendum HB-104): the ?? fallback lets NaN
100
+ // through (Math.max(1, NaN) is NaN — the commit loop below then
101
+ // runs ZERO times and every mutating transaction throws without
102
+ // a single attempt) and lets Infinity through (the loop never
103
+ // ends on a failing store and wedges the transaction mutex —
104
+ // the whole key store deadlocks until restart). Sanitise at the
105
+ // boundary: retries must be a finite integer, clamped to 1..10;
106
+ // a non-finite or negative delay falls back to the default.
107
+ const commitRetries = Number.isFinite(maxCommitRetries) ? Math.min(10, Math.max(1, Math.floor(maxCommitRetries))) : 10;
108
+ const commitDelayMs = Number.isFinite(delayBetweenTriesMs) && delayBetweenTriesMs >= 0 ? delayBetweenTriesMs : 3000;
101
109
  // number of queries made to the DB during the transaction
102
110
  // only there for logging purposes
103
111
  let dbQueriesInTransaction = 0;
@@ -114,6 +122,20 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
114
122
  const transactionContext = new AsyncLocalStorage();
115
123
  const runTransaction = async (work) => {
116
124
  let result;
125
+ // AunuBaileys (1.0.79, HB-236): a nested transaction joins the
126
+ // running one and SHARES its cache/mutations — snapshot both
127
+ // when entering a nested frame. If the nested work throws,
128
+ // the snapshot is restored before the error propagates (see
129
+ // the catch below): without it, an outer frame that catches
130
+ // the throw would commit the inner frame's partial writes as
131
+ // if nothing had failed — the only "rollback" was a throw
132
+ // escaping the OUTERMOST frame.
133
+ const snapshot = transactionsInProgress > 0
134
+ ? {
135
+ cache: Object.fromEntries(Object.entries(transactionCache).map(([type, dict]) => [type, { ...dict }])),
136
+ mutations: Object.fromEntries(Object.entries(mutations).map(([type, dict]) => [type, { ...dict }]))
137
+ }
138
+ : undefined;
117
139
  transactionsInProgress += 1;
118
140
  if (transactionsInProgress === 1) {
119
141
  logger.trace('entering transaction');
@@ -159,6 +181,17 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
159
181
  }
160
182
  }
161
183
  }
184
+ catch (error) {
185
+ // AunuBaileys (1.0.79, HB-236): roll a failed NESTED frame
186
+ // back to its entry snapshot (see above); the outermost
187
+ // frame needs no restore — the finally below resets the
188
+ // shared state outright.
189
+ if (snapshot) {
190
+ transactionCache = snapshot.cache;
191
+ mutations = snapshot.mutations;
192
+ }
193
+ throw error;
194
+ }
162
195
  finally {
163
196
  transactionsInProgress -= 1;
164
197
  if (transactionsInProgress === 0) {
@@ -173,7 +206,14 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
173
206
  get: async (type, ids) => {
174
207
  if (isInTransaction()) {
175
208
  const dict = transactionCache[type];
176
- const idsRequiringFetch = dict ? ids.filter(item => typeof dict[item] === 'undefined') : ids;
209
+ // AunuBaileys (1.0.79, HB-233): cache PRESENCE — not
210
+ // the value — decides whether a fetch is needed. A
211
+ // key explicitly set to undefined in this transaction
212
+ // is a deletion marker; the typeof test could not
213
+ // tell it from "never fetched", so get re-read the
214
+ // stale persisted value and resurrected a record the
215
+ // transaction itself had just deleted.
216
+ const idsRequiringFetch = dict ? ids.filter(item => !Object.prototype.hasOwnProperty.call(dict, item)) : ids;
177
217
  // only fetch if there are any items to fetch
178
218
  if (idsRequiringFetch.length) {
179
219
  dbQueriesInTransaction += 1;
@@ -2,7 +2,7 @@ import { Boom } from '@hapi/boom';
2
2
  import { proto } from '../../WAProto/index.js';
3
3
  import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults/index.js';
4
4
  import { decodeBinaryNode } from '../WABinary/index.js';
5
- import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto.js';
5
+ import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, keyMaterialToBuffer, sha256 } from './crypto.js';
6
6
  const generateIV = (counter) => {
7
7
  const iv = new ArrayBuffer(12);
8
8
  new DataView(iv).setUint32(8, counter);
@@ -94,6 +94,30 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
94
94
  if (!noiseKey?.public || !noiseKey?.private) {
95
95
  throw new Error('AunuBaileys: creds.noiseKey is missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
96
96
  }
97
+ // AunuBaileys (1.0.79, addendum HB-102): the truthiness
98
+ // check above lets a WRONG-TYPED noiseKey through — a
99
+ // number/object died as a bare TypeError inside
100
+ // aesEncryptGCM, and a base64 string was silently
101
+ // encrypted as UTF-8 bytes, so the server received a
102
+ // garbage public key and the handshake failed remotely
103
+ // with no local diagnosis. Normalise with
104
+ // keyMaterialToBuffer (the HB-157 house rule) and
105
+ // require real 32-byte keys.
106
+ const noisePublic = keyMaterialToBuffer(noiseKey.public);
107
+ const noisePrivate = keyMaterialToBuffer(noiseKey.private);
108
+ if (!Buffer.isBuffer(noisePublic) || noisePublic.length !== 32 || !Buffer.isBuffer(noisePrivate) || noisePrivate.length !== 32) {
109
+ throw new Error('AunuBaileys: creds.noiseKey is malformed (public/private must be 32-byte keys) — the auth state is incomplete or corrupt; restore a full backup or pair again');
110
+ }
111
+ // AunuBaileys (1.0.79, HB-238): the first handshake frame
112
+ // is plaintext, so an intercepting middlebox (ISP
113
+ // intercept page, broken proxy) can deliver a decoded
114
+ // handshake with NO serverHello at all — the derefs
115
+ // below then threw a bare TypeError and the host
116
+ // reconnect-looped on it. Fail descriptively, in the
117
+ // HB-80/HB-102 style.
118
+ if (!serverHello || typeof serverHello !== 'object' || !(serverHello.ephemeral instanceof Uint8Array) || !(serverHello.static instanceof Uint8Array) || !(serverHello.payload instanceof Uint8Array)) {
119
+ throw new Error('AunuBaileys: the server handshake is malformed — serverHello or its encrypted parts are missing (the connection may have been intercepted); reconnecting will retry');
120
+ }
97
121
  authenticate(serverHello.ephemeral);
98
122
  await mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral));
99
123
  const decStaticContent = decrypt(serverHello.static);
@@ -104,8 +128,8 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
104
128
  if (issuerSerial !== WA_CERT_DETAILS.SERIAL) {
105
129
  throw new Boom('certification match failed', { statusCode: 400 });
106
130
  }
107
- const keyEnc = encrypt(noiseKey.public);
108
- await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral));
131
+ const keyEnc = encrypt(noisePublic);
132
+ await mixIntoKey(Curve.sharedKey(noisePrivate, serverHello.ephemeral));
109
133
  return keyEnc;
110
134
  },
111
135
  encodeFrame: (data) => {
@@ -100,7 +100,13 @@ export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsg
100
100
  }
101
101
  }
102
102
  const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, keyStore, logger, options }) => {
103
- const meId = creds.me.id;
103
+ // AunuBaileys (1.0.79, HB-131): creds.me is undefined until
104
+ // pairing completes; the bare deref threw a TypeError for any
105
+ // message/notification processed in that window (reached via
106
+ // upsertMessage, which does not pass cleanMessage first). Every
107
+ // consumer of meId below (isRealMessage, areJidsSameUser,
108
+ // jidNormalizedUser) already tolerates undefined.
109
+ const meId = creds.me?.id;
104
110
  const { accountSettings } = creds;
105
111
  const chat = { id: jidNormalizedUser(getChatId(message.key)) };
106
112
  const isRealMsg = isRealMessage(message, meId);
@@ -231,6 +231,12 @@ export const getNextPreKeysNode = async (state, count) => {
231
231
  if (!isKeyMaterial(creds.signedIdentityKey?.public) || !isKeyMaterial(creds.signedPreKey?.keyPair?.public) || !isKeyMaterial(creds.signedPreKey?.signature) || typeof creds.signedPreKey?.keyId === 'undefined') {
232
232
  throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
233
233
  }
234
+ // AunuBaileys (1.0.79, addendum HB-80): same gap as
235
+ // generateRegistrationNode — a missing registrationId silently
236
+ // became registration 00000000 in the node below.
237
+ if (!Number.isFinite(creds.registrationId)) {
238
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — the auth state is incomplete or corrupt; restore a full backup or pair again');
239
+ }
234
240
  const { update, preKeys } = await getNextPreKeys(state, count);
235
241
  const node = {
236
242
  tag: 'iq',
@@ -36,6 +36,21 @@ const getFileLock = (path) => {
36
36
  * Again, I wouldn't endorse this for any production level use other than perhaps a bot.
37
37
  * Would recommend writing an auth state for use with a proper SQL or No-SQL DB
38
38
  * */
39
+ // AunuBaileys (1.0.79, HB-151): the shape a complete creds state
40
+ // must have at load. Key material follows the HB-98 test (a Buffer,
41
+ // or a base64 string in a hand-edited file); the ids must be finite
42
+ // numbers. Anything else is a partial/corrupt restore, not a state
43
+ // the socket can run on.
44
+ const isCredsKeyPair = (kp) => !!kp && typeof kp === 'object' && (Buffer.isBuffer(kp.private) || typeof kp.private === 'string') && (Buffer.isBuffer(kp.public) || typeof kp.public === 'string');
45
+ const isValidCredsShape = (creds) => !!creds &&
46
+ typeof creds === 'object' &&
47
+ !Array.isArray(creds) &&
48
+ isCredsKeyPair(creds.noiseKey) &&
49
+ isCredsKeyPair(creds.signedIdentityKey) &&
50
+ isCredsKeyPair(creds.signedPreKey?.keyPair) &&
51
+ (Buffer.isBuffer(creds.signedPreKey?.signature) || typeof creds.signedPreKey?.signature === 'string') &&
52
+ Number.isFinite(creds.signedPreKey?.keyId) &&
53
+ Number.isFinite(creds.registrationId);
39
54
  export const useMultiFileAuthState = async (folder, logger) => {
40
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
56
  const writeData = async (data, file) => {
@@ -118,7 +133,21 @@ export const useMultiFileAuthState = async (folder, logger) => {
118
133
  await mkdir(folder, { recursive: true });
119
134
  }
120
135
  const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
121
- const creds = (await readData('creds.json')) || initAuthCreds();
136
+ const storedCreds = await readData('creds.json');
137
+ // AunuBaileys (1.0.79, HB-151): a creds.json that parses to {} /
138
+ // a partial object / a non-object used to be accepted as a
139
+ // complete auth state (it is truthy), and every downstream
140
+ // consumer then failed in its own way — reconnect loops with
141
+ // misleading TypeErrors, indistinguishable from a lib bug.
142
+ // Validate at this one load choke point; a present-but-invalid
143
+ // creds is discarded loudly, exactly like a corrupt-JSON file
144
+ // since 1.0.63.
145
+ const creds = isValidCredsShape(storedCreds) ? storedCreds : (() => {
146
+ if (storedCreds) {
147
+ logger?.error({ file: 'creds.json' }, 'aunu: creds.json does not have the shape of a complete auth state — discarding it and starting fresh; restore a full backup or pair again');
148
+ }
149
+ return initAuthCreds();
150
+ })();
122
151
  let credsWriteInFlight = null;
123
152
  let credsWriteQueued = false;
124
153
  return {
@@ -60,7 +60,21 @@ export const generateLoginNode = (userJid, config) => {
60
60
  if (!decoded) {
61
61
  throw new Boom(`Invalid own JID for login node: ${userJid}`, { statusCode: 400 });
62
62
  }
63
- const { user, device } = decoded;
63
+ const { user, device, server } = decoded;
64
+ // AunuBaileys (1.0.79, HB-234): the HB-43 guard above only
65
+ // checks that the JID decodes — a non-numeric user
66
+ // ('abc@s.whatsapp.net') was then silently coerced by +user to
67
+ // username 0 (or clamped to int64-max on a 26-digit overflow)
68
+ // and the login node went out carrying a WRONG identity,
69
+ // looping on server rejects. Validate the parts: the user must
70
+ // be all digits, non-zero and within int64 range; the server
71
+ // must be the user server; and a present device must be a
72
+ // finite number (jidDecode's +device turns 'xx' into NaN,
73
+ // which protobuf then silently encoded as 0).
74
+ const userValue = /^\d+$/.test(user) ? BigInt(user) : 0n;
75
+ if (userValue <= 0n || userValue > 9223372036854775807n || server !== 's.whatsapp.net' || (typeof device !== 'undefined' && !Number.isFinite(device))) {
76
+ throw new Boom(`Invalid own JID for login node: ${userJid}`, { statusCode: 400 });
77
+ }
64
78
  const payload = {
65
79
  ...getClientPayload(config),
66
80
  passive: false,
@@ -83,6 +97,14 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
83
97
  if (!isKeyMaterial(signedIdentityKey?.public) || !isKeyMaterial(signedPreKey?.keyPair?.public) || typeof signedPreKey?.keyId === 'undefined' || !isKeyMaterial(signedPreKey?.signature)) {
84
98
  throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
85
99
  }
100
+ // AunuBaileys (1.0.79, addendum HB-80): registrationId was NOT
101
+ // part of the guard — encodeBigEndian(undefined) does not
102
+ // throw, it silently produces eRegid 00000000, so a partial
103
+ // creds restore registered with a bogus registration id and
104
+ // no local signal at all.
105
+ if (!Number.isFinite(registrationId)) {
106
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — the auth state is incomplete or corrupt; restore a full backup or pair again');
107
+ }
86
108
  // the app version needs to be md5 hashed
87
109
  // and passed in
88
110
  const safeVersion = Array.isArray(config.version) && config.version.length ? config.version : [2, 3000, 1043857760];
@@ -128,6 +150,14 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
128
150
  };
129
151
  export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentityKey, signalIdentities }) => {
130
152
  const msgId = stanza.attrs.id;
153
+ // AunuBaileys (1.0.79, HB-80 sub-instance): the creds fields are
154
+ // dereferenced bare below (Buffer.from(advSecretKey, 'base64'),
155
+ // the identity concat/sign) — a partial creds died as a bare
156
+ // TypeError inside the pair-success handler. Fail descriptively,
157
+ // like the registration guards.
158
+ if (typeof advSecretKey !== 'string' || !advSecretKey || !isKeyMaterial(signedIdentityKey?.public) || !isKeyMaterial(signedIdentityKey?.private)) {
159
+ throw new Error('AunuBaileys: creds.advSecretKey / creds.signedIdentityKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
160
+ }
131
161
  const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success');
132
162
  const deviceIdentityNode = getBinaryNodeChild(pairSuccessNode, 'device-identity');
133
163
  const platformNode = getBinaryNodeChild(pairSuccessNode, 'platform');
@@ -138,6 +168,19 @@ export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentit
138
168
  }
139
169
  const bizName = businessNode?.attrs.name;
140
170
  const jid = deviceNode.attrs.jid;
171
+ // AunuBaileys (1.0.79, HB-239): a pair-success stanza without a
172
+ // device jid used to store me.id = undefined SILENTLY — pairing
173
+ // "succeeded", and the NEXT boot took the login path and looped
174
+ // on the HB-43 Boom 400 forever. Reject the stanza here, where
175
+ // the defect is. The addendum: a device-identity node without
176
+ // content died as protobufjs' bare 'Error: illegal buffer' —
177
+ // same missing-guard class, same fix.
178
+ if (typeof jid !== 'string' || !jid) {
179
+ throw new Boom('Missing jid on the device node in pair success stanza', { data: stanza });
180
+ }
181
+ if (!deviceIdentityNode.content?.length) {
182
+ throw new Boom('Missing content on the device-identity node in pair success stanza', { data: stanza });
183
+ }
141
184
  const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content);
142
185
  const isHostedAccount = accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED;
143
186
  const hmacPrefix = isHostedAccount ? Buffer.from([6, 5]) : Buffer.alloc(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.78",
4
+ "version": "1.0.79",
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",