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.
@@ -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
+ };