@openclaw/crabline 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +99 -12
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +264 -50
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +532 -42
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1241 -22
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1193 -118
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,12 +1,12 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { ProtocolAddress, SessionCipher, SessionRecord } from "libsignal";
3
- import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, } from "./whatsapp-wire/crypto.js";
3
+ import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, ensureSignalPublicKey, generateSignalKeyPair, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, scrubSignalPublicKey, } from "./whatsapp-wire/crypto.js";
4
4
  import { decodeBinaryNode, encodeBinaryNode, S_WHATSAPP_NET, } from "./whatsapp-wire/binary-node.js";
5
5
  import { decodeHandshakeMessage, encodeHandshakeMessage } from "./whatsapp-wire/handshake.js";
6
6
  import { KEY_BUNDLE_TYPE, xmppPreKey, xmppSignedPreKey } from "./whatsapp-wire/signal.js";
7
7
  import { WebSocket, WebSocketServer } from "ws";
8
8
  import { closeWebSocketServer } from "./websocket.js";
9
- import { canonicalizeWhatsAppUserCorrelationJid } from "./whatsapp-jid.js";
9
+ import { canonicalizeWhatsAppChatCorrelationJid, canonicalizeWhatsAppUserCorrelationJid, } from "./whatsapp-jid.js";
10
10
  // Keep the local server independent from Baileys at runtime. Tests use Baileys
11
11
  // as a black-box client to verify this narrow WhatsApp Web wire subset.
12
12
  const EMPTY_BUFFER = Buffer.alloc(0);
@@ -20,9 +20,17 @@ export const MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE = 1_024;
20
20
  export const WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS = 5_000;
21
21
  const MAX_PENDING_WEBSOCKET_BYTES = 8 * 1024 * 1024;
22
22
  const MAX_PENDING_WEBSOCKET_MESSAGES = 32;
23
+ const MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENTS = 10_000;
24
+ const MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENT_AGE_MS = 5 * 60 * 1_000;
25
+ const MAX_WHATSAPP_RECENT_ACKNOWLEDGEMENTS = 10_000;
26
+ const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
23
27
  export const MAX_WHATSAPP_WEBSOCKET_FRAGMENTS = 1_024;
24
28
  export const MAX_WHATSAPP_SIGNAL_BUNDLES = 1_024;
29
+ export const MAX_WHATSAPP_SIGNAL_PREKEYS_PER_BUNDLE = 32;
30
+ export const MAX_WHATSAPP_SIGNAL_SESSIONS_PER_BUNDLE = 32;
25
31
  export const MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES = 123;
32
+ export const WHATSAPP_MESSAGE_ACCEPTANCE_TIMEOUT_MS = 5_000;
33
+ export const WHATSAPP_SIGNAL_PREKEY_RESERVATION_TTL_MS = 5 * 60 * 1_000;
26
34
  const WHATSAPP_NOISE_CERT_CHAIN = Buffer.from("CncKMwjjAhADGiCRKg7Kg1iu4CSulwLBaxX51Tefw6VXGgZqcr5OEbXIRiDQ04bOBijQjZ/TBhJA34Bj82jAHhLpCWBNVBlGnFDieamd8+138S57uMt9ke9mrn5r4+VepwBPKEgHjob6bR70rlCmWDkxZv+CfVjIAxJ2CjIIAxAAGiAcUamsMDmUxsjQuS6hh4pTNHZZnMWZ++o1mX2aqQzOYiCAka6+Bij/3rfcBhJAJw8pRkhTn+1IcOJQVN1OlZg6uikYnCumyO7acFVVX3U3QPXsGSq2TCbCbWrebSC593Su43EgprIDlfU8ZgWFBw==", "base64");
27
35
  export function resolveMaxPendingWhatsAppInboundMessages(value) {
28
36
  const resolved = value ?? MAX_PENDING_INBOUND_MESSAGES;
@@ -31,27 +39,271 @@ export function resolveMaxPendingWhatsAppInboundMessages(value) {
31
39
  }
32
40
  return resolved;
33
41
  }
