@openclaw/crabline 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +47 -22
  2. package/assets/crabline-banner.svg +20 -0
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +25 -1
  5. package/dist/src/cli/program.js +579 -82
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +59 -6
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +116 -92
  10. package/dist/src/config/schema.js +179 -73
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.js +9 -2
  13. package/dist/src/core/errors.js.map +1 -1
  14. package/dist/src/core/exit-codes.js.map +1 -1
  15. package/dist/src/core/matcher.js +7 -6
  16. package/dist/src/core/matcher.js.map +1 -1
  17. package/dist/src/core/message-template.js.map +1 -1
  18. package/dist/src/core/nonces.d.ts +1 -0
  19. package/dist/src/core/nonces.js +4 -2
  20. package/dist/src/core/nonces.js.map +1 -1
  21. package/dist/src/core/reporters.d.ts +1 -0
  22. package/dist/src/core/reporters.js +42 -4
  23. package/dist/src/core/reporters.js.map +1 -1
  24. package/dist/src/core/run.d.ts +3 -0
  25. package/dist/src/core/run.js +415 -107
  26. package/dist/src/core/run.js.map +1 -1
  27. package/dist/src/core/safe-regex.d.ts +3 -0
  28. package/dist/src/core/safe-regex.js +21 -0
  29. package/dist/src/core/safe-regex.js.map +1 -0
  30. package/dist/src/index.d.ts +2 -2
  31. package/dist/src/index.js +1 -1
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/openclaw/artifact-generation.d.ts +38 -0
  34. package/dist/src/openclaw/artifact-generation.js +314 -0
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -0
  36. package/dist/src/openclaw/bridges/matrix.js +97 -12
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +32 -10
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/signal.js +55 -18
  41. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  42. package/dist/src/openclaw/bridges/slack.js +85 -7
  43. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  44. package/dist/src/openclaw/bridges/telegram.js +124 -30
  45. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  46. package/dist/src/openclaw/bridges/whatsapp.js +78 -17
  47. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  48. package/dist/src/openclaw/bridges/zalo.js +14 -5
  49. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  50. package/dist/src/openclaw/outbound-contract.d.ts +5 -0
  51. package/dist/src/openclaw/outbound-contract.js +40 -0
  52. package/dist/src/openclaw/outbound-contract.js.map +1 -0
  53. package/dist/src/openclaw/private-file.d.ts +26 -0
  54. package/dist/src/openclaw/private-file.js +411 -0
  55. package/dist/src/openclaw/private-file.js.map +1 -0
  56. package/dist/src/openclaw/shared.d.ts +21 -2
  57. package/dist/src/openclaw/shared.js +115 -18
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +63 -0
  60. package/dist/src/openclaw/smoke-lock.js +949 -0
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -0
  62. package/dist/src/openclaw.d.ts +16 -8
  63. package/dist/src/openclaw.js +194 -52
  64. package/dist/src/openclaw.js.map +1 -1
  65. package/dist/src/providers/builtin/discord.d.ts +10 -1
  66. package/dist/src/providers/builtin/discord.js +144 -18
  67. package/dist/src/providers/builtin/discord.js.map +1 -1
  68. package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
  69. package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
  70. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
  71. package/dist/src/providers/builtin/feishu.d.ts +19 -2
  72. package/dist/src/providers/builtin/feishu.js +307 -25
  73. package/dist/src/providers/builtin/feishu.js.map +1 -1
  74. package/dist/src/providers/builtin/googlechat.d.ts +20 -1
  75. package/dist/src/providers/builtin/googlechat.js +178 -19
  76. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  77. package/dist/src/providers/builtin/imessage.d.ts +4 -0
  78. package/dist/src/providers/builtin/imessage.js +26 -11
  79. package/dist/src/providers/builtin/imessage.js.map +1 -1
  80. package/dist/src/providers/builtin/loopback.js +92 -30
  81. package/dist/src/providers/builtin/loopback.js.map +1 -1
  82. package/dist/src/providers/builtin/matrix.d.ts +11 -1
  83. package/dist/src/providers/builtin/matrix.js +53 -29
  84. package/dist/src/providers/builtin/matrix.js.map +1 -1
  85. package/dist/src/providers/builtin/mattermost.d.ts +10 -0
  86. package/dist/src/providers/builtin/mattermost.js +38 -15
  87. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  88. package/dist/src/providers/builtin/msteams.d.ts +17 -1
  89. package/dist/src/providers/builtin/msteams.js +113 -14
  90. package/dist/src/providers/builtin/msteams.js.map +1 -1
  91. package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
  92. package/dist/src/providers/builtin/native-local-mock.js +12 -35
  93. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  94. package/dist/src/providers/builtin/script.d.ts +11 -15
  95. package/dist/src/providers/builtin/script.js +566 -104
  96. package/dist/src/providers/builtin/script.js.map +1 -1
  97. package/dist/src/providers/builtin/slack.d.ts +6 -0
  98. package/dist/src/providers/builtin/slack.js +99 -40
  99. package/dist/src/providers/builtin/slack.js.map +1 -1
  100. package/dist/src/providers/builtin/telegram.d.ts +6 -0
  101. package/dist/src/providers/builtin/telegram.js +64 -43
  102. package/dist/src/providers/builtin/telegram.js.map +1 -1
  103. package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
  104. package/dist/src/providers/builtin/whatsapp.js +567 -30
  105. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  106. package/dist/src/providers/builtin/zalo.js +30 -19
  107. package/dist/src/providers/builtin/zalo.js.map +1 -1
  108. package/dist/src/providers/catalog.d.ts +3 -3
  109. package/dist/src/providers/catalog.js +3 -3
  110. package/dist/src/providers/catalog.js.map +1 -1
  111. package/dist/src/providers/local-mock.d.ts +11 -2
  112. package/dist/src/providers/local-mock.js +322 -96
  113. package/dist/src/providers/local-mock.js.map +1 -1
  114. package/dist/src/providers/native-ids.d.ts +1 -0
  115. package/dist/src/providers/native-ids.js +6 -1
  116. package/dist/src/providers/native-ids.js.map +1 -1
  117. package/dist/src/providers/recorder.d.ts +30 -2
  118. package/dist/src/providers/recorder.js +539 -70
  119. package/dist/src/providers/recorder.js.map +1 -1
  120. package/dist/src/providers/registry.d.ts +25 -1
  121. package/dist/src/providers/registry.js +280 -40
  122. package/dist/src/providers/registry.js.map +1 -1
  123. package/dist/src/providers/signed-jwt.d.ts +29 -0
  124. package/dist/src/providers/signed-jwt.js +202 -0
  125. package/dist/src/providers/signed-jwt.js.map +1 -0
  126. package/dist/src/providers/slack-ids.js.map +1 -1
  127. package/dist/src/providers/target-normalizers.d.ts +26 -0
  128. package/dist/src/providers/target-normalizers.js +276 -0
  129. package/dist/src/providers/target-normalizers.js.map +1 -0
  130. package/dist/src/providers/types.d.ts +5 -0
  131. package/dist/src/providers/types.js.map +1 -1
  132. package/dist/src/providers/webhook-server.d.ts +2 -0
  133. package/dist/src/providers/webhook-server.js +72 -25
  134. package/dist/src/providers/webhook-server.js.map +1 -1
  135. package/dist/src/servers/http.d.ts +23 -3
  136. package/dist/src/servers/http.js +213 -20
  137. package/dist/src/servers/http.js.map +1 -1
  138. package/dist/src/servers/index.js +20 -21
  139. package/dist/src/servers/index.js.map +1 -1
  140. package/dist/src/servers/matrix.js +465 -51
  141. package/dist/src/servers/matrix.js.map +1 -1
  142. package/dist/src/servers/mattermost.d.ts +5 -0
  143. package/dist/src/servers/mattermost.js +306 -58
  144. package/dist/src/servers/mattermost.js.map +1 -1
  145. package/dist/src/servers/pending-events.d.ts +2 -0
  146. package/dist/src/servers/pending-events.js +11 -0
  147. package/dist/src/servers/pending-events.js.map +1 -0
  148. package/dist/src/servers/recorder.d.ts +5 -0
  149. package/dist/src/servers/recorder.js +46 -3
  150. package/dist/src/servers/recorder.js.map +1 -1
  151. package/dist/src/servers/signal.d.ts +2 -0
  152. package/dist/src/servers/signal.js +444 -56
  153. package/dist/src/servers/signal.js.map +1 -1
  154. package/dist/src/servers/slack.d.ts +5 -0
  155. package/dist/src/servers/slack.js +515 -49
  156. package/dist/src/servers/slack.js.map +1 -1
  157. package/dist/src/servers/telegram.d.ts +111 -0
  158. package/dist/src/servers/telegram.js +811 -152
  159. package/dist/src/servers/telegram.js.map +1 -1
  160. package/dist/src/servers/webhook-target.d.ts +40 -0
  161. package/dist/src/servers/webhook-target.js +349 -0
  162. package/dist/src/servers/webhook-target.js.map +1 -0
  163. package/dist/src/servers/websocket.d.ts +2 -0
  164. package/dist/src/servers/websocket.js +29 -0
  165. package/dist/src/servers/websocket.js.map +1 -0
  166. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
  167. package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
  168. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  169. package/dist/src/servers/whatsapp-jid.d.ts +5 -0
  170. package/dist/src/servers/whatsapp-jid.js +32 -0
  171. package/dist/src/servers/whatsapp-jid.js.map +1 -0
  172. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
  173. package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
  174. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  175. package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
  176. package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
  177. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  178. package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
  179. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  181. package/dist/src/servers/whatsapp.d.ts +12 -7
  182. package/dist/src/servers/whatsapp.js +269 -107
  183. package/dist/src/servers/whatsapp.js.map +1 -1
  184. package/dist/src/servers/zalo.d.ts +14 -0
  185. package/dist/src/servers/zalo.js +521 -88
  186. package/dist/src/servers/zalo.js.map +1 -1
  187. package/docs/channel-setup.md +197 -19
  188. package/fixtures/examples/crabline.example.yaml +10 -0
  189. package/package.json +20 -15
