@oxidezap/baileyrs 0.1.3 → 0.2.0
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 +60 -0
- package/lib/Bridge/primitives.d.ts +49 -3
- package/lib/Bridge/primitives.js +126 -8
- package/lib/Bridge/schema.js +208 -87
- package/lib/Bridge/types.d.ts +61 -2
- package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
- package/lib/Compatibility/derived-stanza-nodes.js +71 -0
- package/lib/Compatibility/encode-proto.d.ts +17 -0
- package/lib/Compatibility/encode-proto.js +32 -0
- package/lib/Compatibility/proto-runtime.d.ts +10 -0
- package/lib/Compatibility/proto-runtime.js +176 -11
- package/lib/Socket/events.js +35 -6
- package/lib/Socket/groups.js +6 -2
- package/lib/Socket/index.js +4 -3
- package/lib/Socket/messages.js +12 -6
- package/lib/Types/Auth.d.ts +4 -1
- package/lib/Types/Events.d.ts +17 -0
- package/lib/Types/Message.d.ts +17 -1
- package/lib/Utils/event-buffer.js +48 -11
- package/lib/Utils/messages.js +30 -7
- package/lib/Utils/process-history-message.d.ts +11 -2
- package/lib/Utils/process-history-message.js +11 -7
- package/lib/Utils/use-multi-file-auth-state.js +45 -0
- package/package.json +9 -3
|
@@ -143,7 +143,14 @@ const append = (data, historyCache, event, eventData, logger) => {
|
|
|
143
143
|
case 'chats.upsert': {
|
|
144
144
|
for (const chat of eventData) {
|
|
145
145
|
const id = chat.id || '';
|
|
146
|
-
|
|
146
|
+
// The history set is only consulted for a chat that *has* an id.
|
|
147
|
+
// Upstream guards the lookup with `id &&`, and this port had dropped
|
|
148
|
+
// it: an id-less chat then folded into whatever id-less entry a
|
|
149
|
+
// buffered history set happened to carry, summing their unread counts,
|
|
150
|
+
// where upstream releases it as its own `chats.upsert`. Found by the
|
|
151
|
+
// buffer differential once history rows started drawing from the same
|
|
152
|
+
// identity pool as live traffic.
|
|
153
|
+
let existing = data.chatUpserts[id] || (id ? data.historySets.chats[id] : undefined);
|
|
147
154
|
if (existing)
|
|
148
155
|
concatChats(existing, chat);
|
|
149
156
|
else {
|
|
@@ -185,14 +192,37 @@ const append = (data, historyCache, event, eventData, logger) => {
|
|
|
185
192
|
case 'contacts.upsert': {
|
|
186
193
|
for (const contact of eventData) {
|
|
187
194
|
const existing = data.contactUpserts[contact.id] || data.historySets.contacts[contact.id];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
// A `contacts.update` already buffered for this id arrived *before*
|
|
196
|
+
// this upsert, so it is folded in first and the upsert's own values
|
|
197
|
+
// win where the two carry the same field. Folding it last — which is
|
|
198
|
+
// what this did — let a stale name overwrite the one that came after
|
|
199
|
+
// it, and the buffer released the older of the two. Fields only the
|
|
200
|
+
// update carried still survive, which upstream drops.
|
|
192
201
|
const pending = data.contactUpdates[contact.id];
|
|
193
|
-
if (pending)
|
|
194
|
-
Object.assign(existing || contact, pending);
|
|
202
|
+
if (pending)
|
|
195
203
|
delete data.contactUpdates[contact.id];
|
|
204
|
+
if (existing) {
|
|
205
|
+
if (pending)
|
|
206
|
+
Object.assign(existing, pending);
|
|
207
|
+
Object.assign(existing, trimUndefined(contact));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Filling the gaps in `contact` rather than merging onto `pending`
|
|
211
|
+
// and storing that: `pending` was grown from `{}` by the update
|
|
212
|
+
// branch, and accumulating into it — or into a fresh literal —
|
|
213
|
+
// measured about twice the cost of writing into the contact the
|
|
214
|
+
// caller handed us, which arrives with a settled shape. An
|
|
215
|
+
// explicitly-`undefined` field counts as absent, which is what
|
|
216
|
+
// `trimUndefined` would decide and costs no second pass.
|
|
217
|
+
if (pending) {
|
|
218
|
+
const target = contact;
|
|
219
|
+
const source = pending;
|
|
220
|
+
for (const field in source) {
|
|
221
|
+
if (target[field] === undefined)
|
|
222
|
+
target[field] = source[field];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
data.contactUpserts[contact.id] = contact;
|
|
196
226
|
}
|
|
197
227
|
}
|
|
198
228
|
break;
|
|
@@ -326,17 +356,21 @@ const consolidateEvents = (data) => {
|
|
|
326
356
|
if (Array.isArray(values) && values.length > 0)
|
|
327
357
|
events[event] = values;
|
|
328
358
|
};
|
|
359
|
+
// The order of these writes is the contract, not an implementation detail.
|
|
360
|
+
// A flush walks `Object.keys()` of this map, so insertion order decides both
|
|
361
|
+
// the key order a `process()` handler iterates and the order the individual
|
|
362
|
+
// events are re-dispatched to `.on()` listeners. Handlers that assume
|
|
363
|
+
// upstream's sequence — messages before the contacts they reference — see a
|
|
364
|
+
// different interleaving if these move. Keep them aligned with upstream's
|
|
365
|
+
// `consolidateEvents`.
|
|
329
366
|
assignArray('chats.upsert', Object.values(data.chatUpserts));
|
|
330
367
|
assignArray('chats.update', Object.values(data.chatUpdates));
|
|
331
368
|
assignArray('chats.delete', [...data.chatDeletes]);
|
|
332
|
-
assignArray('contacts.upsert', Object.values(data.contactUpserts));
|
|
333
|
-
assignArray('contacts.update', Object.values(data.contactUpdates));
|
|
334
|
-
assignArray('messages.update', Object.values(data.messageUpdates));
|
|
335
|
-
assignArray('groups.update', Object.values(data.groupUpdates));
|
|
336
369
|
const upserts = Object.values(data.messageUpserts);
|
|
337
370
|
if (upserts.length) {
|
|
338
371
|
events['messages.upsert'] = { messages: upserts.map(item => item.message), type: upserts[0].type };
|
|
339
372
|
}
|
|
373
|
+
assignArray('messages.update', Object.values(data.messageUpdates));
|
|
340
374
|
const deleted = Object.values(data.messageDeletes);
|
|
341
375
|
if (deleted.length)
|
|
342
376
|
events['messages.delete'] = { keys: deleted };
|
|
@@ -346,6 +380,9 @@ const consolidateEvents = (data) => {
|
|
|
346
380
|
const receipts = Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) => userReceipt.map(receipt => ({ key, receipt })));
|
|
347
381
|
if (receipts.length)
|
|
348
382
|
events['message-receipt.update'] = receipts;
|
|
383
|
+
assignArray('contacts.upsert', Object.values(data.contactUpserts));
|
|
384
|
+
assignArray('contacts.update', Object.values(data.contactUpdates));
|
|
385
|
+
assignArray('groups.update', Object.values(data.groupUpdates));
|
|
349
386
|
return events;
|
|
350
387
|
};
|
|
351
388
|
/**
|
package/lib/Utils/messages.js
CHANGED
|
@@ -228,22 +228,45 @@ export const generateForwardMessageContent = (message, forceForward) => {
|
|
|
228
228
|
throw new Boom('no content in message', { statusCode: 400 });
|
|
229
229
|
}
|
|
230
230
|
content = normalizeMessageContent(content);
|
|
231
|
-
// Shallow clone —
|
|
231
|
+
// Shallow clone of the outer map — one entry on it is rewritten below.
|
|
232
232
|
content = { ...content };
|
|
233
233
|
let key = Object.keys(content)[0];
|
|
234
234
|
let score = content?.[key]?.contextInfo?.forwardingScore || 0;
|
|
235
235
|
score += message.key.fromMe && !forceForward ? 0 : 1;
|
|
236
|
+
const contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
|
|
237
|
+
// The nested message object is the *caller's*, reached through the shallow
|
|
238
|
+
// clone above, so writing `contextInfo` onto it wrote into their argument.
|
|
239
|
+
// And it replaces rather than merges: a caller who forwarded a quoted message
|
|
240
|
+
// found `stanzaId` and `participant` gone from their own object afterwards.
|
|
241
|
+
// Upstream leaves the argument untouched.
|
|
242
|
+
//
|
|
243
|
+
// Rebuilt rather than deep-copied, and only the one object being written.
|
|
244
|
+
// Upstream's copy is `proto.Message.decode(proto.Message.encode(content))` —
|
|
245
|
+
// a full serialise/parse round trip on a hot send path — which is not worth
|
|
246
|
+
// paying to fix an aliasing bug.
|
|
236
247
|
if (key === 'conversation') {
|
|
237
|
-
|
|
248
|
+
// This object is created here, so nothing of the caller's is aliased and
|
|
249
|
+
// the `contextInfo` goes straight in. Same allocation count as before.
|
|
250
|
+
content.extendedTextMessage = { text: content[key], contextInfo };
|
|
238
251
|
delete content.conversation;
|
|
239
252
|
key = 'extendedTextMessage';
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
return content;
|
|
254
|
+
}
|
|
255
|
+
const nested = content[key];
|
|
256
|
+
// A plain object is the only thing worth copying, and the only thing that can
|
|
257
|
+
// be the caller's to damage. Anything else — an absent slot on an empty
|
|
258
|
+
// message, a primitive, an array — keeps the original assignment, which
|
|
259
|
+
// throws for exactly the inputs it threw for before and that upstream throws
|
|
260
|
+
// for too. Spreading those instead invented a property named `"undefined"`
|
|
261
|
+
// where upstream raised a TypeError.
|
|
262
|
+
if (typeof nested === 'object' && nested !== null && !Array.isArray(nested)) {
|
|
263
|
+
// One shallow spread, of exactly the object being modified. This is the
|
|
264
|
+
// whole cost of the fix, and it is unavoidable: not writing into the
|
|
265
|
+
// caller's object means writing into a different one.
|
|
266
|
+
content[key] = { ...nested, contextInfo };
|
|
244
267
|
}
|
|
245
268
|
else {
|
|
246
|
-
|
|
269
|
+
nested.contextInfo = contextInfo;
|
|
247
270
|
}
|
|
248
271
|
return content;
|
|
249
272
|
};
|
|
@@ -36,6 +36,15 @@ export declare const processHistoryMessage: (item: proto.IHistorySync, logger?:
|
|
|
36
36
|
export declare const downloadHistory: (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => Promise<proto.HistorySync>;
|
|
37
37
|
/** Resolve inline or external history-sync content and normalize its public payload. */
|
|
38
38
|
export declare const downloadAndProcessHistorySyncNotification: (msg: proto.Message.IHistorySyncNotification, options: RequestInit, logger?: ILogger) => Promise<ProcessedHistorySync>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Extract a history-sync notification through the same wrapper normalization as
|
|
41
|
+
* upstream.
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` when the message carries none. It used to throw a Boom
|
|
44
|
+
* 400, which breaks the shape every caller writes against a drop-in API —
|
|
45
|
+
* `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
|
|
46
|
+
* "Absent" is the ordinary case here, not an error: any message that is not a
|
|
47
|
+
* history sync takes this path.
|
|
48
|
+
*/
|
|
49
|
+
export declare const getHistoryMsg: (message: proto.IMessage) => proto.Message.IHistorySyncNotification | undefined;
|
|
41
50
|
//# sourceMappingURL=process-history-message.d.ts.map
|
|
@@ -14,7 +14,6 @@ import { inflateZlib } from '@oxidezap/whatsapp-rust-bridge';
|
|
|
14
14
|
import { proto } from '../WAProto/runtime.js';
|
|
15
15
|
import { WAProto } from '../Types/index.js';
|
|
16
16
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/jid-utils.js';
|
|
17
|
-
import { Boom } from './boom.js';
|
|
18
17
|
import { toNumber } from './generics.js';
|
|
19
18
|
import { downloadContentFromMessage, normalizeMessageContent } from './messages.js';
|
|
20
19
|
import { createSparseArray } from './sparse-array.js';
|
|
@@ -199,13 +198,18 @@ export const downloadAndProcessHistorySyncNotification = async (msg, options, lo
|
|
|
199
198
|
: await downloadHistory(msg, options);
|
|
200
199
|
return processHistoryMessage(historyMsg, logger);
|
|
201
200
|
};
|
|
202
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Extract a history-sync notification through the same wrapper normalization as
|
|
203
|
+
* upstream.
|
|
204
|
+
*
|
|
205
|
+
* Returns `undefined` when the message carries none. It used to throw a Boom
|
|
206
|
+
* 400, which breaks the shape every caller writes against a drop-in API —
|
|
207
|
+
* `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
|
|
208
|
+
* "Absent" is the ordinary case here, not an error: any message that is not a
|
|
209
|
+
* history sync takes this path.
|
|
210
|
+
*/
|
|
203
211
|
export const getHistoryMsg = (message) => {
|
|
204
212
|
const normalizedContent = message ? normalizeMessageContent(message) : undefined;
|
|
205
|
-
|
|
206
|
-
if (!historySyncNotification) {
|
|
207
|
-
throw new Boom('Message does not contain a history sync notification', { statusCode: 400 });
|
|
208
|
-
}
|
|
209
|
-
return historySyncNotification;
|
|
213
|
+
return normalizedContent?.protocolMessage?.historySyncNotification ?? undefined;
|
|
210
214
|
};
|
|
211
215
|
//# sourceMappingURL=process-history-message.js.map
|
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { createDeviceProjection } from '../Compatibility/legacy-store/device.js';
|
|
2
3
|
import { projectNativeStore } from '../Compatibility/legacy-store/native-projection.js';
|
|
3
4
|
import { initAuthCreds } from './generics.js';
|
|
4
5
|
import { useBridgeStore } from './use-bridge-store.js';
|
|
6
|
+
/**
|
|
7
|
+
* The store namespace and keys the engine writes its own device under.
|
|
8
|
+
*
|
|
9
|
+
* `<folder>/device-device.bin` and `<folder>/device-account.bin` on disk.
|
|
10
|
+
*/
|
|
11
|
+
const DEVICE_STORE = 'device';
|
|
12
|
+
const DEVICE_RECORDS = ['device', 'account'];
|
|
13
|
+
/**
|
|
14
|
+
* Rebuild the credential mirror from the device the engine persisted.
|
|
15
|
+
*
|
|
16
|
+
* Without this the mirror is whatever `initAuthCreds()` just made up: every
|
|
17
|
+
* restart handed back `registered: false` and `me: undefined` for a session
|
|
18
|
+
* that was paired and working, because nothing on this path ever read the
|
|
19
|
+
* device back. The hydration itself already existed for `wrapLegacyStore`
|
|
20
|
+
* (`Compatibility/legacy-store/adapter.ts`), which only runs for callers that
|
|
21
|
+
* bring their own `{ creds, keys }`; a caller using this function goes straight
|
|
22
|
+
* to the bridge store and used to skip it entirely.
|
|
23
|
+
*
|
|
24
|
+
* A record that fails to decode is skipped rather than fatal: a mirror missing
|
|
25
|
+
* a field is worth less than a socket that will not start, and the engine reads
|
|
26
|
+
* its own device from the same bytes regardless of what this makes of them.
|
|
27
|
+
*/
|
|
28
|
+
const hydrateFromStore = async (store, creds) => {
|
|
29
|
+
const projection = createDeviceProjection(creds);
|
|
30
|
+
for (const record of DEVICE_RECORDS) {
|
|
31
|
+
const payload = await store.get(DEVICE_STORE, record);
|
|
32
|
+
if (!payload)
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
projection.prepare(record, payload)();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* a record we cannot read leaves that part of the mirror at its default */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
5
42
|
/**
|
|
6
43
|
* Creates a file-based authentication state for the Rust bridge.
|
|
7
44
|
*
|
|
@@ -30,6 +67,14 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
30
67
|
}
|
|
31
68
|
const store = await useBridgeStore(folder);
|
|
32
69
|
const creds = initAuthCreds();
|
|
70
|
+
// Before `projectNativeStore`, and that ordering is the whole point: the
|
|
71
|
+
// projection captures `signedIdentityKey.public` and `registrationId` off
|
|
72
|
+
// `creds` when it is called, and builds the Signal codecs around them.
|
|
73
|
+
// Hydrating afterwards would leave those codecs holding the identity of the
|
|
74
|
+
// throwaway `initAuthCreds()` rather than the device's, so a legacy session
|
|
75
|
+
// written through this store would be imported under the wrong local
|
|
76
|
+
// identity.
|
|
77
|
+
await hydrateFromStore(store, creds);
|
|
33
78
|
const keys = projectNativeStore(store, creds);
|
|
34
79
|
return {
|
|
35
80
|
state: { creds, keys, store },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxidezap/baileyrs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"lib/**/*",
|
|
48
48
|
"!lib/**/*.map",
|
|
49
49
|
"!lib/**/__tests__/**",
|
|
50
|
+
"!lib/__fuzz__/**",
|
|
50
51
|
"!lib/**/*.test.*",
|
|
51
52
|
"!lib/**/*.test-e2e.*"
|
|
52
53
|
],
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"compat:audit:missing": "npm run build --silent && node scripts/compatibility/audit.ts --only-missing",
|
|
59
60
|
"compat:audit:proto": "node scripts/compatibility/proto-runtime-audit.ts --details --strict",
|
|
60
61
|
"compat:audit:wire": "node scripts/compatibility/wire-fidelity-audit.ts --details --strict",
|
|
62
|
+
"compat:audit:lifecycle": "node scripts/compatibility/lifecycle-contract-audit.ts --strict",
|
|
61
63
|
"compat:audit:strict": "npm run build --silent && node scripts/compatibility/audit.ts --strict --only-missing",
|
|
62
64
|
"compat:sync-waproto": "node scripts/compatibility/waproto-facade.ts --sync",
|
|
63
65
|
"compat:check-waproto": "node scripts/compatibility/waproto-facade.ts --check",
|
|
@@ -70,13 +72,17 @@
|
|
|
70
72
|
"prepack": "npm run build && node scripts/check-pack.ts",
|
|
71
73
|
"prepare": "npm run build",
|
|
72
74
|
"test": "node --test",
|
|
75
|
+
"fuzz": "node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
|
|
76
|
+
"fuzz:deep": "FUZZ_MODE=deep node --expose-gc --test --test-timeout=1800000 ./src/__fuzz__/**/*.test.ts",
|
|
77
|
+
"fuzz:record": "FUZZ_RECORD=1 node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
|
|
78
|
+
"fuzz:report": "node scripts/fuzz/report.ts",
|
|
73
79
|
"test:compat-auditor": "node --test scripts/compatibility/__tests__/audit.test.ts",
|
|
74
80
|
"typecheck:compat-auditor": "npm run build --silent && npm run compat:check-waproto --silent && npm run compat:layers --silent && tsc -p scripts/compatibility/tsconfig.json",
|
|
75
|
-
"test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts"
|
|
81
|
+
"test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts ./scripts/compatibility/__tests__/*.test-e2e.ts"
|
|
76
82
|
},
|
|
77
83
|
"dependencies": {
|
|
78
84
|
"@hapi/boom": "^9.1.4",
|
|
79
|
-
"@oxidezap/whatsapp-rust-bridge": "0.
|
|
85
|
+
"@oxidezap/whatsapp-rust-bridge": "0.11.0",
|
|
80
86
|
"long": "^5.3.2",
|
|
81
87
|
"pino": "^10.3.1",
|
|
82
88
|
"protobufjs": "^7.6.5"
|