@pontalabs/baileys 1.0.2 → 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.
@@ -360,11 +360,17 @@ const makeMessagesSocket = (config) => {
360
360
  if (force) {
361
361
  jidsRequiringFetch = jids;
362
362
  } else {
363
- const addrs = jids.map(jid => (signalRepository.jidToSignalProtocolAddress(jid)));
364
- 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.
365
371
  for (const jid of jids) {
366
- const signalId = signalRepository.jidToSignalProtocolAddress(jid);
367
- if (!sessions[signalId]) {
372
+ const sessionValidation = await signalRepository.validateSession(jid);
373
+ if (!sessionValidation.exists) {
368
374
  jidsRequiringFetch.push(jid);
369
375
  }
370
376
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontalabs/baileys",
3
- "version": "1.0.2",
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",