@nexustechpro/baileys 2.0.1 → 2.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.
Files changed (102) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/lib/Defaults/baileys-version.json +6 -2
  4. package/lib/Defaults/index.js +172 -172
  5. package/lib/Signal/libsignal.js +380 -292
  6. package/lib/Signal/lid-mapping.js +264 -171
  7. package/lib/Socket/Client/index.js +2 -2
  8. package/lib/Socket/Client/types.js +10 -10
  9. package/lib/Socket/Client/websocket.js +45 -310
  10. package/lib/Socket/business.js +375 -375
  11. package/lib/Socket/chats.js +909 -963
  12. package/lib/Socket/communities.js +430 -430
  13. package/lib/Socket/groups.js +342 -342
  14. package/lib/Socket/index.js +22 -22
  15. package/lib/Socket/messages-recv.js +777 -743
  16. package/lib/Socket/messages-send.js +667 -393
  17. package/lib/Socket/mex.js +50 -50
  18. package/lib/Socket/newsletter.js +148 -148
  19. package/lib/Socket/nexus-handler.js +75 -261
  20. package/lib/Socket/socket.js +709 -1201
  21. package/lib/Store/index.js +5 -5
  22. package/lib/Store/make-cache-manager-store.js +81 -81
  23. package/lib/Store/make-in-memory-store.js +416 -416
  24. package/lib/Store/make-ordered-dictionary.js +81 -81
  25. package/lib/Store/object-repository.js +30 -30
  26. package/lib/Types/Auth.js +1 -1
  27. package/lib/Types/Bussines.js +1 -1
  28. package/lib/Types/Call.js +1 -1
  29. package/lib/Types/Chat.js +7 -7
  30. package/lib/Types/Contact.js +1 -1
  31. package/lib/Types/Events.js +1 -1
  32. package/lib/Types/GroupMetadata.js +1 -1
  33. package/lib/Types/Label.js +24 -24
  34. package/lib/Types/LabelAssociation.js +6 -6
  35. package/lib/Types/Message.js +10 -10
  36. package/lib/Types/Newsletter.js +28 -28
  37. package/lib/Types/Product.js +1 -1
  38. package/lib/Types/Signal.js +1 -1
  39. package/lib/Types/Socket.js +2 -2
  40. package/lib/Types/State.js +12 -12
  41. package/lib/Types/USync.js +1 -1
  42. package/lib/Types/index.js +25 -25
  43. package/lib/Utils/auth-utils.js +264 -256
  44. package/lib/Utils/baileys-event-stream.js +55 -55
  45. package/lib/Utils/browser-utils.js +27 -27
  46. package/lib/Utils/business.js +228 -230
  47. package/lib/Utils/chat-utils.js +694 -764
  48. package/lib/Utils/crypto.js +109 -135
  49. package/lib/Utils/decode-wa-message.js +310 -314
  50. package/lib/Utils/event-buffer.js +547 -547
  51. package/lib/Utils/generics.js +297 -297
  52. package/lib/Utils/history.js +91 -83
  53. package/lib/Utils/index.js +21 -20
  54. package/lib/Utils/key-store.js +17 -0
  55. package/lib/Utils/link-preview.js +97 -88
  56. package/lib/Utils/logger.js +2 -2
  57. package/lib/Utils/lt-hash.js +47 -47
  58. package/lib/Utils/make-mutex.js +39 -39
  59. package/lib/Utils/message-retry-manager.js +148 -148
  60. package/lib/Utils/messages-media.js +534 -532
  61. package/lib/Utils/messages.js +705 -705
  62. package/lib/Utils/noise-handler.js +255 -255
  63. package/lib/Utils/pre-key-manager.js +105 -105
  64. package/lib/Utils/process-message.js +412 -412
  65. package/lib/Utils/signal.js +160 -158
  66. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  67. package/lib/Utils/validate-connection.js +194 -194
  68. package/lib/WABinary/constants.js +1300 -1300
  69. package/lib/WABinary/decode.js +237 -237
  70. package/lib/WABinary/encode.js +232 -232
  71. package/lib/WABinary/generic-utils.js +252 -211
  72. package/lib/WABinary/index.js +5 -5
  73. package/lib/WABinary/jid-utils.js +279 -95
  74. package/lib/WABinary/types.js +1 -1
  75. package/lib/WAM/BinaryInfo.js +9 -9
  76. package/lib/WAM/constants.js +22852 -22852
  77. package/lib/WAM/encode.js +149 -149
  78. package/lib/WAM/index.js +3 -3
  79. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  80. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  81. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  82. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  83. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  84. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  85. package/lib/WAUSync/Protocols/index.js +4 -4
  86. package/lib/WAUSync/USyncQuery.js +93 -93
  87. package/lib/WAUSync/USyncUser.js +22 -22
  88. package/lib/WAUSync/index.js +3 -3
  89. package/lib/index.js +66 -66
  90. package/package.json +171 -144
  91. package/lib/Signal/Group/ciphertext-message.js +0 -12
  92. package/lib/Signal/Group/group-session-builder.js +0 -30
  93. package/lib/Signal/Group/group_cipher.js +0 -100
  94. package/lib/Signal/Group/index.js +0 -12
  95. package/lib/Signal/Group/keyhelper.js +0 -18
  96. package/lib/Signal/Group/sender-chain-key.js +0 -26
  97. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  98. package/lib/Signal/Group/sender-key-message.js +0 -66
  99. package/lib/Signal/Group/sender-key-name.js +0 -48
  100. package/lib/Signal/Group/sender-key-record.js +0 -41
  101. package/lib/Signal/Group/sender-key-state.js +0 -84
  102. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -1,293 +1,381 @@
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` // v2 slot holds the actual serialized SessionRecord bytes
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
+ // universal deserializer handles all shapes written by any previous version
60
+ const toBuffer = (raw) => {
61
+ if (!raw) return null
62
+ if (raw instanceof Uint8Array) return raw
63
+ if (Buffer.isBuffer(raw)) return raw
64
+ if (raw?.type === 'Buffer' && Array.isArray(raw?.data)) return Buffer.from(raw.data)
65
+ if (Array.isArray(raw)) return Buffer.from(raw)
66
+ if (typeof raw === 'string') return Buffer.from(raw, 'base64')
67
+ if (raw?.data) return Buffer.from(raw.data)
68
+ return null
69
+ }
70
+
71
+ // detects old libsignal JS JSON format — not deserializable by whatsapp-rust-bridge
72
+ const isOldJson = (raw) => {
73
+ if (!raw || raw instanceof Uint8Array || Buffer.isBuffer(raw)) return false
74
+ if (typeof raw === 'object') return 'version' in raw || '_sessions' in raw
75
+ if (typeof raw === 'string') { try { const p = JSON.parse(raw); return 'version' in p || '_sessions' in p } catch { return false } }
76
+ return false
77
+ }
78
+
79
+ // ─── Main Factory ─────────────────────────────────────────────────────────────
80
+
81
+ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
82
+ const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc)
83
+ const storage = signalStorage(auth, lidMapping, logger)
84
+ const parsedKeys = auth.keys
85
+ const migratedCache = new LRUCache({ ttl: 7 * 24 * 60 * 60 * 1000, ttlAutopurge: true, updateAgeOnGet: true })
86
+ const txn = (fn, key) => parsedKeys.transaction(fn, key)
87
+
88
+ return {
89
+ decryptGroupMessage({ group, authorJid, msg }) {
90
+ return txn(() => new GroupCipher(storage, group, jidToAddr(authorJid)).decrypt(msg), group)
91
+ },
92
+
93
+ async processSenderKeyDistributionMessage({ item, authorJid }) {
94
+ if (!item.groupId) throw new Error('Group ID required')
95
+ const senderName = jidToSenderKeyName(item.groupId, authorJid)
96
+ const senderMsg = SenderKeyDistributionMessage.deserialize(item.axolotlSenderKeyDistributionMessage)
97
+ return txn(() => new GroupSessionBuilder(storage).process(senderName, senderMsg), item.groupId)
98
+ },
99
+
100
+ async decryptMessage({ jid, type, ciphertext }) {
101
+ const addr = jidToAddr(jid)
102
+ const cipher = new SessionCipher(storage, addr)
103
+ if (type === 'pkmsg') {
104
+ const identityKey = extractIdentityFromPkmsg(ciphertext)
105
+ if (identityKey) {
106
+ const changed = await storage.saveIdentity(addr.toString(), identityKey)
107
+ if (changed) logger?.info?.({ jid }, '[Signal] Identity key changed, session cleared')
108
+ }
109
+ }
110
+ const doDecrypt = (c, t) => {
111
+ if (t === 'pkmsg') return c.decryptPreKeyWhisperMessage(ciphertext)
112
+ if (t === 'msg') return c.decryptWhisperMessage(ciphertext)
113
+ throw new Error(`Unknown type: ${t}`)
114
+ }
115
+ try {
116
+ return await txn(() => doDecrypt(cipher, type), jid)
117
+ } catch (e) {
118
+ if (e?.message?.includes('DuplicatedMessage')) { logger?.debug?.({ jid }, '[Signal] Duplicate message ignored — offline replay'); return null }
119
+ throw e
120
+ }
121
+ },
122
+
123
+ encryptMessage({ jid, data }) {
124
+ return txn(async () => {
125
+ const { type: sigType, body } = await new SessionCipher(storage, jidToAddr(jid)).encrypt(data)
126
+ return { type: sigType === 3 ? 'pkmsg' : 'msg', ciphertext: Buffer.from(body) }
127
+ }, jid)
128
+ },
129
+
130
+ encryptGroupMessage({ group, meId, data }) {
131
+ return txn(async () => {
132
+ const senderName = jidToSenderKeyName(group, meId)
133
+ const senderKeyDistributionMessage = await new GroupSessionBuilder(storage).create(senderName)
134
+ return { ciphertext: await new GroupCipher(storage, group, jidToAddr(meId)).encrypt(data), senderKeyDistributionMessage: senderKeyDistributionMessage.serialize() }
135
+ }, group)
136
+ },
137
+
138
+ injectE2ESession({ jid, session }) {
139
+ return txn(() => new SessionBuilder(storage, jidToAddr(jid)).processPreKeyBundle(session), jid)
140
+ },
141
+
142
+ jidToSignalProtocolAddress: jid => jidToAddr(jid).toString(),
143
+
144
+ lidMapping,
145
+
146
+ async validateSession(jid) {
147
+ try {
148
+ const addr = jidToAddr(jid).toString()
149
+ const batch = await migrateIndexKey(parsedKeys, 'session')
150
+ const raw = toBuffer(batch[v2Key(addr)]) || toBuffer(batch[addr]) // v2 slot first, fall back to plain
151
+ if (!raw || isOldJson(raw)) return { exists: false, reason: 'no session' }
152
+ const sess = SessionRecord.deserialize(raw)
153
+ if (!sess.haveOpenSession()) return { exists: false, reason: 'no open session' }
154
+ return { exists: true }
155
+ } catch { return { exists: false, reason: 'error' } }
156
+ },
157
+
158
+ async deleteSession(jids) {
159
+ if (!jids.length) return
160
+ return txn(async () => {
161
+ const batch = await migrateIndexKey(parsedKeys, 'session')
162
+ for (const jid of jids) { const addr = jidToAddr(jid).toString(); delete batch[addr]; delete batch[v2Key(addr)] }
163
+ await parsedKeys.set({ session: { 'index': batch } })
164
+ }, `del-${jids.length}`)
165
+ },
166
+
167
+ async migrateSession(fromJid, toJid) {
168
+ if (!fromJid || (!isLidUser(toJid) && !isHostedLidUser(toJid))) return { migrated: 0, skipped: 0, total: 0 }
169
+ if (!isPnUser(fromJid) && !isHostedPnUser(fromJid)) return { migrated: 0, skipped: 0, total: 1 }
170
+ const { user } = jidDecode(fromJid)
171
+ const deviceListBatch = await migrateIndexKey(parsedKeys, 'device-list')
172
+ const userDevices = deviceListBatch[user]
173
+ if (!userDevices?.length) return { migrated: 0, skipped: 0, total: 0 }
174
+ const fromDeviceStr = jidDecode(fromJid).device?.toString() || '0'
175
+ if (!userDevices.includes(fromDeviceStr)) userDevices.push(fromDeviceStr)
176
+ const uncachedDevices = userDevices.filter(d => !migratedCache.has(`${user}.${d}`))
177
+ const sessionBatch = await migrateIndexKey(parsedKeys, 'session')
178
+ const deviceJids = uncachedDevices.map(d => {
179
+ const num = parseInt(d)
180
+ return { addr: `${user}.${d || 0}`, jid: num === 99 ? `${user}:99@hosted` : num === 0 ? `${user}@s.whatsapp.net` : `${user}:${num}@s.whatsapp.net` }
181
+ }).filter(({ addr }) => sessionBatch[v2Key(addr)] || sessionBatch[addr])
182
+ return txn(async () => {
183
+ const updated = { ...sessionBatch }
184
+ let migrated = 0
185
+ for (const { jid } of deviceJids) {
186
+ const pnAddr = jidToAddr(jid).toString()
187
+ const lidAddr = jidToAddr(transferDevice(jid, toJid)).toString()
188
+ const raw = toBuffer(updated[v2Key(pnAddr)]) || toBuffer(updated[pnAddr]) // prefer v2 slot
189
+ if (!raw || isOldJson(raw)) continue
190
+ const sess = SessionRecord.deserialize(raw)
191
+ if (!sess.haveOpenSession()) continue
192
+ updated[v2Key(lidAddr)] = sess.serialize()
193
+ updated[lidAddr] = { version: 'v1', _sessions: {} } // plain slot marker for compat
194
+ delete updated[v2Key(pnAddr)]
195
+ delete updated[pnAddr]
196
+ migrated++
197
+ migratedCache.set(`${user}.${jidDecode(jid).device || 0}`, true)
198
+ }
199
+ if (migrated > 0) await parsedKeys.set({ session: { 'index': updated } })
200
+ return { migrated, skipped: deviceJids.length - migrated, total: deviceJids.length }
201
+ }, `migrate-${deviceJids.length}`)
202
+ },
203
+
204
+ // Batch-migrate all PN-addressed sessions to their LID equivalents.
205
+ // Called once on CB:success before offline messages are processed — one read, one remap, one write.
206
+ async migrateAllPNSessionsToLID() {
207
+ return txn(async () => {
208
+ const sessionBatch = await migrateIndexKey(parsedKeys, 'session')
209
+ const sessionKeys = Object.keys(sessionBatch)
210
+ if (!sessionKeys.length) return 0
211
+
212
+ // collect plain (non-v2) PN-domain keys only — v2 slots are handled via their plain counterpart
213
+ const pnAddrs = sessionKeys.filter(addr => {
214
+ if (addr.endsWith(':v2')) return false
215
+ if (!addr.includes('.')) return false
216
+ const [deviceId] = addr.split('.')
217
+ const [, dt] = deviceId.split('_')
218
+ const domainType = parseInt(dt || '0')
219
+ return domainType === WAJIDDomains.WHATSAPP || domainType === WAJIDDomains.HOSTED
220
+ })
221
+ if (!pnAddrs.length) return 0
222
+
223
+ // batch-fetch LID mappings directly from key store — same format storeLIDPNMappings writes
224
+ const pnUserSet = new Set(pnAddrs.map(addr => addr.split('.')[0].split('_')[0]))
225
+ const stored = await parsedKeys.get('lid-mapping', [...pnUserSet])
226
+
227
+ const pnToLidUserMap = new Map()
228
+ for (const pnUser of pnUserSet) {
229
+ const lidUser = stored[pnUser]
230
+ if (lidUser && typeof lidUser === 'string') pnToLidUserMap.set(pnUser, lidUser)
231
+ }
232
+ if (!pnToLidUserMap.size) return 0
233
+
234
+ let migrated = 0
235
+ const updated = { ...sessionBatch }
236
+
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 // LID session already exists, skip
246
+ const raw = toBuffer(updated[v2Key(addr)]) || toBuffer(updated[addr]) // prefer v2 slot
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: {} } // plain slot marker for compat
252
+ delete updated[v2Key(addr)]
253
+ delete updated[addr]
254
+ migrated++
255
+ migratedCache.set(`${user}.${device}`, true)
256
+ }
257
+
258
+ if (migrated > 0) {
259
+ await parsedKeys.set({ session: { 'index': updated } })
260
+ logger?.info?.({ migrated, totalPN: pnAddrs.length, mappingsFound: pnToLidUserMap.size }, '[Signal] Batch-migrated PN sessions to LID on connect')
261
+ }
262
+ return migrated
263
+ }, 'migrate-all-pn-to-lid')
264
+ }
265
+ }
266
+ }
267
+
268
+ // ─── Storage Adapter ──────────────────────────────────────────────────────────
269
+
270
+ function signalStorage({ creds, keys }, lidMapping, logger) {
271
+ const lidCache = new LRUCache({ max: 500, ttl: 5 * 60 * 1000 }) // cache PN→LID resolutions for 5 min
272
+
273
+ const resolveLID = async (id) => {
274
+ if (!id.includes('.')) return id
275
+ const cached = lidCache.get(id)
276
+ if (cached) return cached
277
+ const [deviceId, device] = id.split('.')
278
+ const [user, dt] = deviceId.split('_')
279
+ const domainType = parseInt(dt || '0')
280
+ if (domainType === WAJIDDomains.LID || domainType === WAJIDDomains.HOSTED_LID) return id
281
+ const pnJid = `${user}${device !== '0' ? `:${device}` : ''}@${domainType === WAJIDDomains.HOSTED ? 'hosted' : 's.whatsapp.net'}`
282
+ const lid = await lidMapping.getLIDForPN(pnJid)
283
+ const result = lid ? jidToAddr(lid).toString() : id
284
+ lidCache.set(id, result)
285
+ return result
286
+ }
287
+
288
+ const getIndex = () => migrateIndexKey(keys, 'session')
289
+ const setIndex = (batch) => keys.set({ session: { 'index': batch } })
290
+
291
+ return {
292
+ loadSession: async (id) => {
293
+ try {
294
+ const addr = await resolveLID(id)
295
+ const batch = await getIndex()
296
+ const v2 = batch[v2Key(addr)]
297
+ if (v2) {
298
+ if (isOldJson(v2)) { logger?.debug?.(`[Signal] Corrupt v2 for ${addr}, will fresh handshake`); return null }
299
+ const buf = toBuffer(v2)
300
+ if (buf) return buf
301
+ }
302
+ const plain = batch[addr]
303
+ if (!plain || isOldJson(plain)) { // old JS JSON format — not usable by rust bridge
304
+ if (plain) logger?.debug?.(`[Signal] Old JSON session for ${addr}, will fresh handshake`)
305
+ return null
306
+ }
307
+ return toBuffer(plain)
308
+ } catch (e) { logger?.error?.(`[Signal] loadSession error: ${e.message}`); return null }
309
+ },
310
+
311
+ storeSession: async (id, session) => {
312
+ const addr = await resolveLID(id)
313
+ const batch = await getIndex()
314
+ batch[v2Key(addr)] = session.serialize() // always write to v2 slot
315
+ batch[addr] = { version: 'v1', _sessions: {} } // plain slot marker so old code sees something
316
+ await setIndex(batch)
317
+ },
318
+
319
+ isTrustedIdentity: () => true,
320
+
321
+ loadIdentityKey: async (id) => {
322
+ const addr = await resolveLID(id)
323
+ const { [addr]: key } = await keys.get('identity-key', [addr])
324
+ const buf = toBuffer(key)
325
+ return buf ? new Uint8Array(buf) : undefined
326
+ },
327
+
328
+ saveIdentity: async (id, identityKey) => {
329
+ const addr = await resolveLID(id)
330
+ const { [addr]: raw } = await keys.get('identity-key', [addr])
331
+ const buf = toBuffer(raw)
332
+ const existing = buf ? new Uint8Array(buf) : null
333
+ const match = existing && existing.length === identityKey.length && existing.every((b, i) => b === identityKey[i])
334
+ if (existing && !match) {
335
+ await keys.set({ session: { [addr]: null }, 'identity-key': { [addr]: identityKey } })
336
+ return true
337
+ }
338
+ if (!existing) {
339
+ await keys.set({ 'identity-key': { [addr]: identityKey } })
340
+ return true
341
+ }
342
+ return false
343
+ },
344
+
345
+ loadPreKey: async (id) => {
346
+ const { [id.toString()]: key } = await keys.get('pre-key', [id.toString()])
347
+ if (!key) return null
348
+ return { pubKey: new Uint8Array(Buffer.from(key.public)), privKey: new Uint8Array(Buffer.from(key.private)) }
349
+ },
350
+
351
+ removePreKey: (id) => keys.set({ 'pre-key': { [id]: null } }),
352
+
353
+ loadSignedPreKey: () => {
354
+ const { signedPreKey: key } = creds
355
+ return { keyId: key.keyId, keyPair: { pubKey: new Uint8Array(Buffer.from(key.keyPair.public)), privKey: new Uint8Array(Buffer.from(key.keyPair.private)) }, signature: new Uint8Array(Buffer.from(key.signature)) }
356
+ },
357
+
358
+ loadSenderKey: async (keyId) => {
359
+ try {
360
+ const id = keyId.toString()
361
+ const { [id]: key } = await keys.get('sender-key', [id])
362
+ return toBuffer(key)
363
+ } catch (e) { logger?.error?.(`[Signal] loadSenderKey error: ${e.message}`); return null }
364
+ },
365
+
366
+ storeSenderKey: async (keyId, record) => {
367
+ const id = keyId.toString()
368
+ const bytes = record instanceof Uint8Array ? record : Buffer.isBuffer(record) ? record : record.serialize()
369
+ await keys.set({ 'sender-key': { [id]: bytes } })
370
+ },
371
+
372
+ getOurRegistrationId: () => creds.registrationId,
373
+
374
+ getOurIdentity: () => {
375
+ const { signedIdentityKey } = creds
376
+ return { pubKey: new Uint8Array(generateSignalPubKey(Buffer.from(signedIdentityKey.public))), privKey: new Uint8Array(Buffer.from(signedIdentityKey.private)) }
377
+ }
378
+ }
379
+ }
380
+
293
381
  export default makeLibSignalRepository