@nexustechpro/baileys 2.0.2 → 2.0.6

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.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/WAProto/index.js +22 -18
  4. package/lib/Defaults/baileys-version.json +6 -2
  5. package/lib/Defaults/index.js +173 -172
  6. package/lib/Signal/libsignal.js +395 -292
  7. package/lib/Signal/lid-mapping.js +264 -171
  8. package/lib/Socket/Client/index.js +2 -2
  9. package/lib/Socket/Client/types.js +10 -10
  10. package/lib/Socket/Client/websocket.js +45 -310
  11. package/lib/Socket/business.js +375 -375
  12. package/lib/Socket/chats.js +916 -963
  13. package/lib/Socket/communities.js +430 -430
  14. package/lib/Socket/groups.js +342 -342
  15. package/lib/Socket/index.js +21 -22
  16. package/lib/Socket/messages-recv.js +963 -743
  17. package/lib/Socket/messages-send.js +273 -321
  18. package/lib/Socket/mex.js +50 -50
  19. package/lib/Socket/newsletter.js +148 -148
  20. package/lib/Socket/nexus-handler.js +296 -247
  21. package/lib/Socket/registration.js +50 -33
  22. package/lib/Socket/socket.js +872 -1201
  23. package/lib/Store/index.js +5 -5
  24. package/lib/Store/make-cache-manager-store.js +81 -81
  25. package/lib/Store/make-in-memory-store.js +416 -416
  26. package/lib/Store/make-ordered-dictionary.js +81 -81
  27. package/lib/Store/object-repository.js +30 -30
  28. package/lib/Types/Auth.js +1 -1
  29. package/lib/Types/Bussines.js +1 -1
  30. package/lib/Types/Call.js +1 -1
  31. package/lib/Types/Chat.js +7 -7
  32. package/lib/Types/Contact.js +1 -1
  33. package/lib/Types/Events.js +1 -1
  34. package/lib/Types/GroupMetadata.js +1 -1
  35. package/lib/Types/Label.js +24 -24
  36. package/lib/Types/LabelAssociation.js +6 -6
  37. package/lib/Types/Message.js +10 -10
  38. package/lib/Types/Newsletter.js +37 -29
  39. package/lib/Types/Product.js +1 -1
  40. package/lib/Types/Signal.js +1 -1
  41. package/lib/Types/Socket.js +2 -2
  42. package/lib/Types/State.js +55 -12
  43. package/lib/Types/USync.js +1 -1
  44. package/lib/Types/index.js +25 -25
  45. package/lib/Utils/auth-utils.js +264 -256
  46. package/lib/Utils/baileys-event-stream.js +55 -55
  47. package/lib/Utils/browser-utils.js +27 -27
  48. package/lib/Utils/business.js +228 -230
  49. package/lib/Utils/chat-utils.js +726 -764
  50. package/lib/Utils/companion-reg-client-utils.js +34 -0
  51. package/lib/Utils/crypto.js +109 -135
  52. package/lib/Utils/decode-wa-message.js +342 -314
  53. package/lib/Utils/event-buffer.js +547 -547
  54. package/lib/Utils/generics.js +295 -297
  55. package/lib/Utils/history.js +91 -83
  56. package/lib/Utils/index.js +25 -20
  57. package/lib/Utils/key-store.js +17 -0
  58. package/lib/Utils/link-preview.js +107 -98
  59. package/lib/Utils/logger.js +2 -2
  60. package/lib/Utils/lt-hash.js +47 -47
  61. package/lib/Utils/make-mutex.js +39 -39
  62. package/lib/Utils/message-retry-manager.js +148 -148
  63. package/lib/Utils/messages-media.js +579 -535
  64. package/lib/Utils/messages.js +821 -706
  65. package/lib/Utils/noise-handler.js +255 -255
  66. package/lib/Utils/pre-key-manager.js +105 -105
  67. package/lib/Utils/process-message.js +430 -412
  68. package/lib/Utils/reporting-utils.js +155 -0
  69. package/lib/Utils/signal.js +191 -159
  70. package/lib/Utils/sync-action-utils.js +33 -0
  71. package/lib/Utils/tc-token-utils.js +162 -0
  72. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  73. package/lib/Utils/validate-connection.js +194 -194
  74. package/lib/WABinary/constants.js +1306 -1300
  75. package/lib/WABinary/decode.js +237 -237
  76. package/lib/WABinary/encode.js +232 -232
  77. package/lib/WABinary/generic-utils.js +252 -211
  78. package/lib/WABinary/index.js +6 -5
  79. package/lib/WABinary/jid-utils.js +279 -95
  80. package/lib/WABinary/types.js +1 -1
  81. package/lib/WAM/BinaryInfo.js +9 -9
  82. package/lib/WAM/constants.js +22852 -22852
  83. package/lib/WAM/encode.js +149 -149
  84. package/lib/WAM/index.js +3 -3
  85. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  86. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  87. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  88. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  89. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  90. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  91. package/lib/WAUSync/Protocols/index.js +4 -4
  92. package/lib/WAUSync/USyncQuery.js +93 -93
  93. package/lib/WAUSync/USyncUser.js +22 -22
  94. package/lib/WAUSync/index.js +3 -3
  95. package/lib/index.js +65 -66
  96. package/package.json +172 -143
  97. package/lib/Signal/Group/ciphertext-message.js +0 -12
  98. package/lib/Signal/Group/group-session-builder.js +0 -30
  99. package/lib/Signal/Group/group_cipher.js +0 -100
  100. package/lib/Signal/Group/index.js +0 -12
  101. package/lib/Signal/Group/keyhelper.js +0 -18
  102. package/lib/Signal/Group/sender-chain-key.js +0 -26
  103. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  104. package/lib/Signal/Group/sender-key-message.js +0 -66
  105. package/lib/Signal/Group/sender-key-name.js +0 -48
  106. package/lib/Signal/Group/sender-key-record.js +0 -41
  107. package/lib/Signal/Group/sender-key-state.js +0 -84
  108. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -1,293 +1,396 @@
