whalibmob 5.24.0 → 5.24.1

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 CHANGED
@@ -1664,6 +1664,82 @@ Everything the CLI does is available as a Node.js library. The sections below
1664
1664
  cover connecting an account, sending and receiving every message type, groups,
1665
1665
  communities, channels, presence, privacy, history sync, and device emulation.
1666
1666
 
1667
+ ### What the package exports
1668
+
1669
+ Two layers. The first is the flat one the rest of this document uses — the
1670
+ eighty-two names the common paths are built from:
1671
+
1672
+ ```js
1673
+ const { WhalibmobClient, createNewStore, requestSmsCode } = require('whalibmob')
1674
+ ```
1675
+
1676
+ The second is everything else. The library is ninety-five modules and close to
1677
+ five hundred exported names, and the flat layer picks out under a fifth of
1678
+ them. The rest are reachable as **namespaces**, each one a module of `lib/`
1679
+ under a name of its own:
1680
+
1681
+ ```js
1682
+ const wa = require('whalibmob')
1683
+
1684
+ wa.MediaService.getMediaKeyName('ptt') // → 'WhatsApp Audio Keys'
1685
+ wa.MessageProto.decodeMessageContainer(buf) // raw bytes → a decoded message
1686
+ wa.Messages.MediaRetry.mediaRetryKey(key) // the retry-receipt key
1687
+ wa.Signal.libsignal.SessionCipher // the vendored libsignal
1688
+ wa.AppState.SyncdProto // app-state mutation records
1689
+ wa.Image.Jpeg // the JPEG header reader
1690
+ ```
1691
+
1692
+ A namespace is named after the module it comes from, and a directory in `lib/`
1693
+ becomes one namespace rather than several — so `Signal` is all of
1694
+ `lib/signal/`, `AppState` all of `lib/appstate/`, `Messages` all of
1695
+ `lib/messages/`. Two names could not be had: **`Devices`** is
1696
+ `lib/DeviceManager.js`, because the flat `DeviceManager` is the class rather
1697
+ than the module, and **`Proto`** is `lib/proto.js` while the message protobufs
1698
+ in `lib/proto/` are **`MessageProto`**.
1699
+
1700
+ Deep requires work as well, and always have — the package declares no `exports`
1701
+ map, so nothing is sealed off:
1702
+
1703
+ ```js
1704
+ const MediaService = require('whalibmob/lib/MediaService')
1705
+ // the same object, by a longer name
1706
+ require('whalibmob').MediaService === MediaService // true
1707
+ ```
1708
+
1709
+ Adding the namespaces renamed and removed nothing: every name the library
1710
+ exported before is still exported, still holding what it held.
1711
+
1712
+ > [!NOTE]
1713
+ > The namespaces are typed as far as the rest of this document goes — media,
1714
+ > the message protobufs, media retry. Past that they are `any`, which is what
1715
+ > the internals have always been in `index.d.ts`. They are reachable, not
1716
+ > described.
1717
+
1718
+ ### ES modules
1719
+
1720
+ The package is CommonJS, and every one of its names can be imported from an ES
1721
+ module — a bot written with `"type": "module"` needs no interop dance:
1722
+
1723
+ ```js
1724
+ import { WhalibmobClient, createNewStore, requestSmsCode } from 'whalibmob'
1725
+ import { MediaService, MessageProto } from 'whalibmob'
1726
+
1727
+ // the default import is the whole export object, if you prefer it
1728
+ import wa from 'whalibmob'
1729
+
1730
+ // deep imports work too — ESM wants the extension, CommonJS does not
1731
+ import { downloadMedia } from 'whalibmob/lib/MediaService.js'
1732
+ ```
1733
+
1734
+ > [!NOTE]
1735
+ > `import { … }` used to fail for all but five names. Node reads a CommonJS
1736
+ > module's names statically, and the reader gives up at the first property of
1737
+ > `module.exports` whose value is not a plain identifier — an inline arrow
1738
+ > function five entries in cost the other 116. `require()` and the default
1739
+ > import were unaffected, which is why it went unnoticed. Everything is now
1740
+ > bound to a name before it is exported, and a test fails if that stops being
1741
+ > true.
1742
+
1667
1743
  ## Connecting Account
1668
1744
 
1669
1745
  ### Register a New Number
@@ -3700,6 +3776,55 @@ const bytes = await client.downloadMedia(d, { verify: true })
3700
3776
  with no media, no CDN location, an unsupported type, or a file that does not
3701
3777
  match the message all say so.
3702
3778
 
