@oxidezap/baileyrs 0.0.32 → 0.1.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 +77 -24
- package/lib/Bridge/adapt.d.ts +1 -1
- package/lib/Bridge/history-sync-wire.d.ts +1 -1
- package/lib/Bridge/history-sync-wire.js +2 -2
- package/lib/Bridge/schema.d.ts +1 -1
- package/lib/Bridge/schema.js +10 -10
- package/lib/Bridge/types.d.ts +1 -1
- package/lib/Compatibility/group-metadata.d.ts +1 -1
- package/lib/Compatibility/legacy-store/codecs/basic.js +1 -1
- package/lib/Compatibility/legacy-store/codecs/signal.js +17 -5
- package/lib/Compatibility/legacy-store/device.js +1 -1
- package/lib/Compatibility/legacy-store/multi-file.js +1 -1
- package/lib/Compatibility/legacy-store/native-projection.d.ts +1 -1
- package/lib/Compatibility/legacy-store/types.d.ts +1 -1
- package/lib/Compatibility/media-type.d.ts +1 -1
- package/lib/Compatibility/message-relay.d.ts +1 -1
- package/lib/Compatibility/proto-runtime.js +119 -45
- package/lib/Compatibility/socket-results.d.ts +1 -1
- package/lib/Compatibility/stanza-responses.d.ts +1 -1
- package/lib/Compatibility/usync/adapter.d.ts +1 -1
- package/lib/Compatibility/websocket-client.d.ts +24 -3
- package/lib/Compatibility/websocket-client.js +47 -18
- package/lib/Socket/bridge-client-owner.d.ts +89 -0
- package/lib/Socket/bridge-client-owner.js +135 -0
- package/lib/Socket/communities.d.ts +2 -2
- package/lib/Socket/contacts.d.ts +1 -1
- package/lib/Socket/events.d.ts +32 -1
- package/lib/Socket/events.js +140 -37
- package/lib/Socket/index.d.ts +32 -6
- package/lib/Socket/index.js +433 -132
- package/lib/Socket/messages.d.ts +4 -1
- package/lib/Socket/messages.js +6 -4
- package/lib/Socket/newsletter.d.ts +3 -3
- package/lib/Socket/terminal-close-reporter.d.ts +79 -0
- package/lib/Socket/terminal-close-reporter.js +108 -0
- package/lib/Socket/terminal-close.d.ts +39 -0
- package/lib/Socket/terminal-close.js +51 -0
- package/lib/Socket/transport.d.ts +1 -1
- package/lib/Socket/types.d.ts +1 -1
- package/lib/Types/Auth.d.ts +1 -1
- package/lib/Types/Message.d.ts +1 -1
- package/lib/Types/Socket.d.ts +2 -2
- package/lib/Utils/crypto.d.ts +2 -2
- package/lib/Utils/crypto.js +1 -1
- package/lib/Utils/event-buffer.js +31 -0
- package/lib/Utils/logger.d.ts +2 -2
- package/lib/Utils/logger.js +122 -25
- package/lib/Utils/messages.d.ts +12 -1
- package/lib/Utils/messages.js +39 -15
- package/lib/Utils/process-history-message.js +6 -10
- package/lib/Utils/process-message.js +1 -1
- package/lib/WAProto/runtime.js +5 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ so existing integrations can migrate with minimal changes. See
|
|
|
25
25
|
| Media encrypt/decrypt | Node.js crypto | Rust AES-256-CBC + HMAC |
|
|
26
26
|
| Media upload/download | JS fetch + temp files | Rust with CDN failover, auth refresh, resumable upload |
|
|
27
27
|
| Key management | JS auth state | Rust `PersistenceManager` |
|
|
28
|
-
| Auto-reconnect | Manual `startSock()` loop |
|
|
28
|
+
| Auto-reconnect | Manual `startSock()` loop | Transient drops retried in Rust (fibonacci backoff); terminal ones still yours |
|
|
29
29
|
|
|
30
30
|
## Documentation
|
|
31
31
|
|
|
@@ -51,9 +51,16 @@ import makeWASocket from '@oxidezap/baileyrs'
|
|
|
51
51
|
### Drop-in replacement for upstream Baileys
|
|
52
52
|
|
|
53
53
|
baileyrs is API-compatible with [@whiskeysockets/baileys](https://github.com/WhiskeySockets/Baileys).
|
|
54
|
-
Existing projects switch over by aliasing the package — **
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Existing projects switch over by aliasing the package — **the API and imports
|
|
55
|
+
need no source changes**. Two things do:
|
|
56
|
+
|
|
57
|
+
- Carrying an existing pairing across takes a one-line import swap, see
|
|
58
|
+
[Migrating from Upstream Baileys](#migrating-from-upstream-baileys).
|
|
59
|
+
- If your `connection.update` handler was written for a version of baileyrs
|
|
60
|
+
before 0.1, see [Gotchas](#gotchas): a `close` now always means the socket
|
|
61
|
+
is finished, and you have to recreate it. Code written against upstream
|
|
62
|
+
Baileys already does the right thing.
|
|
63
|
+
|
|
57
64
|
|
|
58
65
|
```sh
|
|
59
66
|
npm install @whiskeysockets/baileys@npm:@oxidezap/baileyrs
|
|
@@ -78,27 +85,57 @@ now resolves to baileyrs.
|
|
|
78
85
|
import makeWASocket, { Boom, DisconnectReason, useMultiFileAuthState } from '@oxidezap/baileyrs'
|
|
79
86
|
|
|
80
87
|
const { state } = await useMultiFileAuthState('auth_info')
|
|
81
|
-
const sock = makeWASocket({ auth: state })
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
// Auto-reconnect is handled by the Rust engine — no need to call makeWASocket again
|
|
89
|
+
// setTimeout caps at ~2^31-1 ms (~24.8 days) and fires immediately past that,
|
|
90
|
+
// so a long ban has to be waited out in chunks.
|
|
91
|
+
async function waitUntil(deadlineMs: number) {
|
|
92
|
+
for (let left = deadlineMs - Date.now(); left > 0; left = deadlineMs - Date.now()) {
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(left, 2_147_483_647)))
|
|
90
94
|
}
|
|
91
|
-
|
|
92
|
-
console.log('Connected')
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
+
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
97
|
+
async function connectToWhatsApp() {
|
|
98
|
+
const sock = makeWASocket({ auth: state })
|
|
99
|
+
|
|
100
|
+
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
101
|
+
if (connection === 'close') {
|
|
102
|
+
// `close` means this socket is finished — same as upstream Baileys.
|
|
103
|
+
// Transient drops never get here; the Rust engine retries those and
|
|
104
|
+
// reports `connecting`.
|
|
105
|
+
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode
|
|
106
|
+
// See the reconnect table under Gotchas: a few terminal closes
|
|
107
|
+
// reject the replacement just as fast, so they are not worth
|
|
108
|
+
// retrying — or not yet. `Example/example.ts` has the full policy.
|
|
109
|
+
if (statusCode === DisconnectReason.loggedOut || statusCode === 405) {
|
|
110
|
+
console.log('Closed for good', statusCode)
|
|
111
|
+
} else if (statusCode === DisconnectReason.forbidden) {
|
|
112
|
+
// Temporary ban: `expire` is unix seconds. A missing or past
|
|
113
|
+
// expiry means the ban is over — reconnect like any other
|
|
114
|
+
// terminal close rather than staying offline forever.
|
|
115
|
+
const expire = (lastDisconnect?.error as Boom)?.data?.expire
|
|
116
|
+
console.log('Temporarily banned until', expire)
|
|
117
|
+
waitUntil(typeof expire === 'number' ? expire * 1000 : 0).then(connectToWhatsApp)
|
|
118
|
+
} else {
|
|
119
|
+
setTimeout(connectToWhatsApp, 5_000)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (connection === 'open') {
|
|
123
|
+
console.log('Connected')
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Register every handler in here. A replacement socket is a new emitter,
|
|
128
|
+
// so anything attached outside stops firing after the first reconnect.
|
|
129
|
+
sock.ev.on('messages.upsert', ({ messages }) => {
|
|
130
|
+
for (const msg of messages) {
|
|
131
|
+
console.log('received message', msg.key.id)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return sock
|
|
136
|
+
}
|
|
101
137
|
|
|
138
|
+
const sock = await connectToWhatsApp()
|
|
102
139
|
await sock.sendMessage('1234567890@s.whatsapp.net', { text: 'Hello!' })
|
|
103
140
|
```
|
|
104
141
|
|
|
@@ -156,9 +193,25 @@ preserved. No QR re-scan, no logged-out events.
|
|
|
156
193
|
|
|
157
194
|
A few behaviors that differ from upstream — almost always to your advantage:
|
|
158
195
|
|
|
159
|
-
- **Auto-reconnect is built in
|
|
160
|
-
|
|
161
|
-
|
|
196
|
+
- **Auto-reconnect is built in, but `close` still means `close`.** The Rust
|
|
197
|
+
engine retries transient drops on a fibonacci backoff and reports them as
|
|
198
|
+
`connection: 'connecting'`, so the canonical upstream handler never fires
|
|
199
|
+
for those and you never end up with two sockets on one account. A
|
|
200
|
+
`connection: 'close'` is only emitted once the engine has given up — a
|
|
201
|
+
replaced session, an outdated build, a temporary ban, an unrecoverable
|
|
202
|
+
`<failure>` — and by then the socket has already released its resources.
|
|
203
|
+
Ignoring it leaves the bot permanently offline, so handle it the upstream
|
|
204
|
+
way and build a replacement — with three exceptions, because some of those
|
|
205
|
+
failures reject the replacement just as fast:
|
|
206
|
+
|
|
207
|
+
| `statusCode` | what to do |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| `DisconnectReason.loggedOut` (401) | stop; needs a fresh pairing |
|
|
210
|
+
| `405` | stop; the server rejected this build, and the next one too |
|
|
211
|
+
| `DisconnectReason.forbidden` (403) | wait until `lastDisconnect.error.data.expire` (unix seconds) — it is a temporary ban |
|
|
212
|
+
| anything else | reconnect, after a short delay |
|
|
213
|
+
|
|
214
|
+
`Example/example.ts` implements exactly this.
|
|
162
215
|
- **No `getMessage` / `cachedGroupMetadata` polyfill required.** The Rust
|
|
163
216
|
side caches group metadata and message keys natively. You can still pass
|
|
164
217
|
them — they're respected as overrides — but they're optional.
|
package/lib/Bridge/adapt.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (table-driven dispatch, additional validation, schema-derived types,
|
|
8
8
|
* etc.) without rippling through call sites.
|
|
9
9
|
*/
|
|
10
|
-
import type { WhatsAppEvent } from 'whatsapp-rust-bridge';
|
|
10
|
+
import type { WhatsAppEvent } from '@oxidezap/whatsapp-rust-bridge';
|
|
11
11
|
import type { ILogger } from '../Utils/logger.js';
|
|
12
12
|
import type { CanonicalEvent } from './types.js';
|
|
13
13
|
import { adaptBridgeMessageWire } from './schema.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HistorySyncWireBatch, type WhatsAppEvent } from 'whatsapp-rust-bridge';
|
|
1
|
+
import { type HistorySyncWireBatch, type WhatsAppEvent } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { ILogger } from '../Utils/logger.js';
|
|
3
3
|
declare const HISTORY_SYNC_EVENT_TYPE: 'history_sync';
|
|
4
4
|
export interface DecodedHistorySyncWireBatch {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BinaryReader } from 'whatsapp-rust-bridge';
|
|
2
|
-
import { proto as WAProto } from 'whatsapp-rust-bridge/proto-types';
|
|
1
|
+
import { BinaryReader } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
|
+
import { proto as WAProto } from '@oxidezap/whatsapp-rust-bridge/proto-types';
|
|
3
3
|
import { isConversationHistorySync } from '../Utils/process-history-message.js';
|
|
4
4
|
import { createSparseArray } from '../Utils/sparse-array.js';
|
|
5
5
|
const HISTORY_SYNC_EVENT_TYPE = 'history_sync';
|
package/lib/Bridge/schema.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `asNumber`, `asString`) at those leak points only — not boilerplate
|
|
23
23
|
* everywhere.
|
|
24
24
|
*/
|
|
25
|
-
import type { MessageWireInfo, WhatsAppEvent } from 'whatsapp-rust-bridge';
|
|
25
|
+
import type { MessageWireInfo, WhatsAppEvent } from '@oxidezap/whatsapp-rust-bridge';
|
|
26
26
|
import type { ILogger } from '../Utils/logger.js';
|
|
27
27
|
import type { CanonicalEvent, CanonicalMessage } from './types.js';
|
|
28
28
|
export type BridgeEventType = WhatsAppEvent['type'];
|
package/lib/Bridge/schema.js
CHANGED
|
@@ -718,8 +718,15 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
718
718
|
const action = isObject(data.action) ? data.action : undefined;
|
|
719
719
|
if (!action)
|
|
720
720
|
return null;
|
|
721
|
-
|
|
722
|
-
|
|
721
|
+
// The bridge `CallAction` union narrows variant-specific fields only on the
|
|
722
|
+
// matching variant, and its group-call variants (group updates, rekeys,
|
|
723
|
+
// waiting rooms) carry no `call_id` at all. `parseCallActionType` admits
|
|
724
|
+
// none of those, so a runtime read through the action's own shape is safe —
|
|
725
|
+
// and is the cheapest way around TS's inability to follow our
|
|
726
|
+
// string-normalized discriminator back into the bridge union.
|
|
727
|
+
const fields = action;
|
|
728
|
+
const actionType = parseCallActionType(fields.type);
|
|
729
|
+
const callId = asString(fields.call_id);
|
|
723
730
|
if (!actionType || !callId) {
|
|
724
731
|
logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
|
|
725
732
|
return null;
|
|
@@ -727,15 +734,8 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
727
734
|
const canonicalAction = {
|
|
728
735
|
type: actionType,
|
|
729
736
|
callId,
|
|
730
|
-
callCreator: asJidAddressString(
|
|
737
|
+
callCreator: asJidAddressString(fields.call_creator)
|
|
731
738
|
};
|
|
732
|
-
// The bridge `CallAction` union narrows variant-specific fields only
|
|
733
|
-
// on the matching variant. We've already validated `action.type` via
|
|
734
|
-
// `parseCallActionType`, so a runtime cast through the action's own
|
|
735
|
-
// shape is safe — reading fields off `unknown` is the cheapest way
|
|
736
|
-
// around TS's inability to follow our string-normalized discriminator
|
|
737
|
-
// back into the bridge union.
|
|
738
|
-
const fields = action;
|
|
739
739
|
if (actionType === 'offer') {
|
|
740
740
|
canonicalAction.callerPn = asJidString(fields.caller_pn);
|
|
741
741
|
canonicalAction.callerCountryCode = asString(fields.caller_country_code);
|
package/lib/Bridge/types.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - All timestamps are unix seconds.
|
|
14
14
|
* - Optional fields use `?` (omitted) rather than `null`.
|
|
15
15
|
*/
|
|
16
|
-
import type { proto } from 'whatsapp-rust-bridge/proto-types';
|
|
16
|
+
import type { proto } from '@oxidezap/whatsapp-rust-bridge/proto-types';
|
|
17
17
|
import type { Chat, Contact, LIDMapping, WAMessage } from '../Types/index.js';
|
|
18
18
|
import type { BinaryNode, WAProto } from '../Types/index.js';
|
|
19
19
|
export interface CanonicalConnected {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GroupMetadataResult } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { GroupMetadataResult } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { GroupMetadata } from '../Types/GroupMetadata.js';
|
|
3
3
|
/**
|
|
4
4
|
* Translate the bridge's protocol-neutral group DTO into the public Baileys
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Basic value codecs for the Baileys legacy-store projection.
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { proto } from 'whatsapp-rust-bridge/proto-types';
|
|
3
|
+
import { proto } from '@oxidezap/whatsapp-rust-bridge/proto-types';
|
|
4
4
|
import { AppState, JsonByteEncoding, LidMapping, NativeStore, TimeValue } from '../constants.js';
|
|
5
5
|
import { bytesToNumbers, fromJsonBytes, toBytes, toJsonBytes } from '../common.js';
|
|
6
6
|
import { parseLidMappingKey } from '../routing.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { decodeSenderKeyRecordComponents, encodeSenderKeyRecordComponents, importLegacySessionRecordV1, projectLegacySessionRecordV1 } from 'whatsapp-rust-bridge';
|
|
2
|
+
import { decodeSenderKeyRecordComponents, encodeSenderKeyRecordComponents, importLegacySessionRecordV1, projectLegacySessionRecordV1 } from '@oxidezap/whatsapp-rust-bridge';
|
|
3
3
|
import { JsonByteEncoding, LegacySession, NativeStore, TimeValue } from '../constants.js';
|
|
4
4
|
import { EMPTY_BYTES } from '../common.js';
|
|
5
5
|
import { NATIVE_ONLY_PROJECTION } from '../types.js';
|
|
@@ -28,9 +28,18 @@ const integer = (value, label) => {
|
|
|
28
28
|
}
|
|
29
29
|
return value;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* The two shapes order their states in opposite directions: the core keeps the
|
|
33
|
+
* current state at the front and prunes from the back, while the legacy record
|
|
34
|
+
* is read back-to-front — `getSenderKeyState()` takes the last entry and
|
|
35
|
+
* `addSenderKeyState` drops the first one on overflow. Both conversions below
|
|
36
|
+
* therefore reverse. Carrying an order through unchanged writes the record
|
|
37
|
+
* upside down: the old implementation would send under a stale key and evict
|
|
38
|
+
* the freshest state instead of the oldest.
|
|
39
|
+
*/
|
|
31
40
|
function senderKeyToLegacy(nativeBytes) {
|
|
32
41
|
const record = decodeSenderKeyRecordComponents(nativeBytes);
|
|
33
|
-
const states = record.states.map(state => ({
|
|
42
|
+
const states = record.states.toReversed().map(state => ({
|
|
34
43
|
senderKeyId: state.keyId,
|
|
35
44
|
senderChainKey: {
|
|
36
45
|
iteration: state.chainKey.iteration,
|
|
@@ -54,6 +63,8 @@ function senderKeyFromLegacy(value) {
|
|
|
54
63
|
const parsed = JSON.parse(Buffer.from(value).toString(JsonByteEncoding.UTF8), BufferJSON.reviver);
|
|
55
64
|
if (!Array.isArray(parsed))
|
|
56
65
|
throw new TypeError('legacy sender-key record must contain an array');
|
|
66
|
+
// Indexed against the stored order so a validation error names the state the
|
|
67
|
+
// caller can find in the record; reversed afterwards. See `senderKeyToLegacy`.
|
|
57
68
|
const record = {
|
|
58
69
|
states: parsed.map((state, stateIndex) => ({
|
|
59
70
|
keyId: integer(state.senderKeyId, `sender-key state ${stateIndex} id`),
|
|
@@ -71,6 +82,7 @@ function senderKeyFromLegacy(value) {
|
|
|
71
82
|
}))
|
|
72
83
|
}))
|
|
73
84
|
};
|
|
85
|
+
record.states.reverse();
|
|
74
86
|
return encodeSenderKeyRecordComponents(record);
|
|
75
87
|
}
|
|
76
88
|
const messageKeyIndex = (value) => {
|
|
@@ -91,13 +103,13 @@ function messageKeysFromLegacy(messageKeys) {
|
|
|
91
103
|
function messageKeysToLegacy(messageKeys) {
|
|
92
104
|
return Object.fromEntries(messageKeys.map(messageKey => [messageKey.index, toBase64(messageKey.seed)]));
|
|
93
105
|
}
|
|
94
|
-
function legacyEntryToCore(entry) {
|
|
106
|
+
function legacyEntryToCore(entry, recordRegistrationId) {
|
|
95
107
|
const ratchet = entry.currentRatchet;
|
|
96
108
|
if (!entry._chains || typeof entry._chains !== 'object') {
|
|
97
109
|
throw new TypeError('legacy session is missing its chain catalog');
|
|
98
110
|
}
|
|
99
111
|
return {
|
|
100
|
-
registrationId: entry.registrationId ?? TimeValue.UNKNOWN_SECONDS,
|
|
112
|
+
registrationId: entry.registrationId ?? recordRegistrationId ?? TimeValue.UNKNOWN_SECONDS,
|
|
101
113
|
ratchet: {
|
|
102
114
|
keyPair: {
|
|
103
115
|
public: fromBase64(ratchet.ephemeralKeyPair.pubKey, 'sender ratchet public key'),
|
|
@@ -145,7 +157,7 @@ function sessionFromLegacy(value, defaults) {
|
|
|
145
157
|
const decoded = {
|
|
146
158
|
sessions: Object.entries(record._sessions).map(([indexKey, entry]) => ({
|
|
147
159
|
indexKey: fromBase64(indexKey, 'legacy session index key'),
|
|
148
|
-
session: legacyEntryToCore(entry)
|
|
160
|
+
session: legacyEntryToCore(entry, record.registrationId)
|
|
149
161
|
}))
|
|
150
162
|
};
|
|
151
163
|
return importLegacySessionRecordV1(decoded, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Device credential projection for legacy Baileys authentication states.
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { proto } from 'whatsapp-rust-bridge/proto-types';
|
|
3
|
+
import { proto } from '@oxidezap/whatsapp-rust-bridge/proto-types';
|
|
4
4
|
import { WA_DEFAULT_VERSION } from '../../Defaults/index.js';
|
|
5
5
|
import { jidDecode, jidEncode } from '../../WABinary/jid-utils.js';
|
|
6
6
|
import { DeviceRecordKey, JsonByteEncoding, SignalDomain, SignalKeyLength, TimeValue } from './constants.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { proto } from 'whatsapp-rust-bridge/proto-types';
|
|
3
|
+
import { proto } from '@oxidezap/whatsapp-rust-bridge/proto-types';
|
|
4
4
|
import { BufferJSON } from '../../Utils/generics.js';
|
|
5
5
|
import { JsonByteEncoding, LegacyMultiFile, LegacyStore } from './constants.js';
|
|
6
6
|
const PATH_SEPARATOR = /[\\/]/g;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsStoreCallbacks } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { JsStoreCallbacks } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { SignalCreds, SignalKeyStore } from '../../Types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Project a neutral native byte store as the public legacy key store. Values
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MediaType as BridgeMediaType } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { MediaType as BridgeMediaType } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { MediaType } from '../Defaults/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Narrow upstream's semantic media names to the protocol-neutral modes the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MessageRetransmissionInput } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { MessageRetransmissionInput } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { BinaryNode, MessageRelayOptions } from '../Types/index.js';
|
|
3
3
|
type OwnRelayIdentity = {
|
|
4
4
|
id?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { BinaryReader } from 'whatsapp-rust-bridge';
|
|
3
|
+
import { BinaryReader } from '@oxidezap/whatsapp-rust-bridge';
|
|
4
4
|
import { PROTO_ENUM_SCHEMAS, PROTO_FIELD_FLAG, PROTO_FIELD_KIND, PROTO_MESSAGE_SCHEMAS } from '../WAProto/compatibility-schema.js';
|
|
5
5
|
const INSTANCE_SCHEMA = Symbol('proto compatibility schema');
|
|
6
6
|
const EMPTY_ARRAY = Object.freeze([]);
|
|
@@ -138,24 +138,62 @@ const lookupPath = (root, path) => {
|
|
|
138
138
|
}
|
|
139
139
|
return current;
|
|
140
140
|
};
|
|
141
|
-
const installPath = (root, path, value) => {
|
|
142
|
-
const segments = path.split('.');
|
|
143
|
-
const leaf = segments.pop();
|
|
144
|
-
if (!leaf)
|
|
145
|
-
throw new Error(`invalid protobuf path: ${path}`);
|
|
146
|
-
let current = root;
|
|
147
|
-
for (const segment of segments) {
|
|
148
|
-
const next = current[segment];
|
|
149
|
-
if (!isObject(next) && typeof next !== 'function')
|
|
150
|
-
current[segment] = {};
|
|
151
|
-
current = current[segment];
|
|
152
|
-
}
|
|
153
|
-
current[leaf] = value;
|
|
154
|
-
};
|
|
155
141
|
const isSourceCodec = (value) => (isObject(value) || typeof value === 'function') &&
|
|
156
142
|
typeof value.encode === 'function' &&
|
|
157
143
|
typeof value.decode === 'function';
|
|
158
|
-
const
|
|
144
|
+
const buildSchemaTree = (messagePaths, enumPaths) => {
|
|
145
|
+
const roots = new Map();
|
|
146
|
+
const nodeFor = (path) => {
|
|
147
|
+
let level = roots;
|
|
148
|
+
let node;
|
|
149
|
+
for (const segment of path.split('.')) {
|
|
150
|
+
node = level.get(segment);
|
|
151
|
+
if (!node) {
|
|
152
|
+
node = { children: new Map() };
|
|
153
|
+
level.set(segment, node);
|
|
154
|
+
}
|
|
155
|
+
level = node.children;
|
|
156
|
+
}
|
|
157
|
+
if (!node)
|
|
158
|
+
throw new Error(`invalid protobuf path: ${path}`);
|
|
159
|
+
return node;
|
|
160
|
+
};
|
|
161
|
+
messagePaths.forEach((path, messageId) => {
|
|
162
|
+
nodeFor(path).messageId = messageId;
|
|
163
|
+
});
|
|
164
|
+
enumPaths.forEach((path, enumId) => {
|
|
165
|
+
nodeFor(path).enumId = enumId;
|
|
166
|
+
});
|
|
167
|
+
return roots;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Self-replacing getter, so only the first read pays. It stays enumerable,
|
|
171
|
+
* writable and configurable so the entry behaves exactly like the eagerly
|
|
172
|
+
* installed value it replaced.
|
|
173
|
+
*
|
|
174
|
+
* The replacement targets the *receiver*, not `target`: these descriptors are
|
|
175
|
+
* also copied onto the exported namespace, so writing back to `target` would
|
|
176
|
+
* leave the object actually being read still holding an accessor and rebuild on
|
|
177
|
+
* every access — 7.4% of process CPU when this was wrong.
|
|
178
|
+
*/
|
|
179
|
+
const defineLazyValue = (target, key, build) => {
|
|
180
|
+
const settle = (receiver, value) => {
|
|
181
|
+
const owner = (isObject(receiver) || typeof receiver === 'function' ? receiver : target);
|
|
182
|
+
Object.defineProperty(owner, key, { configurable: true, enumerable: true, value, writable: true });
|
|
183
|
+
};
|
|
184
|
+
Object.defineProperty(target, key, {
|
|
185
|
+
configurable: true,
|
|
186
|
+
enumerable: true,
|
|
187
|
+
get() {
|
|
188
|
+
const value = build();
|
|
189
|
+
settle(this, value);
|
|
190
|
+
return value;
|
|
191
|
+
},
|
|
192
|
+
set(value) {
|
|
193
|
+
settle(this, value);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
};
|
|
159
197
|
class ProtoCompatibilityRuntime {
|
|
160
198
|
constructor(sourceNamespace) {
|
|
161
199
|
this.namespace = { ...sourceNamespace };
|
|
@@ -171,15 +209,41 @@ class ProtoCompatibilityRuntime {
|
|
|
171
209
|
const candidate = lookupPath(sourceNamespace, path);
|
|
172
210
|
return isSourceCodec(candidate) ? candidate : undefined;
|
|
173
211
|
});
|
|
174
|
-
|
|
212
|
+
// Building all 498 eagerly cost ~4.6 MB of RSS in every importing process
|
|
213
|
+
// while a full connection touches under a dozen. `Array.from` rather than
|
|
214
|
+
// `new Array(n)` keeps the array packed, so `constructorFor`'s indexed read
|
|
215
|
+
// stays on the fast element kind.
|
|
216
|
+
this.constructors = Array.from({ length: PROTO_MESSAGE_SCHEMAS.length });
|
|
175
217
|
this.unsupportedCodecs = Object.freeze(PROTO_MESSAGE_SCHEMAS.flatMap(([path], schemaId) => (this.sourceCodecs[schemaId] ? [] : [path])));
|
|
176
|
-
const
|
|
177
|
-
for (const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
218
|
+
const tree = buildSchemaTree(PROTO_MESSAGE_SCHEMAS.map(([path]) => path), PROTO_ENUM_SCHEMAS.map(([path]) => path));
|
|
219
|
+
for (const [name, node] of tree)
|
|
220
|
+
this.installNode(this.namespace, name, node);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Nested types are installed only once their container is read, so using a
|
|
224
|
+
* container does not build what is nested inside it.
|
|
225
|
+
*/
|
|
226
|
+
installNode(target, name, node) {
|
|
227
|
+
defineLazyValue(target, name, () => {
|
|
228
|
+
// Build once: this getter is reachable from both this namespace and the
|
|
229
|
+
// exported copy, and a second build would reinstall accessors over
|
|
230
|
+
// entries a caller had already resolved.
|
|
231
|
+
if (node.value === undefined) {
|
|
232
|
+
const value = node.messageId !== undefined
|
|
233
|
+
? this.constructorFor(node.messageId)
|
|
234
|
+
: node.enumId !== undefined
|
|
235
|
+
? this.enums[node.enumId].publicValue
|
|
236
|
+
: {};
|
|
237
|
+
node.value = value;
|
|
238
|
+
for (const [childName, child] of node.children)
|
|
239
|
+
this.installNode(value, childName, child);
|
|
240
|
+
}
|
|
241
|
+
return node.value;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
constructorFor(schemaId) {
|
|
245
|
+
var _a;
|
|
246
|
+
return ((_a = this.constructors)[schemaId] ?? (_a[schemaId] = this.makeConstructor(schemaId, PROTO_MESSAGE_SCHEMAS[schemaId][1], this.sourceCodecs[schemaId])));
|
|
183
247
|
}
|
|
184
248
|
makeEnum(entries) {
|
|
185
249
|
const publicValue = {};
|
|
@@ -312,7 +376,7 @@ class ProtoCompatibilityRuntime {
|
|
|
312
376
|
}
|
|
313
377
|
}
|
|
314
378
|
fromObject(schemaId, input) {
|
|
315
|
-
const constructor = this.
|
|
379
|
+
const constructor = this.constructorFor(schemaId);
|
|
316
380
|
if (input instanceof constructor)
|
|
317
381
|
return input;
|
|
318
382
|
const data = input;
|
|
@@ -494,34 +558,44 @@ class ProtoCompatibilityRuntime {
|
|
|
494
558
|
const reader = new LongBinaryReader(asUint8Array(input));
|
|
495
559
|
return this.hydrate(schemaId, codec.decode(reader, length));
|
|
496
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* A fresh instance rather than an in-place re-parent: the codec installs its
|
|
563
|
+
* own `toJSON` on what it returns, and deleting that normalizes the object
|
|
564
|
+
* into dictionary mode, where every later read is a megamorphic lookup.
|
|
565
|
+
*/
|
|
497
566
|
hydrate(schemaId, value) {
|
|
498
|
-
const
|
|
567
|
+
const source = isObject(value) ? value : {};
|
|
568
|
+
const instance = Object.create(this.constructorFor(schemaId).prototype);
|
|
499
569
|
const messageFields = this.messageFieldsByName[schemaId];
|
|
500
|
-
for (const key in
|
|
570
|
+
for (const key in source) {
|
|
571
|
+
const nested = source[key];
|
|
501
572
|
const field = messageFields[key];
|
|
502
|
-
if (!field)
|
|
503
|
-
|
|
504
|
-
const nested = object[key];
|
|
505
|
-
if (field[3] & PROTO_FIELD_FLAG.repeated) {
|
|
506
|
-
if (Array.isArray(nested))
|
|
507
|
-
for (const item of nested)
|
|
508
|
-
if (isObject(item))
|
|
509
|
-
this.hydrate(field[2], item);
|
|
573
|
+
if (!field) {
|
|
574
|
+
instance[key] = nested;
|
|
510
575
|
}
|
|
511
|
-
else if (field[3] & PROTO_FIELD_FLAG.
|
|
512
|
-
if (
|
|
513
|
-
for (
|
|
576
|
+
else if (field[3] & PROTO_FIELD_FLAG.repeated) {
|
|
577
|
+
if (Array.isArray(nested)) {
|
|
578
|
+
for (let index = 0; index < nested.length; index++) {
|
|
579
|
+
const item = nested[index];
|
|
514
580
|
if (isObject(item))
|
|
515
|
-
this.hydrate(field[2], item);
|
|
581
|
+
nested[index] = this.hydrate(field[2], item);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
instance[key] = nested;
|
|
516
585
|
}
|
|
517
|
-
else if (isObject(nested)) {
|
|
518
|
-
|
|
586
|
+
else if (field[3] & PROTO_FIELD_FLAG.map && isObject(nested)) {
|
|
587
|
+
const entries = {};
|
|
588
|
+
for (const entry in nested) {
|
|
589
|
+
const item = nested[entry];
|
|
590
|
+
entries[entry] = isObject(item) ? this.hydrate(field[2], item) : item;
|
|
591
|
+
}
|
|
592
|
+
instance[key] = entries;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
instance[key] = isObject(nested) ? this.hydrate(field[2], nested) : nested;
|
|
519
596
|
}
|
|
520
597
|
}
|
|
521
|
-
|
|
522
|
-
delete object.toJSON;
|
|
523
|
-
Object.setPrototypeOf(object, this.constructors[schemaId].prototype);
|
|
524
|
-
return object;
|
|
598
|
+
return instance;
|
|
525
599
|
}
|
|
526
600
|
projectForEncode(schemaId, value) {
|
|
527
601
|
if (!isObject(value))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlocklistEntryResult, BusinessProfileResult, MembershipRequestResult, ParticipantChangeResult } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { BlocklistEntryResult, BusinessProfileResult, MembershipRequestResult, ParticipantChangeResult } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { BinaryNode, WABusinessProfile } from '../Types/index.js';
|
|
3
3
|
export type ParticipantUpdateResult = {
|
|
4
4
|
status: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WasmWhatsAppClient } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { WasmWhatsAppClient } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { BinaryNode } from '../Types/index.js';
|
|
3
3
|
type StanzaResponseClient = Pick<WasmWhatsAppClient, 'acknowledgeStanza' | 'rejectStanza' | 'requestMessageRetry'>;
|
|
4
4
|
export interface StanzaResponseContext {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UsyncQuery as NativeUsyncQuery, UsyncResponse as NativeUsyncResponse } from 'whatsapp-rust-bridge';
|
|
1
|
+
import type { UsyncQuery as NativeUsyncQuery, UsyncResponse as NativeUsyncResponse } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import type { BinaryNode } from '../../Types/index.js';
|
|
3
3
|
import { type USyncQuery, type USyncQueryResult } from '../../WAUSync/USyncQuery.js';
|
|
4
4
|
export type RawUSyncQuery = (node: BinaryNode, timeoutMs?: number) => Promise<BinaryNode>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
|
-
import type { WasmWhatsAppClient } from 'whatsapp-rust-bridge';
|
|
3
|
+
import type { WasmWhatsAppClient } from '@oxidezap/whatsapp-rust-bridge';
|
|
4
4
|
import type { SocketConfig } from '../Types/index.js';
|
|
5
5
|
type ReadyState = 0 | 1 | 2 | 3;
|
|
6
6
|
type EventListener = Parameters<EventEmitter['on']>[1];
|
|
@@ -12,8 +12,13 @@ export declare class WebSocketClient extends EventEmitter {
|
|
|
12
12
|
protected readonly socket: {
|
|
13
13
|
readonly readyState: ReadyState;
|
|
14
14
|
};
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* One value rather than a pair of booleans plus a promise that could
|
|
17
|
+
* disagree with them. `closing` carries the in-flight close so a second
|
|
18
|
+
* caller joins it instead of returning while the first `disconnect()` is
|
|
19
|
+
* still running — which is how teardown reached `free()` on a busy client.
|
|
20
|
+
*/
|
|
21
|
+
private closeState;
|
|
17
22
|
private readonly getClient;
|
|
18
23
|
private listenerMutationDepth;
|
|
19
24
|
constructor(url: string | URL, config: SocketConfig, getClient: () => WasmWhatsAppClient | undefined);
|
|
@@ -32,6 +37,22 @@ export declare class WebSocketClient extends EventEmitter {
|
|
|
32
37
|
off(eventName: string | symbol, listener: EventListener): this;
|
|
33
38
|
removeAllListeners(eventName?: string | symbol): this;
|
|
34
39
|
connect(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Idempotent, and a second caller joins the first rather than returning
|
|
42
|
+
* while it is still going.
|
|
43
|
+
*
|
|
44
|
+
* The early return used to be bare: `void ws.close(); await sock.end()` saw
|
|
45
|
+
* the flag, returned immediately, and let teardown reach `free()` with the
|
|
46
|
+
* original `disconnect()` still in flight — the wasm heap corruption
|
|
47
|
+
* `bridge-free-safety.test.ts` documents. Awaiting a *second* `disconnect()`
|
|
48
|
+
* does not join the first one.
|
|
49
|
+
*
|
|
50
|
+
* The state is stored before `disconnect()` is called, and the work is
|
|
51
|
+
* deferred by a microtask to make that ordering hold: an inline async body
|
|
52
|
+
* runs eagerly to its first `await`, so `disconnect()` would be invoked
|
|
53
|
+
* while the state still said `open`, and anything it reaches synchronously
|
|
54
|
+
* that calls back into `close()` would issue a second one.
|
|
55
|
+
*/
|
|
35
56
|
close(): Promise<void>;
|
|
36
57
|
send(str: string | Uint8Array, cb?: (err?: Error) => void): boolean;
|
|
37
58
|
private get readyState();
|