@pontalabs/baileys 1.0.1 → 1.0.3

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.
@@ -43,7 +43,7 @@ const sender_key_record_1 = require("./Group/sender-key-record");
43
43
  const Group_1 = require("./Group");
44
44
  const LIDMappingStore_1 = require("./lid-mapping")
45
45
 
46
- // FIX: sebelumnya pontasockets gak punya deteksi perubahan identity key sama
46
+ // FIX: sebelumnya pontalabs gak punya deteksi perubahan identity key sama
47
47
  // sekali. Kalau identity kontak berubah (kontak install ulang WA, atau kena
48
48
  // resync multi-device pas ada linked device lain online), sesi lama tetap
49
49
  // dipakai terus dan selalu gagal decrypt "Bad MAC" berulang-ulang sampai
@@ -1,5 +1,5 @@
1
1
  "use strict"
2
- // 18 7 2026
2
+
3
3
  var __importDefault = (this && this.__importDefault) || function(mod) {
4
4
  return (mod && mod.__esModule) ? mod : {
5
5
  "default": mod
@@ -59,6 +59,7 @@ const makeMessagesSocket = (config) => {
59
59
 
60
60
  const messageRetryManager = enableRecentMessageCache ? new Utils_1.MessageRetryManager(logger, maxMsgRetryCount) : null
61
61
  const encryptionMutex = make_keyed_mutex_1.makeKeyedMutex()
62
+ const devicesMutex = make_keyed_mutex_1.makeMutex()
62
63
  let mediaConn
63
64
 
64
65
  const refreshMediaConn = async (forceGet = false) => {
@@ -295,6 +296,24 @@ const makeMessagesSocket = (config) => {
295
296
 
296
297
  const result = await executeUSyncQuery(query)
297
298
  if (result) {
299
+ // FIX: pontalabs sebelumnya gak pernah nyimpen mapping LID<->PN
300
+ // dari hasil USync device query ini (nuiisweety nyimpen + langsung
301
+ // force-refresh session buat LID yang baru ketemu). Tanpa ini,
302
+ // pengiriman ke kontak yang butuh LID addressing bisa tetep pakai
303
+ // address PN yang salah/basi -> penerima gak bisa decrypt balasan
304
+ // bot ("Menunggu pesan ini...").
305
+ const lidResults = (result.list || []).filter(item => !!item.lid)
306
+ if (lidResults.length > 0) {
307
+ try {
308
+ await signalRepository.lidMapping.storeLIDPNMappings(
309
+ lidResults.map(item => ({ lid: item.lid, pn: item.id }))
310
+ )
311
+ const lids = lidResults.map(item => item.lid)
312
+ await assertSessions(lids, true)
313
+ } catch (error) {
314
+ logger.warn({ error, count: lidResults.length }, 'failed to store/refresh LID mappings from device sync')
315
+ }
316
+ }
298
317
  const extracted = Utils_1.extractDeviceJids(result?.list, authState.creds.me.id, ignoreZeroDevices)
299
318
  const deviceMap = {}
300
319
  for (const item of extracted) {
@@ -318,16 +337,18 @@ const makeMessagesSocket = (config) => {
318
337
  }
319
338
  }
320
339
 
321
- if (userDevicesCache.mset) {
322
- await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({
323
- key,
324
- value
325
- })))
326
- } else {
327
- for (const key in deviceMap) {
328
- if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
340
+ await devicesMutex.mutex(async () => {
341
+ if (userDevicesCache.mset) {
342
+ await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({
343
+ key,
344
+ value
345
+ })))
346
+ } else {
347
+ for (const key in deviceMap) {
348
+ if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
349
+ }
329
350
  }
330
- }
351
+ })
331
352
  }
332
353
  return deviceResults
333
354
  }
@@ -339,11 +360,17 @@ const makeMessagesSocket = (config) => {
339
360
  if (force) {
340
361
  jidsRequiringFetch = jids;
341
362
  } else {
342
- const addrs = jids.map(jid => (signalRepository.jidToSignalProtocolAddress(jid)));
343
- const sessions = await authState.keys.get('session', addrs);
363
+ // FIX: sebelumnya cuma ngecek "ada record tersimpan atau enggak"
364
+ // (truthy check di authState.keys.get('session', ...)). Record
365
+ // yang kosong/stale/rusak (misal device grup yang session-nya
366
+ // kebentuk tapi gak pernah kepakai/keisi ratchet) tetep truthy,
367
+ // jadi dianggap "udah punya sesi" padahal libsignal-nya beneran
368
+ // kosong -> pas encrypt muncul "SessionError: No sessions".
369
+ // validateSession ngecek session-nya beneran ada & "open" enggak,
370
+ // bukan cuma record-nya ada.
344
371
  for (const jid of jids) {
345
- const signalId = signalRepository.jidToSignalProtocolAddress(jid);
346
- if (!sessions[signalId]) {
372
+ const sessionValidation = await signalRepository.validateSession(jid);
373
+ if (!sessionValidation.exists) {
347
374
  jidsRequiringFetch.push(jid);
348
375
  }
349
376
  }
@@ -1135,6 +1162,14 @@ const makeMessagesSocket = (config) => {
1135
1162
  return message
1136
1163
  },
1137
1164
  sendStatusMentions: async (content, jids = []) => {
1165
+ const MAX_STATUS_MENTIONS = 5
1166
+ if (jids.length > MAX_STATUS_MENTIONS) {
1167
+ logger.warn(
1168
+ { requested: jids.length, max: MAX_STATUS_MENTIONS },
1169
+ `sendStatusMentions: max ${MAX_STATUS_MENTIONS} mentions per status, truncating`
1170
+ )
1171
+ jids = jids.slice(0, MAX_STATUS_MENTIONS)
1172
+ }
1138
1173
  const userJid = WABinary_1.jidNormalizedUser(authState.creds.me.id)
1139
1174
  let allUsers = new Set()
1140
1175
  allUsers.add(userJid)
@@ -81,92 +81,96 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
81
81
  }
82
82
  }
83
83
 
84
- // Module-level specialized mutexes for pre-key operations
85
- const preKeyMutex = new mutex_1.Mutex()
86
- const signedPreKeyMutex = new mutex_1.Mutex()
87
-
88
- /**
89
- * Get the appropriate mutex for the key type
90
- */
91
- const getPreKeyMutex = (keyType) => {
92
- return keyType === 'signed-pre-key' ? signedPreKeyMutex : preKeyMutex
93
- }
94
-
95
84
  /**
96
- * Handles pre-key operations with mutex protection
85
+ * Handles pre-key operations.
86
+ *
87
+ * IMPORTANT: locking for this function is provided by the CALLER via the
88
+ * session-scoped `getKeyTypeMutex(keyType)` mutex (the same one used by
89
+ * get()/set() for that key type). We intentionally do NOT acquire our own
90
+ * mutex here anymore.
91
+ *
92
+ * Previously this used module-level singleton mutexes (`preKeyMutex`,
93
+ * `signedPreKeyMutex`) declared OUTSIDE `addTransactionCapability`. Because
94
+ * those were created once per process (not once per auth state / socket),
95
+ * every session running in the same Node process shared the exact same
96
+ * lock. That caused two problems:
97
+ * 1. Unrelated sessions serialized on each other's pre-key writes
98
+ * (needless contention/latency when running multiple accounts).
99
+ * 2. The lock domain didn't match the one used by get()/withMutexes()
100
+ * (`getKeyTypeMutex`), so reads/writes and deletion-validation could
101
+ * interleave without real mutual exclusion (TOCTOU race on the
102
+ * "does this key still exist" check), which is what let pre-key /
103
+ * signed-pre-key state get corrupted under concurrent access.
97
104
  */
98
105
  async function handlePreKeyOperations(data, keyType, transactionCache, mutations, logger, isInTransaction, state) {
99
- const mutex = getPreKeyMutex(keyType)
100
- await mutex.runExclusive(async () => {
101
- const keyData = data[keyType]
102
- if (!keyData)
103
- return
104
-
105
- // Ensure structures exist
106
- transactionCache[keyType] = transactionCache[keyType] || {}
107
- mutations[keyType] = mutations[keyType] || {}
108
-
109
- // Separate deletions from updates for batch processing
110
- const deletionKeys = []
111
- const updateKeys = []
112
-
113
- for (const keyId in keyData) {
114
- if (keyData[keyId] === null) {
115
- deletionKeys.push(keyId)
116
- }
117
- else {
118
- updateKeys.push(keyId)
119
- }
106
+ const keyData = data[keyType]
107
+ if (!keyData)
108
+ return
109
+
110
+ // Ensure structures exist
111
+ transactionCache[keyType] = transactionCache[keyType] || {}
112
+ mutations[keyType] = mutations[keyType] || {}
113
+
114
+ // Separate deletions from updates for batch processing
115
+ const deletionKeys = []
116
+ const updateKeys = []
117
+
118
+ for (const keyId in keyData) {
119
+ if (keyData[keyId] === null) {
120
+ deletionKeys.push(keyId)
120
121
  }
121
-
122
- // Process updates first (no validation needed)
123
- for (const keyId of updateKeys) {
124
- if (transactionCache[keyType]) {
125
- transactionCache[keyType][keyId] = keyData[keyId]
126
- }
127
- if (mutations[keyType]) {
128
- mutations[keyType][keyId] = keyData[keyId]
129
- }
122
+ else {
123
+ updateKeys.push(keyId)
130
124
  }
131
-
132
- // Process deletions with validation
133
- if (deletionKeys.length === 0)
134
- return
135
-
136
- if (isInTransaction) {
137
- // In transaction, only allow deletion if key exists in cache
138
- for (const keyId of deletionKeys) {
139
- if (transactionCache[keyType]) {
140
- transactionCache[keyType][keyId] = null
141
- if (mutations[keyType]) {
142
- // Mark for deletion in mutations
143
- mutations[keyType][keyId] = null
144
- }
145
- }
146
- else {
147
- logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
148
- }
149
- }
150
- return
125
+ }
126
+
127
+ // Process updates first (no validation needed)
128
+ for (const keyId of updateKeys) {
129
+ if (transactionCache[keyType]) {
130
+ transactionCache[keyType][keyId] = keyData[keyId]
151
131
  }
152
-
153
- // Outside transaction, batch validate all deletions
154
- if (!state)
155
- return
156
-
157
- const existingKeys = await state.get(keyType, deletionKeys)
132
+ if (mutations[keyType]) {
133
+ mutations[keyType][keyId] = keyData[keyId]
134
+ }
135
+ }
136
+
137
+ // Process deletions with validation
138
+ if (deletionKeys.length === 0)
139
+ return
140
+
141
+ if (isInTransaction) {
142
+ // In transaction, only allow deletion if key exists in cache
158
143
  for (const keyId of deletionKeys) {
159
- if (existingKeys[keyId]) {
160
- if (transactionCache[keyType])
161
- transactionCache[keyType][keyId] = null
162
- if (mutations[keyType])
144
+ if (transactionCache[keyType]) {
145
+ transactionCache[keyType][keyId] = null
146
+ if (mutations[keyType]) {
147
+ // Mark for deletion in mutations
163
148
  mutations[keyType][keyId] = null
149
+ }
164
150
  }
165
151
  else {
166
- logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
152
+ logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
167
153
  }
168
154
  }
169
- })
155
+ return
156
+ }
157
+
158
+ // Outside transaction, batch validate all deletions
159
+ if (!state)
160
+ return
161
+
162
+ const existingKeys = await state.get(keyType, deletionKeys)
163
+ for (const keyId of deletionKeys) {
164
+ if (existingKeys[keyId]) {
165
+ if (transactionCache[keyType])
166
+ transactionCache[keyType][keyId] = null
167
+ if (mutations[keyType])
168
+ mutations[keyType][keyId] = null
169
+ }
170
+ else {
171
+ logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
172
+ }
173
+ }
170
174
  }
171
175
 
172
176
  /**
@@ -179,27 +183,34 @@ function handleNormalKeyOperations(data, key, transactionCache, mutations) {
179
183
  }
180
184
 
181
185
  /**
182
- * Process pre-key deletions with validation
186
+ * Process pre-key / signed-pre-key deletions with validation.
187
+ *
188
+ * NOTE: this is only ever called from inside a `withMutexes([..., keyType, ...])`
189
+ * block in `set()` below, which already holds the session-scoped
190
+ * `getKeyTypeMutex(keyType)` lock for the duration of the call. It must NOT
191
+ * acquire its own mutex here - `async-mutex`'s Mutex is not reentrant, so
192
+ * doing so would either deadlock (if it tried to re-acquire the same lock)
193
+ * or - as in the previous version - silently acquire a *different*, global
194
+ * lock that gave no real protection at all.
183
195
  */
184
196
  async function processPreKeyDeletions(data, keyType, state, logger) {
185
- const mutex = getPreKeyMutex(keyType)
186
- await mutex.runExclusive(async () => {
187
- const keyData = data[keyType]
188
- if (!keyData)
189
- return
190
-
191
- // Validate deletions
192
- for (const keyId in keyData) {
193
- if (keyData[keyId] === null) {
194
- const existingKeys = await state.get(keyType, [keyId])
195
- if (!existingKeys[keyId]) {
196
- logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
197
- if (data[keyType])
198
- delete data[keyType][keyId]
199
- }
200
- }
197
+ const keyData = data[keyType]
198
+ if (!keyData)
199
+ return
200
+
201
+ // Validate deletions
202
+ const deletionIds = Object.keys(keyData).filter(id => keyData[id] === null)
203
+ if (deletionIds.length === 0)
204
+ return
205
+
206
+ const existingKeys = await state.get(keyType, deletionIds)
207
+ for (const keyId of deletionIds) {
208
+ if (!existingKeys[keyId]) {
209
+ logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
210
+ if (data[keyType])
211
+ delete data[keyType][keyId]
201
212
  }
202
- })
213
+ }
203
214
  }
204
215
 
205
216
  /**
@@ -403,9 +414,13 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
403
414
  for (const key_ in data) {
404
415
  const key = key_
405
416
  transactionCache[key] = transactionCache[key] || {}
406
- // Special handling for pre-keys and signed-pre-keys
417
+ // Special handling for pre-keys and signed-pre-keys.
418
+ // Locked via the session-scoped getKeyTypeMutex(key) so this can't
419
+ // interleave with a concurrent non-transactional get()/set() for the
420
+ // same key type on this SAME auth state - no cross-session locking,
421
+ // no lock-domain mismatch.
407
422
  if (key === 'pre-key' || key === 'signed-pre-key') {
408
- await handlePreKeyOperations(data, key, transactionCache, mutations, logger, true)
423
+ await getKeyTypeMutex(key).runExclusive(() => handlePreKeyOperations(data, key, transactionCache, mutations, logger, true))
409
424
  }
410
425
  else {
411
426
  // Normal handling for other key types
@@ -442,7 +457,7 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
442
457
  // Process pre-keys and signed-pre-keys separately with specialized mutexes
443
458
  for (const key_ in nonSenderKeyData) {
444
459
  const keyType = key_
445
- if (keyType === 'pre-key') {
460
+ if (keyType === 'pre-key' || keyType === 'signed-pre-key') {
446
461
  await processPreKeyDeletions(nonSenderKeyData, keyType, state, logger)
447
462
  }
448
463
  }
@@ -457,7 +472,7 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
457
472
  // Process pre-keys and signed-pre-keys separately with specialized mutexes
458
473
  for (const key_ in data) {
459
474
  const keyType = key_
460
- if (keyType === 'pre-key') {
475
+ if (keyType === 'pre-key' || keyType === 'signed-pre-key') {
461
476
  await processPreKeyDeletions(data, keyType, state, logger)
462
477
  }
463
478
  }
@@ -165,7 +165,7 @@ const getDecryptionJid = async (sender, repository) => {
165
165
  if (!mapped) {
166
166
  return sender
167
167
  }
168
- // FIX: pontasockets gak punya migrateSession (beda dari levvleys) yang
168
+ // FIX: pontalabs gak punya migrateSession (beda dari levvleys) yang
169
169
  // mindahin sesi Signal dari address PN ke address LID begitu mapping
170
170
  // disimpan. Jadi mapping doang gak jamin ada sesi beneran di address LID
171
171
  // itu -- kalau dipaksa pakai, decrypt gagal "No session record" walau
@@ -184,7 +184,7 @@ const getDecryptionJid = async (sender, repository) => {
184
184
  return sender
185
185
  }
186
186
 
187
- // FIX: pontasockets sebelumnya gak pernah menyimpan mapping LID<->PN saat pesan
187
+ // FIX: pontalabs sebelumnya gak pernah menyimpan mapping LID<->PN saat pesan
188
188
  // masuk (levvleys punya storeMappingFromEnvelope, di sini gak ada). Akibatnya
189
189
  // untuk kontak yang pakai addressing LID, getDecryptionJid bisa gagal nemuin
190
190
  // sesi yang benar secara intermiten (tergantung cache mapping ada atau belum),
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * rich-message-utils.js
5
- * @pontasockets-baileys
5
+ * @pontalabs-baileys
6
6
  * Tambahkan file ini ke: node_modules/@whiskeysockets/baileys/lib/Utils/rich-message-utils.js
7
7
  */
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontalabs/baileys",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "PontaLabs Baileys is a lightweight, modern, and customizable WhatsApp Web API library built on Baileys.",
5
5
  "keywords": [
6
6
  "facebook",
@@ -17,7 +17,7 @@
17
17
  "url": "git+ssh://git@github.com/pontalabs/baileys.git"
18
18
  },
19
19
  "license": "MIT",
20
- "author": "PontaLabs",
20
+ "author": "Pontalabs",
21
21
  "main": "lib/index.js",
22
22
  "types": "lib/index.d.ts",
23
23
  "files": [
@@ -25,12 +25,14 @@
25
25
  "WAProto/**/*"
26
26
  ],
27
27
  "scripts": {
28
- "test": "jest"
28
+ "test": "jest",
29
+ "format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
30
+ "lint": "tsc && eslint src --ext .js,.ts",
31
+ "lint:fix": "npm run format && npm run lint -- --fix"
29
32
  },
30
33
  "dependencies": {
31
34
  "@adiwajshing/keyed-db": "^0.2.4",
32
35
  "@cacheable/node-cache": "1.5.3",
33
- "@pontasockets/eslint-config": "^1.0.0",
34
36
  "libsignal": "^6.0.0",
35
37
  "@hapi/boom": "^9.1.3",
36
38
  "async-mutex": "^0.5.0",
@@ -48,16 +50,24 @@
48
50
  "ws": "^8.13.0"
49
51
  },
50
52
  "devDependencies": {
53
+ "@eslint/eslintrc": "^3.3.1",
54
+ "@eslint/js": "^9.31.0",
51
55
  "@types/jest": "^29.5.14",
52
56
  "@types/node": "^16.0.0",
53
57
  "@types/ws": "^8.0.0",
58
+ "@typescript-eslint/eslint-plugin": "^8",
59
+ "@typescript-eslint/parser": "^8",
60
+ "@whiskeysockets/eslint-config": "^1.0.0",
54
61
  "conventional-changelog-cli": "^2.2.2",
55
- "eslint": "^8.0.0",
62
+ "eslint": "^9",
63
+ "eslint-config-prettier": "^10.1.2",
64
+ "eslint-plugin-prettier": "^5.4.0",
56
65
  "jest": "^30.1.1",
57
66
  "jimp": "^1.6.1",
58
67
  "json": "^11.0.0",
59
68
  "link-preview-js": "^3.0.5",
60
69
  "open": "^8.4.2",
70
+ "prettier": "^3.5.3",
61
71
  "release-it": "^15.10.3",
62
72
  "ts-jest": "^29.4.1",
63
73
  "ts-node": "^10.8.1",