3779
+ ### View-once and disappearing messages
3780
+
3781
+ Some messages arrive inside an envelope. A photo sent as **view-once** is an
3782
+ ordinary `ImageMessage` wrapped in a `ViewOnceMessage`; the same photo in a
3783
+ chat with **disappearing messages** turned on is one wrapped in an
3784
+ `EphemeralMessage`. Nothing about the photo changes — the envelope only records
3785
+ how it was sent.
3786
+
3787
+ The decoder opens them, so nothing special is needed on your side. `msg.decoded`
3788
+ is the photo, and `downloadMedia()` works on it exactly as it does on any other:
3789
+
3790
+ ```js
3791
+ client.on('message', async (msg) => {
3792
+ const d = msg.decoded
3793
+ if (!d || !d.mediaKey) return
3794
+
3795
+ if (d.viewOnce) console.log('sent as view-once')
3796
+ if (d.ephemeral) console.log('from a disappearing chat')
3797
+
3798
+ const bytes = await client.downloadMedia(d) // same call, wrapped or not
3799
+ })
3800
+ ```
3801
+
3802
+ Three flags say how the message was sent, and are absent otherwise:
3803
+
3804
+ | flag | meaning |
3805
+ |---|---|
3806
+ | `d.viewOnce` | sent as view-once — `ViewOnceMessage`, `ViewOnceMessageV2` or the V2 extension a voice note uses |
3807
+ | `d.ephemeral` | from a chat with disappearing messages on |
3808
+ | `d.edited` | the new text of an edited message — the payload is the `protocol` message carrying it |
3809
+
3810
+ They stack. A view-once photo in a disappearing chat carries both:
3811
+
3812
+ ```js
3813
+ if (d.viewOnce && d.ephemeral) { /* … */ }
3814
+ ```
3815
+
3816
+ Documents sent with a caption (`DocumentWithCaptionMessage`) are unwrapped the
3817
+ same way and decode as a plain `document`, with no flag of their own.
3818
+
3819
+ > [!NOTE]
3820
+ > Opening the envelope is what makes this media reachable at all. Before it, a
3821
+ > view-once photo decoded as `{ type: 'unknown' }` — no `mediaKey`, no
3822
+ > `directPath` — and there was nothing for `downloadMedia()` to fetch.
3823
+
3824
+ Nothing here changes when the message came through history sync, or when it is
3825
+ one of your own echoed back to this device from another: those envelopes nest,
3826
+ and are unwrapped down to the message inside.
3827
+
3703
3828
  ### When the file is gone from the CDN
3704
3829
 
3705
3830
  Media is not carried inside the message — the message carries a URL, a hash and
