whalibmob 5.10.9 → 5.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +63 -24
  2. package/lib/auth-utils.js +357 -330
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -928,55 +928,65 @@ const {
928
928
 
929
929
  ### `makeCacheableSignalKeyStore`
930
930
 
931
- Wraps a `SignalStore` with an in-memory NodeCache layer (5-minute TTL). All `get` calls for `sessions`, `preKeys`, `signedPreKeys`, and `identities` are served from cache on subsequent accesses. Writes invalidate the cache automatically.
931
+ Wraps a `SignalStore` with an in-memory cache (5-minute TTL). Reads for sessions, pre-keys, signed pre-keys and identity keys are served from cache on subsequent accesses; `store*` calls write through to both, and `remove*` / `delete*` calls drop the entry.
932
+
933
+ A lookup that finds nothing is **not** cached. Absence is the state most likely to change from underneath — a session about to be built, a pre-key about to be uploaded — so a remembered miss is the one that would hurt.
932
934
 
933
935
  `useClones` is set to `false` so that `SessionRecord` objects — which carry internal state and methods — are returned by reference and never deep-cloned.
934
936
 
935
- The wrapper also forwards `transaction()` and `isInTransaction()` calls to the underlying store when present, making it safe to stack with `addTransactionCapability`.
937
+ Every method the underlying store has is forwarded, including `transaction()` and `isInTransaction()` when present, so the wrapper is a drop-in replacement and safe to stack with `addTransactionCapability`.
936
938
 
937
939
  ```js
938
- const { SignalStore } = require('whalibmob')
939
- const { makeCacheableSignalKeyStore } = require('whalibmob')
940
+ const { SignalStore, makeCacheableSignalKeyStore } = require('whalibmob')
940
941
 
941
- const store = new SignalStore(/* ... */)
942
+ const store = new SignalStore()
942
943
  const cached = makeCacheableSignalKeyStore(store)
943
944
 
944
945
  // reads hit cache after first access
945
- const session = await cached.getSession('919634847671@s.whatsapp.net:0')
946
+ const session = await cached.loadSession('919634847671.0')
946
947
  ```
947
948
 
949
+ Call `await cached.flushCache()` to drop everything — after a key rotation, or when another process may have written to the same session file.
950
+
948
951
  **When to use:** whenever your `SignalStore` is backed by a remote or disk-based store (database, Redis, file system) and you want to reduce repeated lookups for sessions that haven't changed between sends.
949
952
 
950
953
  ### `addTransactionCapability`
951
954
 
952
- Wraps a `SignalStore` with batched-write (transaction) semantics. During a transaction all writes are buffered in memory; they are flushed to the underlying store atomically when `commit()` is called at the end of the transaction.
955
+ Wraps a `SignalStore` with batched-write (transaction) semantics. During a transaction all writes are buffered in memory and flushed to the underlying store in one shot when the callback returns — there is no `commit()` to call. Reads check the buffer first, so a transaction sees its own writes. If the callback throws, nothing is written at all and the error reaches the caller.
953
956
 
954
- Uses `AsyncLocalStorage` to propagate transaction context across async call chains, and a per-key-type `Mutex` with reference-counting to serialize concurrent writers safely.
957
+ Uses `AsyncLocalStorage` to propagate transaction context across async call chains, and a `Mutex` with reference-counting per transaction key.
955
958
 
956
959
  ```js
957
- const { addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
960
+ const { SignalStore, addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
958
961
 
959
962
  // recommended: cache first, then transactions on top
960
- const base = new SignalStore(/* ... */)
961
- const cached = makeCacheableSignalKeyStore(base)
962
- const txnStore = addTransactionCapability(cached)
963
+ const base = new SignalStore()
964
+ const cached = makeCacheableSignalKeyStore(base)
965
+ const txnStore = addTransactionCapability(cached)
963
966
 
964
967
  // inside a send flow
965
968
  await txnStore.transaction(async () => {
966
- // all writes are buffered
967
- await txnStore.setSession('919634847671@s.whatsapp.net:0', sessionRecord)
968
- await txnStore.setPreKey(1, preKeyPair)
969
- // commit is called automatically at the end of the transaction callback
970
- })
969
+ // all writes are buffered until this callback returns
970
+ await txnStore.storeSession('919634847671.0', sessionRecord)
971
+ await txnStore.storePreKey(1, preKeyPair)
972
+ }, 'send')
971
973
  ```
972
974
 
973
- Stacking order matters: put `makeCacheableSignalKeyStore` below `addTransactionCapability` so that the cache always sees the committed state.
975
+ The second argument is a scope: two transactions with different keys run concurrently, two with the same key run one after the other. It defaults to `'default'`. Any key is safe — the transaction locks are kept separate from the locks reads take, so no choice of key can make a transaction wait on itself.
976
+
977
+ Nested `transaction()` calls reuse the enclosing context instead of opening a second one, so an inner transaction does not commit on its own.
978
+
979
+ Stacking order matters: put `makeCacheableSignalKeyStore` below `addTransactionCapability`, so that a transaction which rolls back never reaches the cache. The other order works too — a failed transaction flushes the cache rather than leave it holding writes the store never took — but it throws away good entries to do it.
974
980
 
975
981
  **When to use:** for high-throughput servers that send to many recipients concurrently and need to batch Signal key writes into a single atomic flush per message.
976
982
 
977
983
  ### `assertMeId`
978
984
 
979
- Validates that a store object has a registered phone number and returns the canonical `@s.whatsapp.net` JID. Throws an `Error` if the store lacks a `phoneNumber` or has `registered !== true`.
985
+ Returns the account's JID, or throws an `Error` describing what is missing.
986
+
987
+ Works with both kinds of store — the SMS store from `initAuthCreds` / `createNewStore`, and the companion store from `createNewWebStore`. Only the wording of the error differs, since the way out of "not registered yet" is SMS verification in one case and pairing in the other.
988
+
989
+ Once registered, the JID the server assigned is preferred (`store.me.id`) — on a companion it carries the device suffix, and rebuilding it from the phone number drops that silently. Before registration it throws, including during the pairing window: requesting a pairing code writes a placeholder `me` with no suffix, and that is not treated as being linked.
980
990
 
981
991
  ```js
982
992
  const { assertMeId } = require('whalibmob')
@@ -985,7 +995,8 @@ const store = loadStore(sessFile)
985
995
 
986
996
  try {
987
997
  const jid = assertMeId(store)
988
- // jid === '919634847671@s.whatsapp.net'
998
+ // '919634847671:12@s.whatsapp.net' once connected,
999
+ // '919634847671@s.whatsapp.net' before that
989
1000
  console.log('account JID:', jid)
990
1001
  } catch (err) {
991
1002
  console.error('store is not registered:', err.message)
@@ -996,7 +1007,9 @@ try {
996
1007
 
997
1008
  ### `initAuthCreds`
998
1009
 
999
- Creates a fresh credential store for the given phone number. Functionally equivalent to `createNewStore` but also initialises the extra fields the library expects for account sync: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, and `advSecretKey`.
1010
+ Creates a fresh credential store for the given phone number — the key pairs, registration ID and device identifiers a number needs before it can even ask for an SMS code. This is what `/reg code` calls.
1011
+
1012
+ It returns everything `createNewStore` does, plus a few fields kept for application code that expects them: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, `processedHistoryMessages` and `advSecretKey`. Those extras live on the object only — `saveStore` does not write them, so they are not there again after a reload. Nothing in the library reads them; treat them as a convenience, not as state.
1000
1013
 
1001
1014
  ```js
1002
1015
  const { initAuthCreds, saveStore } = require('whalibmob')
@@ -1013,10 +1026,35 @@ const store = initAuthCreds(phone)
1013
1026
  saveStore(store, sessFile)
1014
1027
  ```
1015
1028
 
1016
- This is the function used internally by the CLI for all new session creation. Prefer it over `createNewStore` for forward compatibility.
1029
+ This is the function the CLI uses for every new SMS session. Prefer it over `createNewStore` for forward compatibility.
1017
1030
 
1018
1031
  > [!NOTE]
1019
- > `initAuthCreds` and `createNewStore` produce equivalent stores for all current library operations. The additional fields from `initAuthCreds` are there for future-proofing and interoperability.
1032
+ > `initAuthCreds` and `createNewStore` produce equivalent stores for every current library operation, and identical files on disk. Use `createNewWebStore` instead when the device will be linked as a companion rather than registered by SMS — that one adds the pairing fields, and its own serialiser persists them.
1033
+
1034
+ The file it writes has exactly these 16 keys:
1035
+
1036
+ ```json
1037
+ {
1038
+ "phoneNumber": "40712345678",
1039
+ "noiseKeyPair": { "private": "…", "public": "…" },
1040
+ "identityKeyPair": { "private": "…", "public": "…" },
1041
+ "signedPreKey": { "id": 3649616, "private": "…", "public": "…", "signature": "…" },
1042
+ "registrationId": 2183,
1043
+ "fdid": "2f701f8b-d693-4728-…",
1044
+ "deviceId": "CBIyyJRAokOdYoTYEjTbng==",
1045
+ "identityId": "SE8Q785oful7dGdBgNpwjg==",
1046
+ "advertisingId": "5c97eea9-783f-4fcc-…",
1047
+ "backupToken": "…",
1048
+ "registered": true,
1049
+ "codePending": false,
1050
+ "name": "Boss",
1051
+ "version": "2.26.9.75",
1052
+ "device": { "os": "ios", "platform": 1, "model": "iPhone 15 Pro", "…": "…" },
1053
+ "advIdentity": "…"
1054
+ }
1055
+ ```
1056
+
1057
+ `registered` and `codePending` are the two that move: both `false` when the store is created, `codePending` flips to `true` once a code has been requested, and `verifyCode` sets `registered` to `true` and `codePending` back to `false`. `advIdentity` stays `null` until the server sends the signed device identity in `<success>`.
1020
1058
 
1021
1059
  ### Recommended Stacking Pattern
1022
1060
 
@@ -1036,7 +1074,8 @@ const {
1036
1074
  let store = loadStore(sessFile) || initAuthCreds(phone)
1037
1075
 
1038
1076
  // 2. build the layered Signal key store
1039
- const signalStore = new SignalStore(store)
1077
+ const signalStore = new SignalStore()
1078
+ signalStore.attachFile(sessFile)
1040
1079
  const cachedStore = makeCacheableSignalKeyStore(signalStore)
1041
1080
  const txnStore = addTransactionCapability(cachedStore)
1042
1081
 
package/lib/auth-utils.js CHANGED
@@ -2,26 +2,31 @@
2
2
 
3
3
  // ─── auth-utils.js ────────────────────────────────────────────────────────────
4
4
  //
5
- // Four utilities built around whalibmob's architecture (method-based
6
- // SignalStore, Buffer key pairs, etc.):
5
+ // Optional helpers for code that manages its own SignalStore — a custom storage
6
+ // backend, a multi-account server, anything that wants caching or transactional
7
+ // writes around the store the library would otherwise use directly.
7
8
  //
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
9
+ // 1. makeCacheableSignalKeyStore — an in-memory cache with a 5-minute TTL in
10
+ // front of the store, serialised by a Mutex
11
+ // 2. addTransactionCapability — buffered writes committed in one shot,
12
+ // scoped with AsyncLocalStorage
13
+ // 3. assertMeId — the account's JID, or a clear error
14
+ // 4. initAuthCreds — a fresh credential set for a phone number
14
15
  //
15
- // Usage example:
16
- // const { SignalStore } = require('whalibmob');
16
+ // Stacking order matters. The cache belongs *underneath* the transaction:
17
+ //
18
+ // const { SignalStore } = require('whalibmob');
17
19
  // const { makeCacheableSignalKeyStore,
18
- // addTransactionCapability,
19
- // assertMeId, initAuthCreds } = require('whalibmob/lib/auth-utils');
20
+ // addTransactionCapability } = require('whalibmob/lib/auth-utils');
21
+ //
22
+ // const raw = new SignalStore();
23
+ // const cached = makeCacheableSignalKeyStore(raw, logger);
24
+ // const store = addTransactionCapability(cached, logger);
20
25
  //
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);
26
+ // That way a transaction that throws never reaches the cache at all — its
27
+ // writes are still sitting in the transaction buffer when it is discarded. The
28
+ // other order works too, and a rollback flushes the cache to stay honest, but
29
+ // it throws away entries that were perfectly good.
25
30
  //
26
31
  // ─────────────────────────────────────────────────────────────────────────────
27
32
 
@@ -33,8 +38,14 @@ const { createNewStore } = require('./Store');
33
38
 
34
39
  // ─── Constants ────────────────────────────────────────────────────────────────
35
40
 
36
- /** In-memory TTL for Signal store cache entries (sessions, keys, identities). */
37
- const SIGNAL_STORE_TTL = '5m';
41
+ /**
42
+ * How long a cached entry lives, in seconds.
43
+ *
44
+ * The option is `stdTTL` and the unit is seconds — `ttl`, and a string like
45
+ * '5m', are both accepted without complaint and then ignored, which leaves a
46
+ * cache that never evicts anything and answers from it forever.
47
+ */
48
+ const SIGNAL_STORE_TTL_SECONDS = 5 * 60;
38
49
 
39
50
  /** Default max retry count for transaction commits. */
40
51
  const MAX_COMMIT_RETRIES = 5;
@@ -48,26 +59,63 @@ function _delay(ms) {
48
59
  return new Promise(resolve => setTimeout(resolve, ms));
49
60
  }
50
61
 
62
+ /**
63
+ * Forward every method the wrapper did not define itself.
64
+ *
65
+ * Both wrappers claim to be drop-in replacements, and a hand-written list stops
66
+ * being one the moment the store grows a method — the call does not fail
67
+ * loudly, it lands on `undefined`. Anything the wrapper has an opinion about is
68
+ * already on it and is left alone; the rest is passed straight through.
69
+ */
70
+ function _fillPassthrough(wrapper, store) {
71
+ const seen = new Set();
72
+ let level = store;
73
+
74
+ while (level && level !== Object.prototype) {
75
+ for (const name of Object.getOwnPropertyNames(level)) {
76
+ if (seen.has(name) || name === 'constructor' || name.startsWith('_')) continue;
77
+ seen.add(name);
78
+ if (name in wrapper) continue; // the wrapper handles it
79
+ let fn;
80
+ try { fn = store[name]; } catch (_) { continue; } // skip throwing getters
81
+ if (typeof fn !== 'function') continue;
82
+ wrapper[name] = (...args) => store[name](...args);
83
+ }
84
+ level = Object.getPrototypeOf(level);
85
+ }
86
+
87
+ return wrapper;
88
+ }
89
+
51
90
  // ─────────────────────────────────────────────────────────────────────────────
52
91
  // 1. makeCacheableSignalKeyStore
53
92
  // ─────────────────────────────────────────────────────────────────────────────
54
93
 
55
94
  /**
56
- * Wraps a whalibmob SignalStore with:
57
- * • NodeCache — in-memory cache with a 5-minute TTL for all key types
95
+ * Wraps a SignalStore with:
96
+ * • NodeCache — an in-memory cache with a 5-minute TTL for every key type
58
97
  * (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.
98
+ * Each entry is evicted after that and re-read on the next access, which
99
+ * bounds memory and puts a ceiling on how long another writer's change can
100
+ * go unnoticed.
101
+ * • Mutex — every get/set is serialised, so two concurrent operations cannot
102
+ * interleave a read against a write to the same key.
103
+ *
104
+ * A lookup that finds nothing is *not* cached. Absence is the state most likely
105
+ * to change from underneath — a session about to be built, a pre-key about to
106
+ * be uploaded — and a remembered miss is the one that hurts.
63
107
  *
64
108
  * @param {SignalStore} store - underlying SignalStore instance
65
109
  * @param {object} logger - optional logger with a .trace() method
66
- * @param {NodeCache} _cache - optional pre-built NodeCache (for testing / sharing)
110
+ * @param {NodeCache} _cache - optional pre-built cache (for testing / sharing)
67
111
  * @returns {object} drop-in SignalStore replacement with caching and serialisation
68
112
  */
69
113
  function makeCacheableSignalKeyStore(store, logger, _cache) {
70
- const cache = _cache || new NodeCache({ ttl: SIGNAL_STORE_TTL, useClones: false });
114
+ const cache = _cache || new NodeCache({
115
+ stdTTL: SIGNAL_STORE_TTL_SECONDS,
116
+ useClones: false,
117
+ deleteOnExpire: true
118
+ });
71
119
  const mutex = new Mutex();
72
120
 
73
121
  /** Unified cache key: "type:id" */
@@ -75,8 +123,59 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
75
123
 
76
124
  const log = (msg, data) => logger && logger.trace(data, msg);
77
125
 
78
- return {
79
- // ── Initialisation / file attachment (pass-through) ──────────────────────
126
+ // Read through the cache, remembering only what was actually found.
127
+ function cachedRead(type, id, load) {
128
+ return mutex.runExclusive(async () => {
129
+ const k = cKey(type, id);
130
+ const hit = cache.get(k);
131
+ if (hit !== undefined) {
132
+ log(type + ' cache hit', { id });
133
+ return hit;
134
+ }
135
+ const val = await load();
136
+ if (val !== undefined && val !== null) cache.set(k, val);
137
+ return val;
138
+ });
139
+ }
140
+
141
+ function cachedWrite(type, id, value, write) {
142
+ return mutex.runExclusive(async () => {
143
+ cache.set(cKey(type, id), value);
144
+ return write();
145
+ });
146
+ }
147
+
148
+ function cachedDelete(type, id, remove) {
149
+ return mutex.runExclusive(async () => {
150
+ cache.del(cKey(type, id));
151
+ return remove();
152
+ });
153
+ }
154
+
155
+ // Named rather than reached for through `this`, so a caller may pull a method
156
+ // off the object — `const { loadSession } = cachedStore` — and still call it.
157
+ async function getLocalRegistrationId() {
158
+ return mutex.runExclusive(async () => {
159
+ const hit = cache.get('meta:registrationId');
160
+ if (hit !== undefined) return hit;
161
+ const val = await store.getLocalRegistrationId();
162
+ if (val !== undefined && val !== null) cache.set('meta:registrationId', val);
163
+ return val;
164
+ });
165
+ }
166
+
167
+ async function identity(load) {
168
+ return mutex.runExclusive(async () => {
169
+ const hit = cache.get('meta:identityKeyPair');
170
+ if (hit !== undefined) return hit;
171
+ const val = await load();
172
+ if (val) cache.set('meta:identityKeyPair', val);
173
+ return val;
174
+ });
175
+ }
176
+
177
+ const wrapper = {
178
+ // ── Initialisation ───────────────────────────────────────────────────────
80
179
 
81
180
  init(identityKeyPair, registrationId) {
82
181
  cache.set('meta:identityKeyPair', identityKeyPair);
@@ -84,171 +183,83 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
84
183
  return store.init(identityKeyPair, registrationId);
85
184
  },
86
185
 
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); },
186
+ /** Synchronous — never cached; reads the store's own in-memory session map. */
187
+ hasSession(encodedAddress) { return store.hasSession(encodedAddress); },
94
188
 
95
189
  // ── Identity / registration ───────────────────────────────────────────────
96
190
 
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
- },
191
+ getOurIdentity() { return identity(() => store.getOurIdentity()); },
192
+ getIdentityKeyPair() { return identity(() => store.getIdentityKeyPair()); },
193
+ getLocalRegistrationId,
194
+ getOurRegistrationId() { return getLocalRegistrationId(); },
106
195
 
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. */
196
+ /** Never cached; the store decides trust from its own current state. */
132
197
  async isTrustedIdentity(identifier, identityKey) {
133
198
  return store.isTrustedIdentity(identifier, identityKey);
134
199
  },
135
200
 
136
- async saveIdentity(identifier, identityKey) {
137
- return mutex.runExclusive(async () => {
138
- cache.del(cKey('identity', identifier));
139
- return store.saveIdentity(identifier, identityKey);
140
- });
201
+ saveIdentity(identifier, identityKey) {
202
+ return cachedDelete('identity', identifier,
203
+ () => store.saveIdentity(identifier, identityKey));
141
204
  },
142
205
 
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
- });
206
+ loadIdentityKey(identifier) {
207
+ return cachedRead('identity', identifier,
208
+ () => store.loadIdentityKey(identifier));
155
209
  },
156
210
 
157
- // ── Sessions (hot path — every message send/receive touches these) ────────
211
+ // ── Sessions (hot path — every message send and receive touches these) ────
158
212
 
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
- });
213
+ loadSession(encodedAddress) {
214
+ return cachedRead('session', encodedAddress,
215
+ () => store.loadSession(encodedAddress));
171
216
  },
172
217
 
173
- async storeSession(encodedAddress, record) {
174
- return mutex.runExclusive(async () => {
175
- cache.set(cKey('session', encodedAddress), record);
176
- return store.storeSession(encodedAddress, record);
177
- });
218
+ storeSession(encodedAddress, record) {
219
+ return cachedWrite('session', encodedAddress, record,
220
+ () => store.storeSession(encodedAddress, record));
178
221
  },
179
222
 
180
- async deleteSession(encodedAddress) {
181
- return mutex.runExclusive(async () => {
182
- cache.del(cKey('session', encodedAddress));
183
- return store.deleteSession(encodedAddress);
184
- });
223
+ deleteSession(encodedAddress) {
224
+ return cachedDelete('session', encodedAddress,
225
+ () => store.deleteSession(encodedAddress));
185
226
  },
186
227
 
187
228
  // ── Pre-keys ──────────────────────────────────────────────────────────────
188
229
 
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
- });
230
+ loadPreKey(keyId) {
231
+ return cachedRead('prekey', keyId, () => store.loadPreKey(keyId));
201
232
  },
202
233
 
203
- async storePreKey(keyId, keyPair) {
204
- return mutex.runExclusive(async () => {
205
- cache.set(cKey('prekey', keyId), keyPair);
206
- return store.storePreKey(keyId, keyPair);
207
- });
234
+ storePreKey(keyId, keyPair) {
235
+ return cachedWrite('prekey', keyId, keyPair,
236
+ () => store.storePreKey(keyId, keyPair));
208
237
  },
209
238
 
210
- async removePreKey(keyId) {
211
- return mutex.runExclusive(async () => {
212
- cache.del(cKey('prekey', keyId));
213
- return store.removePreKey(keyId);
214
- });
239
+ removePreKey(keyId) {
240
+ return cachedDelete('prekey', keyId, () => store.removePreKey(keyId));
215
241
  },
216
242
 
217
243
  // ── Signed pre-keys ───────────────────────────────────────────────────────
218
244
 
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
- });
245
+ loadSignedPreKey(keyId) {
246
+ return cachedRead('signedprekey', keyId, () => store.loadSignedPreKey(keyId));
231
247
  },
232
248
 
233
- async storeSignedPreKey(keyId, keyPair) {
234
- return mutex.runExclusive(async () => {
235
- cache.set(cKey('signedprekey', keyId), keyPair);
236
- return store.storeSignedPreKey(keyId, keyPair);
237
- });
249
+ storeSignedPreKey(keyId, keyPair) {
250
+ return cachedWrite('signedprekey', keyId, keyPair,
251
+ () => store.storeSignedPreKey(keyId, keyPair));
238
252
  },
239
253
 
240
- async removeSignedPreKey(keyId) {
241
- return mutex.runExclusive(async () => {
242
- cache.del(cKey('signedprekey', keyId));
243
- return store.removeSignedPreKey(keyId);
244
- });
254
+ removeSignedPreKey(keyId) {
255
+ return cachedDelete('signedprekey', keyId, () => store.removeSignedPreKey(keyId));
245
256
  },
246
257
 
247
258
  // ── Transaction forwarding ────────────────────────────────────────────────
248
- // If the underlying store was wrapped with addTransactionCapability, forward
249
- // transaction() and isInTransaction() so stacking both wrappers works correctly.
259
+ // Present so the cache can sit on top of addTransactionCapability as well as
260
+ // underneath it, even though underneath is the better place for it.
250
261
 
251
- /** Returns true if the current async context is inside a transaction. */
262
+ /** True when the current async context is inside a transaction. */
252
263
  isInTransaction() {
253
264
  return typeof store.isInTransaction === 'function'
254
265
  ? store.isInTransaction()
@@ -256,9 +267,12 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
256
267
  },
257
268
 
258
269
  /**
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.
270
+ * Delegate to the underlying store's transaction, dropping the cache if it
271
+ * does not go through.
272
+ *
273
+ * Writes reach the cache as they are made, but a transaction that throws
274
+ * never commits them — so without this the cache would keep serving values
275
+ * the store never took, and go on doing it until the entries expired.
262
276
  */
263
277
  transaction(work, key) {
264
278
  if (typeof store.transaction !== 'function') {
@@ -267,16 +281,27 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
267
281
  'with addTransactionCapability()'
268
282
  );
269
283
  }
270
- return store.transaction(work, key);
284
+ return (async () => {
285
+ try {
286
+ return await store.transaction(work, key);
287
+ } catch (error) {
288
+ cache.flushAll();
289
+ logger && logger.trace('transaction failed — cache flushed');
290
+ throw error;
291
+ }
292
+ })();
271
293
  },
272
294
 
273
295
  // ── Cache control ─────────────────────────────────────────────────────────
274
296
 
275
- /** Flush all cached entries (e.g. after a key rotation or re-registration). */
297
+ /** Drop every cached entry, here and in anything wrapped below. */
276
298
  async flushCache() {
277
299
  cache.flushAll();
300
+ if (typeof store.flushCache === 'function') await store.flushCache();
278
301
  }
279
302
  };
303
+
304
+ return _fillPassthrough(wrapper, store);
280
305
  }
281
306
 
282
307
  // ─────────────────────────────────────────────────────────────────────────────
@@ -284,16 +309,13 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
284
309
  // ─────────────────────────────────────────────────────────────────────────────
285
310
 
286
311
  /**
287
- * Adds DB-like transaction capability to a whalibmob SignalStore.
312
+ * Adds DB-like transaction capability to a SignalStore.
288
313
  *
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.
314
+ * Within a transaction all writes are buffered in an in-memory context (scoped
315
+ * with AsyncLocalStorage). Reads check the buffer first so they see their own
316
+ * writes. On commit the buffer is flushed to the underlying store in one shot,
317
+ * with retries. Nested transactions reuse the enclosing context, so there is no
318
+ * double commit.
297
319
  *
298
320
  * @param {SignalStore} state - underlying store (may already be cached)
299
321
  * @param {object} logger - optional logger with .trace() / .warn() / .error()
@@ -302,33 +324,47 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
302
324
  */
303
325
  function addTransactionCapability(state, logger, opts) {
304
326
  opts = opts || {};
305
- const maxRetries = opts.maxCommitRetries ?? MAX_COMMIT_RETRIES;
327
+ const maxRetries = opts.maxCommitRetries ?? MAX_COMMIT_RETRIES;
306
328
  const retryDelay = opts.delayBetweenTriesMs ?? RETRY_DELAY_MS;
307
329
 
308
- const txStorage = new AsyncLocalStorage();
309
- const typeMutexes = new Map(); // type → Mutex
310
- const typeMutexRefs = new Map(); // type → refcount
330
+ const txStorage = new AsyncLocalStorage();
331
+
332
+ // Two separate sets of locks, and they must stay separate.
333
+ //
334
+ // A transaction holds its lock for as long as the work runs, and reads inside
335
+ // that work take a lock of their own. Sharing one map means transaction('…',
336
+ // 'session') takes the session lock and then waits for itself the first time
337
+ // it loads a session — a deadlock with no error and no timeout, reachable
338
+ // from nothing worse than an unlucky choice of key.
339
+ const readMutexes = new Map(); // data type → Mutex, held only for one read
340
+ const txMutexes = new Map(); // transaction key → Mutex, held for the work
341
+ const txMutexRefs = new Map(); // transaction key → refcount
342
+
343
+ function getReadMutex(type) {
344
+ if (!readMutexes.has(type)) readMutexes.set(type, new Mutex());
345
+ return readMutexes.get(type);
346
+ }
311
347
 
312
- function getMutex(type) {
313
- if (!typeMutexes.has(type)) {
314
- typeMutexes.set(type, new Mutex());
315
- typeMutexRefs.set(type, 0);
348
+ function getTxMutex(key) {
349
+ if (!txMutexes.has(key)) {
350
+ txMutexes.set(key, new Mutex());
351
+ txMutexRefs.set(key, 0);
316
352
  }
317
- return typeMutexes.get(type);
353
+ return txMutexes.get(key);
318
354
  }
319
355
 
320
- function acquireRef(type) {
321
- typeMutexRefs.set(type, (typeMutexRefs.get(type) ?? 0) + 1);
356
+ function acquireRef(key) {
357
+ txMutexRefs.set(key, (txMutexRefs.get(key) ?? 0) + 1);
322
358
  }
323
359
 
324
- function releaseRef(type) {
325
- const count = (typeMutexRefs.get(type) ?? 1) - 1;
326
- typeMutexRefs.set(type, count);
360
+ function releaseRef(key) {
361
+ const count = (txMutexRefs.get(key) ?? 1) - 1;
362
+ txMutexRefs.set(key, count);
327
363
  if (count <= 0) {
328
- const m = typeMutexes.get(type);
364
+ const m = txMutexes.get(key);
329
365
  if (m && !m.isLocked()) {
330
- typeMutexes.delete(type);
331
- typeMutexRefs.delete(type);
366
+ txMutexes.delete(key);
367
+ txMutexRefs.delete(key);
332
368
  }
333
369
  }
334
370
  }
@@ -340,7 +376,7 @@ function addTransactionCapability(state, logger, opts) {
340
376
  async function commitWithRetry(ctx) {
341
377
  const { sessions, preKeys, signedPreKeys, identities } = ctx.mutations;
342
378
  const total = (
343
- Object.keys(sessions).length +
379
+ Object.keys(sessions).length +
344
380
  Object.keys(preKeys).length +
345
381
  Object.keys(signedPreKeys).length +
346
382
  Object.keys(identities).length
@@ -357,33 +393,22 @@ function addTransactionCapability(state, logger, opts) {
357
393
  const proms = [];
358
394
 
359
395
  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
- }
396
+ proms.push(rec === null ? state.deleteSession(addr)
397
+ : state.storeSession(addr, rec));
365
398
  }
366
399
 
367
400
  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
- }
401
+ proms.push(kp === null ? state.removePreKey(Number(keyId))
402
+ : state.storePreKey(Number(keyId), kp));
373
403
  }
374
404
 
375
405
  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
- }
406
+ proms.push(kp === null ? state.removeSignedPreKey(Number(keyId))
407
+ : state.storeSignedPreKey(Number(keyId), kp));
381
408
  }
382
409
 
383
410
  for (const [identifier, key] of Object.entries(identities)) {
384
- if (key !== null) {
385
- proms.push(state.saveIdentity(identifier, key));
386
- }
411
+ if (key !== null) proms.push(state.saveIdentity(identifier, key));
387
412
  }
388
413
 
389
414
  await Promise.all(proms);
@@ -398,135 +423,101 @@ function addTransactionCapability(state, logger, opts) {
398
423
  }
399
424
  }
400
425
 
401
- // Build the empty mutation / read-buffer context for a new transaction.
426
+ // The empty read buffer and pending writes for a new transaction.
402
427
  function makeCtx() {
403
428
  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
429
+ // Read buffer: what has already been loaded from the underlying store
430
+ sessions: {},
431
+ preKeys: {},
432
+ signedPreKeys: {},
433
+ identities: {},
434
+ // Pending writes: flushed to the store when the transaction ends
410
435
  mutations: {
411
- sessions: {},
412
- preKeys: {},
413
- signedPreKeys:{},
414
- identities: {}
436
+ sessions: {},
437
+ preKeys: {},
438
+ signedPreKeys: {},
439
+ identities: {}
415
440
  },
416
441
  dbQueries: 0
417
442
  };
418
443
  }
419
444
 
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); },
445
+ // Read through the transaction buffer, or straight through when there is no
446
+ // transaction in this async context.
447
+ async function bufferedRead(bucket, type, id, load) {
448
+ const ctx = txStorage.getStore();
449
+ if (!ctx) return load();
450
+ const k = String(id);
451
+ if (k in ctx[bucket]) return ctx[bucket][k];
452
+ const val = await getReadMutex(type).runExclusive(load);
453
+ ctx[bucket][k] = val;
454
+ ctx.dbQueries++;
455
+ return val;
456
+ }
434
457
 
458
+ async function bufferedWrite(bucket, id, value, write) {
459
+ const ctx = txStorage.getStore();
460
+ if (!ctx) return write();
461
+ const k = String(id);
462
+ ctx[bucket][k] = value;
463
+ ctx.mutations[bucket][k] = value;
464
+ }
465
+
466
+ const wrapper = {
435
467
  // ── Session ───────────────────────────────────────────────────────────────
436
468
 
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;
469
+ loadSession(encodedAddress) {
470
+ return bufferedRead('sessions', 'session', encodedAddress,
471
+ () => state.loadSession(encodedAddress));
446
472
  },
447
473
 
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;
474
+ storeSession(encodedAddress, record) {
475
+ return bufferedWrite('sessions', encodedAddress, record,
476
+ () => state.storeSession(encodedAddress, record));
453
477
  },
454
478
 
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;
479
+ deleteSession(encodedAddress) {
480
+ return bufferedWrite('sessions', encodedAddress, null,
481
+ () => state.deleteSession(encodedAddress));
460
482
  },
461
483
 
462
484
  // ── Pre-keys ──────────────────────────────────────────────────────────────
463
485
 
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;
486
+ loadPreKey(keyId) {
487
+ return bufferedRead('preKeys', 'preKey', keyId, () => state.loadPreKey(keyId));
473
488
  },
474
489
 
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;
490
+ storePreKey(keyId, keyPair) {
491
+ return bufferedWrite('preKeys', keyId, keyPair,
492
+ () => state.storePreKey(keyId, keyPair));
481
493
  },
482
494
 
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;
495
+ removePreKey(keyId) {
496
+ return bufferedWrite('preKeys', keyId, null, () => state.removePreKey(keyId));
489
497
  },
490
498
 
491
499
  // ── Signed pre-keys ───────────────────────────────────────────────────────
492
500
 
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;
501
+ loadSignedPreKey(keyId) {
502
+ return bufferedRead('signedPreKeys', 'signedPreKey', keyId,
503
+ () => state.loadSignedPreKey(keyId));
502
504
  },
503
505
 
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;
506
+ storeSignedPreKey(keyId, keyPair) {
507
+ return bufferedWrite('signedPreKeys', keyId, keyPair,
508
+ () => state.storeSignedPreKey(keyId, keyPair));
510
509
  },
511
510
 
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;
511
+ removeSignedPreKey(keyId) {
512
+ return bufferedWrite('signedPreKeys', keyId, null,
513
+ () => state.removeSignedPreKey(keyId));
518
514
  },
519
515
 
520
516
  // ── Identity keys ─────────────────────────────────────────────────────────
521
517
 
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;
518
+ loadIdentityKey(identifier) {
519
+ return bufferedRead('identities', 'identity', identifier,
520
+ () => state.loadIdentityKey(identifier));
530
521
  },
531
522
 
532
523
  async saveIdentity(identifier, identityKey) {
@@ -534,32 +525,39 @@ function addTransactionCapability(state, logger, opts) {
534
525
  if (!ctx) return state.saveIdentity(identifier, identityKey);
535
526
  ctx.identities[identifier] = identityKey;
536
527
  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).
528
+ // The store answers whether the key changed, and that answer only exists
529
+ // once the write lands. Inside a transaction it has not, so say so.
539
530
  return false;
540
531
  },
541
532
 
542
533
  // ── Transaction control ───────────────────────────────────────────────────
543
534
 
544
- /** Returns true if the current async context is inside a transaction. */
535
+ /** True when the current async context is inside a transaction. */
545
536
  isInTransaction,
546
537
 
538
+ /** Forwarded so a cache wrapped underneath can still be flushed. */
539
+ async flushCache() {
540
+ if (typeof state.flushCache === 'function') return state.flushCache();
541
+ },
542
+
547
543
  /**
548
544
  * Run `work` inside a transaction scoped to `key`.
545
+ *
549
546
  * @param {function} work async function to execute
550
- * @param {string} key mutex key (typically a key type or custom string)
547
+ * @param {string} key scope; separate keys run concurrently
551
548
  */
552
549
  transaction: async (work, key) => {
553
550
  key = key || 'default';
554
551
  const existing = txStorage.getStore();
555
552
 
556
- // Nested transaction — reuse the enclosing context
553
+ // Nested transaction — reuse the enclosing context rather than opening a
554
+ // second one that would commit separately.
557
555
  if (existing) {
558
556
  logger && logger.trace('reusing existing transaction context');
559
557
  return work();
560
558
  }
561
559
 
562
- const mutex = getMutex(key);
560
+ const mutex = getTxMutex(key);
563
561
  acquireRef(key);
564
562
 
565
563
  try {
@@ -581,6 +579,8 @@ function addTransactionCapability(state, logger, opts) {
581
579
  }
582
580
  }
583
581
  };
582
+
583
+ return _fillPassthrough(wrapper, state);
584
584
  }
585
585
 
586
586
  // ─────────────────────────────────────────────────────────────────────────────
@@ -588,18 +588,33 @@ function addTransactionCapability(state, logger, opts) {
588
588
  // ─────────────────────────────────────────────────────────────────────────────
589
589
 
590
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.
591
+ * Returns the authenticated account's JID, or throws a descriptive error when
592
+ * the store is not authenticated yet.
593
+ *
594
+ * Use this anywhere the alternative is reaching for `store.phoneNumber` and
595
+ * hoping, so the failure arrives as a sentence rather than as a null reference
596
+ * several frames later.
597
+ *
598
+ * The JID the server assigned is preferred once there is one — on a companion
599
+ * it carries the device suffix, and rebuilding it from the phone number drops
600
+ * that silently. It is only trusted after registration, though: requesting a
601
+ * pairing code writes a placeholder `me` with no suffix before the exchange has
602
+ * happened, and returning that would defeat the point of the check.
593
603
  *
594
- * Use this anywhere we'd otherwise reach for `store.phoneNumber` directly,
595
- * to fail fast with a clear error instead of a silent null-reference crash.
604
+ * Works the same for both kinds of store. Only the wording of the error
605
+ * differs, because the way out of "not registered yet" is not the same.
596
606
  *
597
- * @param {object} store — whalibmob store object (from createNewStore / loadStore)
598
- * @returns {string} — JID string e.g. "40712345678@s.whatsapp.net"
599
- * @throws {Error} — if store is not initialised or not yet registered
607
+ * @param {object} store — store object (from createNewStore / createNewWebStore / loadStore)
608
+ * @returns {string} — JID, e.g. "40712345678:12@s.whatsapp.net"
609
+ * @throws {Error} — when the store is not initialised or not registered
600
610
  */
601
611
  function assertMeId(store) {
602
- if (!store || !store.phoneNumber) {
612
+ if (!store) {
613
+ throw new Error(
614
+ 'Cannot proceed: no store was given — call createNewStore() first'
615
+ );
616
+ }
617
+ if (!store.phoneNumber) {
603
618
  throw new Error(
604
619
  'Cannot proceed: store has no phoneNumber — call createNewStore() first'
605
620
  );
@@ -607,9 +622,15 @@ function assertMeId(store) {
607
622
  if (!store.registered) {
608
623
  throw new Error(
609
624
  'Cannot proceed: device is not registered yet — ' +
610
- 'complete SMS verification with verifyCode() before sending messages'
625
+ (store.mode === 'web'
626
+ ? 'link this device first, with a pairing code or by scanning the QR'
627
+ : 'complete SMS verification with verifyCode() before sending messages')
611
628
  );
612
629
  }
630
+
631
+ const assigned = store.me && store.me.id;
632
+ if (assigned) return String(assigned);
633
+
613
634
  const phone = String(store.phoneNumber).replace(/^\+/, '').replace(/\D/g, '');
614
635
  return `${phone}@s.whatsapp.net`;
615
636
  }
@@ -619,27 +640,32 @@ function assertMeId(store) {
619
640
  // ─────────────────────────────────────────────────────────────────────────────
620
641
 
621
642
  /**
622
- * Creates a fresh set of whalibmob auth credentials (key pairs, registration ID,
623
- * device identifiers) for a given phone number.
643
+ * Creates a fresh set of auth credentials (key pairs, registration ID, device
644
+ * identifiers) for a given phone number.
624
645
  *
625
- * Returns whalibmob's store format (Buffer key pairs, signedPreKey as
626
- * { id, public, private, signature }).
646
+ * Returns the library's store format: Buffer key pairs, and signedPreKey as
647
+ * { id, public, private, signature }.
627
648
  *
628
649
  * A few extra fields (nextPreKeyId, firstUnuploadedPreKeyId, accountSyncCounter,
629
- * accountSettings, advSecretKey, etc.) are appended for account sync and for
650
+ * accountSettings, advSecretKey and so on) are appended for account sync and for
630
651
  * application code that expects them.
631
652
  *
632
- * @param {string|number} phoneNumber — full international number (e.g. "40712345678")
653
+ * This builds a store for the SMS flow. A companion device is registered by
654
+ * pairing rather than by SMS and needs the extra fields that go with it, so it
655
+ * has its own constructor — createNewWebStore — which starts from the same base
656
+ * as this one.
657
+ *
658
+ * @param {string|number} phoneNumber — full international number, e.g. "40712345678"
633
659
  * @param {object} options — optional overrides:
634
- * registered {boolean} — mark credentials as already registered (default false)
635
- * name {string} — WhatsApp display name (default 'User')
636
- * @returns {object} fresh whalibmob store / credential object
660
+ * registered {boolean} — mark the credentials as already registered (default false)
661
+ * name {string} — display name (default 'User')
662
+ * @returns {object} fresh store / credential object
637
663
  */
638
664
  function initAuthCreds(phoneNumber, options) {
639
665
  if (!phoneNumber) throw new Error('initAuthCreds: phoneNumber is required');
640
666
  options = options || {};
641
667
 
642
- // Use whalibmob's own key-generation machinery (curve25519-js, proper prefixes)
668
+ // The library's own key generation — curve25519, with the right prefixes.
643
669
  const store = createNewStore(phoneNumber);
644
670
 
645
671
  // ── Extra credential fields ──────────────────────────────────────────────────
@@ -648,7 +674,7 @@ function initAuthCreds(phoneNumber, options) {
648
674
  /** Next pre-key ID to generate (monotonically increasing). */
649
675
  store.nextPreKeyId = 1;
650
676
 
651
- /** First pre-key ID not yet uploaded to the WhatsApp server. */
677
+ /** First pre-key ID not yet uploaded to the server. */
652
678
  store.firstUnuploadedPreKeyId = 1;
653
679
 
654
680
  /** Sync counter used in account-sync IQ stanzas. */
@@ -657,16 +683,16 @@ function initAuthCreds(phoneNumber, options) {
657
683
  /** Account-level settings. */
658
684
  store.accountSettings = { unarchiveChats: false };
659
685
 
660
- /** Processed history messages — used to avoid duplicate handling on reconnect. */
686
+ /** Processed history messages — avoids handling the same one twice on reconnect. */
661
687
  store.processedHistoryMessages = [];
662
688
 
663
689
  /**
664
- * 32-byte random secret used for ADV (multi-device) poll encryption.
690
+ * 32-byte random secret used for multi-device poll encryption.
665
691
  * Stored as a base64 string.
666
692
  */
667
693
  store.advSecretKey = crypto.randomBytes(32).toString('base64');
668
694
 
669
- // Fields that are undefined until populated by the pairing / registration flow
695
+ // Fields left undefined until the pairing / registration flow fills them in
670
696
  store.pairingCode = undefined;
671
697
  store.lastPropHash = undefined;
672
698
  store.routingInfo = undefined;
@@ -687,5 +713,6 @@ module.exports = {
687
713
  makeCacheableSignalKeyStore,
688
714
  addTransactionCapability,
689
715
  assertMeId,
690
- initAuthCreds
716
+ initAuthCreds,
717
+ SIGNAL_STORE_TTL_SECONDS
691
718
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.9",
3
+ "version": "5.10.12",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",