42
+ async function awaitWithAbort(operation, signal) {
43
+ if (!signal) {
44
+ return await operation;
45
+ }
46
+ signal.throwIfAborted();
47
+ return await new Promise((resolve, reject) => {
48
+ const abort = () => reject(signal.reason);
49
+ signal.addEventListener("abort", abort, { once: true });
50
+ void operation.then((value) => {
51
+ signal.removeEventListener("abort", abort);
52
+ resolve(value);
53
+ }, (error) => {
54
+ signal.removeEventListener("abort", abort);
55
+ reject(error);
56
+ });
57
+ });
58
+ }
34
59
  export class WhatsAppSignalBundleStore {
35
60
  maxBundles;
61
+ maxPendingAcknowledgements;
62
+ maxRecentAcknowledgements;
63
+ maxPendingAcknowledgementAgeMs;
64
+ now;
65
+ #acknowledgedMessageIds = new Map();
36
66
  #bundles = new Map();
37
67
  #lidByPhoneNumber = new Map();
68
+ #maxPreKeysPerBundle;
69
+ #maxSessionsPerBundle;
70
+ #maxTerminalAcceptances;
71
+ #messagePreKeyProtections = new Map();
72
+ #messageAcceptanceTimeoutMs;
73
+ #pendingAcknowledgements = new Map();
74
+ #pendingTransactions = new Map();
75
+ #persistedMessageIds = new Map();
76
+ #preKeyReservationTtlMs;
38
77
  #sessions = new Map();
39
- constructor(maxBundles = MAX_WHATSAPP_SIGNAL_BUNDLES) {
78
+ #terminalAcceptances = new Map();
79
+ constructor(maxBundles = MAX_WHATSAPP_SIGNAL_BUNDLES, maxPendingAcknowledgements = MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENTS, maxRecentAcknowledgements = MAX_WHATSAPP_RECENT_ACKNOWLEDGEMENTS, maxPendingAcknowledgementAgeMs = MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENT_AGE_MS, now = Date.now, options = {}) {
40
80
  this.maxBundles = maxBundles;
81
+ this.maxPendingAcknowledgements = maxPendingAcknowledgements;
82
+ this.maxRecentAcknowledgements = maxRecentAcknowledgements;
83
+ this.maxPendingAcknowledgementAgeMs = maxPendingAcknowledgementAgeMs;
84
+ this.now = now;
85
+ const maxPreKeysPerBundle = options.maxPreKeysPerBundle ?? MAX_WHATSAPP_SIGNAL_PREKEYS_PER_BUNDLE;
86
+ const maxSessionsPerBundle = options.maxSessionsPerBundle ?? MAX_WHATSAPP_SIGNAL_SESSIONS_PER_BUNDLE;
87
+ const messageAcceptanceTimeoutMs = options.messageAcceptanceTimeoutMs ?? WHATSAPP_MESSAGE_ACCEPTANCE_TIMEOUT_MS;
88
+ const preKeyReservationTtlMs = options.preKeyReservationTtlMs ?? WHATSAPP_SIGNAL_PREKEY_RESERVATION_TTL_MS;
41
89
  if (!Number.isSafeInteger(maxBundles) || maxBundles < 1) {
42
90
  throw new Error("WhatsApp maxSignalBundles must be a positive safe integer.");
43
91
  }
92
+ if (!Number.isSafeInteger(maxSessionsPerBundle) || maxSessionsPerBundle < 1) {
93
+ throw new Error("WhatsApp maxSignalSessionsPerBundle must be a positive safe integer.");
94
+ }
95
+ if (!Number.isSafeInteger(maxPreKeysPerBundle) || maxPreKeysPerBundle < 1) {
96
+ throw new Error("WhatsApp maxSignalPreKeysPerBundle must be a positive safe integer.");
97
+ }
98
+ if (!Number.isSafeInteger(messageAcceptanceTimeoutMs) ||
99
+ messageAcceptanceTimeoutMs < 1 ||
100
+ messageAcceptanceTimeoutMs > MAX_NODE_TIMER_DELAY_MS) {
101
+ throw new Error(`WhatsApp messageAcceptanceTimeoutMs must be a positive integer no greater than ${MAX_NODE_TIMER_DELAY_MS}.`);
102
+ }
103
+ if (!Number.isSafeInteger(preKeyReservationTtlMs) || preKeyReservationTtlMs < 1) {
104
+ throw new Error("WhatsApp preKeyReservationTtlMs must be a positive safe integer.");
105
+ }
106
+ if (!Number.isSafeInteger(maxPendingAcknowledgements) || maxPendingAcknowledgements < 1) {
107
+ throw new Error("WhatsApp maxPendingAcknowledgements must be a positive safe integer.");
108
+ }
109
+ if (!Number.isSafeInteger(maxRecentAcknowledgements) || maxRecentAcknowledgements < 1) {
110
+ throw new Error("WhatsApp maxRecentAcknowledgements must be a positive safe integer.");
111
+ }
112
+ if (!Number.isSafeInteger(maxPendingAcknowledgementAgeMs) ||
113
+ maxPendingAcknowledgementAgeMs < 1) {
114
+ throw new Error("WhatsApp maxPendingAcknowledgementAgeMs must be a positive safe integer.");
115
+ }
116
+ this.#maxPreKeysPerBundle = maxPreKeysPerBundle;
117
+ this.#maxSessionsPerBundle = maxSessionsPerBundle;
118
+ this.#maxTerminalAcceptances = Math.min(Number.MAX_SAFE_INTEGER, maxPendingAcknowledgements + maxRecentAcknowledgements);
119
+ this.#messageAcceptanceTimeoutMs = messageAcceptanceTimeoutMs;
120
+ this.#preKeyReservationTtlMs = preKeyReservationTtlMs;
44
121
  }
45
122
  get size() {
46
123
  return this.#bundles.size;
47
124
  }
125
+ get lidMappingSize() {
126
+ return this.#lidByPhoneNumber.size;
127
+ }
128
+ get sessionCount() {
129
+ let count = 0;
130
+ for (const sessions of this.#sessions.values()) {
131
+ count += sessions.size;
132
+ }
133
+ return count;
134
+ }
135
+ async acceptMessageOnce(messageKey, operation, options = {}) {
136
+ this.#recoverExpiredPendingAcknowledgements();
137
+ const pendingAcceptance = this.#pendingAcknowledgements.get(messageKey);
138
+ if (pendingAcceptance) {
139
+ return await pendingAcceptance.acceptance;
140
+ }
141
+ const terminalAcceptance = this.#terminalAcceptances.get(messageKey);
142
+ if (terminalAcceptance) {
143
+ this.#terminalAcceptances.delete(messageKey);
144
+ this.#terminalAcceptances.set(messageKey, terminalAcceptance);
145
+ throw terminalAcceptance.error;
146
+ }
147
+ if (this.#acknowledgedMessageIds.delete(messageKey)) {
148
+ this.#acknowledgedMessageIds.set(messageKey, true);
149
+ return true;
150
+ }
151
+ if (this.#terminalAcceptances.size + this.#pendingAcknowledgements.size >=
152
+ this.#maxTerminalAcceptances) {
153
+ throw new Error(`WhatsApp terminal acceptance limit exceeded (${this.#maxTerminalAcceptances}).`);
154
+ }
155
+ if (this.#pendingAcknowledgements.size >= this.maxPendingAcknowledgements) {
156
+ throw new Error(`WhatsApp pending acknowledgement limit exceeded (${this.maxPendingAcknowledgements}).`);
157
+ }
158
+ const timeoutMs = options.timeoutMs ?? this.#messageAcceptanceTimeoutMs;
159
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_NODE_TIMER_DELAY_MS) {
160
+ throw new Error(`WhatsApp message acceptance timeout must be a positive integer no greater than ${MAX_NODE_TIMER_DELAY_MS}.`);
161
+ }
162
+ options.signal?.throwIfAborted();
163
+ const controller = new AbortController();
164
+ const operationPromise = Promise.resolve().then(() => operation(controller.signal));
165
+ let terminalFenced = false;
166
+ const fenceTerminalAcceptance = (error) => {
167
+ if (terminalFenced) {
168
+ return;
169
+ }
170
+ terminalFenced = true;
171
+ controller.abort(error);
172
+ const terminalDrain = Promise.all([
173
+ operationPromise.then(() => undefined, () => undefined),
174
+ Promise.resolve()
175
+ .then(() => options.terminalDrain?.())
176
+ .then((disposition) => disposition ?? "retryable", () => "retryable"),
177
+ ]).then(([, disposition]) => disposition);
178
+ this.#rememberTerminalAcceptance(messageKey, error, terminalDrain);
179
+ };
180
+ const abortFromOwner = () => {
181
+ const reason = options.signal?.reason;
182
+ fenceTerminalAcceptance(reason instanceof Error ? reason : new Error("WhatsApp message acceptance cancelled."));
183
+ };
184
+ if (options.signal?.aborted) {
185
+ abortFromOwner();
186
+ }
187
+ else {
188
+ options.signal?.addEventListener("abort", abortFromOwner, { once: true });
189
+ }
190
+ const timeout = setTimeout(() => {
191
+ const error = new Error("WhatsApp message acceptance timed out.");
192
+ fenceTerminalAcceptance(error);
193
+ try {
194
+ options.onTerminalTimeout?.();
195
+ }
196
+ catch { }
197
+ }, timeoutMs);
198
+ timeout.unref();
199
+ let pendingAcknowledgement;
200
+ const acceptance = Promise.resolve().then(async () => {
201
+ try {
202
+ const accepted = await awaitWithAbort(operationPromise, controller.signal);
203
+ if (!accepted) {
204
+ if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
205
+ this.#pendingAcknowledgements.delete(messageKey);
206
+ }
207
+ return false;
208
+ }
209
+ if (pendingAcknowledgement.acknowledged) {
210
+ if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
211
+ this.#pendingAcknowledgements.delete(messageKey);
212
+ }
213
+ this.#rememberAcknowledgedMessage(messageKey);
214
+ }
215
+ else {
216
+ pendingAcknowledgement.acceptedAt = this.now();
217
+ }
218
+ return true;
219
+ }
220
+ catch (error) {
221
+ if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
222
+ this.#pendingAcknowledgements.delete(messageKey);
223
+ }
224
+ throw error;
225
+ }
226
+ finally {
227
+ clearTimeout(timeout);
228
+ options.signal?.removeEventListener("abort", abortFromOwner);
229
+ }
230
+ });
231
+ pendingAcknowledgement = {
232
+ acceptance,
233
+ acceptedAt: undefined,
234
+ acknowledged: false,
235
+ };
236
+ this.#pendingAcknowledgements.set(messageKey, pendingAcknowledgement);
237
+ return await acceptance;
238
+ }
239
+ markMessageAcknowledged(peerJid, messageId) {
240
+ const peer = canonicalizeWhatsAppChatCorrelationJid(peerJid);
241
+ if (peer && messageId) {
242
+ const messageKey = `${peer}\0${messageId}`;
243
+ this.#recoverExpiredPendingAcknowledgements();
244
+ const pendingAcknowledgement = this.#pendingAcknowledgements.get(messageKey);
245
+ if (!pendingAcknowledgement) {
246
+ return;
247
+ }
248
+ if (pendingAcknowledgement.acceptedAt === undefined) {
249
+ pendingAcknowledgement.acknowledged = true;
250
+ return;
251
+ }
252
+ this.#pendingAcknowledgements.delete(messageKey);
253
+ this.#rememberAcknowledgedMessage(messageKey);
254
+ }
255
+ }
256
+ hasPersistedMessage(messageKey) {
257
+ return this.#persistedMessageIds.has(messageKey);
258
+ }
259
+ clearPersistedMessage(messageKey) {
260
+ this.#persistedMessageIds.delete(messageKey);
261
+ this.releaseMessagePreKeyProtection(messageKey);
262
+ }
263
+ releaseMessagePreKeyProtection(messageKey) {
264
+ const protections = this.#messagePreKeyProtections.get(messageKey);
265
+ if (!protections) {
266
+ return;
267
+ }
268
+ this.#messagePreKeyProtections.delete(messageKey);
269
+ for (const [identityKey, preKeyIds] of protections) {
270
+ const bundle = this.#bundles.get(identityKey);
271
+ if (!bundle) {
272
+ continue;
273
+ }
274
+ for (const preKeyId of preKeyIds) {
275
+ const references = bundle.protectedPreKeyIds.get(preKeyId) ?? 0;
276
+ if (references <= 1) {
277
+ bundle.protectedPreKeyIds.delete(preKeyId);
278
+ }
279
+ else {
280
+ bundle.protectedPreKeyIds.set(preKeyId, references - 1);
281
+ }
282
+ }
283
+ }
284
+ }
48
285
  associateLid(phoneNumberJid, lidJid) {
49
286
  const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
50
287
  const lid = canonicalizeWhatsAppUserCorrelationJid(lidJid);
51
288
  if (!phoneNumber?.endsWith("@s.whatsapp.net") || !lid?.endsWith("@lid")) {
52
289
  throw new Error("Invalid WhatsApp PN/LID signal mapping.");
53
290
  }
54
- this.#lidByPhoneNumber.set(signalBundleIdentityKey(phoneNumber), signalBundleIdentityKey(lid));
291
+ const phoneNumberKey = signalBundleIdentityKey(phoneNumber);
292
+ this.#lidByPhoneNumber.delete(phoneNumberKey);
293
+ this.#lidByPhoneNumber.set(phoneNumberKey, signalBundleIdentityKey(lid));
294
+ if (this.#lidByPhoneNumber.size > this.maxBundles) {
295
+ const oldestPhoneNumber = this.#lidByPhoneNumber.keys().next().value;
296
+ if (oldestPhoneNumber !== undefined) {
297
+ this.#lidByPhoneNumber.delete(oldestPhoneNumber);
298
+ }
299
+ }
300
+ }
301
+ resolveAssociatedLid(phoneNumberJid) {
302
+ const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
303
+ if (!phoneNumber?.endsWith("@s.whatsapp.net")) {
304
+ return undefined;
305
+ }
306
+ return this.#lidByPhoneNumber.get(signalBundleIdentityKey(phoneNumber));
55
307
  }