@@ -1,14 +1,274 @@
1
1
  import { Buffer } from "node:buffer";
2
+ import { ProtocolAddress, SessionCipher, SessionRecord } from "libsignal";
2
3
  import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, } from "./whatsapp-wire/crypto.js";
3
4
  import { decodeBinaryNode, encodeBinaryNode, S_WHATSAPP_NET, } from "./whatsapp-wire/binary-node.js";
4
5
  import { decodeHandshakeMessage, encodeHandshakeMessage } from "./whatsapp-wire/handshake.js";
5
6
  import { KEY_BUNDLE_TYPE, xmppPreKey, xmppSignedPreKey } from "./whatsapp-wire/signal.js";
6
7
  import { WebSocket, WebSocketServer } from "ws";
8
+ import { closeWebSocketServer } from "./websocket.js";
9
+ import { canonicalizeWhatsAppUserCorrelationJid } from "./whatsapp-jid.js";
7
10
  // Keep the local server independent from Baileys at runtime. Tests use Baileys
8
11
  // as a black-box client to verify this narrow WhatsApp Web wire subset.
9
12
  const EMPTY_BUFFER = Buffer.alloc(0);
10
13
  const IV_LENGTH = 12;
14
+ const MAX_PENDING_INBOUND_MESSAGES = 1_000;
15
+ export const MAX_WHATSAPP_NOISE_FRAME_BYTES = 2 * 1024 * 1024;
16
+ export const MAX_WHATSAPP_WEBSOCKET_BUFFERED_BYTES = 4 * 1024 * 1024;
17
+ export const MAX_WHATSAPP_WEBSOCKET_MESSAGE_BYTES = 4 * 1024 * 1024;
18
+ export const MAX_WHATSAPP_NOISE_BUFFER_CHUNKS = 1_024;
19
+ export const MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE = 1_024;
20
+ export const WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS = 5_000;
21
+ const MAX_PENDING_WEBSOCKET_BYTES = 8 * 1024 * 1024;
22
+ const MAX_PENDING_WEBSOCKET_MESSAGES = 32;
23
+ export const MAX_WHATSAPP_WEBSOCKET_FRAGMENTS = 1_024;
24
+ export const MAX_WHATSAPP_SIGNAL_BUNDLES = 1_024;
25
+ export const MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES = 123;
11
26
  const WHATSAPP_NOISE_CERT_CHAIN = Buffer.from("CncKMwjjAhADGiCRKg7Kg1iu4CSulwLBaxX51Tefw6VXGgZqcr5OEbXIRiDQ04bOBijQjZ/TBhJA34Bj82jAHhLpCWBNVBlGnFDieamd8+138S57uMt9ke9mrn5r4+VepwBPKEgHjob6bR70rlCmWDkxZv+CfVjIAxJ2CjIIAxAAGiAcUamsMDmUxsjQuS6hh4pTNHZZnMWZ++o1mX2aqQzOYiCAka6+Bij/3rfcBhJAJw8pRkhTn+1IcOJQVN1OlZg6uikYnCumyO7acFVVX3U3QPXsGSq2TCbCbWrebSC593Su43EgprIDlfU8ZgWFBw==", "base64");
