@pontalabs/baileys 1.0.3 → 1.0.5

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.
@@ -116,20 +116,29 @@ function makeLibSignalRepository(auth, onWhatsAppFunc, logger) {
116
116
  }
117
117
  }
118
118
  }
119
- return auth.keys.transaction(async () => {
120
- let result;
121
- switch (type) {
122
- case 'pkmsg':
123
- result = await session.decryptPreKeyWhisperMessage(ciphertext);
124
- break;
125
- case 'msg':
126
- result = await session.decryptWhisperMessage(ciphertext);
127
- break;
128
- default:
129
- throw new Error(`Unknown message type: ${type}`);
119
+ try {
120
+ return await auth.keys.transaction(async () => {
121
+ let result;
122
+ switch (type) {
123
+ case 'pkmsg':
124
+ result = await session.decryptPreKeyWhisperMessage(ciphertext);
125
+ break;
126
+ case 'msg':
127
+ result = await session.decryptWhisperMessage(ciphertext);
128
+ break;
129
+ default:
130
+ throw new Error(`Unknown message type: ${type}`);
131
+ }
132
+ return result;
133
+ }, jid);
134
+ } catch (error) {
135
+ const errText = String(error?.message || error || '')
136
+ if (/bad mac|no session|invalid message|duplicate message|cannot decrypt/i.test(errText)) {
137
+ await storage.removeSession(addr.toString()).catch(() => null)
138
+ logger?.warn?.({ jid, type, errText }, 'reset stale session after decrypt failure')
130
139
  }
131
- return result;
132
- }, jid);
140
+ throw error
141
+ }
133
142
  },