56
308
  resolveMany(jids) {
57
309
  const uniqueNewIdentities = new Set();
@@ -68,74 +320,326 @@ export class WhatsAppSignalBundleStore {
68
320
  }
69
321
  }
70
322
  if (this.#bundles.size + uniqueNewIdentities.size > this.maxBundles) {
323
+ // Published bundle identities stay stable for the store lifetime.
71
324
  throw new Error(`WhatsApp signal bundle limit exceeded (${this.maxBundles}).`);
72
325
  }
73
- return identityKeys.map((identityKey) => this.#resolve(identityKey));
326
+ const stagedNewBundles = new Map();
327
+ const stagedReservations = new Map();
328
+ const reservationNow = this.now();
329
+ const stagedForIdentity = (identityKey) => {
330
+ let bundle = this.#bundles.get(identityKey) ?? stagedNewBundles.get(identityKey);
331
+ if (!bundle) {
332
+ bundle = this.#createBundle();
333
+ stagedNewBundles.set(identityKey, bundle);
334
+ }
335
+ let staged = stagedReservations.get(identityKey);
336
+ if (!staged) {
337
+ const preKeys = new Map(bundle.preKeys);
338
+ const preKeyReservations = new Map(bundle.preKeyReservations);
339
+ for (const [preKeyId, reservedAt] of preKeyReservations) {
340
+ if (reservationNow - reservedAt >= this.#preKeyReservationTtlMs &&
341
+ !bundle.protectedPreKeyIds.has(preKeyId)) {
342
+ preKeyReservations.delete(preKeyId);
343
+ preKeys.delete(preKeyId);
344
+ }
345
+ }
346
+ staged = {
347
+ availablePreKeyIds: bundle.availablePreKeyIds.filter((id) => preKeys.has(id)),
348
+ bundle,
349
+ nextPreKeyId: bundle.nextPreKeyId,
350
+ preKeyReservations,
351
+ preKeys,
352
+ };
353
+ stagedReservations.set(identityKey, staged);
354
+ }
355
+ return staged;
356
+ };
357
+ const requestedPreKeys = new Map();
358
+ for (const identityKey of identityKeys) {
359
+ requestedPreKeys.set(identityKey, (requestedPreKeys.get(identityKey) ?? 0) + 1);
360
+ }
361
+ for (const [identityKey, requested] of requestedPreKeys) {
362
+ const staged = stagedForIdentity(identityKey);
363
+ const available = staged.availablePreKeyIds.length;
364
+ const creatable = Math.max(0, this.#maxPreKeysPerBundle - staged.preKeys.size - staged.bundle.pendingPreKeyIds.size);
365
+ if (requested > available + creatable) {
366
+ throw new Error(`WhatsApp Signal prekey reservation limit exceeded (${this.#maxPreKeysPerBundle}).`);
367
+ }
368
+ }
369
+ const reservedBundles = identityKeys.map((identityKey) => {
370
+ const staged = stagedForIdentity(identityKey);
371
+ const { bundle } = staged;
372
+ let preKeyId = staged.availablePreKeyIds.shift();
373
+ if (preKeyId === undefined) {
374
+ const created = this.#createPreKey(staged.preKeys, staged.nextPreKeyId, staged.bundle.pendingPreKeyIds);
375
+ preKeyId = created.id;
376
+ staged.preKeys.set(created.id, created.keyPair);
377
+ staged.nextPreKeyId = created.nextPreKeyId;
378
+ }
379
+ const preKey = staged.preKeys.get(preKeyId);
380
+ if (!preKey) {
381
+ throw new Error("WhatsApp Signal prekey reservation is unavailable.");
382
+ }
383
+ staged.preKeyReservations.set(preKeyId, reservationNow);
384
+ return {
385
+ identityKey: bundle.identityKey,
386
+ preKey,
387
+ preKeyId,
388
+ registrationId: bundle.registrationId,
389
+ signedPreKey: bundle.signedPreKey,
390
+ };
391
+ });
392
+ for (const [identityKey, bundle] of stagedNewBundles) {
393
+ this.#bundles.set(identityKey, bundle);
394
+ }
395
+ for (const staged of stagedReservations.values()) {
396
+ staged.bundle.availablePreKeyIds = staged.availablePreKeyIds;
397
+ staged.bundle.nextPreKeyId = staged.nextPreKeyId;
398
+ staged.bundle.preKeyReservations = staged.preKeyReservations;
399
+ staged.bundle.preKeys = staged.preKeys;
400
+ }
401
+ return reservedBundles;
74
402
  }
75
403
  async decryptDirectMessage(params) {
404
+ const result = await this.transactDirectMessage({
405
+ ...params,
406
+ accept: async (plaintext) => plaintext,
407
+ });
408
+ if (result.status === "decrypt-failed") {
409
+ throw result.error;
410
+ }
411
+ return result.status === "accepted" ? result.value : undefined;
412
+ }
413
+ async transactDirectMessage(params) {
414
+ params.signal?.throwIfAborted();
76
415
  const recipientJid = canonicalizeWhatsAppUserCorrelationJid(params.recipientJid);
77
416
  const recipientIdentityKey = recipientJid ? signalBundleIdentityKey(recipientJid) : undefined;
78
417
  const mappedLidIdentityKey = recipientIdentityKey
79
- ? this.#lidByPhoneNumber.get(recipientIdentityKey)
418
+ ? this.resolveAssociatedLid(recipientIdentityKey)
80
419
  : undefined;
81
420
  const identityKey = mappedLidIdentityKey && this.#bundles.has(mappedLidIdentityKey)
82
421
  ? mappedLidIdentityKey
83
422
  : recipientIdentityKey;
84
423
  const bundle = identityKey ? this.#bundles.get(identityKey) : undefined;
85
424
  if (!identityKey || !bundle) {
86
- return undefined;
425
+ return { status: "unavailable" };
87
426
  }
88
427
  const remoteAddress = signalProtocolAddress(params.remoteJid);
89
428
  if (!remoteAddress) {
90
- return undefined;
429
+ return { status: "unavailable" };
91
430
  }
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;
431
+ const address = new ProtocolAddress(remoteAddress.name, remoteAddress.deviceId);
432
+ // Capacity checks, staged writes, and commits stay atomic per recipient bundle.
433
+ return await this.#runTransaction(identityKey, async () => {
434
+ params.signal?.throwIfAborted();
435
+ const sessions = this.#sessions.get(identityKey) ?? new Map();
436
+ const sessionLimitError = new Error("WhatsApp Signal session limit exceeded.");
437
+ const maxSessionsPerBundle = this.#maxSessionsPerBundle;
438
+ let stagedNewSessionCount = 0;
439
+ const stagedPreKeyRemovals = new Set();
440
+ const stagedSessions = new Map();
441
+ const storage = {
442
+ async loadSession(id) {
443
+ const session = stagedSessions.get(id) ?? sessions.get(id);
444
+ return session ? cloneSignalSession(session) : undefined;
445
+ },
446
+ async storeSession(id, session) {
447
+ if (!sessions.has(id) && !stagedSessions.has(id)) {
448
+ if (sessions.size + stagedNewSessionCount >= maxSessionsPerBundle) {
449
+ throw sessionLimitError;
450
+ }
451
+ stagedNewSessionCount += 1;
452
+ }
453
+ stagedSessions.set(id, cloneSignalSession(session));
454
+ },
455
+ isTrustedIdentity: () => true,
456
+ loadPreKey: async (id) => {
457
+ const preKey = bundle.preKeys.get(Number(id));
458
+ if (preKey && params.messageKey) {
459
+ this.#protectMessagePreKey(params.messageKey, identityKey, Number(id));
460
+ }
461
+ return preKey ? signalKeyPair(preKey) : undefined;
462
+ },
463
+ removePreKey(id) {
464
+ stagedPreKeyRemovals.add(id);
465
+ },
466
+ loadSignedPreKey: () => signalKeyPair(bundle.signedPreKey.keyPair),
467
+ getOurRegistrationId: () => bundle.registrationId,
468
+ getOurIdentity: () => signalKeyPair(bundle.identityKey),
469
+ };
470
+ const cipher = new SessionCipher(storage, address);
471
+ let plaintext;
472
+ try {
473
+ plaintext =
474
+ params.type === "pkmsg"
475
+ ? await cipher.decryptPreKeyWhisperMessage(params.ciphertext)
476
+ : await cipher.decryptWhisperMessage(params.ciphertext);
477
+ }
478
+ catch (error) {
479
+ if (error === sessionLimitError) {
480
+ return { status: "rejected" };
107
481
  }
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 = {
482
+ return { error, status: "decrypt-failed" };
483
+ }
484
+ params.signal?.throwIfAborted();
485
+ let stagedNextPreKeyId = bundle.nextPreKeyId;
486
+ const stagedPreKeyIds = new Set();
487
+ const replacementPreKeys = [...stagedPreKeyRemovals]
488
+ .filter((id) => bundle.preKeys.has(id))
489
+ .map(() => {
490
+ const unavailableIds = new Set([...bundle.pendingPreKeyIds, ...stagedPreKeyIds]);
491
+ const created = this.#createPreKey(bundle.preKeys, stagedNextPreKeyId, unavailableIds);
492
+ stagedNextPreKeyId = created.nextPreKeyId;
493
+ stagedPreKeyIds.add(created.id);
494
+ return created;
495
+ });
496
+ for (const replacementPreKey of replacementPreKeys) {
497
+ bundle.pendingPreKeyIds.add(replacementPreKey.id);
498
+ }
499
+ bundle.nextPreKeyId = stagedNextPreKeyId;
500
+ try {
501
+ const accepted = await awaitWithAbort(Promise.resolve().then(() => params.accept(plaintext)), params.signal);
502
+ if (accepted === undefined) {
503
+ return { status: "rejected" };
504
+ }
505
+ params.signal?.throwIfAborted();
506
+ for (const [id, session] of stagedSessions) {
507
+ sessions.set(id, session);
508
+ }
509
+ if (stagedSessions.size > 0) {
510
+ this.#sessions.set(identityKey, sessions);
511
+ }
512
+ for (const id of stagedPreKeyRemovals) {
513
+ bundle.preKeyReservations.delete(id);
514
+ bundle.preKeys.delete(id);
515
+ }
516
+ for (const replacementPreKey of replacementPreKeys) {
517
+ bundle.preKeys.set(replacementPreKey.id, replacementPreKey.keyPair);
518
+ bundle.availablePreKeyIds.push(replacementPreKey.id);
519
+ }
520
+ return { status: "accepted", value: accepted };
521
+ }
522
+ finally {
523
+ for (const replacementPreKey of replacementPreKeys) {
524
+ bundle.pendingPreKeyIds.delete(replacementPreKey.id);
525
+ }
526
+ }
527
+ });
528
+ }
529
+ #createBundle() {
530
+ const identityKey = generateSignalKeyPair();
531
+ return {
532
+ availablePreKeyIds: [],
131
533
  identityKey,
132
- preKey: Curve.generateKeyPair(),
133
- preKeyId: 1,
534
+ nextPreKeyId: 1,
535
+ pendingPreKeyIds: new Set(),
536
+ preKeyReservations: new Map(),
537
+ preKeys: new Map(),
538
+ protectedPreKeyIds: new Map(),
134
539
  registrationId: 1,
135
540
  signedPreKey: signedKeyPair(identityKey, 1),
136
541
  };