1
- /* @ts-ignore */
2
- import * as libsignal from "libsignal"
3
- import { LRUCache } from "lru-cache"
4
- import { generateSignalPubKey } from "../Utils/index.js"
5
- import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from "../WABinary/index.js"
6
- import { SenderKeyName } from "./Group/sender-key-name.js"
7
- import { SenderKeyRecord } from "./Group/sender-key-record.js"
8
- import { SenderKeyState } from "./Group/sender-key-state.js"
9
- import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from "./Group/index.js"
10
- import { LIDMappingStore } from "./lid-mapping.js"
11
-
12
- const jidToAddr = jid => {
13
- const { user, device, server, domainType } = jidDecode(jid)
14
- if (!user) throw new Error(`Invalid JID: "${jid}"`)
15
- const signalUser = domainType !== WAJIDDomains.WHATSAPP ? `${user}_${domainType}` : user
16
- if (device === 99 && server !== "hosted" && server !== "hosted.lid") throw new Error("Invalid device 99:" + jid)
17
- return new libsignal.ProtocolAddress(signalUser, device || 0)
18
- }
19
-
20
- const jidToSenderKeyName = (group, user) => new SenderKeyName(group, jidToAddr(user))
21
-
22
- export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
23
- const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc)
24
- const storage = signalStorage(auth, lidMapping, logger)
25
- const parsedKeys = auth.keys
26
- const migratedCache = new LRUCache({ ttl: 7 * 24 * 60 * 60 * 1000, ttlAutopurge: true, updateAgeOnGet: true })
27
-
28
- const txn = (fn, key) => parsedKeys.transaction(fn, key)
29
-
30
- return {
31
- decryptGroupMessage: ({ group, authorJid, msg }) => {
32
- const cipher = new GroupCipher(storage, jidToSenderKeyName(group, authorJid))
33
- return txn(() => cipher.decrypt(msg), group)
34
- },
35
-
36
- processSenderKeyDistributionMessage: async ({ item, authorJid }) => {
37
- if (!item.groupId) throw new Error("Group ID required")
38
- const builder = new GroupSessionBuilder(storage)
39
- const senderName = jidToSenderKeyName(item.groupId, authorJid)
40
- const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
41
- return txn(async () => {
42
- let record = await storage.loadSenderKey(senderName)
43
- if (!record) {
44
- record = new SenderKeyRecord()
45
- await storage.storeSenderKey(senderName, record)
46
- }
47
- await builder.process(senderName, senderMsg)
48
- logger?.info?.(`[Signal] Sender key from ${authorJid}`)
49
- }, item.groupId)
50
- },
51
-
52
- decryptMessage: async ({ jid, type, ciphertext, alternateJid }) => {
53
- const addr = jidToAddr(jid)
54
- const session = new libsignal.SessionCipher(storage, addr)
55
- try {
56
- return txn(async () => {
57
- switch (type) {
58
- case "pkmsg": return await session.decryptPreKeyWhisperMessage(ciphertext)
59
- case "msg": return await session.decryptWhisperMessage(ciphertext)
60
- }
61
- }, jid)
62
- } catch (e) {
63
- const msg = e?.message || ""
64
- if (msg.includes("Bad MAC") || msg.includes("Key used already")) {
65
- logger?.warn?.({ jid, error: msg }, "Session corrupted")
66
- }
67
- // Retry with alternate JID if available and error is specifically "No matching sessions found"
68
- if (alternateJid && msg.includes("No matching sessions found for message")) {
69
- logger?.debug?.({ jid, alternateJid }, "Retrying decryption with alternate address")
70
- const altAddr = jidToAddr(alternateJid)
71
- const altSession = new libsignal.SessionCipher(storage, altAddr)
72
- try {
73
- return txn(async () => {
74
- switch (type) {
75
- case "pkmsg": return await altSession.decryptPreKeyWhisperMessage(ciphertext)
76
- case "msg": return await altSession.decryptWhisperMessage(ciphertext)
77
- }
78
- }, alternateJid)
79
- } catch (altErr) {
80
- const altMsg = altErr?.message || ""
81
- logger?.warn?.({ alternateJid, error: altMsg }, "Decryption with alternate address also failed")
82
- throw e
83
- }
84
- }
85
- throw e
86
- }
87
- },
88
-
89
- encryptMessage: ({ jid, data }) => txn(async () => {
90
- const cipher = new libsignal.SessionCipher(storage, jidToAddr(jid))
91
- const { type: sigType, body } = await cipher.encrypt(data)
92
- return { type: sigType === 3 ? "pkmsg" : "msg", ciphertext: Buffer.from(body, "binary") }
93
- }, jid),
94
-
95
- encryptGroupMessage: async ({ group, meId, data }) => {
96
- const senderName = jidToSenderKeyName(group, meId)
97
- const builder = new GroupSessionBuilder(storage)
98
- return txn(async () => {
99
- let record = await storage.loadSenderKey(senderName)
100
- if (!record?.getSenderKeyStates?.()?.length) {
101
- record = new SenderKeyRecord()
102
- await storage.storeSenderKey(senderName, record)
103
- }
104
- const senderKeyDistMsg = await builder.create(senderName)
105
- const session = new GroupCipher(storage, senderName)
106
- return { ciphertext: await session.encrypt(data), senderKeyDistributionMessage: senderKeyDistMsg.serialize() }
107
- }, group)
108
- },
109
-
110
- injectE2ESession: ({ jid, session }) => txn(() => new libsignal.SessionBuilder(storage, jidToAddr(jid)).initOutgoing(session), jid),
111
-
112
- jidToSignalProtocolAddress: jid => jidToAddr(jid).toString(),
113
-
114
- lidMapping,
115
-
116
- validateSession: async jid => {
117
- try {
118
- const sess = await storage.loadSession(jidToAddr(jid).toString())
119
- return { exists: sess?.haveOpenSession?.() || false, reason: sess ? null : "no session" }
120
- } catch (e) {
121
- return { exists: false, reason: "error" }
122
- }
123
- },
124
-
125
- deleteSession: jids => jids.length && txn(async () => {
126
- const sessionAddrs = jids.map(j => jidToAddr(j).toString())
127
- // Load batched sessions
128
- const batchData = await parsedKeys.get("session", ["_index"])
129
- const sessionBatch = batchData?.['_index'] || {}
130
-
131
- // Remove the specified sessions
132
- sessionAddrs.forEach(addr => {
133
- delete sessionBatch[addr]
134
- })
135
-
136
- // Store updated batch
137
- await parsedKeys.set({ session: { "_index": sessionBatch } })
138
- }, `del-${jids.length}`),
139
-
140
- migrateSession: async (fromJid, toJid) => {
141
- if (!fromJid || (!isLidUser(toJid) && !isHostedLidUser(toJid))) return { migrated: 0, skipped: 0, total: 0 }
142
- if (!isPnUser(fromJid) && !isHostedPnUser(fromJid)) return { migrated: 0, skipped: 0, total: 1 }
143
-
144
- const { user } = jidDecode(fromJid)
145
- // Load device-list from batched storage
146
- const batchData = await parsedKeys.get("device-list", ["_index"])
147
- const deviceListBatch = batchData?.['_index'] || {}
148
- const userDevices = deviceListBatch[user]
149
- if (!userDevices?.length) return { migrated: 0, skipped: 0, total: 0 }
150
-
151
- const { device: fromDevice } = jidDecode(fromJid)
152
- const fromDeviceStr = fromDevice?.toString() || "0"
153
- if (!userDevices.includes(fromDeviceStr)) userDevices.push(fromDeviceStr)
154
-
155
- const uncachedDevices = userDevices.filter(d => !migratedCache.has(`${user}.${d}`))
156
-
157
- // Load batched sessions
158
- const sessionBatchData = await parsedKeys.get("session", ["_index"])
159
- const sessionBatch = sessionBatchData?.['_index'] || {}
160
-
161
- const deviceJids = uncachedDevices
162
- .map(d => {
163
- const num = Number.parseInt(d)
164
- const addrStr = num === 0 ? `${user}.0` : `${user}.${d}`
165
- return { addr: addrStr, jid: num === 0 ? `${user}@s.whatsapp.net` : num === 99 ? `${user}:99@hosted` : `${user}:${num}@s.whatsapp.net` }
166
- })
167
- .filter(({ addr }) => sessionBatch[addr])
168
-
169
- return txn(async () => {
170
- const pnAddrStrs = Array.from(new Set(deviceJids.map(d => jidToAddr(d.jid).toString())))
171
- const updatedBatch = { ...sessionBatch }
172
- let migrated = 0
173
-
174
- for (const { jid } of deviceJids) {
175
- const pnAddr = jidToAddr(jid).toString()
176
- const lidAddr = jidToAddr(transferDevice(jid, toJid)).toString()
177
- const pnSession = updatedBatch[pnAddr]
178
-
179
- if (pnSession) {
180
- const sess = libsignal.SessionRecord.deserialize(pnSession)
181
- if (sess.haveOpenSession()) {
182
- updatedBatch[lidAddr] = sess.serialize()
183
- delete updatedBatch[pnAddr]
184
- migrated++
185
- migratedCache.set(`${user}.${jidDecode(jid).device || 0}`, true)
186
- }
187
- }
188
- }
189
-
190
- if (migrated > 0) {
191
- await parsedKeys.set({ session: { "_index": updatedBatch } })
192
- }
193
- return { migrated, skipped: deviceJids.length - migrated, total: deviceJids.length }
194
- }, `migrate-${deviceJids.length}`)
195
- },
196
- }
197
- }
198
-
199
- function signalStorage({ creds, keys }, lidMapping, logger) {
200
- const resolveLID = async id => {
201
- if (!id.includes(".")) return id
202
- const [deviceId, device] = id.split(".")
203
- const [user, dt] = deviceId.split("_")
204
- const domainType = Number.parseInt(dt || "0")
205
- if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
206
- const pnJid = `${user}${device !== "0" ? `:${device}` : ""}@${domainType === WAJIDDomains.HOSTED ? "hosted" : "s.whatsapp.net"}`
207
- const lid = await lidMapping.getLIDForPN(pnJid)
208
- return lid ? jidToAddr(lid).toString() : id
209
- }
210
-
211
- return {
212
- loadSession: async id => {
213
- try {
214
- const addr = await resolveLID(id)
215
- // Load from batched session storage
216
- const batchData = await keys.get("session", ["_index"])
217
- const sessionBatch = batchData?.['_index'] || {}
218
- const sess = sessionBatch[addr]
219
- return sess ? libsignal.SessionRecord.deserialize(sess) : null
220
- } catch (e) {
221
- logger?.error?.(`[Signal] Load session: ${e.message}`)
222
- return null
223
- }
224
- },
225
-
226
- storeSession: async (id, session) => {
227
- const addr = await resolveLID(id)
228
- // Store in batched session storage
229
- const existingData = await keys.get("session", ["_index"])
230
- const sessionBatch = existingData?.['_index'] || {}
231
-
232
- // Add/update the session
233
- sessionBatch[addr] = session.serialize()
234
-
235
- // Keep only the most recent 1000 sessions to prevent unlimited growth
236
- const sessionKeys = Object.keys(sessionBatch).sort()
237
- const recentSessions = sessionKeys.slice(-1000)
238
- const trimmedBatch = {}
239
- recentSessions.forEach(key => {
240
- trimmedBatch[key] = sessionBatch[key]
241
- })
242
-
243
- await keys.set({ session: { "_index": trimmedBatch } })
244
- },
245
-
246
- isTrustedIdentity: () => true,
247
-
248
- loadPreKey: async id => {
249
- const { [id]: key } = await keys.get("pre-key", [id.toString()])
250
- return key ? { privKey: Buffer.from(key.private), pubKey: Buffer.from(key.public) } : null
251
- },
252
-
253
- removePreKey: id => keys.set({ "pre-key": { [id]: null } }),
254
-
255
- loadSignedPreKey: () => {
256
- const key = creds.signedPreKey
257
- return { privKey: Buffer.from(key.keyPair.private), pubKey: Buffer.from(key.keyPair.public) }
258
- },
259
-
260
- loadSenderKey: async senderKeyName => {
261
- try {
262
- const id = senderKeyName.toString()
263
- const { [id]: key } = await keys.get("sender-key", [id])
264
- if (!key) return new SenderKeyRecord()
265
- try {
266
- return SenderKeyRecord.deserialize(key)
267
- } catch (e) {
268
- logger?.warn?.(`[Signal] Deserialize error, creating new`)
269
- return new SenderKeyRecord()
270
- }
271
- } catch (e) {
272
- logger?.error?.(`[Signal] Load sender key: ${e.message}`)
273
- return new SenderKeyRecord()
274
- }
275
- },
276
-
277
- storeSenderKey: async (senderKeyName, key) => {
278
- const id = senderKeyName.toString()
279
- const serialized = key.serialize()
280
- const buf = typeof serialized === "string" ? Buffer.from(serialized, "utf-8") : Buffer.from(JSON.stringify(serialized), "utf-8")
281
- await keys.set({ "sender-key": { [id]: buf } })
282
- },
283
-
284
- getOurRegistrationId: () => creds.registrationId,
285
-
286
- getOurIdentity: () => {
287
- const { signedIdentityKey } = creds
288
- return { privKey: Buffer.from(signedIdentityKey.private), pubKey: Buffer.from(generateSignalPubKey(signedIdentityKey.public)) }
289
- },
290
- }
291
- }
292
-
1
+ import {
2
+ SessionCipher,
3
+ SessionBuilder,
4
+ SessionRecord,
5
+ ProtocolAddress,
6
+ GroupCipher,
7
+ GroupSessionBuilder,
8
+ SenderKeyName,
9
+ SenderKeyDistributionMessage,
10
+ } from 'whatsapp-rust-bridge'
11
+ import { LRUCache } from 'lru-cache'
12
+ import { generateSignalPubKey, migrateIndexKey } from '../Utils/index.js'
13
+ import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js'
14
+ import { LIDMappingStore } from './lid-mapping.js'
15
+
16
+ // ─── Address Helpers ──────────────────────────────────────────────────────────
17
+
18
+ const jidToAddr = (jid) => {
19
+ const { user, device, server, domainType } = jidDecode(jid)
20
+ if (!user) throw new Error(`Invalid JID: "${jid}"`)
21
+ if (device === 99 && server !== 'hosted' && server !== 'hosted.lid') throw new Error('Invalid device 99: ' + jid)
22
+ const signalUser = domainType !== WAJIDDomains.WHATSAPP ? `${user}_${domainType}` : user
23
+ return new ProtocolAddress(signalUser, device || 0)
24
+ }
25
+
26
+ const jidToSenderKeyName = (group, user) => new SenderKeyName(group, jidToAddr(user))
27
+
28
+ const v2Key = (addr) => `${addr}:v2`
29
+
30
+ // ─── Identity Extraction ──────────────────────────────────────────────────────
31
+
32
+ function extractIdentityFromPkmsg(ciphertext) {
33
+ try {
34
+ if (!ciphertext || ciphertext.length < 2) return undefined
35
+ if ((ciphertext[0] & 0xf) !== 3) return undefined
36
+ const buf = ciphertext.slice(1)
37
+ let i = 0
38
+ while (i < buf.length) {
39
+ const tag = buf[i++]
40
+ const fieldNum = tag >> 3
41
+ const wireType = tag & 0x7
42
+ if (wireType === 2) {
43
+ let len = 0, shift = 0
44
+ while (i < buf.length) { const b = buf[i++]; len |= (b & 0x7f) << shift; if (!(b & 0x80)) break; shift += 7 }
45
+ if (fieldNum === 4 && len === 33) return new Uint8Array(buf.slice(i, i + len))
46
+ i += len
47
+ } else if (wireType === 0) {
48
+ while (i < buf.length && buf[i++] & 0x80) { }
49
+ } else if (wireType === 5) { i += 4 }
50
+ else if (wireType === 1) { i += 8 }
51
+ else break
52
+ }
53
+ } catch { }
54
+ return undefined
55
+ }
56
+
57
+ // ─── Buffer Utils ─────────────────────────────────────────────────────────────
58
+
59
+ const toBuffer = (raw) => {
60
+ if (!raw) return null
61
+ if (raw instanceof Uint8Array) return raw
62
+ if (Buffer.isBuffer(raw)) return raw
63
+ if (raw?.type === 'Buffer' && Array.isArray(raw?.data)) return Buffer.from(raw.data)
64
+ if (Array.isArray(raw)) return Buffer.from(raw)
65
+ if (typeof raw === 'string') return Buffer.from(raw, 'base64')
66
+ if (raw?.data) return Buffer.from(raw.data)
67
+ return null
68
+ }
69
+
70
+ const isOldJson = (raw) => {
71
+ if (!raw || raw instanceof Uint8Array || Buffer.isBuffer(raw)) return false
72
+ if (typeof raw === 'object') return 'version' in raw || '_sessions' in raw
73
+ if (typeof raw === 'string') { try { const p = JSON.parse(raw); return 'version' in p || '_sessions' in p } catch { return false } }
74
+ return false
75
+ }
76
+
77
+ // ─── Main Factory ─────────────────────────────────────────────────────────────
78
+
79
+ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
80
+ const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc)
81
+ const storage = signalStorage(auth, lidMapping, logger)
82
+ const parsedKeys = auth.keys
83
+ const migratedCache = new LRUCache({ ttl: 7 * 24 * 60 * 60 * 1000, ttlAutopurge: true, updateAgeOnGet: true })
84
+ const txn = (fn, key) => parsedKeys.transaction(fn, key)
85
+
86
+ return {
87
+ decryptGroupMessage({ group, authorJid, msg }) {
88
+ return txn(() => new GroupCipher(storage, group, jidToAddr(authorJid)).decrypt(msg), group)
89
+ },
90
+
91
+ async processSenderKeyDistributionMessage({ item, authorJid }) {
92
+ if (!item.groupId) throw new Error('Group ID required')
93
+ const senderName = jidToSenderKeyName(item.groupId, authorJid)
94
+ const senderMsg = SenderKeyDistributionMessage.deserialize(item.axolotlSenderKeyDistributionMessage)
95
+ return txn(() => new GroupSessionBuilder(storage).process(senderName, senderMsg), item.groupId)
96
+ },
97
+
98
+ async encryptGroupMessage({ group, meId, data }) {
99
+ const senderName = jidToSenderKeyName(group, meId)
100
+ const skdm = await new GroupSessionBuilder(storage).create(senderName)
101
+ const plaintext = data instanceof Uint8Array && data.constructor === Uint8Array
102
+ ? data
103
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
104
+ const ciphertext = await new GroupCipher(storage, group, jidToAddr(meId)).encrypt(plaintext)
105
+ return { ciphertext, senderKeyDistributionMessage: skdm.serialize() }
106
+ },
107
+
108
+ async getSenderKeyDistributionMessage({ group, meId }) {
109
+ const senderName = jidToSenderKeyName(group, meId)
110
+ const skdm = await new GroupSessionBuilder(storage).create(senderName)
111
+ return skdm.serialize()
112
+ },
113
+
114
+ async hasSenderKey({ group, meId }) {
115
+ const name = jidToSenderKeyName(group, meId).toString()
116
+ const { [name]: key } = await parsedKeys.get('sender-key', [name])
117
+ return !!toBuffer(key)
118
+ },
119
+
120
+ async decryptMessage({ jid, type, ciphertext }) {
121
+ const addr = jidToAddr(jid)
122
+ const cipher = new SessionCipher(storage, addr)
123
+ if (type === 'pkmsg') {
124
+ const identityKey = extractIdentityFromPkmsg(ciphertext)
125
+ if (identityKey) {
126
+ const changed = await storage.saveIdentity(addr.toString(), identityKey)
127
+ if (changed) logger?.info?.({ jid }, '[Signal] Identity key changed, session cleared')
128
+ }
129
+ }
130
+ try {
131
+ return await txn(() => {
132
+ if (type === 'pkmsg') return cipher.decryptPreKeyWhisperMessage(ciphertext)
133
+ if (type === 'msg') return cipher.decryptWhisperMessage(ciphertext)
134
+ throw new Error(`Unknown type: ${type}`)
135
+ }, jid)
136
+ } catch (e) {
137
+ if (e?.message?.includes('DuplicatedMessage')) { logger?.debug?.({ jid }, '[Signal] Duplicate message ignored'); return null }
138
+ throw e
139
+ }
140
+ },
141
+
142
+ encryptMessage({ jid, data }) {
143
+ return txn(async () => {
144
+ const { type: sigType, body } = await new SessionCipher(storage, jidToAddr(jid)).encrypt(data)
145
+ return { type: sigType === 3 ? 'pkmsg' : 'msg', ciphertext: Buffer.from(body) }
146
+ }, jid)
147
+ },
148
+
149
+ injectE2ESession({ jid, session }) {
150
+ return txn(() => new SessionBuilder(storage, jidToAddr(jid)).processPreKeyBundle(session), jid)
151
+ },
152
+
153
+ jidToSignalProtocolAddress: jid => jidToAddr(jid).toString(),
154
+
155
+ lidMapping,
156
+
157
+ async validateSession(jid) {
158
+ try {
159
+ const addr = jidToAddr(jid).toString()
160
+ const batch = await migrateIndexKey(parsedKeys, 'session')
161
+ const raw = toBuffer(batch[v2Key(addr)]) || toBuffer(batch[addr])
162
+ if (!raw || isOldJson(raw)) return { exists: false, reason: 'no session' }
163
+ const sess = SessionRecord.deserialize(raw)
164
+ if (!sess.haveOpenSession()) return { exists: false, reason: 'no open session' }
165
+ return { exists: true }
166
+ } catch { return { exists: false, reason: 'error' } }
167
+ },
168
+
169
+ async deleteSession(jids) {
170
+ if (!jids.length) return
171
+ return txn(async () => {
172
+ const batch = await migrateIndexKey(parsedKeys, 'session')
173
+ for (const jid of jids) { const addr = jidToAddr(jid).toString(); delete batch[addr]; delete batch[v2Key(addr)] }
174
+ await parsedKeys.set({ session: { index: batch } })
175
+ }, `del-${jids.length}`)
176
+ },
177
+
178
+ async migrateSession(fromJid, toJid) {
179
+ if (!fromJid || (!isLidUser(toJid) && !isHostedLidUser(toJid))) return { migrated: 0, skipped: 0, total: 0 }
180
+ if (!isPnUser(fromJid) && !isHostedPnUser(fromJid)) return { migrated: 0, skipped: 0, total: 1 }
181
+ const { user } = jidDecode(fromJid)
182
+ const deviceListBatch = await migrateIndexKey(parsedKeys, 'device-list')
183
+ const userDevices = deviceListBatch[user]
184
+ if (!userDevices?.length) return { migrated: 0, skipped: 0, total: 0 }
185
+ const fromDeviceStr = jidDecode(fromJid).device?.toString() || '0'
186
+ if (!userDevices.includes(fromDeviceStr)) userDevices.push(fromDeviceStr)
187
+ const uncachedDevices = userDevices.filter(d => !migratedCache.has(`${user}.${d}`))
188
+ const sessionBatch = await migrateIndexKey(parsedKeys, 'session')
189
+ const deviceJids = uncachedDevices.map(d => {
190
+ const num = parseInt(d)
191
+ return { addr: `${user}.${d || 0}`, jid: num === 99 ? `${user}:99@hosted` : num === 0 ? `${user}@s.whatsapp.net` : `${user}:${num}@s.whatsapp.net` }
192
+ }).filter(({ addr }) => sessionBatch[v2Key(addr)] || sessionBatch[addr])
193
+ return txn(async () => {
194
+ const updated = { ...sessionBatch }
195
+ let migrated = 0
196
+ for (const { jid } of deviceJids) {
197
+ const pnAddr = jidToAddr(jid).toString()
198
+ const lidAddr = jidToAddr(transferDevice(jid, toJid)).toString()
199
+ const raw = toBuffer(updated[v2Key(pnAddr)]) || toBuffer(updated[pnAddr])
200
+ if (!raw || isOldJson(raw)) continue
201
+ const sess = SessionRecord.deserialize(raw)
202
+ if (!sess.haveOpenSession()) continue
203
+ updated[v2Key(lidAddr)] = sess.serialize()
204
+ updated[lidAddr] = { version: 'v1', _sessions: {} }
205
+ delete updated[v2Key(pnAddr)]
206
+ delete updated[pnAddr]
207
+ migrated++
208
+ migratedCache.set(`${user}.${jidDecode(jid).device || 0}`, true)
209
+ }
210
+ if (migrated > 0) await parsedKeys.set({ session: { index: updated } })
211
+ return { migrated, skipped: deviceJids.length - migrated, total: deviceJids.length }
212
+ }, `migrate-${deviceJids.length}`)
213
+ },
214
+
215
+ async migrateAllPNSessionsToLID() {
216
+ return txn(async () => {
217
+ const sessionBatch = await migrateIndexKey(parsedKeys, 'session')
218
+ const sessionKeys = Object.keys(sessionBatch)
219
+ if (!sessionKeys.length) return 0
220
+ const pnAddrs = sessionKeys.filter(addr => {
221
+ if (addr.endsWith(':v2') || !addr.includes('.')) return false
222
+ const [, dt] = addr.split('.')[0].split('_')
223
+ const domainType = parseInt(dt || '0')
224
+ return domainType === WAJIDDomains.WHATSAPP || domainType === WAJIDDomains.HOSTED
225
+ })
226
+ if (!pnAddrs.length) return 0
227
+ const pnUserSet = new Set(pnAddrs.map(addr => addr.split('.')[0].split('_')[0]))
228
+ const stored = await parsedKeys.get('lid-mapping', [...pnUserSet])
229
+ const pnToLidUserMap = new Map()
230
+ for (const pnUser of pnUserSet) {
231
+ const lidUser = stored[pnUser]
232
+ if (lidUser && typeof lidUser === 'string') pnToLidUserMap.set(pnUser, lidUser)
233
+ }
234
+ if (!pnToLidUserMap.size) return 0
235
+ let migrated = 0
236
+ const updated = { ...sessionBatch }
237
+ for (const addr of pnAddrs) {
238
+ const [deviceId, device] = addr.split('.')
239
+ const [user, dt] = deviceId.split('_')
240
+ const domainType = parseInt(dt || '0')
241
+ const lidUser = pnToLidUserMap.get(user)
242
+ if (!lidUser) continue
243
+ const lidDomainType = domainType === WAJIDDomains.HOSTED ? WAJIDDomains.HOSTED_LID : WAJIDDomains.LID
244
+ const lidAddr = `${lidUser}_${lidDomainType}.${device}`
245
+ if (updated[v2Key(lidAddr)]) continue
246
+ const raw = toBuffer(updated[v2Key(addr)]) || toBuffer(updated[addr])
247
+ if (!raw || isOldJson(raw)) continue
248
+ const sess = SessionRecord.deserialize(raw)
249
+ if (!sess.haveOpenSession()) continue
250
+ updated[v2Key(lidAddr)] = sess.serialize()
251
+ updated[lidAddr] = { version: 'v1', _sessions: {} }
252
+ delete updated[v2Key(addr)]
253
+ delete updated[addr]
254
+ migrated++
255
+ migratedCache.set(`${user}.${device}`, true)
256
+ }
257
+ if (migrated > 0) {
258
+ await parsedKeys.set({ session: { index: updated } })
259
+ logger?.info?.({ migrated, totalPN: pnAddrs.length, mappingsFound: pnToLidUserMap.size }, '[Signal] Batch-migrated PN sessions to LID on connect')
260
+ }
261
+ return migrated
262
+ }, 'migrate-all-pn-to-lid')
263
+ },
264
+
265
+ deleteSenderKey(group, authorJid) {
266
+ const senderName = jidToSenderKeyName(group, authorJid).toString()
267
+ return parsedKeys.set({ 'sender-key': { [senderName]: null } })
268
+ },
269
+
270
+ close() {
271
+ migratedCache.clear()
272
+ lidMapping.close?.()
273
+ }
274
+ }
275
+ }
276
+
277
+ // ─── Storage Adapter ──────────────────────────────────────────────────────────
278
+
279
+ function signalStorage({ creds, keys }, lidMapping, logger) {
280
+ const lidCache = new LRUCache({ max: 500, ttl: 5 * 60 * 1000 })
281
+
282
+ const resolveLID = async (id) => {
283
+ if (!id.includes('.')) return id
284
+ const cached = lidCache.get(id)
285
+ if (cached) return cached
286
+ const [deviceId, device] = id.split('.')
287
+ const [user, dt] = deviceId.split('_')
288
+ const domainType = parseInt(dt || '0')
289
+ if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
290
+ const pnJid = `${user}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
291
+ const lid = await lidMapping.getLIDForPN(pnJid)
292
+ const result = lid ? jidToAddr(lid).toString() : id
293
+ lidCache.set(id, result)
294
+ return result
295
+ }
296
+
297
+ const getIndex = () => migrateIndexKey(keys, 'session')
298
+ const setIndex = (batch) => keys.set({ session: { index: batch } })
299
+
300
+ return {
301
+ loadSession: async (id) => {
302
+ try {
303
+ const addr = await resolveLID(id)
304
+ const batch = await getIndex()
305
+ const v2 = batch[v2Key(addr)]
306
+ if (v2) {
307
+ if (isOldJson(v2)) { logger?.debug?.(`[Signal] Corrupt v2 for ${addr}, will fresh handshake`); return null }
308
+ const buf = toBuffer(v2)
309
+ if (buf) return buf
310
+ }
311
+ const plain = batch[addr]
312
+ if (!plain || isOldJson(plain)) {
313
+ if (plain) logger?.debug?.(`[Signal] Old JSON session for ${addr}, will fresh handshake`)
314
+ return null
315
+ }
316
+ return toBuffer(plain)
317
+ } catch (e) { logger?.error?.(`[Signal] loadSession error: ${e.message}`); return null }
318
+ },
319
+
320
+ storeSession: async (id, session) => {
321
+ const addr = await resolveLID(id)
322
+ const batch = await getIndex()
323
+ batch[v2Key(addr)] = session.serialize()
324
+ batch[addr] = { version: 'v1', _sessions: {} }
325
+ await setIndex(batch)
326
+ },
327
+
328
+ isTrustedIdentity: () => true,
329
+
330
+ loadIdentityKey: async (id) => {
331
+ const addr = await resolveLID(id)
332
+ const { [addr]: key } = await keys.get('identity-key', [addr])
333
+ const buf = toBuffer(key)
334
+ return buf ? new Uint8Array(buf) : undefined
335
+ },
336
+
337
+ saveIdentity: async (id, identityKey) => {
338
+ const addr = await resolveLID(id)
339
+ const { [addr]: raw } = await keys.get('identity-key', [addr])
340
+ const buf = toBuffer(raw)
341
+ const existing = buf ? new Uint8Array(buf) : null
342
+ const match = existing && existing.length === identityKey.length && existing.every((b, i) => b === identityKey[i])
343
+ if (existing && !match) {
344
+ await keys.set({ session: { [addr]: null }, 'identity-key': { [addr]: identityKey } })
345
+ return true
346
+ }
347
+ if (!existing) {
348
+ await keys.set({ 'identity-key': { [addr]: identityKey } })
349
+ return true
350
+ }
351
+ return false
352
+ },
353
+
354
+ loadPreKey: async (id) => {
355
+ const { [id.toString()]: key } = await keys.get('pre-key', [id.toString()])
356
+ if (!key) return null
357
+ return { pubKey: new Uint8Array(Buffer.from(key.public)), privKey: new Uint8Array(Buffer.from(key.private)) }
358
+ },
359
+
360
+ removePreKey: (id) => keys.set({ 'pre-key': { [id]: null } }),
361
+
362
+ loadSignedPreKey: () => {
363
+ const { signedPreKey: key } = creds
364
+ return {
365
+ keyId: key.keyId,
366
+ keyPair: { pubKey: new Uint8Array(Buffer.from(key.keyPair.public)), privKey: new Uint8Array(Buffer.from(key.keyPair.private)) },
367
+ signature: new Uint8Array(Buffer.from(key.signature))
368
+ }
369
+ },
370
+
371
+ loadSenderKey: async (keyId) => {
372
+ try {
373
+ const id = keyId.toString()
374
+ const { [id]: key } = await keys.get('sender-key', [id])
375
+ return toBuffer(key)
376
+ } catch (e) { logger?.error?.(`[Signal] loadSenderKey error: ${e.message}`); return null }
377
+ },
378
+
379
+ storeSenderKey: async (keyId, record) => {
380
+ const id = keyId.toString()
381
+ await keys.set({ 'sender-key': { [id]: Buffer.from(record) } })
382
+ },
383
+
384
+ getOurRegistrationId: () => creds.registrationId,
385
+
386
+ getOurIdentity: () => {
387
+ const { signedIdentityKey } = creds
388
+ return {
389
+ pubKey: new Uint8Array(generateSignalPubKey(Buffer.from(signedIdentityKey.public))),
390
+ privKey: new Uint8Array(Buffer.from(signedIdentityKey.private))
391
+ }
392
+ }
393
+ }
394
+ }
395
+
293
396
  export default makeLibSignalRepository