27
+ export function resolveMaxPendingWhatsAppInboundMessages(value) {
28
+ const resolved = value ?? MAX_PENDING_INBOUND_MESSAGES;
29
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
30
+ throw new Error("WhatsApp maxPendingInboundMessages must be a positive safe integer.");
31
+ }
32
+ return resolved;
33
+ }
34
+ export class WhatsAppSignalBundleStore {
35
+ maxBundles;
36
+ #bundles = new Map();
37
+ #lidByPhoneNumber = new Map();
38
+ #sessions = new Map();
39
+ constructor(maxBundles = MAX_WHATSAPP_SIGNAL_BUNDLES) {
40
+ this.maxBundles = maxBundles;
41
+ if (!Number.isSafeInteger(maxBundles) || maxBundles < 1) {
42
+ throw new Error("WhatsApp maxSignalBundles must be a positive safe integer.");
43
+ }
44
+ }
45
+ get size() {
46
+ return this.#bundles.size;
47
+ }
48
+ associateLid(phoneNumberJid, lidJid) {
49
+ const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
50
+ const lid = canonicalizeWhatsAppUserCorrelationJid(lidJid);
51
+ if (!phoneNumber?.endsWith("@s.whatsapp.net") || !lid?.endsWith("@lid")) {
52
+ throw new Error("Invalid WhatsApp PN/LID signal mapping.");
53
+ }
54
+ this.#lidByPhoneNumber.set(signalBundleIdentityKey(phoneNumber), signalBundleIdentityKey(lid));
55
+ }
56
+ resolveMany(jids) {
57
+ const uniqueNewIdentities = new Set();
58
+ const identityKeys = [];
59
+ for (const jid of jids) {
60
+ const canonical = canonicalizeWhatsAppUserCorrelationJid(jid);
61
+ if (!canonical) {
62
+ throw new Error(`Invalid WhatsApp signal bundle JID: ${jid}.`);
63
+ }
64
+ const identityKey = signalBundleIdentityKey(canonical);
65
+ identityKeys.push(identityKey);
66
+ if (!this.#bundles.has(identityKey)) {
67
+ uniqueNewIdentities.add(identityKey);
68
+ }
69
+ }
70
+ if (this.#bundles.size + uniqueNewIdentities.size > this.maxBundles) {
71
+ throw new Error(`WhatsApp signal bundle limit exceeded (${this.maxBundles}).`);
72
+ }
73
+ return identityKeys.map((identityKey) => this.#resolve(identityKey));
74
+ }
75
+ async decryptDirectMessage(params) {
76
+ const recipientJid = canonicalizeWhatsAppUserCorrelationJid(params.recipientJid);
77
+ const recipientIdentityKey = recipientJid ? signalBundleIdentityKey(recipientJid) : undefined;
78
+ const mappedLidIdentityKey = recipientIdentityKey
79
+ ? this.#lidByPhoneNumber.get(recipientIdentityKey)
80
+ : undefined;
81
+ const identityKey = mappedLidIdentityKey && this.#bundles.has(mappedLidIdentityKey)
82
+ ? mappedLidIdentityKey
83
+ : recipientIdentityKey;
84
+ const bundle = identityKey ? this.#bundles.get(identityKey) : undefined;
85
+ if (!identityKey || !bundle) {
86
+ return undefined;
87
+ }
88
+ const remoteAddress = signalProtocolAddress(params.remoteJid);
89
+ if (!remoteAddress) {
90
+ return undefined;
91
+ }
92
+ const sessions = this.#sessions.get(identityKey) ?? new Map();
93
+ this.#sessions.set(identityKey, sessions);
94
+ const stagedSessions = new Map();
95
+ const storage = {
96
+ async loadSession(id) {
97
+ const session = stagedSessions.get(id) ?? sessions.get(id);
98
+ return session ? cloneSignalSession(session) : undefined;
99
+ },
100
+ async storeSession(id, session) {
101
+ stagedSessions.set(id, cloneSignalSession(session));
102
+ },
103
+ isTrustedIdentity: () => true,
104
+ async loadPreKey(id) {
105
+ if (String(id) !== String(bundle.preKeyId)) {
106
+ return undefined;
107
+ }
108
+ return signalKeyPair(bundle.preKey);
109
+ },
110
+ removePreKey: () => undefined,
111
+ loadSignedPreKey: () => signalKeyPair(bundle.signedPreKey.keyPair),
112
+ getOurRegistrationId: () => bundle.registrationId,
113
+ getOurIdentity: () => signalKeyPair(bundle.identityKey),
114
+ };
115
+ const cipher = new SessionCipher(storage, new ProtocolAddress(remoteAddress.name, remoteAddress.deviceId));
116
+ const plaintext = params.type === "pkmsg"
117
+ ? await cipher.decryptPreKeyWhisperMessage(params.ciphertext)
118
+ : await cipher.decryptWhisperMessage(params.ciphertext);
119
+ for (const [id, session] of stagedSessions) {
120
+ sessions.set(id, session);
121
+ }
122
+ return plaintext;
123
+ }
124
+ #resolve(bundleKey) {
125
+ const existing = this.#bundles.get(bundleKey);
126
+ if (existing) {
127
+ return existing;
128
+ }
129
+ const identityKey = Curve.generateKeyPair();
130
+ const bundle = {
131
+ identityKey,
132
+ preKey: Curve.generateKeyPair(),
133
+ preKeyId: 1,
134
+ registrationId: 1,
135
+ signedPreKey: signedKeyPair(identityKey, 1),
136
+ };
137
+ this.#bundles.set(bundleKey, bundle);
138
+ return bundle;
139
+ }
140
+ }
141
+ export function createSerializedMessageHandler(processMessage, onError, options = {}) {
142
+ const maxPendingBytes = options.maxPendingBytes ?? MAX_PENDING_WEBSOCKET_BYTES;
143
+ const maxPendingMessages = options.maxPendingMessages ?? MAX_PENDING_WEBSOCKET_MESSAGES;
144
+ const sizeOf = options.sizeOf ?? (() => 1);
145
+ let failed = false;
146
+ let pendingBytes = 0;
147
+ let pendingMessages = 0;
148
+ let pending = Promise.resolve();
149
+ const fail = (error) => {
150
+ if (!failed) {
151
+ failed = true;
152
+ onError(error);
153
+ }
154
+ };
155
+ return (message) => {
156
+ if (failed) {
157
+ return pending;
158
+ }
159
+ const messageBytes = sizeOf(message);
160
+ if (!Number.isSafeInteger(messageBytes) || messageBytes < 0) {
161
+ fail(new Error("WhatsApp WebSocket message size must be a non-negative safe integer."));
162
+ return pending;
163
+ }
164
+ if (pendingMessages >= maxPendingMessages || pendingBytes + messageBytes > maxPendingBytes) {
165
+ fail(new Error("WhatsApp WebSocket inbound backlog limit exceeded."));
166
+ return pending;
167
+ }
168
+ pendingMessages += 1;
169
+ pendingBytes += messageBytes;
170
+ const next = pending
171
+ .then(async () => {
172
+ if (!failed) {
173
+ await processMessage(message);
174
+ }
175
+ })
176
+ .finally(() => {
177
+ pendingMessages -= 1;
178
+ pendingBytes -= messageBytes;
179
+ });
180
+ pending = next.catch((error) => {
181
+ fail(error);
182
+ });
183
+ return pending;
184
+ };
185
+ }
186
+ export class WhatsAppNoiseFrameDecoder {
187
+ #bufferedBytes = 0;
188
+ #chunks = [];
189
+ #expectIntro = true;
190
+ #offset = 0;
191
+ get bufferedBytes() {
192
+ return this.#bufferedBytes;
193
+ }
194
+ decodeFrames(data) {
195
+ let chunk = rawDataToBuffer(data);
196
+ if (this.#expectIntro) {
197
+ chunk = removeNoiseIntroHeader(chunk);
198
+ this.#expectIntro = false;
199
+ }
200
+ if (chunk.length > 0) {
201
+ if (this.#chunks.length >= MAX_WHATSAPP_NOISE_BUFFER_CHUNKS) {
202
+ throw new Error(`WhatsApp Noise buffer exceeds ${MAX_WHATSAPP_NOISE_BUFFER_CHUNKS} chunks.`);
203
+ }
204
+ this.#chunks.push(chunk);
205
+ this.#bufferedBytes += chunk.length;
206
+ }
207
+ const frames = [];
208
+ while (this.#bufferedBytes >= 3) {
209
+ const size = (this.#peekByte(0) << 16) | (this.#peekByte(1) << 8) | this.#peekByte(2);
210
+ if (size > MAX_WHATSAPP_NOISE_FRAME_BYTES) {
211
+ throw new Error(`WhatsApp Noise frame exceeds ${MAX_WHATSAPP_NOISE_FRAME_BYTES} bytes.`);
212
+ }
213
+ if (this.#bufferedBytes < size + 3) {
214
+ break;
215
+ }
216
+ if (frames.length >= MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE) {
217
+ throw new Error(`WhatsApp Noise message exceeds ${MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE} frames.`);
218
+ }
219
+ this.#consume(3);
220
+ frames.push(this.#read(size));
221
+ }
222
+ return frames;
223
+ }
224
+ #consume(length) {
225
+ let remaining = length;
226
+ while (remaining > 0) {
227
+ const chunk = this.#chunks[0];
228
+ if (!chunk) {
229
+ throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
230
+ }
231
+ const available = chunk.length - this.#offset;
232
+ const consumed = Math.min(available, remaining);
233
+ this.#offset += consumed;
234
+ this.#bufferedBytes -= consumed;
235
+ remaining -= consumed;
236
+ if (this.#offset === chunk.length) {
237
+ this.#chunks.shift();
238
+ this.#offset = 0;
239
+ }
240
+ }
241
+ }
242
+ #peekByte(index) {
243
+ let remaining = index + this.#offset;
244
+ for (const chunk of this.#chunks) {
245
+ if (remaining < chunk.length) {
246
+ return chunk[remaining];
247
+ }
248
+ remaining -= chunk.length;
249
+ }
250
+ throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
251
+ }
252
+ #read(length) {
253
+ const result = Buffer.allocUnsafe(length);
254
+ let resultOffset = 0;
255
+ while (resultOffset < length) {
256
+ const chunk = this.#chunks[0];
257
+ if (!chunk) {
258
+ throw new Error("Unexpected end of WhatsApp Noise frame buffer.");
259
+ }
260
+ const copied = chunk.copy(result, resultOffset, this.#offset, Math.min(chunk.length, this.#offset + length - resultOffset));
261
+ this.#offset += copied;
262
+ this.#bufferedBytes -= copied;
263
+ resultOffset += copied;
264
+ if (this.#offset === chunk.length) {
265
+ this.#chunks.shift();
266
+ this.#offset = 0;
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ }
12
272
  class TransportState {
13
273
  encKey;
14
274
  decKey;
@@ -31,9 +291,8 @@ class BaileysNoiseServer {
31
291
  #counter = 0;
32
292
  #decKey;
33
293
  #encKey;
34
- #expectIntro = true;
294
+ #frames = new WhatsAppNoiseFrameDecoder();
35
295
  #hash;
36
- #inBytes = Buffer.alloc(0);
37
296
  #salt;
38
297
  #serverEphemeralKey;
39
298
  #serverStaticKey;
@@ -47,28 +306,7 @@ class BaileysNoiseServer {
47
306
  this.#authenticate(NOISE_WA_HEADER);
48
307
  }
49
308
  decodeFrames(data) {
50
- let chunk = Buffer.from(Buffer.isBuffer(data) ? data : Buffer.from(data));
51
- if (this.#expectIntro) {
52
- chunk = this.#removeIntroHeader(chunk);
53
- this.#expectIntro = false;
54
- }
55
- this.#inBytes = this.#inBytes.length ? Buffer.concat([this.#inBytes, chunk]) : chunk;
56
- const frames = [];
57
- while (this.#inBytes.length >= 3) {
58
- const head0 = this.#inBytes[0];
59
- const head1 = this.#inBytes[1];
60
- const head2 = this.#inBytes[2];
61
- if (head0 === undefined || head1 === undefined || head2 === undefined) {
62
- break;
63
- }
64
- const size = (head0 << 16) | (head1 << 8) | head2;
65
- if (this.#inBytes.length < size + 3) {
66
- break;
67
- }
68
- frames.push(this.#inBytes.subarray(3, size + 3));
69
- this.#inBytes = this.#inBytes.subarray(size + 3);
70
- }
71
- return frames;
309
+ return this.#frames.decodeFrames(data);
72
310
  }
73
311
  async decodeTransportNode(frame) {
74
312
  if (!this.#transport) {
@@ -140,66 +378,65 @@ class BaileysNoiseServer {
140
378
  this.#decKey = readKey;
141
379
  this.#counter = 0;
142
380
  }
143
- #removeIntroHeader(chunk) {
144
- if (chunk.subarray(0, NOISE_WA_HEADER.length).equals(NOISE_WA_HEADER)) {
145
- return chunk.subarray(NOISE_WA_HEADER.length);
146
- }
147
- if (chunk.length >= 11 && chunk.subarray(0, 2).toString("utf8") === "ED" && chunk[3] === 1) {
148
- const routingInfoPrefix = chunk[4];
149
- if (routingInfoPrefix === undefined) {
150
- throw new Error("Invalid Baileys Noise routing header.");
151
- }
152
- const routingInfoLength = (routingInfoPrefix << 16) | chunk.readUInt16BE(5);
153
- const headerLength = 7 + routingInfoLength + NOISE_WA_HEADER.length;
154
- if (chunk.subarray(headerLength - NOISE_WA_HEADER.length, headerLength).equals(NOISE_WA_HEADER)) {
155
- return chunk.subarray(headerLength);
156
- }
157
- }
158
- throw new Error("Invalid Baileys Noise intro header.");
159
- }
160
381
  }
161
382
  class WhatsAppBaileysWebSocketSession {
162
383
  socket;
163
384
  params;
164
385
  #handshakeState = "client-hello";
386
+ #handleSerializedMessage;
165
387
  #noise = new BaileysNoiseServer();
166
388
  constructor(socket, params) {
167
389
  this.socket = socket;
168
390
  this.params = params;
391
+ this.#handleSerializedMessage = createSerializedMessageHandler((data) => this.#handleMessage(data), (error) => {
392
+ const close = resolveWhatsAppWebSocketClose(error);
393
+ this.socket.close(close.code, close.reason);
394
+ }, {
395
+ sizeOf: rawDataByteLength,
396
+ });
397
+ }
398
+ get isOpen() {
399
+ return this.#handshakeState === "open" && this.socket.readyState === WebSocket.OPEN;
169
400
  }
170
401
  handleMessage(data) {
171
- void this.#handleMessage(data).catch((error) => {
172
- this.socket.close(1011, error instanceof Error ? error.message : String(error));
173
- });
402
+ void this.#handleSerializedMessage(data);
174
403
  }
175
- deliverInboundMessage(message) {
176
- if (this.#handshakeState !== "open" || this.socket.readyState !== WebSocket.OPEN) {
177
- return;
404
+ async deliverInboundMessage(message) {
405
+ if (!this.isOpen) {
406
+ return false;
407
+ }
408
+ try {
409
+ await this.#sendNode(createInboundMessageNode(message));
410
+ return true;
411
+ }
412
+ catch {
413
+ this.socket.terminate();
414
+ return false;
178
415
  }
179
- this.#sendNode(createInboundMessageNode(message));
180
416
  }
181
417
  async #handleMessage(data) {
182
418
  for (const frame of this.#noise.decodeFrames(data)) {
183
419
  if (this.#handshakeState === "client-hello") {
184
- this.socket.send(this.#noise.createServerHello(frame));
420
+ await sendWhatsAppWebSocketPayload(this.socket, this.#noise.createServerHello(frame));
185
421
  this.#handshakeState = "client-finish";
186
422
  continue;
187
423
  }
188
424
  if (this.#handshakeState === "client-finish") {
189
425
  this.#noise.finishClientHandshake(frame);
190
426
  this.#handshakeState = "open";
191
- this.#sendNode({
427
+ await this.#sendNode({
192
428
  attrs: {
193
429
  lid: lidForJid(this.params.selfJid),
194
430
  t: unixSeconds(),
195
431
  },
196
432
  tag: "success",
197
433
  });
198
- this.#sendNode({
434
+ await this.#sendNode({
199
435
  attrs: {},
200
436
  content: [{ attrs: { count: "0" }, tag: "offline" }],
201
437
  tag: "ib",
202
438
  });
439
+ this.params.onOpen(this);
203
440
  continue;
204
441
  }
205
442
  await this.#handleNode(await this.#noise.decodeTransportNode(frame));
@@ -208,19 +445,37 @@ class WhatsAppBaileysWebSocketSession {
208
445
  async #handleNode(node) {
209
446
  await this.#recordNode(node);
210
447
  if (node.tag === "iq") {
211
- this.#sendNode(this.#createIqResult(node));
448
+ await this.#sendNode(this.#createIqResult(node));
212
449
  return;
213
450
  }
214
451
  if (node.tag === "message") {
215
- this.#sendNode({
452
+ const peer = requireAttr(node, "to");
453
+ const normalizedMessage = await normalizeAcceptedBaileysMessage({
454
+ node,
455
+ remoteJid: this.params.selfJid,
456
+ signalBundles: this.params.signalBundles,
457
+ });
458
+ await this.#sendNode({
216
459
  attrs: {
217
- class: "receipt",
460
+ class: "message",
461
+ from: peer,
218
462
  id: requireAttr(node, "id"),
219
- to: requireAttr(node, "to"),
220
- type: "server",
463
+ to: this.params.selfJid,
464
+ ...(node.attrs.type ? { type: node.attrs.type } : {}),
221
465
  },
222
466
  tag: "ack",
223
467
  });
468
+ if (normalizedMessage) {
469
+ await this.params.appendEvent({
470
+ accepted: true,
471
+ at: new Date().toISOString(),
472
+ body: normalizedMessage,
473
+ method: "WEBSOCKET",
474
+ path: this.params.path,
475
+ query: {},
476
+ type: "api",
477
+ });
478
+ }
224
479
  }
225
480
  }
226
481
  #createIqResult(node) {
@@ -239,7 +494,25 @@ class WhatsAppBaileysWebSocketSession {
239
494
  return { attrs, content: [{ attrs: {}, tag: "digest" }], tag: "iq" };
240
495
  }
241
496
  if (node.attrs.xmlns === "encrypt" && child?.tag === "key") {
242
- return { attrs, content: [this.#createKeyList(child)], tag: "iq" };
497
+ try {
498
+ return { attrs, content: [this.#createKeyList(child)], tag: "iq" };
499
+ }
500
+ catch (error) {
501
+ return {
502
+ attrs: { ...attrs, type: "error" },
503
+ content: [
504
+ {
505
+ attrs: {
506
+ code: "400",
507
+ text: error instanceof Error ? error.message : String(error),
508
+ type: "modify",
509
+ },
510
+ tag: "error",
511
+ },
512
+ ],
513
+ tag: "iq",
514
+ };
515
+ }
243
516
  }
244
517
  if (node.attrs.xmlns === "usync" && child?.tag === "usync") {
245
518
  return { attrs, content: [this.#createUSyncResult(child)], tag: "iq" };
@@ -301,6 +574,18 @@ class WhatsAppBaileysWebSocketSession {
301
574
  };
302
575
  }
303
576
  if (node.attrs.xmlns === "w:g2") {
577
+ if (node.attrs.type !== "get" || child?.tag !== "query") {
578
+ return {
579
+ attrs: { ...attrs, type: "error" },
580
+ content: [
581
+ {
582
+ attrs: { code: "501", text: "unsupported group operation", type: "cancel" },
583
+ tag: "error",
584
+ },
585
+ ],
586
+ tag: "iq",
587
+ };
588
+ }
304
589
  return {
305
590
  attrs,
306
591
  content: [
@@ -322,14 +607,15 @@ class WhatsAppBaileysWebSocketSession {
322
607
  }
323
608
  #createKeyList(keyNode) {
324
609
  const users = children(keyNode).filter((child) => child.tag === "user");
610
+ const jids = users.map((userNode) => requireAttr(userNode, "jid"));
611
+ const bundles = this.params.signalBundles.resolveMany(jids);
325
612
  return {
326
613
  attrs: {},
327
- content: users.map((userNode) => this.#createKeyUser(requireAttr(userNode, "jid"))),
614
+ content: jids.map((jid, index) => this.#createKeyUser(jid, bundles[index])),
328
615
  tag: "list",
329
616
  };
330
617
  }
331
- #createKeyUser(jid) {
332
- const bundle = resolveSignalBundle(this.params.signalBundles, jid);
618
+ #createKeyUser(jid, bundle) {
333
619
  return {
334
620
  attrs: { jid },
335
621
  content: [
@@ -364,6 +650,10 @@ class WhatsAppBaileysWebSocketSession {
364
650
  };
365
651
  }
366
652
  #createUSyncUser(jid) {
653
+ const lid = lidForJid(jid);
654
+ if (canonicalizeWhatsAppUserCorrelationJid(jid)?.endsWith("@s.whatsapp.net")) {
655
+ this.params.signalBundles.associateLid(jid, lid);
656
+ }
367
657
  return {
368
658
  attrs: { jid },
369
659
  content: [
@@ -378,7 +668,7 @@ class WhatsAppBaileysWebSocketSession {
378
668
  ],
379
669
  tag: "devices",
380
670
  },
381
- { attrs: { val: lidForJid(jid) }, tag: "lid" },
671
+ { attrs: { val: lid }, tag: "lid" },
382
672
  ],
383
673
  tag: "user",
384
674
  };
@@ -393,23 +683,114 @@ class WhatsAppBaileysWebSocketSession {
393
683
  type: "api",
394
684
  });
395
685
  }
396
- #sendNode(node) {
397
- this.socket.send(this.#noise.encodeNode(node));
686
+ async #sendNode(node) {
687
+ await sendWhatsAppWebSocketPayload(this.socket, this.#noise.encodeNode(node));
398
688
  }
399
689
  }
690
+ export async function sendWhatsAppWebSocketPayload(socket, payload) {
691
+ if (socket.readyState !== WebSocket.OPEN) {
692
+ throw new Error("WhatsApp WebSocket is not open.");
693
+ }
694
+ if (socket.bufferedAmount + payload.byteLength > MAX_WHATSAPP_WEBSOCKET_BUFFERED_BYTES) {
695
+ socket.terminate();
696
+ throw new Error("WhatsApp WebSocket outbound buffer limit exceeded.");
697
+ }
698
+ await new Promise((resolve, reject) => {
699
+ let settled = false;
700
+ const finish = (error) => {
701
+ if (settled) {
702
+ return;
703
+ }
704
+ settled = true;
705
+ clearTimeout(timeout);
706
+ if (error) {
707
+ reject(error);
708
+ }
709
+ else {
710
+ resolve();
711
+ }
712
+ };
713
+ const timeout = setTimeout(() => {
714
+ socket.terminate();
715
+ finish(new Error("WhatsApp WebSocket send timed out."));
716
+ }, WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS);
717
+ try {
718
+ socket.send(payload, finish);
719
+ }
720
+ catch (error) {
721
+ finish(error instanceof Error ? error : new Error(String(error)));
722
+ }
723
+ });
724
+ }
725
+ export function resolveWhatsAppWebSocketClose(error) {
726
+ const message = error instanceof Error ? error.message : String(error);
727
+ const code = /(?:backlog|exceeds|limit|payload is too large|too many)/iu.test(message)
728
+ ? 1009
729
+ : /(?:baileys|binary node|handshake|noise|protocol|unexpected end|unsupported|invalid)/iu.test(message)
730
+ ? 1002
731
+ : 1011;
732
+ return { code, reason: truncateWebSocketCloseReason(message) };
733
+ }
734
+ function truncateWebSocketCloseReason(reason) {
735
+ let result = "";
736
+ for (const character of reason) {
737
+ if (Buffer.byteLength(result + character) > MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES) {
738
+ break;
739
+ }
740
+ result += character;
741
+ }
742
+ return result;
743
+ }
400
744
  export function attachWhatsAppBaileysWebSocketServer(params) {
401
- const signalBundles = new Map();
745
+ const signalBundles = new WhatsAppSignalBundleStore();
402
746
  const sessions = new Set();
403
- const wss = new WebSocketServer({ noServer: true });
747
+ const pendingMessages = [];
748
+ const maxPendingInboundMessages = resolveMaxPendingWhatsAppInboundMessages(params.maxPendingInboundMessages);
749
+ let pendingReservations = 0;
750
+ let closing = false;
751
+ let flushPromise = Promise.resolve();
752
+ const webSocketServerOptions = {
753
+ maxBufferedChunks: MAX_WHATSAPP_WEBSOCKET_FRAGMENTS,
754
+ maxFragments: MAX_WHATSAPP_WEBSOCKET_FRAGMENTS,
755
+ maxPayload: MAX_WHATSAPP_WEBSOCKET_MESSAGE_BYTES,
756
+ noServer: true,
757
+ };
758
+ const wss = new WebSocketServer(webSocketServerOptions);
759
+ const flushPendingMessages = () => {
760
+ const next = flushPromise.then(async () => {
761
+ while (pendingMessages.length > 0) {
762
+ if (closing) {
763
+ return;
764
+ }
765
+ const message = pendingMessages[0];
766
+ if (!message) {
767
+ return;
768
+ }
769
+ const results = await Promise.all([...sessions].map((session) => session.deliverInboundMessage(message)));
770
+ if (!results.some(Boolean)) {
771
+ return;
772
+ }
773
+ pendingMessages.shift();
774
+ }
775
+ });
776
+ flushPromise = next.catch(() => undefined);
777
+ return next;
778
+ };
779
+ const rejectUpgrade = (socket, response) => {
780
+ socket.end(response, () => socket.destroy());
781
+ };
404
782
  const handleUpgrade = (request, socket, head) => {
405
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
783
+ const url = parseWhatsAppWebSocketUpgradeUrl(request.url);
784
+ if (!url) {
785
+ rejectUpgrade(socket, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
786
+ return;
787
+ }
406
788
  if (url.pathname !== params.path) {
407
789
  socket.destroy();
408
790
  return;
409
791
  }
410
792
  if (url.searchParams.get("access_token") !== params.accessToken) {
411
- socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
412
- socket.destroy();
793
+ rejectUpgrade(socket, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
413
794
  return;
414
795
  }
415
796
  wss.handleUpgrade(request, socket, head, (ws) => {
@@ -420,40 +801,299 @@ export function attachWhatsAppBaileysWebSocketServer(params) {
420
801
  wss.on("connection", (socket) => {
421
802
  const session = new WhatsAppBaileysWebSocketSession(socket, {
422
803
  appendEvent: params.appendEvent,
804
+ onOpen: () => {
805
+ void flushPendingMessages();
806
+ },
423
807
  path: params.path,
424
808
  selfJid: params.selfJid,
425
809
  signalBundles,
426
810
  });
427
811
  sessions.add(session);
428
812
  socket.once("close", () => sessions.delete(session));
813
+ socket.on("error", () => {
814
+ sessions.delete(session);
815
+ socket.terminate();
816
+ });
429
817
  socket.on("message", (data) => session.handleMessage(data));
430
818
  });
431
819
  return {
432
820
  async close() {
821
+ closing = true;
433
822
  params.httpServer.off("upgrade", handleUpgrade);
434
- for (const client of wss.clients) {
435
- client.close();
436
- }
437
- await new Promise((resolve, reject) => {
438
- wss.close((error) => {
439
- if (error) {
440
- reject(error);
441
- return;
442
- }
443
- resolve();
444
- });
445
- });
823
+ pendingMessages.length = 0;
824
+ await closeWebSocketServer(wss);
825
+ await flushPromise;
446
826
  },
447
- deliverInboundMessage(message) {
448
- for (const session of sessions) {
449
- session.deliverInboundMessage(message);
827
+ prepareInboundMessage(message) {
828
+ if (pendingMessages.length + pendingReservations >= maxPendingInboundMessages) {
829
+ return undefined;
450
830
  }
831
+ let reserved = true;
832
+ let settled = false;
833
+ pendingReservations += 1;
834
+ const releaseReservation = () => {
835
+ if (reserved) {
836
+ reserved = false;
837
+ pendingReservations -= 1;
838
+ }
839
+ };
840
+ return {
841
+ cancel() {
842
+ if (!settled) {
843
+ settled = true;
844
+ releaseReservation();
845
+ }
846
+ },
847
+ async commit() {
848
+ if (settled) {
849
+ throw new Error("WhatsApp inbound delivery reservation is already settled.");
850
+ }
851
+ settled = true;
852
+ try {
853
+ releaseReservation();
854
+ if (pendingMessages.length >= maxPendingInboundMessages) {
855
+ throw new Error("WhatsApp inbound delivery reservation exceeded queue capacity.");
856
+ }
857
+ pendingMessages.push(message);
858
+ await flushPendingMessages();
859
+ return pendingMessages.includes(message) ? "queued" : "delivered";
860
+ }
861
+ finally {
862
+ releaseReservation();
863
+ }
864
+ },
865
+ };
451
866
  },
452
867
  };
453
868
  }
869
+ export function parseWhatsAppWebSocketUpgradeUrl(requestTarget) {
870
+ try {
871
+ return new URL(requestTarget ?? "/", "http://127.0.0.1");
872
+ }
873
+ catch {
874
+ return undefined;
875
+ }
876
+ }
877
+ function rawDataByteLength(data) {
878
+ if (Array.isArray(data)) {
879
+ return data.reduce((total, chunk) => total + chunk.byteLength, 0);
880
+ }
881
+ return data.byteLength;
882
+ }
883
+ function rawDataToBuffer(data) {
884
+ if (Buffer.isBuffer(data)) {
885
+ return data;
886
+ }
887
+ if (Array.isArray(data)) {
888
+ return Buffer.concat(data);
889
+ }
890
+ return Buffer.from(data);
891
+ }
892
+ function removeNoiseIntroHeader(chunk) {
893
+ if (chunk.subarray(0, NOISE_WA_HEADER.length).equals(NOISE_WA_HEADER)) {
894
+ return chunk.subarray(NOISE_WA_HEADER.length);
895
+ }
896
+ if (chunk.length >= 11 && chunk.subarray(0, 2).toString("utf8") === "ED" && chunk[3] === 1) {
897
+ const routingInfoPrefix = chunk[4];
898
+ if (routingInfoPrefix === undefined) {
899
+ throw new Error("Invalid Baileys Noise routing header.");
900
+ }
901
+ const routingInfoLength = (routingInfoPrefix << 16) | chunk.readUInt16BE(5);
902
+ const headerLength = 7 + routingInfoLength + NOISE_WA_HEADER.length;
903
+ if (chunk.subarray(headerLength - NOISE_WA_HEADER.length, headerLength).equals(NOISE_WA_HEADER)) {
904
+ return chunk.subarray(headerLength);
905
+ }
906
+ }
907
+ throw new Error("Invalid Baileys Noise intro header.");
908
+ }
454
909
  function children(node) {
455
910
  return Array.isArray(node.content) ? node.content : [];
456
911
  }
912
+ async function normalizeAcceptedBaileysMessage(params) {
913
+ const peer = canonicalizeWhatsAppUserCorrelationJid(params.node.attrs.to ?? "");
914
+ const messageId = params.node.attrs.id;
915
+ if (!peer || !messageId) {
916
+ return undefined;
917
+ }
918
+ const candidates = encryptedMessageCandidates(params.node);
919
+ const remoteCorrelationJid = canonicalizeWhatsAppUserCorrelationJid(params.remoteJid);
920
+ candidates.sort((left, right) => {
921
+ const leftIsSelf = canonicalizeWhatsAppUserCorrelationJid(left.recipientJid) === remoteCorrelationJid;
922
+ const rightIsSelf = canonicalizeWhatsAppUserCorrelationJid(right.recipientJid) === remoteCorrelationJid;
923
+ return Number(leftIsSelf) - Number(rightIsSelf);
924
+ });
925
+ for (const candidate of candidates) {
926
+ try {
927
+ const decrypted = await params.signalBundles.decryptDirectMessage({
928
+ ciphertext: candidate.ciphertext,
929
+ recipientJid: candidate.recipientJid,
930
+ remoteJid: params.remoteJid,
931
+ type: candidate.type,
932
+ });
933
+ const text = decrypted ? readWhatsAppConversation(unpadRandomMax16(decrypted)) : undefined;
934
+ if (text) {
935
+ return {
936
+ key: {
937
+ fromMe: true,
938
+ id: messageId,
939
+ remoteJid: peer,
940
+ },
941
+ message: { conversation: text },
942
+ messageTimestamp: Math.floor(Date.now() / 1000),
943
+ };
944
+ }
945
+ }
946
+ catch {
947
+ // Other participant envelopes may not belong to this mock server identity.
948
+ }
949
+ }
950
+ return undefined;
951
+ }
952
+ function encryptedMessageCandidates(node) {
953
+ const result = [];
954
+ const visit = (current, recipientJid) => {
955
+ const nextRecipient = current.tag === "to" ? current.attrs.jid : recipientJid;
956
+ if (current.tag === "enc" &&
957
+ nextRecipient &&
958
+ (current.attrs.type === "msg" || current.attrs.type === "pkmsg") &&
959
+ current.content instanceof Uint8Array) {
960
+ result.push({
961
+ ciphertext: current.content,
962
+ recipientJid: nextRecipient,
963
+ type: current.attrs.type,
964
+ });
965
+ }
966
+ for (const child of children(current)) {
967
+ visit(child, nextRecipient);
968
+ }
969
+ };
970
+ visit(node);
971
+ return result;
972
+ }
973
+ function signalKeyPair(pair) {
974
+ return {
975
+ privKey: Buffer.from(pair.private),
976
+ pubKey: pair.public.length === 33
977
+ ? Buffer.from(pair.public)
978
+ : Buffer.concat([KEY_BUNDLE_TYPE, pair.public]),
979
+ };
980
+ }
981
+ function cloneSignalSession(session) {
982
+ return SessionRecord.deserialize(session.serialize());
983
+ }
984
+ export function signalBundleIdentityKey(jid) {
985
+ return jid.replace(/:\d+(?=@)/u, "");
986
+ }
987
+ function signalProtocolAddress(jid) {
988
+ const match = /^(\d{7,15})(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
989
+ if (!match) {
990
+ return undefined;
991
+ }
992
+ const deviceId = Number(match[2] ?? "0");
993
+ if (!Number.isSafeInteger(deviceId) || deviceId < 0) {
994
+ return undefined;
995
+ }
996
+ return {
997
+ deviceId,
998
+ name: match[3].toLowerCase() === "lid" ? `${match[1]}_1` : match[1],
999
+ };
1000
+ }
1001
+ function unpadRandomMax16(value) {
1002
+ const buffer = Buffer.from(value);
1003
+ const padding = buffer.at(-1);
1004
+ if (!padding || padding > 16 || padding > buffer.length) {
1005
+ throw new Error("Invalid WhatsApp message padding.");
1006
+ }
1007
+ for (let index = buffer.length - padding; index < buffer.length; index += 1) {
1008
+ if (buffer[index] !== padding) {
1009
+ throw new Error("Invalid WhatsApp message padding.");
1010
+ }
1011
+ }
1012
+ return buffer.subarray(0, buffer.length - padding);
1013
+ }
1014
+ function readWhatsAppConversation(message) {
1015
+ const fields = readProtobufLengthDelimitedFields(message);
1016
+ const conversation = readUtf8(fields.get(1)?.[0]);
1017
+ if (conversation?.trim()) {
1018
+ return conversation;
1019
+ }
1020
+ const extendedText = fields.get(6)?.[0];
1021
+ const extendedConversation = extendedText
1022
+ ? readUtf8(readProtobufLengthDelimitedFields(extendedText).get(1)?.[0])
1023
+ : undefined;
1024
+ if (extendedConversation?.trim()) {
1025
+ return extendedConversation;
1026
+ }
1027
+ const deviceSentMessage = fields.get(31)?.[0];
1028
+ const nestedMessage = deviceSentMessage
1029
+ ? readProtobufLengthDelimitedFields(deviceSentMessage).get(2)?.[0]
1030
+ : undefined;
1031
+ return nestedMessage ? readWhatsAppConversation(nestedMessage) : undefined;
1032
+ }
1033
+ function readProtobufLengthDelimitedFields(value) {
1034
+ const fields = new Map();
1035
+ let offset = 0;
1036
+ while (offset < value.length) {
1037
+ const tag = readProtobufVarint(value, offset);
1038
+ offset = tag.offset;
1039
+ const fieldNumber = tag.value >>> 3;
1040
+ const wireType = tag.value & 7;
1041
+ if (fieldNumber < 1) {
1042
+ throw new Error("Invalid WhatsApp protobuf field.");
1043
+ }
1044
+ if (wireType === 2) {
1045
+ const length = readProtobufVarint(value, offset);
1046
+ offset = length.offset;
1047
+ const end = offset + length.value;
1048
+ if (end > value.length) {
1049
+ throw new Error("Invalid WhatsApp protobuf length.");
1050
+ }
1051
+ const entries = fields.get(fieldNumber) ?? [];
1052
+ entries.push(value.subarray(offset, end));
1053
+ fields.set(fieldNumber, entries);
1054
+ offset = end;
1055
+ continue;
1056
+ }
1057
+ if (wireType === 0) {
1058
+ offset = readProtobufVarint(value, offset).offset;
1059
+ continue;
1060
+ }
1061
+ if (wireType === 1) {
1062
+ offset += 8;
1063
+ continue;
1064
+ }
1065
+ if (wireType === 5) {
1066
+ offset += 4;
1067
+ continue;
1068
+ }
1069
+ throw new Error(`Unsupported WhatsApp protobuf wire type: ${wireType}.`);
1070
+ }
1071
+ return fields;
1072
+ }
1073
+ function readProtobufVarint(value, start) {
1074
+ let result = 0;
1075
+ let shift = 0;
1076
+ let offset = start;
1077
+ while (offset < value.length && shift <= 28) {
1078
+ const byte = value[offset++];
1079
+ result += (byte & 0x7f) * 2 ** shift;
1080
+ if ((byte & 0x80) === 0) {
1081
+ if (!Number.isSafeInteger(result)) {
1082
+ break;
1083
+ }
1084
+ return { offset, value: result };
1085
+ }
1086
+ shift += 7;
1087
+ }
1088
+ throw new Error("Invalid WhatsApp protobuf varint.");
1089
+ }
1090
+ function readUtf8(value) {
1091
+ if (!value) {
1092
+ return undefined;
1093
+ }
1094
+ const text = Buffer.from(value).toString("utf8");
1095
+ return Buffer.from(text, "utf8").equals(Buffer.from(value)) ? text : undefined;
1096
+ }
457
1097
  function createInboundMessageNode(message) {
458
1098
  const from = message.key.remoteJid;
459
1099
  const attrs = {
@@ -521,22 +1161,6 @@ function lidForJid(jid) {
521
1161
  const user = jid.split("@", 1)[0]?.split(":", 1)[0] ?? "15550000000";
522
1162
  return `${user}@lid`;
523
1163
  }
524
- function resolveSignalBundle(signalBundles, jid) {
525
- const existing = signalBundles.get(jid);
526
- if (existing) {
527
- return existing;
528
- }
529
- const identityKey = Curve.generateKeyPair();
530
- const bundle = {
531
- identityKey,
532
- preKey: Curve.generateKeyPair(),
533
- preKeyId: 1,
534
- registrationId: 1,
535
- signedPreKey: signedKeyPair(identityKey, 1),
536
- };
537
- signalBundles.set(jid, bundle);
538
- return bundle;
539
- }
540
1164
  function sanitizeNodeForJson(value) {
541
1165
  if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
542
1166
  return { base64: Buffer.from(value).toString("base64"), type: "Buffer" };