137
- this.#bundles.set(bundleKey, bundle);
138
- return bundle;
542
+ }
543
+ #createPreKey(preKeys, startingId, stagedIds = new Set()) {
544
+ let id = startingId;
545
+ while (preKeys.has(id) || stagedIds.has(id)) {
546
+ id = id === 0xff_ff_ff ? 1 : id + 1;
547
+ if (id === startingId) {
548
+ throw new Error("WhatsApp Signal prekey identifier space exhausted.");
549
+ }
550
+ }
551
+ return {
552
+ id,
553
+ keyPair: generateSignalKeyPair(),
554
+ nextPreKeyId: id === 0xff_ff_ff ? 1 : id + 1,
555
+ };
556
+ }
557
+ #recoverExpiredPendingAcknowledgements() {
558
+ const now = this.now();
559
+ for (const [messageKey, pendingAcknowledgement] of this.#pendingAcknowledgements) {
560
+ const { acceptedAt } = pendingAcknowledgement;
561
+ if (acceptedAt === undefined) {
562
+ continue;
563
+ }
564
+ if (now - acceptedAt < this.maxPendingAcknowledgementAgeMs) {
565
+ continue;
566
+ }
567
+ this.#pendingAcknowledgements.delete(messageKey);
568
+ this.#rememberAcknowledgedMessage(messageKey);
569
+ }
570
+ }
571
+ #rememberAcknowledgedMessage(messageKey) {
572
+ this.#acknowledgedMessageIds.delete(messageKey);
573
+ this.#acknowledgedMessageIds.set(messageKey, true);
574
+ if (this.#acknowledgedMessageIds.size > this.maxRecentAcknowledgements) {
575
+ const oldestMessageKey = this.#acknowledgedMessageIds.keys().next().value;
576
+ if (oldestMessageKey !== undefined) {
577
+ this.#acknowledgedMessageIds.delete(oldestMessageKey);
578
+ }
579
+ }
580
+ }
581
+ #rememberPersistedMessage(messageKey) {
582
+ this.#persistedMessageIds.delete(messageKey);
583
+ this.#persistedMessageIds.set(messageKey, true);
584
+ if (this.#persistedMessageIds.size > this.maxRecentAcknowledgements) {
585
+ const oldestMessageKey = this.#persistedMessageIds.keys().next().value;
586
+ if (oldestMessageKey !== undefined) {
587
+ this.#persistedMessageIds.delete(oldestMessageKey);
588
+ this.releaseMessagePreKeyProtection(oldestMessageKey);
589
+ }
590
+ }
591
+ }
592
+ #protectMessagePreKey(messageKey, identityKey, preKeyId) {
593
+ let protections = this.#messagePreKeyProtections.get(messageKey);
594
+ if (!protections) {
595
+ protections = new Map();
596
+ this.#messagePreKeyProtections.set(messageKey, protections);
597
+ }
598
+ let preKeyIds = protections.get(identityKey);
599
+ if (!preKeyIds) {
600
+ preKeyIds = new Set();
601
+ protections.set(identityKey, preKeyIds);
602
+ }
603
+ if (preKeyIds.has(preKeyId)) {
604
+ return;
605
+ }
606
+ preKeyIds.add(preKeyId);
607
+ const bundle = this.#bundles.get(identityKey);
608
+ if (bundle) {
609
+ bundle.protectedPreKeyIds.set(preKeyId, (bundle.protectedPreKeyIds.get(preKeyId) ?? 0) + 1);
610
+ }
611
+ }
612
+ #rememberTerminalAcceptance(messageKey, error, drain) {
613
+ const terminalAcceptance = { error };
614
+ this.#terminalAcceptances.set(messageKey, terminalAcceptance);
615
+ void drain.then((disposition) => {
616
+ if (this.#terminalAcceptances.get(messageKey) === terminalAcceptance) {
617
+ this.#terminalAcceptances.delete(messageKey);
618
+ }
619
+ if (disposition === "acknowledged") {
620
+ this.#rememberAcknowledgedMessage(messageKey);
621
+ }
622
+ else if (disposition === "persisted") {
623
+ this.#rememberPersistedMessage(messageKey);
624
+ }
625
+ });
626
+ }
627
+ async #runTransaction(key, operation) {
628
+ return await this.#runSerialized(this.#pendingTransactions, key, operation);
629
+ }
630
+ async #runSerialized(pending, key, operation) {
631
+ const previous = pending.get(key) ?? Promise.resolve();
632
+ const result = previous.catch(() => undefined).then(operation);
633
+ const settled = result.then(() => undefined, () => undefined);
634
+ pending.set(key, settled);
635
+ try {
636
+ return await result;
637
+ }
638
+ finally {
639
+ if (pending.get(key) === settled) {
640
+ pending.delete(key);
641
+ }
642
+ }
139
643
  }