package/index.d.ts CHANGED
@@ -229,6 +229,15 @@ export interface PollSendResult extends SendResult {
229
229
 
230
230
  export interface DecodedBase {
231
231
  type: string;
232
+ /**
233
+ * The message arrived inside a view-once envelope. The content is decoded
234
+ * normally — media included — this only records how it was sent.
235
+ */
236
+ viewOnce?: boolean;
237
+ /** The message belongs to a chat with disappearing messages turned on. */
238
+ ephemeral?: boolean;
239
+ /** The message is the new text of an edit. */
240
+ edited?: boolean;
232
241
  [key: string]: any;
233
242
  }
234
243
 
@@ -586,9 +595,22 @@ export interface MediaRetryNotification {
586
595
  }
587
596
 
588
597
  export interface MediaRetryResult {
598
+ /** `true` only when the phone actually re-uploaded the file. */
589
599
  ok: boolean;
590
- directPath?: string;
591
- url?: string;
600
+ /**
601
+ * `MediaRetryNotification.result` — `1` is SUCCESS. `null` when the phone
602
+ * answered with an error rather than a payload.
603
+ */
604
+ result: number | null;
605
+ /**
606
+ * Where the file was put back. **`null`, not absent**, whenever `ok` is
607
+ * false — check it against `null`, or just read `ok`.
608
+ */
609
+ directPath: string | null;
610
+ /** The message the retry was for. Absent when the phone answered an error. */
611
+ stanzaId?: string;
612
+ /** Set instead of a location when the phone no longer has the file either. */
613
+ error?: any;
592
614
  [key: string]: any;
593
615
  }
594
616
 
@@ -1112,3 +1134,199 @@ export declare const WEB_EVENTS: any;
1112
1134
  export declare const WEB_GLOBALS: any;
1113
1135
  export declare function decodeArgo(buf: Buffer): any;
1114
1136
  export declare function tryDecodeArgo(buf: Buffer): any;
1137
+
1138
+ // ────────────────────────────────────────────────────────────────────────────
1139
+ // Namespaces
1140
+ // ────────────────────────────────────────────────────────────────────────────
1141
+
1142
+ /**
1143
+ * Everything above this line is a name picked out of a module by hand.
1144
+ * Everything below is the rest of the library: each module of `lib/`, whole,
1145
+ * under a name of its own. Nothing above is renamed or removed by any of it.
1146
+ *
1147
+ * `wa.MediaService` is the object `require('whalibmob/lib/MediaService')`
1148
+ * returns — deep requires work too, and always have. Four namespaces are
1149
+ * gathered from the several modules of a directory rather than being one
1150
+ * module: `Signal`, `Signal.libsignal`, `AppState` and `Image`.
1151
+ *
1152
+ * The members declared on each namespace below are typed. The rest of a module
1153
+ * is reachable and untyped, the same way `MessageSender` and `SignalProtocol`
1154
+ * are above — this file has never claimed to describe the internals, and a
1155
+ * namespace does not change that.
1156
+ */
1157
+ export interface LibModule {
1158
+ [name: string]: any;
1159
+ }
1160
+
1161
+ /**
1162
+ * `whalibmob/lib/MediaService` — the encryption, upload and download beneath
1163
+ * `client.downloadMedia()`. Everything it exports is typed.
1164
+ */
1165
+ export declare const MediaService: {
1166
+ /** Upload path and HKDF info string per media type. */
1167
+ MEDIA_PATH: Record<string, { path: string; keyName: string }>;
1168
+ encryptMedia(plaintext: Buffer, mediaKey: Buffer, keyName: string): EncryptedMedia;
1169
+ decryptMedia(encryptedWithMac: Buffer, mediaKey: Buffer, keyName: string): Buffer;
1170
+ uploadMedia(...args: any[]): Promise<any>;
1171
+ /** `opts.fileEncSha256` checks the blob as it arrived, before decrypting. */
1172
+ downloadMedia(
1173
+ url: string,
1174
+ mediaKey: Buffer,
1175
+ keyName: string,
1176
+ opts?: { web?: boolean; fileEncSha256?: Buffer }
1177
+ ): Promise<Buffer>;
1178
+ /** Plain GET, no decryption — profile pictures are served in the clear. */
1179
+ httpGetBuffer(url: string, opts?: { web?: boolean }): Promise<Buffer>;
1180
+ /** HKDF-SHA256 over the media key: 112 bytes of IV, cipher key and MAC key. */
1181
+ deriveMediaKeyData(mediaKey: Buffer, keyName: string): Buffer;
1182
+ /** `'image'` → `'WhatsApp Image Keys'`. `null` for a type it does not know. */
1183
+ getMediaKeyName(mediaType: string): string | null;
1184
+ /** The absolute URL, built from `directPath` when there is no `url`. */
1185
+ resolveMediaUrl(url: string | null, directPath: string | null): string | null;
1186
+ };
1187
+
1188
+ /** `whalibmob/lib/proto/MessageProto` — the message protobufs. */
1189
+ export declare const MessageProto: LibModule & {
1190
+ /**
1191
+ * Raw decrypted bytes into a `DecodedMessage`. Envelopes — view-once,
1192
+ * ephemeral, deviceSent, documentWithCaption, edited — are opened, so what
1193
+ * comes back is the message inside with `viewOnce` / `ephemeral` / `edited`
1194
+ * recorded on it. `depth` is internal; leave it unset.
1195
+ */
1196
+ decodeMessageContainer(buf: Buffer, depth?: number): DecodedMessage;
1197
+ /** The generic protobuf field walker, keyed by field number. */
1198
+ decodeFields(buf: Buffer): Record<number, any>;
1199
+ encodeMessage(...args: any[]): Buffer;
1200
+ encodeImageMessage(opts: Record<string, any>): Buffer;
1201
+ encodeVideoMessage(opts: Record<string, any>): Buffer;
1202
+ encodeAudioMessage(opts: Record<string, any>): Buffer;
1203
+ encodeDocumentMessage(opts: Record<string, any>): Buffer;
1204
+ encodeStickerMessage(opts: Record<string, any>): Buffer;
1205
+ encodePollCreationMessage(opts: Record<string, any>):
1206
+ { payload: Buffer; messageSecret: Buffer; encKey: Buffer };
1207
+ };
1208
+
1209
+ /** `whalibmob/lib/messages/` — sending, and the receipts that go with it. */
1210
+ export declare const Messages: {
1211
+ MessageSender: LibModule;
1212
+ /**
1213
+ * Asking the sender's phone to upload a file again. `client
1214
+ * .requestMediaRetry()` and `client.decryptMediaRetry()` are these, wired up.
1215
+ */
1216
+ MediaRetry: {
1217
+ /** HKDF-SHA256(mediaKey, "WhatsApp Media Retry Notification"), 32 bytes. */
1218
+ mediaRetryKey(mediaKey: Buffer): Buffer;
1219
+ encryptRetryReceipt(msgId: string, mediaKey: Buffer):
1220
+ { ciphertext: Buffer; iv: Buffer };
1221
+ decryptRetryNotification(
1222
+ notif: MediaRetryNotification, mediaKey: Buffer): MediaRetryResult;
1223
+ buildRetryReceiptNode(
1224
+ info: { id: string; chatJid: Jid; fromMe: boolean; participant?: Jid },
1225
+ mediaKey: Buffer, ownJid: Jid, BinaryNode: any): any;
1226
+ /** `null` when the node is not a media-retry notification. */
1227
+ parseRetryNotification(node: any, helpers: {
1228
+ findChild: (node: any, tag: string) => any;
1229
+ getContent: (node: any) => Buffer | null;
1230
+ }): MediaRetryNotification | null;
1231
+ RETRY_KEY_INFO: string;
1232
+ };
1233
+ ReportingToken: LibModule;
1234
+ TcTokenStore: LibModule;
1235
+ };
1236
+
1237
+ /**
1238
+ * `whalibmob/lib/signal/` — SignalProtocol.js, SignalStore.js and SenderKey.js
1239
+ * gathered into one namespace, with the two vendored libraries under their own.
1240
+ */
1241
+ export declare const Signal: LibModule & {
1242
+ SignalProtocol: any;
1243
+ SignalStore: any;
1244
+ SenderKeyStore: any;
1245
+ SenderKeyCrypto: any;
1246
+ /** Group ciphers and sender-key records. */
1247
+ WaSignalGroup: LibModule;
1248
+ /**
1249
+ * The vendored libsignal. Its own barrel leaves six modules out; they are
1250
+ * here — `BaseKeyType`, `ChainType`, `protobufs`, `queueJob`,
1251
+ * `FingerprintGenerator`, `textsecure`.
1252
+ */
1253
+ libsignal: LibModule;
1254
+ };
1255
+
1256
+ /** `whalibmob/lib/appstate/` — the synced state a companion keeps in step. */
1257
+ export declare const AppState: LibModule & {
1258
+ AppStateStore: any;
1259
+ COLLECTIONS: string[];
1260
+ AppStateSync: LibModule;
1261
+ LTHash: LibModule;
1262
+ Mutations: LibModule;
1263
+ SyncdProto: LibModule;
1264
+ };
1265
+
1266
+ /**
1267
+ * `whalibmob/lib/image/` — the decoders behind the inline thumbnails, and the
1268
+ * JPEG encoder that writes them. No native dependency; sharp or jimp are used
1269
+ * when installed and this is what runs when they are not.
1270
+ */
1271
+ export declare const Image: LibModule & {
1272
+ /** Width and height from the file header, for every format below. */
1273
+ decodeImage(buf: Buffer): any;
1274
+ canDecode(buf: Buffer): boolean;
1275
+ toSquareJpeg(...args: any[]): Buffer | null;
1276
+ toScaledJpeg(...args: any[]): Buffer | null;
1277
+ encodeJpeg(...args: any[]): Buffer;
1278
+ Jpeg: LibModule;
1279
+ JpegEncoder: LibModule;
1280
+ Png: LibModule;
1281
+ Gif: LibModule;
1282
+ Bmp: LibModule;
1283
+ };
1284
+
1285
+ /** Registration, sessions, and the device a session presents itself as. */
1286
+ export declare const Client: LibModule;
1287
+ export declare const Registration: LibModule;
1288
+ export declare const Store: LibModule;
1289
+ export declare const WebStore: LibModule;
1290
+ export declare const DeviceConfig: LibModule;
1291
+ /**
1292
+ * `whalibmob/lib/DeviceManager` — the module, not the class. The flat
1293
+ * `DeviceManager` export is the class; this is what it comes from, so
1294
+ * `makeDeviceJid`, `phoneFromJid` and `jidStrToObj` can be reached too.
1295
+ */
1296
+ export declare const Devices: LibModule;
1297
+ export declare const PlayStore: LibModule;
1298
+ export declare const PlayStoreDevice: LibModule;
1299
+ export declare const AndroidApk: LibModule;
1300
+ export declare const Attestation: LibModule;
1301
+ export declare const Tokens: LibModule;
1302
+ export declare const PushClient: LibModule;
1303
+ export declare const Fcm: LibModule;
1304
+ export declare const FcmMcs: LibModule;
1305
+
1306
+ /** Linking to an account that already exists. */
1307
+ export declare const PairingCode: LibModule;
1308
+ export declare const CompanionPairing: LibModule;
1309
+ export declare const QrPairing: LibModule;
1310
+ export declare const WebVersion: LibModule;
1311
+ export declare const WebProto: LibModule;
1312
+
1313
+ /** The wire. */
1314
+ export declare const BinaryNode: LibModule;
1315
+ export declare const Noise: LibModule;
1316
+ export declare const WebSocketStream: LibModule;
1317
+ export declare const Socks: LibModule;
1318
+ export declare const OfflineNodeProcessor: LibModule;
1319
+ export declare const Constants: LibModule;
1320
+ export declare const Logger: LibModule;
1321
+ /**
1322
+ * `whalibmob/lib/proto.js` — the handshake payloads. The message protobufs are
1323
+ * the directory of the same name, and are `MessageProto`.
1324
+ */
1325
+ export declare const Proto: LibModule;
1326
+
1327
+ export declare const MediaThumbnail: LibModule;
1328
+ export declare const GroupParticipant: LibModule;
1329
+ export declare const HistorySyncHandler: LibModule;
1330
+ export declare const AuthUtils: LibModule;
1331
+ export declare const Argo: LibModule;
1332
+ export declare const WAUSync: LibModule;
package/index.js CHANGED
@@ -55,27 +55,159 @@ const {
55
55
  HistorySyncTypeName
56
56
  } = require('./lib/HistorySyncHandler');
57
57
 
58
+ // ─── Everything below is bound to a name before it is exported ───────────────
59
+ //
60
+ // Not for tidiness. Node reads a CommonJS module's names statically when
61
+ // something imports it as ESM, and the reader it uses gives up at the first
62
+ // property of module.exports whose value is not a plain identifier. There used
63
+ // to be an arrow function five entries in, and everything after it — a hundred
64
+ // and sixteen of the hundred and twenty-one names — could not be imported by
65
+ // name at all:
66
+ //
67
+ // import { createNewStore } from 'whalibmob'
68
+ // SyntaxError: Named export 'createNewStore' not found.
69
+ //
70
+ // The default import always worked and still does, so this broke no CommonJS
71
+ // caller and no `import wa from 'whalibmob'`. It broke `import { … }`, which is
72
+ // how a bot written as an ES module reaches for anything.
73
+ //
74
+ // So: functions defined here get a name first, member accesses get a name
75
+ // first, and the modules below get a name first. The object at the bottom is
76
+ // nothing but identifiers, and test/public-surface.test.js fails if any name
77
+ // stops being importable that way.
78
+
79
+ // Receive the verification code as a silent push, without an SMS — opens the
80
+ // listener the native client of that platform keeps open. See "Receiving the
81
+ // code over push" in the README.
82
+ //
83
+ // Routed through the push client for the device's platform: Android opens the
84
+ // Firebase MCS stream, iOS resolves null because APNs is not implemented and
85
+ // an iOS session holds no Firebase identity to listen with.
86
+ const receivePushCode = (store, device, opts) => {
87
+ const dev = device || (store && store.device);
88
+ return require('./lib/PushClient')
89
+ .pushClientFor(dev)
90
+ .receivePushCode(store, dev, opts);
91
+ };
92
+
93
+ // Whether the device profile can do push verification at all.
94
+ const supportsPush = (device) => require('./lib/PushClient').supportsPush(device);
95
+
96
+ // Where a session's files live: one folder per number inside the
97
+ // authentication folder. The same resolver the CLI uses.
98
+ const {
99
+ defaultBaseDir, sessionDirFor, storeFileFor, webStoreFileFor,
100
+ listSessions, migrateSession
101
+ } = SessionPaths;
102
+
103
+ const { encodeWAM, BinaryInfo, WEB_EVENTS, WEB_GLOBALS } = WAM;
104
+
105
+ // ─── The namespaces ──────────────────────────────────────────────────────────
106
+ //
107
+ // Every module of lib/, whole, under a name of its own — see the note at the
108
+ // foot of module.exports for what the names mean and why they are these.
109
+
110
+ const Client = require('./lib/Client');
111
+ const Registration = require('./lib/Registration');
112
+ const Store = require('./lib/Store');
113
+ const WebStore = require('./lib/WebStore');
114
+ const DeviceConfig = require('./lib/DeviceConfig');
115
+ const Devices = require('./lib/DeviceManager');
116
+ const PlayStore = require('./lib/PlayStore');
117
+ const PlayStoreDevice = require('./lib/playstore-device');
118
+ const AndroidApk = require('./lib/AndroidApk');
119
+ const Attestation = require('./lib/Attestation');
120
+ const Tokens = require('./lib/tokens');
121
+ const PushClient = require('./lib/PushClient');
122
+ const Fcm = require('./lib/fcm');
123
+ const FcmMcs = require('./lib/fcm-mcs');
124
+
125
+ const PairingCode = require('./lib/PairingCode');
126
+ const CompanionPairing = require('./lib/CompanionPairing');
127
+ const QrPairing = require('./lib/QrPairing');
128
+ const WebVersion = require('./lib/WebVersion');
129
+ const WebProto = require('./lib/webproto');
130
+
131
+ const BinaryNode = require('./lib/BinaryNode');
132
+ const Noise = require('./lib/noise');
133
+ const WebSocketStream = require('./lib/WebSocketStream');
134
+ const Socks = require('./lib/socks');
135
+ const OfflineNodeProcessor = require('./lib/OfflineNodeProcessor');
136
+ const Constants = require('./lib/constants');
137
+ const Logger = require('./lib/logger');
138
+ // lib/proto.js — the handshake payloads. The message protobufs are the
139
+ // directory of the same name, and are MessageProto.
140
+ const Proto = require('./lib/proto.js');
141
+
142
+ const MessageProto = require('./lib/proto/MessageProto');
143
+ const MediaService = require('./lib/MediaService');
144
+ const MediaThumbnail = require('./lib/MediaThumbnail');
145
+ const GroupParticipant = require('./lib/GroupParticipant');
146
+
147
+ const Messages = {
148
+ MessageSender: require('./lib/messages/MessageSender'),
149
+ MediaRetry: require('./lib/messages/MediaRetry'),
150
+ ReportingToken: require('./lib/messages/ReportingToken'),
151
+ TcTokenStore: require('./lib/messages/TcTokenStore')
152
+ };
153
+
154
+ // The decoders behind the inline thumbnails, and the encoder that makes them.
155
+ // lib/image/index.js is the whole story for most callers; the per-format
156
+ // modules are here for the rest.
157
+ const Image = Object.assign({}, require('./lib/image'), {
158
+ Jpeg: require('./lib/image/Jpeg'),
159
+ JpegEncoder: require('./lib/image/JpegEncoder'),
160
+ Png: require('./lib/image/Png'),
161
+ Gif: require('./lib/image/Gif'),
162
+ Bmp: require('./lib/image/Bmp')
163
+ });
164
+
165
+ const HistorySyncHandler = require('./lib/HistorySyncHandler');
166
+ const AuthUtils = require('./lib/auth-utils');
167
+ const Argo = require('./lib/argo/ArgoDecoder');
168
+ const WAUSync = require('./lib/WAUSync');
169
+
170
+ // Signal — lib/signal/. SignalProtocol.js, SignalStore.js and SenderKey.js have
171
+ // no name in common, so the three are one namespace. The two vendored libraries
172
+ // keep theirs, being libraries.
173
+ const Signal = Object.assign({},
174
+ require('./lib/signal/SignalProtocol'),
175
+ require('./lib/signal/SignalStore'),
176
+ require('./lib/signal/SenderKey'),
177
+ {
178
+ WaSignalGroup: require('./lib/signal/WaSignalGroup'),
179
+ // libsignal's own barrel leaves six of its modules out. They are added to a
180
+ // copy of it here rather than written into it, so
181
+ // require('whalibmob/lib/signal/libsignal') stays exactly what it was.
182
+ libsignal: Object.assign({}, require('./lib/signal/libsignal'), {
183
+ BaseKeyType: require('./lib/signal/libsignal/base_key_type'),
184
+ ChainType: require('./lib/signal/libsignal/chain_type'),
185
+ protobufs: require('./lib/signal/libsignal/protobufs'),
186
+ queueJob: require('./lib/signal/libsignal/queue_job'),
187
+ FingerprintGenerator:
188
+ require('./lib/signal/libsignal/numeric_fingerprint').FingerprintGenerator,
189
+ textsecure:
190
+ require('./lib/signal/libsignal/WhisperTextProtocol').textsecure
191
+ })
192
+ });
193
+
194
+ // App state — lib/appstate/. AppStateStore.js is spread because the flat
195
+ // AppStateStore is its class; the other three are whole.
196
+ const AppState = Object.assign({}, require('./lib/appstate/AppStateStore'), {
197
+ AppStateSync: require('./lib/appstate/AppStateSync'),
198
+ LTHash: require('./lib/appstate/LTHash'),
199
+ Mutations: require('./lib/appstate/Mutations'),
200
+ SyncdProto: require('./lib/appstate/SyncdProto')
201
+ });
202
+
58
203
  module.exports = {
59
204
  WhalibmobClient,
60
205
  checkNumberStatus,
61
206
  checkIfRegistered,
62
207
  requestSmsCode,
63
208
  verifyCode,
64
- // Receive the verification code as a silent push, without an SMS — opens the
65
- // listener the native client of that platform keeps open. See "Receiving the
66
- // code over push" in the README.
67
- //
68
- // Routed through the push client for the device's platform: Android opens the
69
- // Firebase MCS stream, iOS resolves null because APNs is not implemented and
70
- // an iOS session holds no Firebase identity to listen with.
71
- receivePushCode: (store, device, opts) => {
72
- const dev = device || (store && store.device);
73
- return require('./lib/PushClient')
74
- .pushClientFor(dev)
75
- .receivePushCode(store, dev, opts);
76
- },
77
- // Whether the device profile can do push verification at all.
78
- supportsPush: (device) => require('./lib/PushClient').supportsPush(device),
209
+ receivePushCode,
210
+ supportsPush,
79
211
  assertRegistrationKeys,
80
212
  // Version fetch — use fetchWaVersion for device-aware (iOS or Android) fetching.
81
213
  // fetchIosVersion is kept for backward compatibility.
@@ -89,12 +221,12 @@ module.exports = {
89
221
  // Where a session's files live: one folder per number inside the
90
222
  // authentication folder. The same resolver the CLI uses.
91
223
  SessionPaths,
92
- defaultBaseDir: SessionPaths.defaultBaseDir,
93
- sessionDirFor: SessionPaths.sessionDirFor,
94
- storeFileFor: SessionPaths.storeFileFor,
95
- webStoreFileFor: SessionPaths.webStoreFileFor,
96
- listSessions: SessionPaths.listSessions,
97
- migrateSession: SessionPaths.migrateSession,
224
+ defaultBaseDir,
225
+ sessionDirFor,
226
+ storeFileFor,
227
+ webStoreFileFor,
228
+ listSessions,
229
+ migrateSession,
98
230
  // Device config — reads WA_OS / WA_DEVICE / WA_DEVICE_* from process.env
99
231
  getDeviceConfig,
100
232
  // Store helpers
@@ -123,10 +255,10 @@ module.exports = {
123
255
  // encoder and the BinaryInfo holder, all as the reference client defines
124
256
  // them. See client.wamBuffer / client.sendWAMBuffer().
125
257
  WAM,
126
- encodeWAM: WAM.encodeWAM,
127
- BinaryInfo: WAM.BinaryInfo,
128
- WEB_EVENTS: WAM.WEB_EVENTS,
129
- WEB_GLOBALS: WAM.WEB_GLOBALS,
258
+ encodeWAM,
259
+ BinaryInfo,
260
+ WEB_EVENTS,
261
+ WEB_GLOBALS,
130
262
  // Signal / encryption internals
131
263
  SignalProtocol,
132
264
  SignalStore,
@@ -171,5 +303,79 @@ module.exports = {
171
303
  decryptHistoryBlob,
172
304
  deriveHistoryKeys,
173
305
  HistorySyncType,
174
- HistorySyncTypeName
306
+ HistorySyncTypeName,
307
+
308
+ // ─── Namespaces ────────────────────────────────────────────────────────────
309
+ //
310
+ // Everything above is a name picked out of a module by hand — eighty-two of
311
+ // the four hundred and ninety-five the library actually has. What follows is
312
+ // the rest: every module, whole, under a name of its own. Nothing above is
313
+ // renamed, moved or removed by any of it.
314
+ //
315
+ // The rule is that a namespace is named after the module it comes from, and a
316
+ // directory in lib/ becomes one namespace rather than several. Two names
317
+ // could not be had: Devices is lib/DeviceManager.js, because the flat export
318
+ // of that name is the class rather than the module, and Proto is lib/proto.js,
319
+ // because lib/proto/ would want the same name and is MessageProto.
320
+ //
321
+ // Most are the very object a deep require returns —
322
+ // wa.MediaService === require('whalibmob/lib/MediaService') — which is worth
323
+ // knowing because deep requires work and always have: the package declares no
324
+ // `exports` map, so nothing here is the only way in. The four built with
325
+ // Object.assign (Signal, AppState, Image, Signal.libsignal) are new objects
326
+ // holding the same functions, gathered from the several modules of a
327
+ // directory. Nothing is copied out of a module and nothing is written back
328
+ // into one.
329
+ //
330
+ // test/public-surface.test.js walks lib/ and fails if any module has an
331
+ // export that none of this reaches, so the coverage is checked rather than
332
+ // claimed.
333
+
334
+ // Registration, sessions and the device a session presents itself as
335
+ Client,
336
+ Registration,
337
+ Store,
338
+ WebStore,
339
+ DeviceConfig,
340
+ Devices,
341
+ PlayStore,
342
+ PlayStoreDevice,
343
+ AndroidApk,
344
+ Attestation,
345
+ Tokens,
346
+ PushClient,
347
+ Fcm,
348
+ FcmMcs,
349
+
350
+ // Linking to an account that already exists
351
+ PairingCode,
352
+ CompanionPairing,
353
+ QrPairing,
354
+ WebVersion,
355
+ WebProto,
356
+
357
+ // The wire
358
+ BinaryNode,
359
+ Noise,
360
+ WebSocketStream,
361
+ Socks,
362
+ OfflineNodeProcessor,
363
+ Constants,
364
+ Logger,
365
+ Proto,
366
+
367
+ // Messages and media
368
+ MessageProto,
369
+ Messages,
370
+ MediaService,
371
+ MediaThumbnail,
372
+ GroupParticipant,
373
+ Image,
374
+
375
+ HistorySyncHandler,
376
+ AuthUtils,
377
+ Argo,
378
+ WAUSync,
379
+ Signal,
380
+ AppState
175
381
  };
@@ -116,8 +116,14 @@ function encryptRetryReceipt(msgId, mediaKey) {
116
116
  *
117
117
  * @param {object} notif { messageId, ciphertext, iv } off the wire
118
118
  * @param {Buffer} mediaKey the same media key the receipt was sent with
119
- * @returns {{result: number, directPath: string|null, url: string|null,
120
- * handle: string|null, ciphertextSha256: Buffer|null}}
119
+ * @returns {{result: number|null, ok: boolean, directPath: string|null,
120
+ * stanzaId: string}}
121
+ * directPath is null rather than absent when the phone declined, so
122
+ * `ok` — or a check against null — is what tells the two apart.
123
+ *
124
+ * This used to promise a url, a handle and a ciphertextSha256. None of
125
+ * the three has ever been returned; the notification does not carry
126
+ * them.
121
127
  */
122
128
  function decryptRetryNotification(notif, mediaKey) {
123
129
  const { messageId, ciphertext, iv } = notif;
@@ -534,17 +534,67 @@ function _dbl(buf) {
534
534
  return buf.readDoubleLE(0);
535
535
  }
536
536
 
537
- function decodeMessageContainer(buf) {
537
+ // ─── Envelopes ───────────────────────────────────────────────────────────────
538
+ //
539
+ // Several entries in Message carry no content of their own. They hold another
540
+ // Message and exist only to say something about it: that a photo may be opened
541
+ // once, that a message belongs to a disappearing chat, that a document was sent
542
+ // with a caption. The inner message is an ordinary ImageMessage or
543
+ // DocumentMessage — nothing about it changes.
544
+ //
545
+ // A decoder that does not open the envelope sees a field it has no branch for
546
+ // and calls the whole thing unknown. That is why view-once and disappearing
547
+ // media could not be downloaded at all: the mediaKey never reached the caller,
548
+ // and downloadMedia had no message to work from.
549
+ //
550
+ // Every one of these but DeviceSentMessage is a FutureProofMessage — one shape,
551
+ // { message = 1 }, under several names.
552
+ //
553
+ // field inner what it is
554
+ // 31 2 DeviceSentMessage { destinationJid=1, message=2, phash=3 }
555
+ // 37 1 viewOnceMessage
556
+ // 55 1 viewOnceMessageV2
557
+ // 59 1 viewOnceMessageV2Extension
558
+ // 40 1 ephemeralMessage
559
+ // 53 1 documentWithCaptionMessage
560
+ // 58 1 editedMessage
561
+ //
562
+ // All three view-once fields are read, not just the current one: V2 is what
563
+ // today's clients produce for a photo or a video and the extension what they
564
+ // produce for a voice note, but the original 37 is still what older senders and
565
+ // history sync deliver.
566
+ const MESSAGE_WRAPPERS = [
567
+ { field: 31, inner: 2, flag: null },
568
+ { field: 37, inner: 1, flag: 'viewOnce' },
569
+ { field: 55, inner: 1, flag: 'viewOnce' },
570
+ { field: 59, inner: 1, flag: 'viewOnce' },
571
+ { field: 40, inner: 1, flag: 'ephemeral' },
572
+ { field: 53, inner: 1, flag: null },
573
+ { field: 58, inner: 1, flag: 'edited' }
574
+ ];
575
+
576
+ // Real messages nest two or three deep at the most — a view-once photo inside a
577
+ // disappearing chat, echoed to our own devices. Each level is a length-
578
+ // delimited field of the one above it, so the buffer strictly shrinks and this
579
+ // cannot run away; the bound is only here so a malformed payload cannot recurse
580
+ // deep enough to exhaust the stack.
581
+ const MAX_WRAPPER_DEPTH = 8;
582
+
583
+ // The Message inside an envelope, or null when the field is absent, is not an
584
+ // envelope after all, or holds nothing.
585
+ function _unwrap(envelope, innerField) {
586
+ if (!envelope || !Buffer.isBuffer(envelope)) return null;
587
+ try {
588
+ const inner = _decodeFields(envelope)[innerField];
589
+ return (Buffer.isBuffer(inner) && inner.length) ? inner : null;
590
+ } catch (_) { return null; }
591
+ }
592
+
593
+ function decodeMessageContainer(buf, depth) {
538
594
  if (!buf || buf.length === 0) return { type: 'unknown' };
539
595
  try {
540
596
  const f = _decodeFields(buf);
541
-
542
- // Field 31: deviceSentMessage — wraps the real message at field 2
543
- // (sent to our own linked devices). Unwrap and re-decode.
544
- if (f[31]) {
545
- const dsm = _decodeFields(f[31]);
546
- if (dsm[2]) return decodeMessageContainer(dsm[2]);
547
- }
597
+ const level = depth || 0;
548
598
 
549
599
  // Field 2: senderKeyDistributionMessage — used to set up sender keys.
550
600
  // In WhatsApp Multi-Device, SKDM is bundled WITH the first real message in a new
@@ -578,6 +628,29 @@ function decodeMessageContainer(buf) {
578
628
  // Decode the actual message payload (independent of SKDM).
579
629
  // We use a helper so we can attach skdmInfo to every result.
580
630
  let msgResult = null;
631
+ // What the envelopes said about it, raised on the result below.
632
+ let envelopeFlags = null;
633
+
634
+ // An envelope is opened before anything else is looked at: whatever is
635
+ // inside it is the message, and the fields beside it here are the
636
+ // envelope's own. A distribution or a message secret riding on this level
637
+ // still belongs to what comes out, which is why both are read first and
638
+ // carried onto the result — unwrapping used to return straight out of here
639
+ // and lose them.
640
+ if (level < MAX_WRAPPER_DEPTH) {
641
+ for (const w of MESSAGE_WRAPPERS) {
642
+ const inner = _unwrap(f[w.field], w.inner);
643
+ if (!inner) continue;
644
+ const decoded = decodeMessageContainer(inner, level + 1);
645
+ // Nothing this decoder knows was in there. Leave msgResult unset so the
646
+ // branches below still get their turn, rather than reporting an empty
647
+ // envelope as the whole message.
648
+ if (!decoded || decoded.type === 'unknown') continue;
649
+ msgResult = decoded;
650
+ if (w.flag) envelopeFlags = Object.assign({}, envelopeFlags, { [w.flag]: true });
651
+ break;
652
+ }
653
+ }
581
654
 
582
655
  // Field 1: conversation — plain text
583
656
  if (!msgResult && f[1] && Buffer.isBuffer(f[1])) {
@@ -841,10 +914,24 @@ function decodeMessageContainer(buf) {
841
914
  msgResult = { type: 'pollVote' };
842
915
  }
843
916
 
844
- // If we found a real message, attach skdmInfo / messageSecret and return
917
+ // If we found a real message, attach skdmInfo / messageSecret and return.
918
+ //
919
+ // Anything the inner message already carries stays as it is: a nested
920
+ // envelope has set its own flags and read its own secret on the way up, and
921
+ // this level only fills in what is still missing.
845
922
  if (msgResult) {
846
- if (messageSecret) msgResult = Object.assign({}, msgResult, { messageSecret });
847
- return skdmInfo ? Object.assign({}, msgResult, { skdm: skdmInfo }) : msgResult;
923
+ if (envelopeFlags) msgResult = Object.assign({}, msgResult, envelopeFlags);
924
+ if (messageSecret && !msgResult.messageSecret) {
925
+ msgResult = Object.assign({}, msgResult, { messageSecret });
926
+ }
927
+ // A poll inside an envelope keeps its messageContextInfo out here, beside
928
+ // the envelope rather than beside the poll. Its votes cannot be decrypted
929
+ // without that secret, so it is taken from whichever level carried it.
930
+ if (msgResult.type === 'poll' && !msgResult.encKey && messageSecret) {
931
+ msgResult = Object.assign({}, msgResult, { encKey: messageSecret });
932
+ }
933
+ if (skdmInfo && !msgResult.skdm) msgResult = Object.assign({}, msgResult, { skdm: skdmInfo });
934
+ return msgResult;
848
935
  }
849
936
 
850
937
  // If we only have an SKDM (no other message content), return senderKeyDistribution
@@ -900,6 +987,10 @@ module.exports = {
900
987
  encodeVarint,
901
988
  // The generic field walker, for the protobufs that live outside this file.
902
989
  decodeFields: _decodeFields,
990
+ // Exported so a test can check the bound at its edge rather than guess where
991
+ // the edge is: a test that hardcodes the number goes on passing after the
992
+ // constant moves, which is the same as not testing it.
993
+ MAX_WRAPPER_DEPTH,
903
994
  field,
904
995
  varint,
905
996
  str,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.24.0",
3
+ "version": "5.24.1",
4
4
  "description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",
@@ -21,7 +21,7 @@
21
21
  "type": "commonjs",
22
22
  "scripts": {
23
23
  "test": "node --test",
24
- "test:types": "tsc --noEmit --strict --skipLibCheck index.d.ts",
24
+ "test:types": "tsc --noEmit --strict --skipLibCheck index.d.ts && tsc --noEmit --strict --skipLibCheck --types node types/usage-check.ts",
25
25
  "test:all": "npm test && npm run test:types"
26
26
  },
27
27
  "license": "MIT",