@periskope/baileys 6.7.18-17-7 → 6.7.18-17-9
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.
- package/lib/Signal/Group/group_cipher.d.ts +0 -1
- package/lib/Signal/Group/group_cipher.js +29 -39
- package/lib/Signal/libsignal.js +70 -32
- package/lib/Signal/lid-mapping.js +1 -1
- package/lib/Socket/chats.js +4 -2
- package/lib/Socket/messages-recv.js +3 -2
- package/lib/Socket/messages-send.js +1 -1
- package/lib/Socket/socket.js +2 -1
- package/lib/Types/Auth.d.ts +1 -1
- package/lib/Utils/auth-utils.d.ts +1 -1
- package/lib/Utils/auth-utils.js +391 -75
- package/lib/Utils/decode-wa-message.d.ts +5 -0
- package/lib/Utils/decode-wa-message.js +89 -27
- package/lib/Utils/process-message.js +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,6 @@ export declare class GroupCipher {
|
|
|
8
8
|
private readonly senderKeyStore;
|
|
9
9
|
private readonly senderKeyName;
|
|
10
10
|
constructor(senderKeyStore: SenderKeyStore, senderKeyName: SenderKeyName);
|
|
11
|
-
private queueJob;
|
|
12
11
|
encrypt(paddedPlaintext: Uint8Array | string): Promise<Uint8Array>;
|
|
13
12
|
decrypt(senderKeyMessageBytes: Uint8Array): Promise<Uint8Array>;
|
|
14
13
|
private getSenderKey;
|
|
@@ -1,55 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.GroupCipher = void 0;
|
|
4
|
+
/* @ts-ignore */
|
|
7
5
|
const crypto_1 = require("libsignal/src/crypto");
|
|
8
|
-
const queue_job_1 = __importDefault(require("./queue-job"));
|
|
9
6
|
const sender_key_message_1 = require("./sender-key-message");
|
|
10
7
|
class GroupCipher {
|
|
11
8
|
constructor(senderKeyStore, senderKeyName) {
|
|
12
9
|
this.senderKeyStore = senderKeyStore;
|
|
13
10
|
this.senderKeyName = senderKeyName;
|
|
14
11
|
}
|
|
15
|
-
queueJob(awaitable) {
|
|
16
|
-
return (0, queue_job_1.default)(this.senderKeyName.toString(), awaitable);
|
|
17
|
-
}
|
|
18
12
|
async encrypt(paddedPlaintext) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return senderKeyMessage.serialize();
|
|
34
|
-
});
|
|
13
|
+
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
|
|
14
|
+
if (!record) {
|
|
15
|
+
throw new Error('No SenderKeyRecord found for encryption');
|
|
16
|
+
}
|
|
17
|
+
const senderKeyState = record.getSenderKeyState();
|
|
18
|
+
if (!senderKeyState) {
|
|
19
|
+
throw new Error('No session to encrypt message');
|
|
20
|
+
}
|
|
21
|
+
const iteration = senderKeyState.getSenderChainKey().getIteration();
|
|
22
|
+
const senderKey = this.getSenderKey(senderKeyState, iteration === 0 ? 0 : iteration + 1);
|
|
23
|
+
const ciphertext = await this.getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext);
|
|
24
|
+
const senderKeyMessage = new sender_key_message_1.SenderKeyMessage(senderKeyState.getKeyId(), senderKey.getIteration(), ciphertext, senderKeyState.getSigningKeyPrivate());
|
|
25
|
+
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record);
|
|
26
|
+
return senderKeyMessage.serialize();
|
|
35
27
|
}
|
|
36
28
|
async decrypt(senderKeyMessageBytes) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return plaintext;
|
|
52
|
-
});
|
|
29
|
+
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
|
|
30
|
+
if (!record) {
|
|
31
|
+
throw new Error('No SenderKeyRecord found for decryption');
|
|
32
|
+
}
|
|
33
|
+
const senderKeyMessage = new sender_key_message_1.SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
|
|
34
|
+
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
|
|
35
|
+
if (!senderKeyState) {
|
|
36
|
+
throw new Error('No session found to decrypt message');
|
|
37
|
+
}
|
|
38
|
+
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
|
|
39
|
+
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
|
|
40
|
+
const plaintext = await this.getPlainText(senderKey.getIv(), senderKey.getCipherKey(), senderKeyMessage.getCipherText());
|
|
41
|
+
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record);
|
|
42
|
+
return plaintext;
|
|
53
43
|
}
|
|
54
44
|
getSenderKey(senderKeyState, iteration) {
|
|
55
45
|
let senderChainKey = senderKeyState.getSenderChainKey();
|
package/lib/Signal/libsignal.js
CHANGED
|
@@ -51,11 +51,26 @@ function makeLibSignalRepository(auth) {
|
|
|
51
51
|
max: 500,
|
|
52
52
|
ttl: 5 * 60 * 1000
|
|
53
53
|
});
|
|
54
|
+
const parsedKeys = auth.keys;
|
|
55
|
+
function isLikelySyncMessage(addr) {
|
|
56
|
+
const key = addr.toString();
|
|
57
|
+
// Only bypass for WhatsApp system addresses, not regular user contacts
|
|
58
|
+
// Be very specific about sync service patterns
|
|
59
|
+
return (key.includes('@lid.whatsapp.net') || // WhatsApp system messages
|
|
60
|
+
key.includes('@broadcast') || // Broadcast messages
|
|
61
|
+
key.includes('@newsletter') || // Newsletter messages
|
|
62
|
+
key === 'status@broadcast' || // Status updates
|
|
63
|
+
key.includes('@g.us.history') || // Group history sync
|
|
64
|
+
key.includes('.whatsapp.net.history'));
|
|
65
|
+
}
|
|
54
66
|
const repository = {
|
|
55
67
|
decryptGroupMessage({ group, authorJid, msg }) {
|
|
56
68
|
const senderName = jidToSignalSenderKeyName(group, authorJid);
|
|
57
69
|
const cipher = new Group_1.GroupCipher(storage, senderName);
|
|
58
|
-
|
|
70
|
+
// Use transaction to ensure atomicity
|
|
71
|
+
return parsedKeys.transaction(async () => {
|
|
72
|
+
return cipher.decrypt(msg);
|
|
73
|
+
}, group);
|
|
59
74
|
},
|
|
60
75
|
async processSenderKeyDistributionMessage({ item, authorJid }) {
|
|
61
76
|
const builder = new Group_1.GroupSessionBuilder(storage);
|
|
@@ -69,23 +84,39 @@ function makeLibSignalRepository(auth) {
|
|
|
69
84
|
if (!senderKey) {
|
|
70
85
|
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
71
86
|
}
|
|
72
|
-
|
|
87
|
+
return parsedKeys.transaction(async () => {
|
|
88
|
+
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
89
|
+
if (!senderKey) {
|
|
90
|
+
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
91
|
+
}
|
|
92
|
+
await builder.process(senderName, senderMsg);
|
|
93
|
+
}, item.groupId);
|
|
73
94
|
},
|
|
74
95
|
async decryptMessage({ jid, type, ciphertext }) {
|
|
75
96
|
const addr = jidToSignalProtocolAddress(jid);
|
|
76
97
|
const session = new libsignal.SessionCipher(storage, addr);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
async function doDecrypt() {
|
|
99
|
+
let result;
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'pkmsg':
|
|
102
|
+
result = await session.decryptPreKeyWhisperMessage(ciphertext);
|
|
103
|
+
break;
|
|
104
|
+
case 'msg':
|
|
105
|
+
result = await session.decryptWhisperMessage(ciphertext);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
if (isLikelySyncMessage(addr)) {
|
|
111
|
+
// If it's a sync message, we can skip the transaction
|
|
112
|
+
// as it is likely to be a system message that doesn't require strict atomicity
|
|
113
|
+
return await doDecrypt();
|
|
87
114
|
}
|
|
88
|
-
|
|
115
|
+
// If it's not a sync message, we need to ensure atomicity
|
|
116
|
+
// For regular messages, we use a transaction to ensure atomicity
|
|
117
|
+
return parsedKeys.transaction(async () => {
|
|
118
|
+
return await doDecrypt();
|
|
119
|
+
}, jid);
|
|
89
120
|
},
|
|
90
121
|
async encryptMessage({ jid, data }) {
|
|
91
122
|
// LID SINGLE SOURCE OF TRUTH: Always prefer LID when available
|
|
@@ -114,29 +145,36 @@ function makeLibSignalRepository(auth) {
|
|
|
114
145
|
}
|
|
115
146
|
const addr = jidToSignalProtocolAddress(encryptionJid);
|
|
116
147
|
const cipher = new libsignal.SessionCipher(storage, addr);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
// Use transaction to ensure atomicity
|
|
149
|
+
return parsedKeys.transaction(async () => {
|
|
150
|
+
const { type: sigType, body } = await cipher.encrypt(data);
|
|
151
|
+
const type = sigType === 3 ? 'pkmsg' : 'msg';
|
|
152
|
+
return { type, ciphertext: Buffer.from(body, 'binary') };
|
|
153
|
+
}, jid);
|
|
120
154
|
},
|
|
121
155
|
async encryptGroupMessage({ group, meId, data }) {
|
|
122
156
|
const senderName = jidToSignalSenderKeyName(group, meId);
|
|
123
157
|
const builder = new Group_1.GroupSessionBuilder(storage);
|
|
124
158
|
const senderNameStr = senderName.toString();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
return parsedKeys.transaction(async () => {
|
|
160
|
+
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
161
|
+
if (!senderKey) {
|
|
162
|
+
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
163
|
+
}
|
|
164
|
+
const senderKeyDistributionMessage = await builder.create(senderName);
|
|
165
|
+
const session = new Group_1.GroupCipher(storage, senderName);
|
|
166
|
+
const ciphertext = await session.encrypt(data);
|
|
167
|
+
return {
|
|
168
|
+
ciphertext,
|
|
169
|
+
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
|
|
170
|
+
};
|
|
171
|
+
}, group);
|
|
136
172
|
},
|
|
137
173
|
async injectE2ESession({ jid, session }) {
|
|
138
174
|
const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid));
|
|
139
|
-
|
|
175
|
+
return parsedKeys.transaction(async () => {
|
|
176
|
+
await cipher.initOutgoing(session);
|
|
177
|
+
}, jid);
|
|
140
178
|
},
|
|
141
179
|
jidToSignalProtocolAddress(jid) {
|
|
142
180
|
return jidToSignalProtocolAddress(jid).toString();
|
|
@@ -165,9 +203,9 @@ function makeLibSignalRepository(auth) {
|
|
|
165
203
|
},
|
|
166
204
|
async deleteSession(jid) {
|
|
167
205
|
const addr = jidToSignalProtocolAddress(jid);
|
|
168
|
-
return
|
|
206
|
+
return parsedKeys.transaction(async () => {
|
|
169
207
|
await auth.keys.set({ session: { [addr.toString()]: null } });
|
|
170
|
-
});
|
|
208
|
+
}, jid);
|
|
171
209
|
},
|
|
172
210
|
async migrateSession(fromJid, toJid) {
|
|
173
211
|
// Only migrate PN → LID
|
|
@@ -191,7 +229,7 @@ function makeLibSignalRepository(auth) {
|
|
|
191
229
|
recentMigrations.set(migrationKey, true);
|
|
192
230
|
return;
|
|
193
231
|
}
|
|
194
|
-
return
|
|
232
|
+
return parsedKeys.transaction(async () => {
|
|
195
233
|
// Store mapping
|
|
196
234
|
await lidMapping.storeLIDPNMapping(toJid, fromJid);
|
|
197
235
|
// Load and copy session
|
|
@@ -207,7 +245,7 @@ function makeLibSignalRepository(auth) {
|
|
|
207
245
|
await auth.keys.set({ session: { [fromAddr.toString()]: null } });
|
|
208
246
|
}
|
|
209
247
|
recentMigrations.set(migrationKey, true);
|
|
210
|
-
});
|
|
248
|
+
}, fromJid);
|
|
211
249
|
},
|
|
212
250
|
async encryptMessageWithWire({ encryptionJid, wireJid, data }) {
|
|
213
251
|
const result = await repository.encryptMessage({ jid: encryptionJid, data });
|
package/lib/Socket/chats.js
CHANGED
|
@@ -356,6 +356,7 @@ const makeChatsSocket = (config) => {
|
|
|
356
356
|
};
|
|
357
357
|
};
|
|
358
358
|
const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => {
|
|
359
|
+
var _a, _b;
|
|
359
360
|
// we use this to determine which events to fire
|
|
360
361
|
// otherwise when we resync from scratch -- all notifications will fire
|
|
361
362
|
const initialVersionMap = {};
|
|
@@ -455,7 +456,7 @@ const makeChatsSocket = (config) => {
|
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
}
|
|
458
|
-
});
|
|
459
|
+
}, ((_b = (_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.me) === null || _b === void 0 ? void 0 : _b.id) || 'resync-app-state');
|
|
459
460
|
const { onMutation } = newAppStateChunkHandler(isInitialSync);
|
|
460
461
|
for (const key in globalMutationMap) {
|
|
461
462
|
onMutation(globalMutationMap[key]);
|
|
@@ -578,6 +579,7 @@ const makeChatsSocket = (config) => {
|
|
|
578
579
|
let initial;
|
|
579
580
|
let encodeResult;
|
|
580
581
|
await processingMutex.mutex(async () => {
|
|
582
|
+
var _a, _b;
|
|
581
583
|
await authState.keys.transaction(async () => {
|
|
582
584
|
logger.debug({ patch: patchCreate }, 'applying app patch');
|
|
583
585
|
await resyncAppState([name], false);
|
|
@@ -618,7 +620,7 @@ const makeChatsSocket = (config) => {
|
|
|
618
620
|
};
|
|
619
621
|
await query(node);
|
|
620
622
|
await authState.keys.set({ 'app-state-sync-version': { [name]: state } });
|
|
621
|
-
});
|
|
623
|
+
}, ((_b = (_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.me) === null || _b === void 0 ? void 0 : _b.id) || 'app-patch');
|
|
622
624
|
});
|
|
623
625
|
if (config.emitOwnEvents) {
|
|
624
626
|
const { onMutation } = newAppStateChunkHandler(false);
|
|
@@ -110,6 +110,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
110
110
|
await query(stanza);
|
|
111
111
|
};
|
|
112
112
|
const sendRetryRequest = async (node, forceIncludeKeys = false) => {
|
|
113
|
+
var _a, _b;
|
|
113
114
|
const { fullMessage } = (0, Utils_1.decodeMessageNode)(node, authState.creds.me.id, authState.creds.me.lid || '');
|
|
114
115
|
const { key: msgKey } = fullMessage;
|
|
115
116
|
const msgId = msgKey.id;
|
|
@@ -123,7 +124,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
123
124
|
retryCount += 1;
|
|
124
125
|
msgRetryCache.set(key, retryCount);
|
|
125
126
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
|
|
126
|
-
if (retryCount
|
|
127
|
+
if (retryCount <= 2) {
|
|
127
128
|
//request a resend via phone
|
|
128
129
|
const msgId = await requestPlaceholderResend(msgKey);
|
|
129
130
|
logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
|
|
@@ -180,7 +181,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
180
181
|
}
|
|
181
182
|
await sendNode(receipt);
|
|
182
183
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
|
|
183
|
-
});
|
|
184
|
+
}, ((_b = (_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.me) === null || _b === void 0 ? void 0 : _b.id) || 'sendRetryRequest');
|
|
184
185
|
};
|
|
185
186
|
const handleEncryptNotification = async (node) => {
|
|
186
187
|
const from = node.attrs.from;
|
package/lib/Socket/socket.js
CHANGED
|
@@ -192,13 +192,14 @@ const makeSocket = (config) => {
|
|
|
192
192
|
};
|
|
193
193
|
/** generates and uploads a set of pre-keys to the server */
|
|
194
194
|
const uploadPreKeys = async (count = Defaults_1.INITIAL_PREKEY_COUNT) => {
|
|
195
|
+
var _a, _b;
|
|
195
196
|
await keys.transaction(async () => {
|
|
196
197
|
logger.info({ count }, 'uploading pre-keys');
|
|
197
198
|
const { update, node } = await (0, Utils_1.getNextPreKeysNode)({ creds, keys }, count);
|
|
198
199
|
await query(node);
|
|
199
200
|
ev.emit('creds.update', update);
|
|
200
201
|
logger.info({ count }, 'uploaded pre-keys');
|
|
201
|
-
});
|
|
202
|
+
}, ((_b = (_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.me) === null || _b === void 0 ? void 0 : _b.id) || 'pre-keys');
|
|
202
203
|
};
|
|
203
204
|
const uploadPreKeysToServerIfRequired = async () => {
|
|
204
205
|
const preKeyCount = await getAvailablePreKeysOnServer();
|
package/lib/Types/Auth.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export type SignalKeyStore = {
|
|
|
87
87
|
};
|
|
88
88
|
export type SignalKeyStoreWithTransaction = SignalKeyStore & {
|
|
89
89
|
isInTransaction: () => boolean;
|
|
90
|
-
transaction<T>(exec: () => Promise<T
|
|
90
|
+
transaction<T>(exec: () => Promise<T>, key: string): Promise<T>;
|
|
91
91
|
};
|
|
92
92
|
export type TransactionCapabilityOptions = {
|
|
93
93
|
maxCommitRetries: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthenticationCreds, CacheStore, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types';
|
|
2
|
-
import { ILogger } from './logger';
|
|
2
|
+
import type { ILogger } from './logger';
|
|
3
3
|
/**
|
|
4
4
|
* Adds caching capability to a SignalKeyStore
|
|
5
5
|
* @param store the store to add caching to
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.initAuthCreds = exports.addTransactionCapability = void 0;
|
|
7
7
|
exports.makeCacheableSignalKeyStore = makeCacheableSignalKeyStore;
|
|
8
8
|
const node_cache_1 = __importDefault(require("@cacheable/node-cache"));
|
|
9
|
+
const async_mutex_1 = require("async-mutex");
|
|
9
10
|
const crypto_1 = require("crypto");
|
|
10
11
|
const Defaults_1 = require("../Defaults");
|
|
11
12
|
const crypto_2 = require("./crypto");
|
|
@@ -23,45 +24,51 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
23
24
|
useClones: false,
|
|
24
25
|
deleteOnExpire: true
|
|
25
26
|
});
|
|
27
|
+
// Mutex for protecting cache operations
|
|
28
|
+
const cacheMutex = new async_mutex_1.Mutex();
|
|
26
29
|
function getUniqueId(type, id) {
|
|
27
30
|
return `${type}.${id}`;
|
|
28
31
|
}
|
|
29
32
|
return {
|
|
30
33
|
async get(type, ids) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
idsToFetch.push(id);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (idsToFetch.length) {
|
|
43
|
-
logger === null || logger === void 0 ? void 0 : logger.trace({ items: idsToFetch.length }, 'loading from store');
|
|
44
|
-
const fetched = await store.get(type, idsToFetch);
|
|
45
|
-
for (const id of idsToFetch) {
|
|
46
|
-
const item = fetched[id];
|
|
47
|
-
if (item) {
|
|
34
|
+
return cacheMutex.runExclusive(async () => {
|
|
35
|
+
const data = {};
|
|
36
|
+
const idsToFetch = [];
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
const item = cache.get(getUniqueId(type, id));
|
|
39
|
+
if (typeof item !== 'undefined') {
|
|
48
40
|
data[id] = item;
|
|
49
|
-
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
idsToFetch.push(id);
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
if (idsToFetch.length) {
|
|
47
|
+
logger === null || logger === void 0 ? void 0 : logger.trace({ items: idsToFetch.length }, 'loading from store');
|
|
48
|
+
const fetched = await store.get(type, idsToFetch);
|
|
49
|
+
for (const id of idsToFetch) {
|
|
50
|
+
const item = fetched[id];
|
|
51
|
+
if (item) {
|
|
52
|
+
data[id] = item;
|
|
53
|
+
cache.set(getUniqueId(type, id), item);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
});
|
|
54
59
|
},
|
|
55
60
|
async set(data) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for (const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
return cacheMutex.runExclusive(async () => {
|
|
62
|
+
let keys = 0;
|
|
63
|
+
for (const type in data) {
|
|
64
|
+
for (const id in data[type]) {
|
|
65
|
+
cache.set(getUniqueId(type, id), data[type][id]);
|
|
66
|
+
keys += 1;
|
|
67
|
+
}
|
|
61
68
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
logger === null || logger === void 0 ? void 0 : logger.trace({ keys }, 'updated cache');
|
|
70
|
+
await store.set(data);
|
|
71
|
+
});
|
|
65
72
|
},
|
|
66
73
|
async clear() {
|
|
67
74
|
var _a;
|
|
@@ -70,6 +77,145 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
70
77
|
}
|
|
71
78
|
};
|
|
72
79
|
}
|
|
80
|
+
// Module-level specialized mutexes for pre-key operations
|
|
81
|
+
const preKeyMutex = new async_mutex_1.Mutex();
|
|
82
|
+
const signedPreKeyMutex = new async_mutex_1.Mutex();
|
|
83
|
+
/**
|
|
84
|
+
* Get the appropriate mutex for the key type
|
|
85
|
+
*/
|
|
86
|
+
const getPreKeyMutex = (keyType) => {
|
|
87
|
+
return keyType === 'signed-pre-key' ? signedPreKeyMutex : preKeyMutex;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Handles pre-key operations with mutex protection
|
|
91
|
+
*/
|
|
92
|
+
async function handlePreKeyOperations(data, keyType, transactionCache, mutations, logger, isInTransaction, state) {
|
|
93
|
+
const mutex = getPreKeyMutex(keyType);
|
|
94
|
+
await mutex.runExclusive(async () => {
|
|
95
|
+
const keyData = data[keyType];
|
|
96
|
+
if (!keyData)
|
|
97
|
+
return;
|
|
98
|
+
// Ensure structures exist
|
|
99
|
+
transactionCache[keyType] = transactionCache[keyType] || {};
|
|
100
|
+
mutations[keyType] = mutations[keyType] || {};
|
|
101
|
+
// Separate deletions from updates for batch processing
|
|
102
|
+
const deletionKeys = [];
|
|
103
|
+
const updateKeys = [];
|
|
104
|
+
for (const keyId in keyData) {
|
|
105
|
+
if (keyData[keyId] === null) {
|
|
106
|
+
deletionKeys.push(keyId);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
updateKeys.push(keyId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Process updates first (no validation needed)
|
|
113
|
+
for (const keyId of updateKeys) {
|
|
114
|
+
if (transactionCache[keyType]) {
|
|
115
|
+
transactionCache[keyType][keyId] = keyData[keyId];
|
|
116
|
+
}
|
|
117
|
+
if (mutations[keyType]) {
|
|
118
|
+
mutations[keyType][keyId] = keyData[keyId];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Process deletions with validation
|
|
122
|
+
if (deletionKeys.length === 0)
|
|
123
|
+
return;
|
|
124
|
+
if (isInTransaction) {
|
|
125
|
+
// In transaction, only allow deletion if key exists in cache
|
|
126
|
+
for (const keyId of deletionKeys) {
|
|
127
|
+
if (transactionCache[keyType]) {
|
|
128
|
+
transactionCache[keyType][keyId] = null;
|
|
129
|
+
if (mutations[keyType]) {
|
|
130
|
+
// Mark for deletion in mutations
|
|
131
|
+
mutations[keyType][keyId] = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Outside transaction, batch validate all deletions
|
|
141
|
+
if (!state)
|
|
142
|
+
return;
|
|
143
|
+
const existingKeys = await state.get(keyType, deletionKeys);
|
|
144
|
+
for (const keyId of deletionKeys) {
|
|
145
|
+
if (existingKeys[keyId]) {
|
|
146
|
+
if (transactionCache[keyType])
|
|
147
|
+
transactionCache[keyType][keyId] = null;
|
|
148
|
+
if (mutations[keyType])
|
|
149
|
+
mutations[keyType][keyId] = null;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Handles normal key operations for transactions
|
|
159
|
+
*/
|
|
160
|
+
function handleNormalKeyOperations(data, key, transactionCache, mutations) {
|
|
161
|
+
Object.assign(transactionCache[key], data[key]);
|
|
162
|
+
mutations[key] = mutations[key] || {};
|
|
163
|
+
Object.assign(mutations[key], data[key]);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Process pre-key deletions with validation
|
|
167
|
+
*/
|
|
168
|
+
async function processPreKeyDeletions(data, keyType, state, logger) {
|
|
169
|
+
const mutex = getPreKeyMutex(keyType);
|
|
170
|
+
await mutex.runExclusive(async () => {
|
|
171
|
+
const keyData = data[keyType];
|
|
172
|
+
if (!keyData)
|
|
173
|
+
return;
|
|
174
|
+
// Validate deletions
|
|
175
|
+
for (const keyId in keyData) {
|
|
176
|
+
if (keyData[keyId] === null) {
|
|
177
|
+
const existingKeys = await state.get(keyType, [keyId]);
|
|
178
|
+
if (!existingKeys[keyId]) {
|
|
179
|
+
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`);
|
|
180
|
+
if (data[keyType])
|
|
181
|
+
delete data[keyType][keyId];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Executes a function with mutexes acquired for given key types
|
|
189
|
+
* Uses async-mutex's runExclusive with efficient batching
|
|
190
|
+
*/
|
|
191
|
+
async function withMutexes(keyTypes, getKeyTypeMutex, fn) {
|
|
192
|
+
if (keyTypes.length === 0) {
|
|
193
|
+
return fn();
|
|
194
|
+
}
|
|
195
|
+
if (keyTypes.length === 1) {
|
|
196
|
+
return getKeyTypeMutex(keyTypes[0]).runExclusive(fn);
|
|
197
|
+
}
|
|
198
|
+
// For multiple mutexes, sort by key type to prevent deadlocks
|
|
199
|
+
// Then acquire all mutexes in order using Promise.all for better efficiency
|
|
200
|
+
const sortedKeyTypes = [...keyTypes].sort();
|
|
201
|
+
const mutexes = sortedKeyTypes.map(getKeyTypeMutex);
|
|
202
|
+
// Acquire all mutexes in order to prevent deadlocks
|
|
203
|
+
const releases = [];
|
|
204
|
+
try {
|
|
205
|
+
for (const mutex of mutexes) {
|
|
206
|
+
releases.push(await mutex.acquire());
|
|
207
|
+
}
|
|
208
|
+
return await fn();
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
// Release in reverse order
|
|
212
|
+
while (releases.length > 0) {
|
|
213
|
+
const release = releases.pop();
|
|
214
|
+
if (release)
|
|
215
|
+
release();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
73
219
|
/**
|
|
74
220
|
* Adds DB like transaction capability (https://en.wikipedia.org/wiki/Database_transaction) to the SignalKeyStore,
|
|
75
221
|
* this allows batch read & write operations & improves the performance of the lib
|
|
@@ -83,7 +229,119 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
83
229
|
let dbQueriesInTransaction = 0;
|
|
84
230
|
let transactionCache = {};
|
|
85
231
|
let mutations = {};
|
|
232
|
+
// Map to hold mutexes for different key types
|
|
233
|
+
const mutexMap = new Map();
|
|
234
|
+
// Track last usage time for sender key mutexes (for cleanup)
|
|
235
|
+
const mutexLastUsed = new Map();
|
|
236
|
+
// Mutex expiration time: 1 hour in milliseconds
|
|
237
|
+
const SENDER_KEY_MUTEX_EXPIRY_MS = 60 * 60 * 1000;
|
|
238
|
+
// Cleanup interval: every 30 minutes
|
|
239
|
+
const CLEANUP_INTERVAL_MS = 30 * 60 * 1000;
|
|
240
|
+
// Cleanup timer
|
|
241
|
+
let cleanupTimer = null;
|
|
242
|
+
// Start cleanup timer if not already running
|
|
243
|
+
function startCleanupTimer() {
|
|
244
|
+
if (!cleanupTimer) {
|
|
245
|
+
cleanupTimer = setInterval(() => {
|
|
246
|
+
cleanupExpiredMutexes();
|
|
247
|
+
}, CLEANUP_INTERVAL_MS);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
startCleanupTimer();
|
|
251
|
+
// Clean up expired mutexes
|
|
252
|
+
function cleanupExpiredMutexes() {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const expiredKeys = [];
|
|
255
|
+
for (const [key, lastUsed] of mutexLastUsed.entries()) {
|
|
256
|
+
if (now - lastUsed > SENDER_KEY_MUTEX_EXPIRY_MS) {
|
|
257
|
+
const mutex = mutexMap.get(key);
|
|
258
|
+
if (mutex && !mutex.isLocked()) {
|
|
259
|
+
expiredKeys.push(key);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (expiredKeys.length > 0) {
|
|
264
|
+
for (const key of expiredKeys) {
|
|
265
|
+
mutexMap.delete(key);
|
|
266
|
+
mutexLastUsed.delete(key);
|
|
267
|
+
}
|
|
268
|
+
logger.info({ expiredKeys: expiredKeys.length }, 'cleaned up expired mutexes');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
86
271
|
let transactionsInProgress = 0;
|
|
272
|
+
function getKeyTypeMutex(type) {
|
|
273
|
+
return getMutex(`keytype:${type}`);
|
|
274
|
+
}
|
|
275
|
+
function getSenderKeyMutex(senderKeyName) {
|
|
276
|
+
return getMutex(`senderkey:${senderKeyName}`);
|
|
277
|
+
}
|
|
278
|
+
function getTransactionMutex(key) {
|
|
279
|
+
return getMutex(`transaction:${key}`);
|
|
280
|
+
}
|
|
281
|
+
// Get or create a mutex for a specific key name
|
|
282
|
+
function getMutex(key) {
|
|
283
|
+
let mutex = mutexMap.get(key);
|
|
284
|
+
if (!mutex) {
|
|
285
|
+
mutex = new async_mutex_1.Mutex();
|
|
286
|
+
mutexMap.set(key, mutex);
|
|
287
|
+
if (mutexMap.size === 1) {
|
|
288
|
+
startCleanupTimer();
|
|
289
|
+
}
|
|
290
|
+
logger.info({ key }, 'created new mutex');
|
|
291
|
+
}
|
|
292
|
+
// Atualizar último uso para cleanup
|
|
293
|
+
mutexLastUsed.set(key, Date.now());
|
|
294
|
+
return mutex;
|
|
295
|
+
}
|
|
296
|
+
// Sender key operations with proper mutex sequencing
|
|
297
|
+
function queueSenderKeyOperation(senderKeyName, operation) {
|
|
298
|
+
return getSenderKeyMutex(senderKeyName).runExclusive(operation);
|
|
299
|
+
}
|
|
300
|
+
// Check if we are currently in a transaction
|
|
301
|
+
function isInTransaction() {
|
|
302
|
+
return transactionsInProgress > 0;
|
|
303
|
+
}
|
|
304
|
+
// Helper function to handle transaction commit with retries
|
|
305
|
+
async function commitTransaction() {
|
|
306
|
+
if (!Object.keys(mutations).length) {
|
|
307
|
+
logger.trace('no mutations in transaction');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
logger.trace('committing transaction');
|
|
311
|
+
let tries = maxCommitRetries;
|
|
312
|
+
while (tries > 0) {
|
|
313
|
+
tries -= 1;
|
|
314
|
+
try {
|
|
315
|
+
await state.set(mutations);
|
|
316
|
+
logger.trace({ dbQueriesInTransaction }, 'committed transaction');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
|
|
321
|
+
if (tries > 0) {
|
|
322
|
+
await (0, generics_1.delay)(delayBetweenTriesMs);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Helper function to clean up transaction state
|
|
328
|
+
function cleanupTransactionState() {
|
|
329
|
+
transactionsInProgress -= 1;
|
|
330
|
+
if (transactionsInProgress === 0) {
|
|
331
|
+
transactionCache = {};
|
|
332
|
+
mutations = {};
|
|
333
|
+
dbQueriesInTransaction = 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Helper function to execute work within transaction
|
|
337
|
+
async function executeTransactionWork(work) {
|
|
338
|
+
const result = await work();
|
|
339
|
+
// commit if this is the outermost transaction
|
|
340
|
+
if (transactionsInProgress === 1) {
|
|
341
|
+
await commitTransaction();
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
87
345
|
return {
|
|
88
346
|
get: async (type, ids) => {
|
|
89
347
|
if (isInTransaction()) {
|
|
@@ -92,9 +350,30 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
92
350
|
// only fetch if there are any items to fetch
|
|
93
351
|
if (idsRequiringFetch.length) {
|
|
94
352
|
dbQueriesInTransaction += 1;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
353
|
+
// Use per-sender-key queue for sender-key operations when possible
|
|
354
|
+
if (type === 'sender-key') {
|
|
355
|
+
logger.info({ idsRequiringFetch }, 'processing sender keys in transaction');
|
|
356
|
+
// For sender keys, process each one with queued operations to maintain serialization
|
|
357
|
+
for (const senderKeyName of idsRequiringFetch) {
|
|
358
|
+
await queueSenderKeyOperation(senderKeyName, async () => {
|
|
359
|
+
logger.info({ senderKeyName }, 'fetching sender key in transaction');
|
|
360
|
+
const result = await state.get(type, [senderKeyName]);
|
|
361
|
+
// Update transaction cache
|
|
362
|
+
transactionCache[type] || (transactionCache[type] = {});
|
|
363
|
+
Object.assign(transactionCache[type], result);
|
|
364
|
+
logger.info({ senderKeyName, hasResult: !!result[senderKeyName] }, 'sender key fetch complete');
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Use runExclusive for cleaner mutex handling
|
|
370
|
+
await getKeyTypeMutex(type).runExclusive(async () => {
|
|
371
|
+
const result = await state.get(type, idsRequiringFetch);
|
|
372
|
+
// Update transaction cache
|
|
373
|
+
transactionCache[type] || (transactionCache[type] = {});
|
|
374
|
+
Object.assign(transactionCache[type], result);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
98
377
|
}
|
|
99
378
|
return ids.reduce((dict, id) => {
|
|
100
379
|
var _a;
|
|
@@ -106,10 +385,22 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
106
385
|
}, {});
|
|
107
386
|
}
|
|
108
387
|
else {
|
|
109
|
-
|
|
388
|
+
// Not in transaction, fetch directly with queue protection
|
|
389
|
+
if (type === 'sender-key') {
|
|
390
|
+
// For sender keys, use individual queues to maintain per-key serialization
|
|
391
|
+
const results = {};
|
|
392
|
+
for (const senderKeyName of ids) {
|
|
393
|
+
const result = await queueSenderKeyOperation(senderKeyName, async () => await state.get(type, [senderKeyName]));
|
|
394
|
+
Object.assign(results, result);
|
|
395
|
+
}
|
|
396
|
+
return results;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
return await getKeyTypeMutex(type).runExclusive(() => state.get(type, ids));
|
|
400
|
+
}
|
|
110
401
|
}
|
|
111
402
|
},
|
|
112
|
-
set: data => {
|
|
403
|
+
set: async (data) => {
|
|
113
404
|
if (isInTransaction()) {
|
|
114
405
|
logger.trace({ types: Object.keys(data) }, 'caching in transaction');
|
|
115
406
|
for (const key in data) {
|
|
@@ -120,58 +411,83 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
120
411
|
}
|
|
121
412
|
}
|
|
122
413
|
else {
|
|
123
|
-
|
|
414
|
+
// Not in transaction, apply directly with mutex protection
|
|
415
|
+
const hasSenderKeys = 'sender-key' in data;
|
|
416
|
+
const senderKeyNames = hasSenderKeys ? Object.keys(data['sender-key'] || {}) : [];
|
|
417
|
+
if (hasSenderKeys) {
|
|
418
|
+
logger.info({ senderKeyNames }, 'processing sender key set operations');
|
|
419
|
+
// Handle sender key operations with per-key queues
|
|
420
|
+
for (const senderKeyName of senderKeyNames) {
|
|
421
|
+
await queueSenderKeyOperation(senderKeyName, async () => {
|
|
422
|
+
// Create data subset for this specific sender key
|
|
423
|
+
const senderKeyData = {
|
|
424
|
+
'sender-key': {
|
|
425
|
+
[senderKeyName]: data['sender-key'][senderKeyName]
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
logger.trace({ senderKeyName }, 'storing sender key');
|
|
429
|
+
// Apply changes to the store
|
|
430
|
+
await state.set(senderKeyData);
|
|
431
|
+
logger.trace({ senderKeyName }, 'sender key stored');
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// Handle any non-sender-key data with regular mutexes
|
|
435
|
+
const nonSenderKeyData = { ...data };
|
|
436
|
+
delete nonSenderKeyData['sender-key'];
|
|
437
|
+
if (Object.keys(nonSenderKeyData).length > 0) {
|
|
438
|
+
await withMutexes(Object.keys(nonSenderKeyData), getKeyTypeMutex, async () => {
|
|
439
|
+
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
440
|
+
for (const key_ in nonSenderKeyData) {
|
|
441
|
+
const keyType = key_;
|
|
442
|
+
if (keyType === 'pre-key') {
|
|
443
|
+
await processPreKeyDeletions(nonSenderKeyData, keyType, state, logger);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Apply changes to the store
|
|
447
|
+
await state.set(nonSenderKeyData);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// No sender keys - use original logic
|
|
453
|
+
await withMutexes(Object.keys(data), getKeyTypeMutex, async () => {
|
|
454
|
+
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
455
|
+
for (const key_ in data) {
|
|
456
|
+
const keyType = key_;
|
|
457
|
+
if (keyType === 'pre-key') {
|
|
458
|
+
await processPreKeyDeletions(data, keyType, state, logger);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Apply changes to the store
|
|
462
|
+
await state.set(data);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
124
465
|
}
|
|
125
466
|
},
|
|
126
467
|
isInTransaction,
|
|
127
|
-
async transaction(work) {
|
|
128
|
-
|
|
129
|
-
transactionsInProgress += 1;
|
|
130
|
-
if (transactionsInProgress === 1) {
|
|
131
|
-
logger.trace('entering transaction');
|
|
132
|
-
}
|
|
468
|
+
async transaction(work, key) {
|
|
469
|
+
const releaseTxMutex = await getTransactionMutex(key).acquire();
|
|
133
470
|
try {
|
|
134
|
-
|
|
135
|
-
// commit if this is the outermost transaction
|
|
471
|
+
transactionsInProgress += 1;
|
|
136
472
|
if (transactionsInProgress === 1) {
|
|
137
|
-
|
|
138
|
-
logger.trace('committing transaction');
|
|
139
|
-
// retry mechanism to ensure we've some recovery
|
|
140
|
-
// in case a transaction fails in the first attempt
|
|
141
|
-
let tries = maxCommitRetries;
|
|
142
|
-
while (tries) {
|
|
143
|
-
tries -= 1;
|
|
144
|
-
//eslint-disable-next-line max-depth
|
|
145
|
-
try {
|
|
146
|
-
await state.set(mutations);
|
|
147
|
-
logger.trace({ dbQueriesInTransaction }, 'committed transaction');
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
|
|
152
|
-
await (0, generics_1.delay)(delayBetweenTriesMs);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
logger.trace('no mutations in transaction');
|
|
158
|
-
}
|
|
473
|
+
logger.trace('entering transaction');
|
|
159
474
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
475
|
+
// Release the transaction mutex now that we've updated the counter
|
|
476
|
+
// This allows other transactions to start preparing
|
|
477
|
+
releaseTxMutex();
|
|
478
|
+
try {
|
|
479
|
+
return await executeTransactionWork(work);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
cleanupTransactionState();
|
|
167
483
|
}
|
|
168
484
|
}
|
|
169
|
-
|
|
485
|
+
catch (error) {
|
|
486
|
+
releaseTxMutex();
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
170
489
|
}
|
|
171
490
|
};
|
|
172
|
-
function isInTransaction() {
|
|
173
|
-
return transactionsInProgress > 0;
|
|
174
|
-
}
|
|
175
491
|
};
|
|
176
492
|
exports.addTransactionCapability = addTransactionCapability;
|
|
177
493
|
const initAuthCreds = () => {
|
|
@@ -3,6 +3,11 @@ import { BinaryNode } from '../WABinary';
|
|
|
3
3
|
import { ILogger } from './logger';
|
|
4
4
|
export declare const NO_MESSAGE_FOUND_ERROR_TEXT = "Message absent from node";
|
|
5
5
|
export declare const MISSING_KEYS_ERROR_TEXT = "Key used already or never filled";
|
|
6
|
+
export declare const DECRYPTION_RETRY_CONFIG: {
|
|
7
|
+
maxRetries: number;
|
|
8
|
+
baseDelayMs: number;
|
|
9
|
+
sessionRecordErrors: string[];
|
|
10
|
+
};
|
|
6
11
|
export declare const NACK_REASONS: {
|
|
7
12
|
ParsingError: number;
|
|
8
13
|
UnrecognizedStanza: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.decryptMessageNode = exports.extractAddressingContext = exports.NACK_REASONS = exports.MISSING_KEYS_ERROR_TEXT = exports.NO_MESSAGE_FOUND_ERROR_TEXT = void 0;
|
|
3
|
+
exports.decryptMessageNode = exports.extractAddressingContext = exports.NACK_REASONS = exports.DECRYPTION_RETRY_CONFIG = exports.MISSING_KEYS_ERROR_TEXT = exports.NO_MESSAGE_FOUND_ERROR_TEXT = void 0;
|
|
4
4
|
exports.decodeMessageNode = decodeMessageNode;
|
|
5
5
|
const boom_1 = require("@hapi/boom");
|
|
6
6
|
const WAProto_1 = require("../../WAProto");
|
|
@@ -34,6 +34,12 @@ const storeMappingFromEnvelope = async (stanza, sender, decryptionJid, repositor
|
|
|
34
34
|
};
|
|
35
35
|
exports.NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node';
|
|
36
36
|
exports.MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled';
|
|
37
|
+
// Retry configuration for failed decryption
|
|
38
|
+
exports.DECRYPTION_RETRY_CONFIG = {
|
|
39
|
+
maxRetries: 3,
|
|
40
|
+
baseDelayMs: 100,
|
|
41
|
+
sessionRecordErrors: ['No session record', 'SessionError: No session record']
|
|
42
|
+
};
|
|
37
43
|
exports.NACK_REASONS = {
|
|
38
44
|
ParsingError: 487,
|
|
39
45
|
UnrecognizedStanza: 488,
|
|
@@ -186,30 +192,30 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
186
192
|
let msgBuffer;
|
|
187
193
|
try {
|
|
188
194
|
const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
195
|
+
if (e2eType !== 'plaintext') {
|
|
196
|
+
msgBuffer = await decryptWithRetry(async () => {
|
|
197
|
+
switch (e2eType) {
|
|
198
|
+
case 'skmsg':
|
|
199
|
+
return await repository.decryptGroupMessage({
|
|
200
|
+
group: sender,
|
|
201
|
+
authorJid: author,
|
|
202
|
+
msg: content
|
|
203
|
+
});
|
|
204
|
+
case 'pkmsg':
|
|
205
|
+
case 'msg':
|
|
206
|
+
const user = (0, WABinary_1.isJidUser)(sender) ? sender : author;
|
|
207
|
+
return await repository.decryptMessage({
|
|
208
|
+
jid: user,
|
|
209
|
+
type: e2eType,
|
|
210
|
+
ciphertext: content
|
|
211
|
+
});
|
|
212
|
+
default:
|
|
213
|
+
throw new Error(`Unknown e2e type: ${e2eType}`);
|
|
214
|
+
}
|
|
215
|
+
}, logger, fullMessage.key, e2eType);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
msgBuffer = content;
|
|
213
219
|
}
|
|
214
220
|
let msg = WAProto_1.proto.Message.decode(e2eType !== 'plaintext' ? (0, generics_1.unpadRandomMax16)(msgBuffer) : msgBuffer);
|
|
215
221
|
msg = ((_a = msg.deviceSentMessage) === null || _a === void 0 ? void 0 : _a.message) || msg;
|
|
@@ -222,7 +228,7 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
222
228
|
});
|
|
223
229
|
}
|
|
224
230
|
catch (err) {
|
|
225
|
-
logger.error({ key: fullMessage.key, err }, 'failed to
|
|
231
|
+
logger.error({ key: fullMessage.key, err }, 'failed to process sender key distribution message');
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
if (fullMessage.message) {
|
|
@@ -233,7 +239,15 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
catch (err) {
|
|
236
|
-
|
|
242
|
+
const errorContext = {
|
|
243
|
+
key: fullMessage.key,
|
|
244
|
+
err,
|
|
245
|
+
messageType: tag === 'plaintext' ? 'plaintext' : attrs.type,
|
|
246
|
+
sender,
|
|
247
|
+
author,
|
|
248
|
+
isSessionRecordError: isSessionRecordError(err)
|
|
249
|
+
};
|
|
250
|
+
logger.error(errorContext, 'failed to decrypt message');
|
|
237
251
|
fullMessage.messageStubType = WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT;
|
|
238
252
|
fullMessage.messageStubParameters = [err.message];
|
|
239
253
|
}
|
|
@@ -248,3 +262,51 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
248
262
|
};
|
|
249
263
|
};
|
|
250
264
|
exports.decryptMessageNode = decryptMessageNode;
|
|
265
|
+
/**
|
|
266
|
+
* Utility function to check if an error is related to missing session record
|
|
267
|
+
*/
|
|
268
|
+
function isSessionRecordError(error) {
|
|
269
|
+
const errorMessage = (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.toString()) || '';
|
|
270
|
+
return exports.DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(errorPattern => errorMessage.includes(errorPattern));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Sleep utility for retry delays
|
|
274
|
+
*/
|
|
275
|
+
function sleep(ms) {
|
|
276
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Decrypt a single message with retry logic for session record errors
|
|
280
|
+
*/
|
|
281
|
+
async function decryptWithRetry(decryptFn, logger, messageKey, messageType) {
|
|
282
|
+
let lastError;
|
|
283
|
+
for (let attempt = 0; attempt <= exports.DECRYPTION_RETRY_CONFIG.maxRetries; attempt++) {
|
|
284
|
+
try {
|
|
285
|
+
return await decryptFn();
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
lastError = error;
|
|
289
|
+
// Only retry for session record errors
|
|
290
|
+
if (!isSessionRecordError(error)) {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
// Don't retry on the last attempt
|
|
294
|
+
if (attempt === exports.DECRYPTION_RETRY_CONFIG.maxRetries) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
// Calculate delay with exponential backoff
|
|
298
|
+
const delay = exports.DECRYPTION_RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
|
|
299
|
+
logger.warn({
|
|
300
|
+
key: messageKey,
|
|
301
|
+
attempt: attempt + 1,
|
|
302
|
+
maxRetries: exports.DECRYPTION_RETRY_CONFIG.maxRetries + 1,
|
|
303
|
+
error: error.message,
|
|
304
|
+
messageType,
|
|
305
|
+
delayMs: delay
|
|
306
|
+
}, 'Session record error detected, retrying decryption');
|
|
307
|
+
await sleep(delay);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// If all retries failed, throw the last error
|
|
311
|
+
throw lastError;
|
|
312
|
+
}
|
|
@@ -165,7 +165,7 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
165
165
|
newAppStateSyncKeyId = strKeyId;
|
|
166
166
|
}
|
|
167
167
|
logger === null || logger === void 0 ? void 0 : logger.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys');
|
|
168
|
-
});
|
|
168
|
+
}, meId);
|
|
169
169
|
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
|
|
170
170
|
}
|
|
171
171
|
else {
|