140
644
  }
141
645
  export function createSerializedMessageHandler(processMessage, onError, options = {}) {
@@ -382,6 +886,7 @@ class BaileysNoiseServer {
382
886
  class WhatsAppBaileysWebSocketSession {
383
887
  socket;
384
888
  params;
889
+ #acceptanceAbort = new AbortController();
385
890
  #handshakeState = "client-hello";
386
891
  #handleSerializedMessage;
387
892
  #noise = new BaileysNoiseServer();
@@ -399,7 +904,12 @@ class WhatsAppBaileysWebSocketSession {
399
904
  return this.#handshakeState === "open" && this.socket.readyState === WebSocket.OPEN;
400
905
  }
401
906
  handleMessage(data) {
402
- void this.#handleSerializedMessage(data);
907
+ return this.#handleSerializedMessage(data);
908
+ }
909
+ abortAcceptance(reason) {
910
+ if (!this.#acceptanceAbort.signal.aborted) {
911
+ this.#acceptanceAbort.abort(reason);
912
+ }
403
913
  }
404
914
  async deliverInboundMessage(message) {
405
915
  if (!this.isOpen) {
@@ -450,32 +960,33 @@ class WhatsAppBaileysWebSocketSession {
450
960
  }
451
961
  if (node.tag === "message") {
452
962
  const peer = requireAttr(node, "to");
453
- const normalizedMessage = await normalizeAcceptedBaileysMessage({
963
+ const messageId = requireAttr(node, "id");
964
+ const accepted = await persistAcceptedBaileysMessage({
965
+ acceptanceSignal: this.#acceptanceAbort.signal,
966
+ appendEvent: this.params.appendEvent,
454
967
  node,
968
+ onAcceptanceTimeout: () => {
969
+ const close = resolveWhatsAppWebSocketClose(new Error("WhatsApp message acceptance timed out."));
970
+ this.socket.close(close.code, close.reason);
971
+ },
972
+ path: this.params.path,
455
973
  remoteJid: this.params.selfJid,
456
974
  signalBundles: this.params.signalBundles,
457
975
  });
976
+ if (!accepted) {
977
+ return;
978
+ }
458
979
  await this.#sendNode({
459
980
  attrs: {
460
981
  class: "message",
461
982
  from: peer,
462
- id: requireAttr(node, "id"),
983
+ id: messageId,
463
984
  to: this.params.selfJid,
464
985
  ...(node.attrs.type ? { type: node.attrs.type } : {}),
465
986
  },
466
987
  tag: "ack",
467
988
  });
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
- }
989
+ this.params.signalBundles.markMessageAcknowledged(peer, messageId);
479
990
  }
480
991
  }
481
992
  #createIqResult(node) {
@@ -621,7 +1132,11 @@ class WhatsAppBaileysWebSocketSession {
621
1132
  content: [
622
1133
  { attrs: {}, content: encodeBigEndian(bundle.registrationId), tag: "registration" },
623
1134
  { attrs: {}, content: KEY_BUNDLE_TYPE, tag: "type" },
624
- { attrs: {}, content: bundle.identityKey.public, tag: "identity" },
1135
+ {
1136
+ attrs: {},
1137
+ content: scrubSignalPublicKey(bundle.identityKey.public),
1138
+ tag: "identity",
1139
+ },
625
1140
  xmppSignedPreKey(bundle.signedPreKey),
626
1141
  xmppPreKey(bundle.preKey, bundle.preKeyId),
627
1142
  ],
@@ -742,8 +1257,9 @@ function truncateWebSocketCloseReason(reason) {
742
1257
  return result;
743
1258
  }
744
1259
  export function attachWhatsAppBaileysWebSocketServer(params) {
745
- const signalBundles = new WhatsAppSignalBundleStore();
1260
+ const signalBundles = new WhatsAppSignalBundleStore(undefined, undefined, undefined, undefined, undefined, { messageAcceptanceTimeoutMs: params.messageAcceptanceTimeoutMs });
746
1261
  const sessions = new Set();
1262
+ const pendingSessionMessages = new Set();
747
1263
  const pendingMessages = [];
748
1264
  const maxPendingInboundMessages = resolveMaxPendingWhatsAppInboundMessages(params.maxPendingInboundMessages);
749
1265
  let pendingReservations = 0;
@@ -809,20 +1325,39 @@ export function attachWhatsAppBaileysWebSocketServer(params) {
809
1325
  signalBundles,
810
1326
  });
811
1327
  sessions.add(session);
812
- socket.once("close", () => sessions.delete(session));
1328
+ socket.once("close", () => {
1329
+ session.abortAcceptance(new Error("WhatsApp WebSocket closed."));
1330
+ sessions.delete(session);
1331
+ });
813
1332
  socket.on("error", () => {
1333
+ session.abortAcceptance(new Error("WhatsApp WebSocket failed."));
814
1334
  sessions.delete(session);
815
1335
  socket.terminate();
816
1336
  });
817
- socket.on("message", (data) => session.handleMessage(data));
1337
+ socket.on("message", (data) => {
1338
+ const pending = session.handleMessage(data);
1339
+ pendingSessionMessages.add(pending);
1340
+ void pending.then(() => pendingSessionMessages.delete(pending), () => pendingSessionMessages.delete(pending));
1341
+ });
818
1342
  });
819
1343
  return {
820
1344
  async close() {
821
1345
  closing = true;
822
1346
  params.httpServer.off("upgrade", handleUpgrade);
823
1347
  pendingMessages.length = 0;
1348
+ for (const session of sessions) {
1349
+ session.abortAcceptance(new Error("WhatsApp WebSocket server is shutting down."));
1350
+ }
824
1351
  await closeWebSocketServer(wss);
1352
+ const messageResults = await Promise.allSettled([...pendingSessionMessages]);
825
1353
  await flushPromise;
1354
+ const messageErrors = messageResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1355
+ if (messageErrors.length === 1) {
1356
+ throw messageErrors[0];
1357
+ }
1358
+ if (messageErrors.length > 1) {
1359
+ throw new AggregateError(messageErrors, "WhatsApp WebSocket message drain failed.");
1360
+ }
826
1361
  },
827
1362
  prepareInboundMessage(message) {
828
1363
  if (pendingMessages.length + pendingReservations >= maxPendingInboundMessages) {
@@ -909,45 +1444,133 @@ function removeNoiseIntroHeader(chunk) {
909
1444
  function children(node) {
910
1445
  return Array.isArray(node.content) ? node.content : [];
911
1446
  }
912
- async function normalizeAcceptedBaileysMessage(params) {
1447
+ export async function persistAcceptedBaileysMessage(params) {
913
1448
  const peer = canonicalizeWhatsAppUserCorrelationJid(params.node.attrs.to ?? "");
914
1449
  const messageId = params.node.attrs.id;
915
1450
  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);
1451
+ return false;
1452
+ }
1453
+ const messageKey = `${peer}\0${messageId}`;
1454
+ const wasPreviouslyPersisted = params.signalBundles.hasPersistedMessage(messageKey);
1455
+ let persistenceSucceeded = wasPreviouslyPersisted;
1456
+ let operationSettled;
1457
+ const operationSettlement = new Promise((resolve) => {
1458
+ operationSettled = resolve;
924
1459
  });
925
- for (const candidate of candidates) {
1460
+ let requiresSignalCommit = wasPreviouslyPersisted;
1461
+ let signalCommitSucceeded = false;
1462
+ const pendingAcceptanceOperations = new Set();
1463
+ const trackAcceptanceOperation = (operation) => {
1464
+ pendingAcceptanceOperations.add(operation);
1465
+ void operation.then(() => {
1466
+ persistenceSucceeded = true;
1467
+ pendingAcceptanceOperations.delete(operation);
1468
+ }, () => pendingAcceptanceOperations.delete(operation));
1469
+ return operation;
1470
+ };
1471
+ const drainAcceptanceOperations = async () => {
1472
+ while (pendingAcceptanceOperations.size > 0) {
1473
+ await Promise.allSettled([...pendingAcceptanceOperations]);
1474
+ }
1475
+ await operationSettlement;
1476
+ if (!persistenceSucceeded) {
1477
+ params.signalBundles.releaseMessagePreKeyProtection(messageKey);
1478
+ return "retryable";
1479
+ }
1480
+ if (!requiresSignalCommit || signalCommitSucceeded) {
1481
+ params.signalBundles.releaseMessagePreKeyProtection(messageKey);
1482
+ return "acknowledged";
1483
+ }
1484
+ return "persisted";
1485
+ };
1486
+ const accepted = await params.signalBundles.acceptMessageOnce(messageKey, async (acceptanceSignal) => {
926
1487
  try {
927
- const decrypted = await params.signalBundles.decryptDirectMessage({
928
- ciphertext: candidate.ciphertext,
929
- recipientJid: candidate.recipientJid,
930
- remoteJid: params.remoteJid,
931
- type: candidate.type,
1488
+ acceptanceSignal.throwIfAborted();
1489
+ const candidates = encryptedMessageCandidates(params.node);
1490
+ requiresSignalCommit = candidates.length > 0;
1491
+ if (candidates.length === 0) {
1492
+ if (!wasPreviouslyPersisted) {
1493
+ await awaitWithAbort(trackAcceptanceOperation(params.appendEvent({
1494
+ accepted: true,
1495
+ at: new Date().toISOString(),
1496
+ body: sanitizeNodeForJson(params.node),
1497
+ method: "WEBSOCKET",
1498
+ path: params.path,
1499
+ query: {},
1500
+ type: "api",
1501
+ })), acceptanceSignal);
1502
+ }
1503
+ return true;
1504
+ }
1505
+ const remoteCorrelationJid = canonicalizeWhatsAppUserCorrelationJid(params.remoteJid);
1506
+ candidates.sort((left, right) => {
1507
+ const leftIsSelf = canonicalizeWhatsAppUserCorrelationJid(left.recipientJid) === remoteCorrelationJid;
1508
+ const rightIsSelf = canonicalizeWhatsAppUserCorrelationJid(right.recipientJid) === remoteCorrelationJid;
1509
+ return Number(leftIsSelf) - Number(rightIsSelf);
932
1510
  });
933
- const text = decrypted ? readWhatsAppConversation(unpadRandomMax16(decrypted)) : undefined;
934
- if (text) {
935
- return {
936
- key: {
937
- fromMe: true,
938
- id: messageId,
939
- remoteJid: peer,
1511
+ for (const candidate of candidates) {
1512
+ const result = await params.signalBundles.transactDirectMessage({
1513
+ accept: async (decrypted) => {
1514
+ let text;
1515
+ try {
1516
+ text = readWhatsAppConversation(unpadRandomMax16(decrypted));
1517
+ }
1518
+ catch {
1519
+ text = undefined;
1520
+ }
1521
+ const message = text
1522
+ ? {
1523
+ key: {
1524
+ fromMe: true,
1525
+ id: messageId,
1526
+ remoteJid: peer,
1527
+ },
1528
+ message: { conversation: text },
1529
+ messageTimestamp: Math.floor(Date.now() / 1000),
1530
+ }
1531
+ : undefined;
1532
+ if (!wasPreviouslyPersisted) {
1533
+ await awaitWithAbort(trackAcceptanceOperation(params.appendEvent({
1534
+ accepted: true,
1535
+ at: new Date().toISOString(),
1536
+ body: message ?? sanitizeNodeForJson(params.node),
1537
+ method: "WEBSOCKET",
1538
+ path: params.path,
1539
+ query: {},
1540
+ type: "api",
1541
+ })), acceptanceSignal);
1542
+ }
1543
+ return true;
940
1544
  },
941
- message: { conversation: text },
942
- messageTimestamp: Math.floor(Date.now() / 1000),
943
- };
1545
+ ciphertext: candidate.ciphertext,
1546
+ messageKey,
1547
+ recipientJid: candidate.recipientJid,
1548
+ remoteJid: params.remoteJid,
1549
+ signal: acceptanceSignal,
1550
+ type: candidate.type,
1551
+ });
1552
+ if (result.status === "accepted") {
1553
+ signalCommitSucceeded = true;
1554
+ return true;
1555
+ }
944
1556
  }
1557
+ return false;
945
1558
  }
946
- catch {
947
- // Other participant envelopes may not belong to this mock server identity.
1559
+ finally {
1560
+ if (!acceptanceSignal.aborted && !signalCommitSucceeded) {
1561
+ params.signalBundles.releaseMessagePreKeyProtection(messageKey);
1562
+ }
1563
+ operationSettled();
948
1564
  }
1565
+ }, {
1566
+ onTerminalTimeout: params.onAcceptanceTimeout,
1567
+ signal: params.acceptanceSignal,
1568
+ terminalDrain: drainAcceptanceOperations,
1569
+ });
1570
+ if (accepted) {
1571
+ params.signalBundles.clearPersistedMessage(messageKey);
949
1572
  }
950
- return undefined;
1573
+ return accepted;
951
1574
  }
952
1575
  function encryptedMessageCandidates(node) {
953
1576
  const result = [];
@@ -973,29 +1596,29 @@ function encryptedMessageCandidates(node) {
973
1596
  function signalKeyPair(pair) {
974
1597
  return {
975
1598
  privKey: Buffer.from(pair.private),
976
- pubKey: pair.public.length === 33
977
- ? Buffer.from(pair.public)
978
- : Buffer.concat([KEY_BUNDLE_TYPE, pair.public]),
1599
+ pubKey: ensureSignalPublicKey(pair.public),
979
1600
  };
980
1601
  }
981
1602
  function cloneSignalSession(session) {
982
1603
  return SessionRecord.deserialize(session.serialize());
983
1604
  }
984
1605
  export function signalBundleIdentityKey(jid) {
985
- return jid.replace(/:\d+(?=@)/u, "");
1606
+ return jid.replace(/(?:_\d+)?(?::\d+)?(?=@)/u, "");
986
1607
  }
987
1608
  function signalProtocolAddress(jid) {
988
- const match = /^(\d{7,15})(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
1609
+ const match = /^(\d{7,15})(?:_(\d+))?(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
989
1610
  if (!match) {
990
1611
  return undefined;
991
1612
  }
992
- const deviceId = Number(match[2] ?? "0");
1613
+ const deviceId = Number(match[3] ?? "0");
993
1614
  if (!Number.isSafeInteger(deviceId) || deviceId < 0) {
994
1615
  return undefined;
995
1616
  }
1617
+ const agent = match[2];
1618
+ const server = match[4].toLowerCase();
996
1619
  return {
997
1620
  deviceId,
998
- name: match[3].toLowerCase() === "lid" ? `${match[1]}_1` : match[1],
1621
+ name: agent !== undefined ? `${match[1]}_${agent}` : server === "lid" ? `${match[1]}_1` : match[1],
999
1622
  };
1000
1623
  }
1001
1624
  function unpadRandomMax16(value) {
@@ -1030,6 +1653,7 @@ function readWhatsAppConversation(message) {
1030
1653
  : undefined;
1031
1654
  return nestedMessage ? readWhatsAppConversation(nestedMessage) : undefined;
1032
1655
  }
1656
+ const MAX_WHATSAPP_PROTOBUF_GROUP_DEPTH = 64;
1033
1657
  function readProtobufLengthDelimitedFields(value) {
1034
1658
  const fields = new Map();
1035
1659
  let offset = 0;
@@ -1055,20 +1679,66 @@ function readProtobufLengthDelimitedFields(value) {
1055
1679
  continue;
1056
1680
  }
1057
1681
  if (wireType === 0) {
1058
- offset = readProtobufVarint(value, offset).offset;
1682
+ offset = skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, 0);
1059
1683
  continue;
1060
1684
  }
1061
- if (wireType === 1) {
1062
- offset += 8;
1063
- continue;
1685
+ offset = skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, 0);
1686
+ }
1687
+ return fields;
1688
+ }
1689
+ function skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, groupDepth) {
1690
+ if (wireType === 0) {
1691
+ return readProtobufVarint(value, offset).offset;
1692
+ }
1693
+ if (wireType === 1) {
1694
+ const end = offset + 8;
1695
+ if (end > value.length) {
1696
+ throw new Error("Invalid WhatsApp protobuf fixed64 field.");
1064
1697
  }
1065
- if (wireType === 5) {
1066
- offset += 4;
1067
- continue;
1698
+ return end;
1699
+ }
1700
+ if (wireType === 2) {
1701
+ const length = readProtobufVarint(value, offset);
1702
+ const end = length.offset + length.value;
1703
+ if (end > value.length) {
1704
+ throw new Error("Invalid WhatsApp protobuf length.");
1068
1705
  }
1069
- throw new Error(`Unsupported WhatsApp protobuf wire type: ${wireType}.`);
1706
+ return end;
1070
1707
  }
1071
- return fields;
1708
+ if (wireType === 3) {
1709
+ if (groupDepth >= MAX_WHATSAPP_PROTOBUF_GROUP_DEPTH) {
1710
+ throw new Error("WhatsApp protobuf group nesting exceeds the supported depth.");
1711
+ }
1712
+ let nestedOffset = offset;
1713
+ while (nestedOffset < value.length) {
1714
+ const tag = readProtobufVarint(value, nestedOffset);
1715
+ nestedOffset = tag.offset;
1716
+ const nestedFieldNumber = tag.value >>> 3;
1717
+ const nestedWireType = tag.value & 7;
1718
+ if (nestedFieldNumber < 1) {
1719
+ throw new Error("Invalid WhatsApp protobuf field.");
1720
+ }
1721
+ if (nestedWireType === 4) {
1722
+ if (nestedFieldNumber !== fieldNumber) {
1723
+ throw new Error("Mismatched WhatsApp protobuf end group.");
1724
+ }
1725
+ return nestedOffset;
1726
+ }
1727
+ nestedOffset = skipWhatsAppProtobufField(value, nestedOffset, nestedFieldNumber, nestedWireType, groupDepth + 1);
1728
+ }
1729
+ throw new Error("Unterminated WhatsApp protobuf group.");
1730
+ }
1731
+ if (wireType === 4) {
1732
+ throw new Error("Unexpected WhatsApp protobuf end group.");
1733
+ }
1734
+ if (wireType === 5) {
1735
+ const end = offset + 4;
1736
+ if (end > value.length) {
1737
+ throw new Error("Invalid WhatsApp protobuf fixed32 field.");
1738
+ }
1739
+ return end;
1740
+ }
1741
+ throw new Error(`Unsupported WhatsApp protobuf wire type: ${wireType}.`);
1072
1742
  }
1073
1743
  function readProtobufVarint(value, start) {
1074
1744
  let result = 0;