whalibmob 5.5.21 → 5.5.23
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/README.md +151 -0
- package/cli.js +13 -7
- package/index.js +12 -1
- package/lib/Client.js +19 -0
- package/lib/DeviceManager.js +461 -52
- package/lib/Registration.js +10 -5
- package/lib/auth-utils.js +693 -0
- package/lib/messages/MessageSender.js +85 -24
- package/lib/noise.js +9 -4
- package/lib/signal/SignalStore.js +20 -2
- package/package.json +1 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─── auth-utils.js ────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Provides four utilities that mirror Baileys' auth-utils, adapted to
|
|
6
|
+
// whalibmob's architecture (method-based SignalStore, Buffer key pairs, etc.):
|
|
7
|
+
//
|
|
8
|
+
// 1. makeCacheableSignalKeyStore — wraps SignalStore with NodeCache (5-min TTL)
|
|
9
|
+
// and a Mutex for all read/write operations
|
|
10
|
+
// 2. addTransactionCapability — wraps SignalStore with transaction support
|
|
11
|
+
// backed by AsyncLocalStorage + per-type Mutex
|
|
12
|
+
// 3. assertMeId — validates auth credentials, returns JID or throws
|
|
13
|
+
// 4. initAuthCreds — creates a fresh credential set for a phone number
|
|
14
|
+
//
|
|
15
|
+
// Usage example:
|
|
16
|
+
// const { SignalStore } = require('whalibmob');
|
|
17
|
+
// const { makeCacheableSignalKeyStore,
|
|
18
|
+
// addTransactionCapability,
|
|
19
|
+
// assertMeId, initAuthCreds } = require('whalibmob/lib/auth-utils');
|
|
20
|
+
//
|
|
21
|
+
// const rawStore = new SignalStore();
|
|
22
|
+
// // Optionally stack both capabilities:
|
|
23
|
+
// const txStore = addTransactionCapability(rawStore, logger, { maxCommitRetries: 5, delayBetweenTriesMs: 250 });
|
|
24
|
+
// const cachedStore = makeCacheableSignalKeyStore(txStore, logger);
|
|
25
|
+
//
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const { NodeCache } = require('@cacheable/node-cache');
|
|
29
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
30
|
+
const { Mutex } = require('async-mutex');
|
|
31
|
+
const crypto = require('crypto');
|
|
32
|
+
const { createNewStore } = require('./Store');
|
|
33
|
+
|
|
34
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** In-memory TTL for Signal store cache entries (sessions, keys, identities). */
|
|
37
|
+
const SIGNAL_STORE_TTL = '5m';
|
|
38
|
+
|
|
39
|
+
/** Default max retry count for transaction commits. */
|
|
40
|
+
const MAX_COMMIT_RETRIES = 5;
|
|
41
|
+
|
|
42
|
+
/** Milliseconds to wait between commit retries. */
|
|
43
|
+
const RETRY_DELAY_MS = 250;
|
|
44
|
+
|
|
45
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function _delay(ms) {
|
|
48
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// 1. makeCacheableSignalKeyStore
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wraps a whalibmob SignalStore with:
|
|
57
|
+
* • NodeCache — in-memory cache with a 5-minute TTL for all key types
|
|
58
|
+
* (sessions, pre-keys, signed pre-keys, identity keys, registration ID).
|
|
59
|
+
* After 5 minutes each entry is evicted and re-read from disk on the
|
|
60
|
+
* next access, keeping memory bounded without ever returning stale data.
|
|
61
|
+
* • Mutex — every get/set is serialised through a single Mutex, preventing
|
|
62
|
+
* Signal session race conditions that would produce undecryptable ciphertexts.
|
|
63
|
+
*
|
|
64
|
+
* @param {SignalStore} store - underlying SignalStore instance
|
|
65
|
+
* @param {object} logger - optional logger with a .trace() method
|
|
66
|
+
* @param {NodeCache} _cache - optional pre-built NodeCache (for testing / sharing)
|
|
67
|
+
* @returns {object} drop-in SignalStore replacement with caching and serialisation
|
|
68
|
+
*/
|
|
69
|
+
function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
70
|
+
const cache = _cache || new NodeCache({ ttl: SIGNAL_STORE_TTL, useClones: false });
|
|
71
|
+
const mutex = new Mutex();
|
|
72
|
+
|
|
73
|
+
/** Unified cache key: "type:id" */
|
|
74
|
+
const cKey = (type, id) => `${type}:${String(id)}`;
|
|
75
|
+
|
|
76
|
+
const log = (msg, data) => logger && logger.trace(data, msg);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
// ── Initialisation / file attachment (pass-through) ──────────────────────
|
|
80
|
+
|
|
81
|
+
init(identityKeyPair, registrationId) {
|
|
82
|
+
cache.set('meta:identityKeyPair', identityKeyPair);
|
|
83
|
+
cache.set('meta:registrationId', registrationId);
|
|
84
|
+
return store.init(identityKeyPair, registrationId);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
attachFile(filePath) { return store.attachFile(filePath); },
|
|
88
|
+
setLidMapping(phone, lid) { return store.setLidMapping(phone, lid); },
|
|
89
|
+
getLidMappings() { return store.getLidMappings(); },
|
|
90
|
+
preKeyCount() { return store.preKeyCount(); },
|
|
91
|
+
nextPreKeyId() { return store.nextPreKeyId(); },
|
|
92
|
+
/** Synchronous hasSession — never cached; always reads from in-memory sessions map. */
|
|
93
|
+
hasSession(encodedAddress) { return store.hasSession(encodedAddress); },
|
|
94
|
+
|
|
95
|
+
// ── Identity / registration ───────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
async getOurIdentity() {
|
|
98
|
+
return mutex.runExclusive(async () => {
|
|
99
|
+
const hit = cache.get('meta:identityKeyPair');
|
|
100
|
+
if (hit !== undefined) return hit;
|
|
101
|
+
const val = await store.getOurIdentity();
|
|
102
|
+
if (val) cache.set('meta:identityKeyPair', val);
|
|
103
|
+
return val;
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async getIdentityKeyPair() {
|
|
108
|
+
return mutex.runExclusive(async () => {
|
|
109
|
+
const hit = cache.get('meta:identityKeyPair');
|
|
110
|
+
if (hit !== undefined) return hit;
|
|
111
|
+
const val = await store.getIdentityKeyPair();
|
|
112
|
+
if (val) cache.set('meta:identityKeyPair', val);
|
|
113
|
+
return val;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async getLocalRegistrationId() {
|
|
118
|
+
return mutex.runExclusive(async () => {
|
|
119
|
+
const hit = cache.get('meta:registrationId');
|
|
120
|
+
if (hit !== undefined) return hit;
|
|
121
|
+
const val = await store.getLocalRegistrationId();
|
|
122
|
+
cache.set('meta:registrationId', val);
|
|
123
|
+
return val;
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async getOurRegistrationId() {
|
|
128
|
+
return this.getLocalRegistrationId();
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/** isTrustedIdentity — never cached; always delegates to underlying store. */
|
|
132
|
+
async isTrustedIdentity(identifier, identityKey) {
|
|
133
|
+
return store.isTrustedIdentity(identifier, identityKey);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async saveIdentity(identifier, identityKey) {
|
|
137
|
+
return mutex.runExclusive(async () => {
|
|
138
|
+
cache.del(cKey('identity', identifier));
|
|
139
|
+
return store.saveIdentity(identifier, identityKey);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async loadIdentityKey(identifier) {
|
|
144
|
+
return mutex.runExclusive(async () => {
|
|
145
|
+
const k = cKey('identity', identifier);
|
|
146
|
+
const hit = cache.get(k);
|
|
147
|
+
if (hit !== undefined) {
|
|
148
|
+
log('identity cache hit', { identifier });
|
|
149
|
+
return hit;
|
|
150
|
+
}
|
|
151
|
+
const val = await store.loadIdentityKey(identifier);
|
|
152
|
+
cache.set(k, val !== null && val !== undefined ? val : null);
|
|
153
|
+
return val;
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ── Sessions (hot path — every message send/receive touches these) ────────
|
|
158
|
+
|
|
159
|
+
async loadSession(encodedAddress) {
|
|
160
|
+
return mutex.runExclusive(async () => {
|
|
161
|
+
const k = cKey('session', encodedAddress);
|
|
162
|
+
const hit = cache.get(k);
|
|
163
|
+
if (hit !== undefined) {
|
|
164
|
+
log('session cache hit', { encodedAddress });
|
|
165
|
+
return hit;
|
|
166
|
+
}
|
|
167
|
+
const val = await store.loadSession(encodedAddress);
|
|
168
|
+
if (val) cache.set(k, val);
|
|
169
|
+
return val;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async storeSession(encodedAddress, record) {
|
|
174
|
+
return mutex.runExclusive(async () => {
|
|
175
|
+
cache.set(cKey('session', encodedAddress), record);
|
|
176
|
+
return store.storeSession(encodedAddress, record);
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async deleteSession(encodedAddress) {
|
|
181
|
+
return mutex.runExclusive(async () => {
|
|
182
|
+
cache.del(cKey('session', encodedAddress));
|
|
183
|
+
return store.deleteSession(encodedAddress);
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// ── Pre-keys ──────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
async loadPreKey(keyId) {
|
|
190
|
+
return mutex.runExclusive(async () => {
|
|
191
|
+
const k = cKey('prekey', keyId);
|
|
192
|
+
const hit = cache.get(k);
|
|
193
|
+
if (hit !== undefined) {
|
|
194
|
+
log('prekey cache hit', { keyId });
|
|
195
|
+
return hit;
|
|
196
|
+
}
|
|
197
|
+
const val = await store.loadPreKey(keyId);
|
|
198
|
+
if (val) cache.set(k, val);
|
|
199
|
+
return val;
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
async storePreKey(keyId, keyPair) {
|
|
204
|
+
return mutex.runExclusive(async () => {
|
|
205
|
+
cache.set(cKey('prekey', keyId), keyPair);
|
|
206
|
+
return store.storePreKey(keyId, keyPair);
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async removePreKey(keyId) {
|
|
211
|
+
return mutex.runExclusive(async () => {
|
|
212
|
+
cache.del(cKey('prekey', keyId));
|
|
213
|
+
return store.removePreKey(keyId);
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// ── Signed pre-keys ───────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
async loadSignedPreKey(keyId) {
|
|
220
|
+
return mutex.runExclusive(async () => {
|
|
221
|
+
const k = cKey('signedprekey', keyId);
|
|
222
|
+
const hit = cache.get(k);
|
|
223
|
+
if (hit !== undefined) {
|
|
224
|
+
log('signed-prekey cache hit', { keyId });
|
|
225
|
+
return hit;
|
|
226
|
+
}
|
|
227
|
+
const val = await store.loadSignedPreKey(keyId);
|
|
228
|
+
if (val) cache.set(k, val);
|
|
229
|
+
return val;
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async storeSignedPreKey(keyId, keyPair) {
|
|
234
|
+
return mutex.runExclusive(async () => {
|
|
235
|
+
cache.set(cKey('signedprekey', keyId), keyPair);
|
|
236
|
+
return store.storeSignedPreKey(keyId, keyPair);
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async removeSignedPreKey(keyId) {
|
|
241
|
+
return mutex.runExclusive(async () => {
|
|
242
|
+
cache.del(cKey('signedprekey', keyId));
|
|
243
|
+
return store.removeSignedPreKey(keyId);
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// ── Transaction forwarding ────────────────────────────────────────────────
|
|
248
|
+
// If the underlying store was wrapped with addTransactionCapability, forward
|
|
249
|
+
// transaction() and isInTransaction() so stacking both wrappers works correctly.
|
|
250
|
+
|
|
251
|
+
/** Returns true if the current async context is inside a transaction. */
|
|
252
|
+
isInTransaction() {
|
|
253
|
+
return typeof store.isInTransaction === 'function'
|
|
254
|
+
? store.isInTransaction()
|
|
255
|
+
: false;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Delegate to the underlying store's transaction() if available.
|
|
260
|
+
* This lets you stack makeCacheableSignalKeyStore on top of addTransactionCapability
|
|
261
|
+
* and still call .transaction() on the outermost wrapper.
|
|
262
|
+
*/
|
|
263
|
+
transaction(work, key) {
|
|
264
|
+
if (typeof store.transaction !== 'function') {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'transaction() is only available when the underlying store was wrapped ' +
|
|
267
|
+
'with addTransactionCapability()'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return store.transaction(work, key);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// ── Cache control ─────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/** Flush all cached entries (e.g. after a key rotation or re-registration). */
|
|
276
|
+
async flushCache() {
|
|
277
|
+
cache.flushAll();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
283
|
+
// 2. addTransactionCapability
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Adds DB-like transaction capability to a whalibmob SignalStore.
|
|
288
|
+
*
|
|
289
|
+
* Within a transaction all writes are buffered in an in-memory context
|
|
290
|
+
* (scoped via AsyncLocalStorage). Reads check the buffer first so they see
|
|
291
|
+
* their own writes. On commit the buffer is flushed to the underlying store
|
|
292
|
+
* in one shot with retry logic. Nested transactions transparently reuse the
|
|
293
|
+
* enclosing context — no double-commit.
|
|
294
|
+
*
|
|
295
|
+
* Per-type Mutex objects serialise concurrent reads against the underlying
|
|
296
|
+
* store when multiple transactions are in flight simultaneously.
|
|
297
|
+
*
|
|
298
|
+
* @param {SignalStore} state - underlying store (may already be cached)
|
|
299
|
+
* @param {object} logger - optional logger with .trace() / .warn() / .error()
|
|
300
|
+
* @param {object} opts - { maxCommitRetries, delayBetweenTriesMs }
|
|
301
|
+
* @returns {object} drop-in SignalStore replacement with transaction support
|
|
302
|
+
*/
|
|
303
|
+
function addTransactionCapability(state, logger, opts) {
|
|
304
|
+
opts = opts || {};
|
|
305
|
+
const maxRetries = opts.maxCommitRetries ?? MAX_COMMIT_RETRIES;
|
|
306
|
+
const retryDelay = opts.delayBetweenTriesMs ?? RETRY_DELAY_MS;
|
|
307
|
+
|
|
308
|
+
const txStorage = new AsyncLocalStorage();
|
|
309
|
+
const typeMutexes = new Map(); // type → Mutex
|
|
310
|
+
const typeMutexRefs = new Map(); // type → refcount
|
|
311
|
+
|
|
312
|
+
function getMutex(type) {
|
|
313
|
+
if (!typeMutexes.has(type)) {
|
|
314
|
+
typeMutexes.set(type, new Mutex());
|
|
315
|
+
typeMutexRefs.set(type, 0);
|
|
316
|
+
}
|
|
317
|
+
return typeMutexes.get(type);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function acquireRef(type) {
|
|
321
|
+
typeMutexRefs.set(type, (typeMutexRefs.get(type) ?? 0) + 1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function releaseRef(type) {
|
|
325
|
+
const count = (typeMutexRefs.get(type) ?? 1) - 1;
|
|
326
|
+
typeMutexRefs.set(type, count);
|
|
327
|
+
if (count <= 0) {
|
|
328
|
+
const m = typeMutexes.get(type);
|
|
329
|
+
if (m && !m.isLocked()) {
|
|
330
|
+
typeMutexes.delete(type);
|
|
331
|
+
typeMutexRefs.delete(type);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isInTransaction() {
|
|
337
|
+
return !!txStorage.getStore();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function commitWithRetry(ctx) {
|
|
341
|
+
const { sessions, preKeys, signedPreKeys, identities } = ctx.mutations;
|
|
342
|
+
const total = (
|
|
343
|
+
Object.keys(sessions).length +
|
|
344
|
+
Object.keys(preKeys).length +
|
|
345
|
+
Object.keys(signedPreKeys).length +
|
|
346
|
+
Object.keys(identities).length
|
|
347
|
+
);
|
|
348
|
+
if (total === 0) {
|
|
349
|
+
logger && logger.trace('no mutations in transaction');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
logger && logger.trace({ mutationCount: total }, 'committing transaction');
|
|
354
|
+
|
|
355
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
356
|
+
try {
|
|
357
|
+
const proms = [];
|
|
358
|
+
|
|
359
|
+
for (const [addr, rec] of Object.entries(sessions)) {
|
|
360
|
+
if (rec === null) {
|
|
361
|
+
proms.push(state.deleteSession(addr));
|
|
362
|
+
} else {
|
|
363
|
+
proms.push(state.storeSession(addr, rec));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const [keyId, kp] of Object.entries(preKeys)) {
|
|
368
|
+
if (kp === null) {
|
|
369
|
+
proms.push(state.removePreKey(Number(keyId)));
|
|
370
|
+
} else {
|
|
371
|
+
proms.push(state.storePreKey(Number(keyId), kp));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const [keyId, kp] of Object.entries(signedPreKeys)) {
|
|
376
|
+
if (kp === null) {
|
|
377
|
+
proms.push(state.removeSignedPreKey(Number(keyId)));
|
|
378
|
+
} else {
|
|
379
|
+
proms.push(state.storeSignedPreKey(Number(keyId), kp));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const [identifier, key] of Object.entries(identities)) {
|
|
384
|
+
if (key !== null) {
|
|
385
|
+
proms.push(state.saveIdentity(identifier, key));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await Promise.all(proms);
|
|
390
|
+
logger && logger.trace({ mutationCount: total }, 'committed transaction');
|
|
391
|
+
return;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const retriesLeft = maxRetries - attempt - 1;
|
|
394
|
+
logger && logger.warn(`failed to commit transaction mutations, retries left=${retriesLeft}`);
|
|
395
|
+
if (retriesLeft === 0) throw error;
|
|
396
|
+
await _delay(retryDelay);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Build the empty mutation / read-buffer context for a new transaction.
|
|
402
|
+
function makeCtx() {
|
|
403
|
+
return {
|
|
404
|
+
// Read buffer: what we've already loaded from the underlying store
|
|
405
|
+
sessions: {},
|
|
406
|
+
preKeys: {},
|
|
407
|
+
signedPreKeys:{},
|
|
408
|
+
identities: {},
|
|
409
|
+
// Pending writes: committed to disk on transaction end
|
|
410
|
+
mutations: {
|
|
411
|
+
sessions: {},
|
|
412
|
+
preKeys: {},
|
|
413
|
+
signedPreKeys:{},
|
|
414
|
+
identities: {}
|
|
415
|
+
},
|
|
416
|
+
dbQueries: 0
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
// ── Pass-through non-transactional methods ────────────────────────────────
|
|
422
|
+
init(ikp, regId) { return state.init(ikp, regId); },
|
|
423
|
+
attachFile(fp) { return state.attachFile(fp); },
|
|
424
|
+
setLidMapping(phone, lid) { return state.setLidMapping(phone, lid); },
|
|
425
|
+
getLidMappings() { return state.getLidMappings(); },
|
|
426
|
+
preKeyCount() { return state.preKeyCount(); },
|
|
427
|
+
nextPreKeyId() { return state.nextPreKeyId(); },
|
|
428
|
+
hasSession(addr) { return state.hasSession(addr); },
|
|
429
|
+
getOurIdentity() { return state.getOurIdentity(); },
|
|
430
|
+
getIdentityKeyPair() { return state.getIdentityKeyPair(); },
|
|
431
|
+
getLocalRegistrationId() { return state.getLocalRegistrationId(); },
|
|
432
|
+
getOurRegistrationId() { return state.getOurRegistrationId(); },
|
|
433
|
+
isTrustedIdentity(a, b) { return state.isTrustedIdentity(a, b); },
|
|
434
|
+
|
|
435
|
+
// ── Session ───────────────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
async loadSession(encodedAddress) {
|
|
438
|
+
const ctx = txStorage.getStore();
|
|
439
|
+
if (!ctx) return state.loadSession(encodedAddress);
|
|
440
|
+
if (encodedAddress in ctx.sessions) return ctx.sessions[encodedAddress];
|
|
441
|
+
// Not in buffer: fetch under mutex, cache in buffer
|
|
442
|
+
const val = await getMutex('session').runExclusive(() => state.loadSession(encodedAddress));
|
|
443
|
+
ctx.sessions[encodedAddress] = val;
|
|
444
|
+
ctx.dbQueries++;
|
|
445
|
+
return val;
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
async storeSession(encodedAddress, record) {
|
|
449
|
+
const ctx = txStorage.getStore();
|
|
450
|
+
if (!ctx) return state.storeSession(encodedAddress, record);
|
|
451
|
+
ctx.sessions[encodedAddress] = record;
|
|
452
|
+
ctx.mutations.sessions[encodedAddress] = record;
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
async deleteSession(encodedAddress) {
|
|
456
|
+
const ctx = txStorage.getStore();
|
|
457
|
+
if (!ctx) return state.deleteSession(encodedAddress);
|
|
458
|
+
ctx.sessions[encodedAddress] = null;
|
|
459
|
+
ctx.mutations.sessions[encodedAddress] = null;
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
// ── Pre-keys ──────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
async loadPreKey(keyId) {
|
|
465
|
+
const ctx = txStorage.getStore();
|
|
466
|
+
const k = String(keyId);
|
|
467
|
+
if (!ctx) return state.loadPreKey(keyId);
|
|
468
|
+
if (k in ctx.preKeys) return ctx.preKeys[k];
|
|
469
|
+
const val = await getMutex('preKey').runExclusive(() => state.loadPreKey(keyId));
|
|
470
|
+
ctx.preKeys[k] = val;
|
|
471
|
+
ctx.dbQueries++;
|
|
472
|
+
return val;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
async storePreKey(keyId, keyPair) {
|
|
476
|
+
const ctx = txStorage.getStore();
|
|
477
|
+
const k = String(keyId);
|
|
478
|
+
if (!ctx) return state.storePreKey(keyId, keyPair);
|
|
479
|
+
ctx.preKeys[k] = keyPair;
|
|
480
|
+
ctx.mutations.preKeys[k] = keyPair;
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
async removePreKey(keyId) {
|
|
484
|
+
const ctx = txStorage.getStore();
|
|
485
|
+
const k = String(keyId);
|
|
486
|
+
if (!ctx) return state.removePreKey(keyId);
|
|
487
|
+
ctx.preKeys[k] = null;
|
|
488
|
+
ctx.mutations.preKeys[k] = null;
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// ── Signed pre-keys ───────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
async loadSignedPreKey(keyId) {
|
|
494
|
+
const ctx = txStorage.getStore();
|
|
495
|
+
const k = String(keyId);
|
|
496
|
+
if (!ctx) return state.loadSignedPreKey(keyId);
|
|
497
|
+
if (k in ctx.signedPreKeys) return ctx.signedPreKeys[k];
|
|
498
|
+
const val = await getMutex('signedPreKey').runExclusive(() => state.loadSignedPreKey(keyId));
|
|
499
|
+
ctx.signedPreKeys[k] = val;
|
|
500
|
+
ctx.dbQueries++;
|
|
501
|
+
return val;
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
async storeSignedPreKey(keyId, keyPair) {
|
|
505
|
+
const ctx = txStorage.getStore();
|
|
506
|
+
const k = String(keyId);
|
|
507
|
+
if (!ctx) return state.storeSignedPreKey(keyId, keyPair);
|
|
508
|
+
ctx.signedPreKeys[k] = keyPair;
|
|
509
|
+
ctx.mutations.signedPreKeys[k] = keyPair;
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
async removeSignedPreKey(keyId) {
|
|
513
|
+
const ctx = txStorage.getStore();
|
|
514
|
+
const k = String(keyId);
|
|
515
|
+
if (!ctx) return state.removeSignedPreKey(keyId);
|
|
516
|
+
ctx.signedPreKeys[k] = null;
|
|
517
|
+
ctx.mutations.signedPreKeys[k] = null;
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
// ── Identity keys ─────────────────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
async loadIdentityKey(identifier) {
|
|
523
|
+
const ctx = txStorage.getStore();
|
|
524
|
+
if (!ctx) return state.loadIdentityKey(identifier);
|
|
525
|
+
if (identifier in ctx.identities) return ctx.identities[identifier];
|
|
526
|
+
const val = await getMutex('identity').runExclusive(() => state.loadIdentityKey(identifier));
|
|
527
|
+
ctx.identities[identifier] = val;
|
|
528
|
+
ctx.dbQueries++;
|
|
529
|
+
return val;
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
async saveIdentity(identifier, identityKey) {
|
|
533
|
+
const ctx = txStorage.getStore();
|
|
534
|
+
if (!ctx) return state.saveIdentity(identifier, identityKey);
|
|
535
|
+
ctx.identities[identifier] = identityKey;
|
|
536
|
+
ctx.mutations.identities[identifier] = identityKey;
|
|
537
|
+
// Inside a transaction we can't know yet if the key changed — return false
|
|
538
|
+
// (the actual changed-flag will be resolved at commit time by the store).
|
|
539
|
+
return false;
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
// ── Transaction control ───────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
/** Returns true if the current async context is inside a transaction. */
|
|
545
|
+
isInTransaction,
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Run `work` inside a transaction scoped to `key`.
|
|
549
|
+
* @param {function} work async function to execute
|
|
550
|
+
* @param {string} key mutex key (typically a key type or custom string)
|
|
551
|
+
*/
|
|
552
|
+
transaction: async (work, key) => {
|
|
553
|
+
key = key || 'default';
|
|
554
|
+
const existing = txStorage.getStore();
|
|
555
|
+
|
|
556
|
+
// Nested transaction — reuse the enclosing context
|
|
557
|
+
if (existing) {
|
|
558
|
+
logger && logger.trace('reusing existing transaction context');
|
|
559
|
+
return work();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const mutex = getMutex(key);
|
|
563
|
+
acquireRef(key);
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
return await mutex.runExclusive(async () => {
|
|
567
|
+
const ctx = makeCtx();
|
|
568
|
+
logger && logger.trace('entering transaction');
|
|
569
|
+
try {
|
|
570
|
+
const result = await txStorage.run(ctx, work);
|
|
571
|
+
await commitWithRetry(ctx);
|
|
572
|
+
logger && logger.trace({ dbQueries: ctx.dbQueries }, 'transaction completed');
|
|
573
|
+
return result;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
logger && logger.error({ error }, 'transaction failed, rolling back');
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
} finally {
|
|
580
|
+
releaseRef(key);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
587
|
+
// 3. assertMeId
|
|
588
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Returns the authenticated user's JID (phone@s.whatsapp.net) or throws
|
|
592
|
+
* a descriptive error if the store is not yet fully authenticated.
|
|
593
|
+
*
|
|
594
|
+
* Equivalent to Baileys' assertMeId(creds) but for whalibmob's store format.
|
|
595
|
+
* Use this anywhere we'd otherwise reach for `store.phoneNumber` directly,
|
|
596
|
+
* to fail fast with a clear error instead of a silent null-reference crash.
|
|
597
|
+
*
|
|
598
|
+
* @param {object} store — whalibmob store object (from createNewStore / loadStore)
|
|
599
|
+
* @returns {string} — JID string e.g. "40712345678@s.whatsapp.net"
|
|
600
|
+
* @throws {Error} — if store is not initialised or not yet registered
|
|
601
|
+
*/
|
|
602
|
+
function assertMeId(store) {
|
|
603
|
+
if (!store || !store.phoneNumber) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
'Cannot proceed: store has no phoneNumber — call createNewStore() first'
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (!store.registered) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
'Cannot proceed: device is not registered yet — ' +
|
|
611
|
+
'complete SMS verification with verifyCode() before sending messages'
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const phone = String(store.phoneNumber).replace(/^\+/, '').replace(/\D/g, '');
|
|
615
|
+
return `${phone}@s.whatsapp.net`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
619
|
+
// 4. initAuthCreds
|
|
620
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Creates a fresh set of whalibmob auth credentials (key pairs, registration ID,
|
|
624
|
+
* device identifiers) for a given phone number.
|
|
625
|
+
*
|
|
626
|
+
* Equivalent to Baileys' initAuthCreds() but returns whalibmob's store format
|
|
627
|
+
* (Buffer key pairs, signedPreKey as { id, public, private, signature }).
|
|
628
|
+
*
|
|
629
|
+
* Additional Baileys-compatible fields (nextPreKeyId, firstUnuploadedPreKeyId,
|
|
630
|
+
* accountSyncCounter, accountSettings, advSecretKey, etc.) are appended so
|
|
631
|
+
* code that was written against Baileys' credential shape works unchanged.
|
|
632
|
+
*
|
|
633
|
+
* @param {string|number} phoneNumber — full international number (e.g. "40712345678")
|
|
634
|
+
* @param {object} options — optional overrides:
|
|
635
|
+
* registered {boolean} — mark credentials as already registered (default false)
|
|
636
|
+
* name {string} — WhatsApp display name (default 'User')
|
|
637
|
+
* @returns {object} fresh whalibmob store / credential object
|
|
638
|
+
*/
|
|
639
|
+
function initAuthCreds(phoneNumber, options) {
|
|
640
|
+
if (!phoneNumber) throw new Error('initAuthCreds: phoneNumber is required');
|
|
641
|
+
options = options || {};
|
|
642
|
+
|
|
643
|
+
// Use whalibmob's own key-generation machinery (curve25519-js, proper prefixes)
|
|
644
|
+
const store = createNewStore(phoneNumber);
|
|
645
|
+
|
|
646
|
+
// ── Extra Baileys-compatible fields ──────────────────────────────────────────
|
|
647
|
+
// These are not required by whalibmob internally but are commonly expected by
|
|
648
|
+
// application code that was originally written against Baileys.
|
|
649
|
+
|
|
650
|
+
/** Next pre-key ID to generate (monotonically increasing). */
|
|
651
|
+
store.nextPreKeyId = 1;
|
|
652
|
+
|
|
653
|
+
/** First pre-key ID not yet uploaded to the WhatsApp server. */
|
|
654
|
+
store.firstUnuploadedPreKeyId = 1;
|
|
655
|
+
|
|
656
|
+
/** Sync counter used in account-sync IQ stanzas. */
|
|
657
|
+
store.accountSyncCounter = 0;
|
|
658
|
+
|
|
659
|
+
/** Account-level settings mirroring Baileys' IAccountSettings shape. */
|
|
660
|
+
store.accountSettings = { unarchiveChats: false };
|
|
661
|
+
|
|
662
|
+
/** Processed history messages — used to avoid duplicate handling on reconnect. */
|
|
663
|
+
store.processedHistoryMessages = [];
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 32-byte random secret used for ADV (multi-device) poll encryption.
|
|
667
|
+
* Stored as a base64 string, consistent with Baileys' advSecretKey.
|
|
668
|
+
*/
|
|
669
|
+
store.advSecretKey = crypto.randomBytes(32).toString('base64');
|
|
670
|
+
|
|
671
|
+
// Fields that are undefined until populated by the pairing / registration flow
|
|
672
|
+
store.pairingCode = undefined;
|
|
673
|
+
store.lastPropHash = undefined;
|
|
674
|
+
store.routingInfo = undefined;
|
|
675
|
+
store.additionalData = undefined;
|
|
676
|
+
|
|
677
|
+
// Apply optional caller overrides
|
|
678
|
+
if (options.name) store.name = String(options.name);
|
|
679
|
+
if (options.registered) store.registered = !!options.registered;
|
|
680
|
+
|
|
681
|
+
return store;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
685
|
+
// Exports
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
module.exports = {
|
|
689
|
+
makeCacheableSignalKeyStore,
|
|
690
|
+
addTransactionCapability,
|
|
691
|
+
assertMeId,
|
|
692
|
+
initAuthCreds
|
|
693
|
+
};
|