134
143
  async encryptMessage({ jid, data }) {
135
144
  const addr = jidToSignalProtocolAddress(jid);
@@ -203,6 +212,9 @@ function signalStorage({ creds, keys }) {
203
212
  storeSession: async (id, session) => {
204
213
  await keys.set({ session: { [id]: session.serialize() } });
205
214
  },
215
+ removeSession: async (id) => {
216
+ await keys.set({ session: { [id]: null } });
217
+ },
206
218
  isTrustedIdentity: () => {
207
219
  return true;
208
220
  },
@@ -197,6 +197,12 @@ const makeSocket = (config) => {
197
197
  let lastDateRecv
198
198
  let epoch = 1
199
199
  let keepAliveReq
200
+ // FIX: sebelumnya uploadPreKeysToServerIfRequired() cuma dipanggil SEKALI pas
201
+ // 'CB:success' (baru connect/reconnect). Kalau proses bot jalan lama (berhari-hari)
202
+ // tanpa reconnect, prekey yang kepakai server gak pernah dicek ulang sampai jumlahnya
203
+ // abis - baru ketahuan pas sudah kejadian error decrypt di penerima. Interval ini
204
+ // bikin prekey selalu "terupdate", dicek ulang berkala tanpa perlu nunggu reconnect.
205
+ let preKeyCheckInterval
200
206
  let qrTimer
201
207
  let closed = false
202
208
 
@@ -516,6 +522,7 @@ const makeSocket = (config) => {
516
522
  trace: error?.stack
517
523
  }, error ? 'connection errored' : 'connection closed')
518
524
  clearInterval(keepAliveReq)
525
+ clearInterval(preKeyCheckInterval)
519
526
  clearTimeout(qrTimer)
520
527
  ws.removeAllListeners('close')
521
528
  ws.removeAllListeners('open')
@@ -822,6 +829,16 @@ const makeSocket = (config) => {
822
829
  ws.on('CB:success', async (node) => {
823
830
  try {
824
831
  await uploadPreKeysToServerIfRequired()
832
+ // Jalankan ulang pengecekan prekey tiap 6 jam selama koneksi hidup, bukan
833
+ // cuma sekali pas connect. uploadPreKeysToServerIfRequired() sendiri sudah
834
+ // pintar (cuma benar-benar upload kalau count di server rendah/ada yang
835
+ // hilang), jadi interval ini murah dan aman dijalankan berkala.
836
+ clearInterval(preKeyCheckInterval)
837
+ preKeyCheckInterval = setInterval(() => {
838
+ uploadPreKeysToServerIfRequired().catch((err) => {
839
+ logger.warn({ err }, 'periodic pre-key check failed')
840
+ })
841
+ }, 6 * 60 * 60 * 1000)
825
842
  await sendPassiveIq('active')
826
843
  } catch (err) {
827
844
  logger.warn({
@@ -14,6 +14,16 @@ const crypto_2 = require("./crypto")
14
14
  const generics_1 = require("./generics")
15
15
  const mutex_1 = require("async-mutex")
16
16
 
17
+
18
+ // Cache bypass for signal-sensitive keys so runtime state doesn't keep stale
19
+ // session/pre-key material alive after decrypt failures or identity rotation.
20
+ const NO_CACHE_TYPES = new Set([
21
+ 'pre-key',
22
+ 'session',
23
+ 'signed-pre-key',
24
+ 'identity-key'
25
+ ])
26
+
17
27
  /**
18
28
  * Adds caching capability to a SignalKeyStore
19
29
  * @param store the store to add caching to
@@ -35,6 +45,11 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
35
45
  }
36
46
  return {
37
47
  async get(type, ids) {
48
+ if (NO_CACHE_TYPES.has(type)) {
49
+ logger?.trace({ type, items: ids.length }, 'bypassing cache for sensitive key type')
50
+ return await store.get(type, ids)
51
+ }
52
+
38
53
  return cacheMutex.runExclusive(async () => {
39
54
  const data = {}
40
55
  const idsToFetch = []
@@ -65,6 +80,9 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
65
80
  return cacheMutex.runExclusive(async () => {
66
81
  let keys = 0
67
82
  for (const type in data) {
83
+ if (NO_CACHE_TYPES.has(type)) {
84
+ continue
85
+ }
68
86
  for (const id in data[type]) {
69
87
  await cache.set(getUniqueId(type, id), data[type][id])
70
88
  keys += 1
@@ -222,7 +222,24 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
222
222
  async decrypt() {
223
223
  let decryptables = 0
224
224
  if (Array.isArray(stanza.content)) {
225
- for (const { tag, attrs, content } of stanza.content) {
225
+ // FIX: proses node non-skmsg (msg/pkmsg/plaintext) LEBIH DULU sebelum node
226
+ // skmsg. Root cause "bot diam/nunggu" pas ada member baru join grup lalu
227
+ // langsung kirim pesan: satu stanza berisi 2 node <enc> - satu tipe
228
+ // pkmsg/msg (isinya SenderKeyDistributionMessage buat kamu) dan satu lagi
229
+ // tipe skmsg (isi pesan asli, dienkripsi pakai sender-key grup). Urutan
230
+ // array stanza.content dari server TIDAK dijamin naruh pkmsg/msg duluan.
231
+ // Kalau skmsg kebetulan diproses duluan, sender-key belum ada di store ->
232
+ // decryptGroupMessage() gagal "No SenderKeyRecord found" -> pesan pertama
233
+ // dari member baru selalu gagal decrypt & jadi stub CIPHERTEXT, padahal
234
+ // SKDM-nya baru "sampai" sepersekian detik kemudian di iterasi berikutnya.
235
+ // Reorder di sini (stable, gak ubah urutan relatif node lain seperti
236
+ // verified_name/unavailable) supaya skmsg selalu diproses PALING TERAKHIR,
237
+ // setelah semua kemungkinan SKDM sudah didaftarkan ke sender-key store.
238
+ const orderedContent = [
239
+ ...stanza.content.filter(({ tag, attrs }) => !(tag === 'enc' && attrs.type === 'skmsg')),
240
+ ...stanza.content.filter(({ tag, attrs }) => tag === 'enc' && attrs.type === 'skmsg')
241
+ ]
242
+ for (const { tag, attrs, content } of orderedContent) {
226
243
  if (tag === 'verified_name' && content instanceof Uint8Array) {
227
244
  const cert = WAProto_1.proto.VerifiedNameCertificate.decode(content)
228
245
  const details = WAProto_1.proto.VerifiedNameCertificate.Details.decode(cert.details)
@@ -17,4 +17,5 @@ export * from './use-single-file-auth-state'
17
17
  export * from './use-multi-file-auth-state'
18
18
  export * from './link-preview'
19
19
  export * from './event-buffer'
20
- export * from './process-message'
20
+ export * from './process-message'
21
+ export * from './signal-housekeeping'
@@ -37,4 +37,5 @@ __exportStar(require("./use-single-file-auth-state"), exports)
37
37
  __exportStar(require("./use-multi-file-auth-state"), exports)
38
38
  __exportStar(require("./link-preview"), exports)
39
39
  __exportStar(require("./event-buffer"), exports)
40
- __exportStar(require("./process-message"), exports)
40
+ __exportStar(require("./process-message"), exports)
41
+ __exportStar(require("./signal-housekeeping"), exports)
@@ -0,0 +1,19 @@
1
+ import type { Logger } from 'pino'
2
+
3
+ export interface CleanupSignalAuthFolderOptions {
4
+ /** File yang mtime-nya lebih tua dari ini (dalam hari) akan dihapus. Default 30. */
5
+ maxAgeDays?: number
6
+ /** Prefix nama file yang boleh dibersihkan. Default ['session-', 'sender-key-']. */
7
+ prefixes?: string[]
8
+ logger?: Logger
9
+ }
10
+
11
+ export interface CleanupSignalAuthFolderResult {
12
+ removed: string[]
13
+ scanned: number
14
+ }
15
+
16
+ export function cleanupSignalAuthFolder(
17
+ folder: string,
18
+ options?: CleanupSignalAuthFolderOptions
19
+ ): Promise<CleanupSignalAuthFolderResult>
@@ -0,0 +1,73 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+ exports.cleanupSignalAuthFolder = cleanupSignalAuthFolder
5
+
6
+ const fs = require("fs/promises")
7
+ const path = require("path")
8
+
9
+ /**
10
+ * Bersihkan file session/sender-key yang sudah usang di folder auth yang dipakai
11
+ * bareng useMultiFileAuthState(). Tujuannya supaya store signal tidak numpuk terus
12
+ * selama bot jalan berbulan-bulan dengan ribuan kontak/grup yang sudah gak aktif.
13
+ *
14
+ * Aman dipakai karena: mtime sebuah file session-*.json / sender-key-*.json selalu
15
+ * ke-update setiap kali ada pertukaran pesan dengan kontak/grup itu (storeSession /
16
+ * storeSenderKey menulis ulang filenya). Jadi kalau sebuah file gak pernah disentuh
17
+ * lagi lebih dari `maxAgeDays`, itu tandanya kontak/grup itu memang sudah gak aktif -
18
+ * hapus filenya aman, nanti kalau ada pesan baru sesi/sender-key otomatis dibuat ulang
19
+ * dari prekey bundle / SenderKeyDistributionMessage yang baru.
20
+ *
21
+ * creds.json, app-state-sync-key-*.json, dan pre-key-*.json SENGAJA tidak disentuh
22
+ * di sini secara default - itu bukan sumber "numpuk", dan prekey yang sudah dipakai
23
+ * memang sudah dihapus otomatis lewat removePreKey() di libsignal.js.
24
+ *
25
+ * @param {string} folder - folder yang sama dipakai di useMultiFileAuthState()
26
+ * @param {object} [options]
27
+ * @param {number} [options.maxAgeDays=30] - file yang mtime-nya lebih tua dari ini akan dihapus
28
+ * @param {string[]} [options.prefixes] - prefix nama file yang boleh dibersihkan
29
+ * @param {import('pino').Logger} [options.logger]
30
+ * @returns {Promise<{ removed: string[], scanned: number }>}
31
+ */
32
+ async function cleanupSignalAuthFolder(folder, options = {}) {
33
+ const {
34
+ maxAgeDays = 30,
35
+ prefixes = ['session-', 'sender-key-'],
36
+ logger
37
+ } = options
38
+
39
+ const removed = []
40
+ let scanned = 0
41
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000
42
+ const now = Date.now()
43
+
44
+ let entries
45
+ try {
46
+ entries = await fs.readdir(folder)
47
+ } catch (err) {
48
+ logger?.warn?.({ err, folder }, '[cleanupSignalAuthFolder] gagal baca folder auth')
49
+ return { removed, scanned }
50
+ }
51
+
52
+ for (const name of entries) {
53
+ if (!prefixes.some((p) => name.startsWith(p))) {
54
+ continue
55
+ }
56
+ scanned++
57
+ const filePath = path.join(folder, name)
58
+ try {
59
+ const stat = await fs.stat(filePath)
60
+ if (now - stat.mtimeMs > maxAgeMs) {
61
+ await fs.unlink(filePath)
62
+ removed.push(name)
63
+ }
64
+ } catch {
65
+ // File mungkin sudah kehapus proses lain (race antar-worker), aman diabaikan.
66
+ }
67
+ }
68
+
69
+ logger?.info?.({ scanned, removedCount: removed.length, maxAgeDays }, '[cleanupSignalAuthFolder] selesai')
70
+ return { removed, scanned }
71
+ }
72
+
73
+ module.exports = { cleanupSignalAuthFolder }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontalabs/baileys",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "PontaLabs Baileys is a lightweight, modern, and customizable WhatsApp Web API library built on Baileys.",
5
5
  "keywords": [
6
6
  "facebook",