@zapo-js/fake-server 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/README.md +3 -3
  2. package/dist/api/AppStateSyncManager.d.ts +0 -1
  3. package/dist/api/AppStateSyncManager.js +0 -1
  4. package/dist/api/FakePairingDriver.d.ts +0 -1
  5. package/dist/api/FakePairingDriver.js +0 -1
  6. package/dist/api/FakePeer.d.ts +77 -4
  7. package/dist/api/FakePeer.js +201 -1
  8. package/dist/api/FakeWaServer.d.ts +11 -1
  9. package/dist/api/FakeWaServer.js +34 -2
  10. package/dist/api/PreKeyDispenser.d.ts +0 -1
  11. package/dist/api/PreKeyDispenser.js +0 -1
  12. package/dist/api/Scenario.d.ts +0 -1
  13. package/dist/api/Scenario.js +0 -1
  14. package/dist/api/ServerRegistries.d.ts +0 -1
  15. package/dist/api/ServerRegistries.js +0 -1
  16. package/dist/api/iq-handlers.d.ts +0 -1
  17. package/dist/api/iq-handlers.js +0 -1
  18. package/dist/{cli.d.ts → bin.d.ts} +0 -1
  19. package/dist/{cli.js → bin.js} +1 -2
  20. package/dist/esm/api/AppStateSyncManager.js +146 -0
  21. package/dist/esm/api/FakePairingDriver.js +31 -0
  22. package/dist/esm/api/FakePeer.js +604 -0
  23. package/dist/esm/api/FakeWaServer.js +680 -0
  24. package/dist/esm/api/PreKeyDispenser.js +93 -0
  25. package/dist/esm/api/Scenario.js +32 -0
  26. package/dist/esm/api/ServerRegistries.js +261 -0
  27. package/dist/esm/api/iq-handlers.js +492 -0
  28. package/dist/esm/bin.js +246 -0
  29. package/dist/esm/index.js +26 -0
  30. package/dist/esm/infra/WaFakeConnection.js +49 -0
  31. package/dist/esm/infra/WaFakeConnectionPipeline.js +223 -0
  32. package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
  33. package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
  34. package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
  35. package/dist/esm/infra/WaFakeTransport.js +31 -0
  36. package/dist/esm/infra/WaFakeWsServer.js +96 -0
  37. package/dist/esm/package.json +3 -0
  38. package/dist/esm/protocol/auth/cert-chain.js +39 -0
  39. package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
  40. package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
  41. package/dist/esm/protocol/auth/pair-device.js +87 -0
  42. package/dist/esm/protocol/auth/success-node.js +26 -0
  43. package/dist/esm/protocol/iq/abprops.js +30 -0
  44. package/dist/esm/protocol/iq/appstate-sync.js +146 -0
  45. package/dist/esm/protocol/iq/business.js +59 -0
  46. package/dist/esm/protocol/iq/dirty-bits.js +19 -0
  47. package/dist/esm/protocol/iq/group-ops.js +154 -0
  48. package/dist/esm/protocol/iq/newsletter.js +14 -0
  49. package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
  50. package/dist/esm/protocol/iq/privacy-token.js +26 -0
  51. package/dist/esm/protocol/iq/privacy.js +123 -0
  52. package/dist/esm/protocol/iq/profile.js +72 -0
  53. package/dist/esm/protocol/iq/router.js +111 -0
  54. package/dist/esm/protocol/iq/usync.js +64 -0
  55. package/dist/esm/protocol/push/app-state-key-share.js +26 -0
  56. package/dist/esm/protocol/push/call-failure.js +27 -0
  57. package/dist/esm/protocol/push/chatstate.js +23 -0
  58. package/dist/esm/protocol/push/error-stanza.js +13 -0
  59. package/dist/esm/protocol/push/history-sync.js +111 -0
  60. package/dist/esm/protocol/push/message.js +38 -0
  61. package/dist/esm/protocol/push/notification.js +29 -0
  62. package/dist/esm/protocol/push/presence.js +14 -0
  63. package/dist/esm/protocol/push/receipt.js +21 -0
  64. package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
  65. package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
  66. package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
  67. package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
  68. package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
  69. package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
  70. package/dist/esm/protocol/signal/prekey-upload.js +107 -0
  71. package/dist/esm/protocol/stream/stream-error.js +56 -0
  72. package/dist/esm/state/fake-app-state-collection.js +134 -0
  73. package/dist/esm/state/fake-media-store.js +53 -0
  74. package/dist/esm/transport/codec.js +8 -0
  75. package/dist/esm/transport/crypto.js +8 -0
  76. package/dist/esm/transport/protos.js +7 -0
  77. package/dist/esm/transport/util.js +8 -0
  78. package/dist/index.d.ts +0 -1
  79. package/dist/index.js +0 -1
  80. package/dist/infra/WaFakeConnection.d.ts +0 -1
  81. package/dist/infra/WaFakeConnection.js +0 -1
  82. package/dist/infra/WaFakeConnectionPipeline.d.ts +0 -1
  83. package/dist/infra/WaFakeConnectionPipeline.js +27 -24
  84. package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
  85. package/dist/infra/WaFakeFrameSocket.js +0 -1
  86. package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
  87. package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
  88. package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
  89. package/dist/infra/WaFakeNoiseHandshake.js +19 -24
  90. package/dist/infra/WaFakeTransport.d.ts +2 -4
  91. package/dist/infra/WaFakeTransport.js +2 -1
  92. package/dist/infra/WaFakeWsServer.d.ts +0 -1
  93. package/dist/infra/WaFakeWsServer.js +0 -1
  94. package/dist/protocol/auth/cert-chain.d.ts +0 -1
  95. package/dist/protocol/auth/cert-chain.js +0 -1
  96. package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
  97. package/dist/protocol/auth/client-payload-validate.js +0 -1
  98. package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
  99. package/dist/protocol/auth/fake-primary-device.js +1 -3
  100. package/dist/protocol/auth/pair-device.d.ts +0 -1
  101. package/dist/protocol/auth/pair-device.js +4 -16
  102. package/dist/protocol/auth/success-node.d.ts +0 -1
  103. package/dist/protocol/auth/success-node.js +0 -1
  104. package/dist/protocol/iq/abprops.d.ts +0 -1
  105. package/dist/protocol/iq/abprops.js +0 -1
  106. package/dist/protocol/iq/appstate-sync.d.ts +0 -1
  107. package/dist/protocol/iq/appstate-sync.js +0 -1
  108. package/dist/protocol/iq/business.d.ts +0 -1
  109. package/dist/protocol/iq/business.js +0 -1
  110. package/dist/protocol/iq/dirty-bits.d.ts +0 -1
  111. package/dist/protocol/iq/dirty-bits.js +0 -1
  112. package/dist/protocol/iq/group-ops.d.ts +0 -1
  113. package/dist/protocol/iq/group-ops.js +0 -1
  114. package/dist/protocol/iq/newsletter.d.ts +0 -1
  115. package/dist/protocol/iq/newsletter.js +0 -1
  116. package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
  117. package/dist/protocol/iq/prekey-fetch.js +0 -1
  118. package/dist/protocol/iq/privacy-token.d.ts +0 -1
  119. package/dist/protocol/iq/privacy-token.js +0 -1
  120. package/dist/protocol/iq/privacy.d.ts +0 -1
  121. package/dist/protocol/iq/privacy.js +15 -16
  122. package/dist/protocol/iq/profile.d.ts +0 -1
  123. package/dist/protocol/iq/profile.js +0 -1
  124. package/dist/protocol/iq/router.d.ts +0 -1
  125. package/dist/protocol/iq/router.js +0 -1
  126. package/dist/protocol/iq/usync.d.ts +0 -1
  127. package/dist/protocol/iq/usync.js +0 -1
  128. package/dist/protocol/push/app-state-key-share.d.ts +0 -1
  129. package/dist/protocol/push/app-state-key-share.js +0 -1
  130. package/dist/protocol/push/call-failure.d.ts +0 -1
  131. package/dist/protocol/push/call-failure.js +0 -1
  132. package/dist/protocol/push/chatstate.d.ts +0 -1
  133. package/dist/protocol/push/chatstate.js +0 -1
  134. package/dist/protocol/push/error-stanza.d.ts +0 -1
  135. package/dist/protocol/push/error-stanza.js +0 -1
  136. package/dist/protocol/push/history-sync.d.ts +0 -1
  137. package/dist/protocol/push/history-sync.js +0 -1
  138. package/dist/protocol/push/message.d.ts +0 -1
  139. package/dist/protocol/push/message.js +0 -1
  140. package/dist/protocol/push/notification.d.ts +0 -1
  141. package/dist/protocol/push/notification.js +0 -1
  142. package/dist/protocol/push/presence.d.ts +0 -1
  143. package/dist/protocol/push/presence.js +0 -1
  144. package/dist/protocol/push/receipt.d.ts +0 -1
  145. package/dist/protocol/push/receipt.js +0 -1
  146. package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
  147. package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
  148. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
  149. package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
  150. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
  151. package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
  152. package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
  153. package/dist/protocol/signal/fake-peer-identity.js +0 -1
  154. package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
  155. package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
  156. package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
  157. package/dist/protocol/signal/fake-sender-key.js +5 -9
  158. package/dist/protocol/signal/prekey-upload.d.ts +0 -1
  159. package/dist/protocol/signal/prekey-upload.js +0 -1
  160. package/dist/protocol/stream/stream-error.d.ts +0 -1
  161. package/dist/protocol/stream/stream-error.js +0 -1
  162. package/dist/state/fake-app-state-collection.d.ts +0 -1
  163. package/dist/state/fake-app-state-collection.js +0 -1
  164. package/dist/state/fake-media-store.d.ts +0 -1
  165. package/dist/state/fake-media-store.js +2 -2
  166. package/dist/transport/codec.d.ts +2 -3
  167. package/dist/transport/codec.js +2 -3
  168. package/dist/transport/crypto.d.ts +5 -7
  169. package/dist/transport/crypto.js +6 -11
  170. package/dist/transport/protos.d.ts +2 -4
  171. package/dist/transport/protos.js +2 -4
  172. package/dist/transport/util.d.ts +8 -0
  173. package/dist/transport/util.js +15 -0
  174. package/package.json +21 -6
  175. package/dist/api/AppStateSyncManager.d.ts.map +0 -1
  176. package/dist/api/AppStateSyncManager.js.map +0 -1
  177. package/dist/api/FakePairingDriver.d.ts.map +0 -1
  178. package/dist/api/FakePairingDriver.js.map +0 -1
  179. package/dist/api/FakePeer.d.ts.map +0 -1
  180. package/dist/api/FakePeer.js.map +0 -1
  181. package/dist/api/FakeWaServer.d.ts.map +0 -1
  182. package/dist/api/FakeWaServer.js.map +0 -1
  183. package/dist/api/PreKeyDispenser.d.ts.map +0 -1
  184. package/dist/api/PreKeyDispenser.js.map +0 -1
  185. package/dist/api/Scenario.d.ts.map +0 -1
  186. package/dist/api/Scenario.js.map +0 -1
  187. package/dist/api/ServerRegistries.d.ts.map +0 -1
  188. package/dist/api/ServerRegistries.js.map +0 -1
  189. package/dist/api/iq-handlers.d.ts.map +0 -1
  190. package/dist/api/iq-handlers.js.map +0 -1
  191. package/dist/cli.d.ts.map +0 -1
  192. package/dist/cli.js.map +0 -1
  193. package/dist/index.d.ts.map +0 -1
  194. package/dist/index.js.map +0 -1
  195. package/dist/infra/WaFakeConnection.d.ts.map +0 -1
  196. package/dist/infra/WaFakeConnection.js.map +0 -1
  197. package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
  198. package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
  199. package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
  200. package/dist/infra/WaFakeFrameSocket.js.map +0 -1
  201. package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
  202. package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
  203. package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
  204. package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
  205. package/dist/infra/WaFakeTransport.d.ts.map +0 -1
  206. package/dist/infra/WaFakeTransport.js.map +0 -1
  207. package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
  208. package/dist/infra/WaFakeWsServer.js.map +0 -1
  209. package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
  210. package/dist/protocol/auth/cert-chain.js.map +0 -1
  211. package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
  212. package/dist/protocol/auth/client-payload-validate.js.map +0 -1
  213. package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
  214. package/dist/protocol/auth/fake-primary-device.js.map +0 -1
  215. package/dist/protocol/auth/pair-device.d.ts.map +0 -1
  216. package/dist/protocol/auth/pair-device.js.map +0 -1
  217. package/dist/protocol/auth/success-node.d.ts.map +0 -1
  218. package/dist/protocol/auth/success-node.js.map +0 -1
  219. package/dist/protocol/iq/abprops.d.ts.map +0 -1
  220. package/dist/protocol/iq/abprops.js.map +0 -1
  221. package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
  222. package/dist/protocol/iq/appstate-sync.js.map +0 -1
  223. package/dist/protocol/iq/business.d.ts.map +0 -1
  224. package/dist/protocol/iq/business.js.map +0 -1
  225. package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
  226. package/dist/protocol/iq/dirty-bits.js.map +0 -1
  227. package/dist/protocol/iq/group-ops.d.ts.map +0 -1
  228. package/dist/protocol/iq/group-ops.js.map +0 -1
  229. package/dist/protocol/iq/newsletter.d.ts.map +0 -1
  230. package/dist/protocol/iq/newsletter.js.map +0 -1
  231. package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
  232. package/dist/protocol/iq/prekey-fetch.js.map +0 -1
  233. package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
  234. package/dist/protocol/iq/privacy-token.js.map +0 -1
  235. package/dist/protocol/iq/privacy.d.ts.map +0 -1
  236. package/dist/protocol/iq/privacy.js.map +0 -1
  237. package/dist/protocol/iq/profile.d.ts.map +0 -1
  238. package/dist/protocol/iq/profile.js.map +0 -1
  239. package/dist/protocol/iq/router.d.ts.map +0 -1
  240. package/dist/protocol/iq/router.js.map +0 -1
  241. package/dist/protocol/iq/usync.d.ts.map +0 -1
  242. package/dist/protocol/iq/usync.js.map +0 -1
  243. package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
  244. package/dist/protocol/push/app-state-key-share.js.map +0 -1
  245. package/dist/protocol/push/call-failure.d.ts.map +0 -1
  246. package/dist/protocol/push/call-failure.js.map +0 -1
  247. package/dist/protocol/push/chatstate.d.ts.map +0 -1
  248. package/dist/protocol/push/chatstate.js.map +0 -1
  249. package/dist/protocol/push/error-stanza.d.ts.map +0 -1
  250. package/dist/protocol/push/error-stanza.js.map +0 -1
  251. package/dist/protocol/push/history-sync.d.ts.map +0 -1
  252. package/dist/protocol/push/history-sync.js.map +0 -1
  253. package/dist/protocol/push/message.d.ts.map +0 -1
  254. package/dist/protocol/push/message.js.map +0 -1
  255. package/dist/protocol/push/notification.d.ts.map +0 -1
  256. package/dist/protocol/push/notification.js.map +0 -1
  257. package/dist/protocol/push/presence.d.ts.map +0 -1
  258. package/dist/protocol/push/presence.js.map +0 -1
  259. package/dist/protocol/push/receipt.d.ts.map +0 -1
  260. package/dist/protocol/push/receipt.js.map +0 -1
  261. package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
  262. package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
  263. package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
  264. package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
  265. package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
  266. package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
  267. package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
  268. package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
  269. package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
  270. package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
  271. package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
  272. package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
  273. package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
  274. package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
  275. package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
  276. package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
  277. package/dist/protocol/signal/fake-peer-session.js +0 -207
  278. package/dist/protocol/signal/fake-peer-session.js.map +0 -1
  279. package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
  280. package/dist/protocol/signal/fake-sender-key.js.map +0 -1
  281. package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
  282. package/dist/protocol/signal/prekey-upload.js.map +0 -1
  283. package/dist/protocol/stream/stream-error.d.ts.map +0 -1
  284. package/dist/protocol/stream/stream-error.js.map +0 -1
  285. package/dist/state/fake-app-state-collection.d.ts.map +0 -1
  286. package/dist/state/fake-app-state-collection.js.map +0 -1
  287. package/dist/state/fake-media-store.d.ts.map +0 -1
  288. package/dist/state/fake-media-store.js.map +0 -1
  289. package/dist/transport/codec.d.ts.map +0 -1
  290. package/dist/transport/codec.js.map +0 -1
  291. package/dist/transport/crypto.d.ts.map +0 -1
  292. package/dist/transport/crypto.js.map +0 -1
  293. package/dist/transport/index.d.ts +0 -4
  294. package/dist/transport/index.d.ts.map +0 -1
  295. package/dist/transport/index.js +0 -20
  296. package/dist/transport/index.js.map +0 -1
  297. package/dist/transport/protos.d.ts.map +0 -1
  298. package/dist/transport/protos.js.map +0 -1
@@ -0,0 +1,604 @@
1
+ /** Fake peer used by tests to encrypt and push Signal messages to the lib. */
2
+ import { buildAppStateSyncKeyShareMessage } from '../protocol/push/app-state-key-share.js';
3
+ import { buildHistorySyncExternalMessage, buildHistorySyncMessage } from '../protocol/push/history-sync.js';
4
+ import { buildMessage } from '../protocol/push/message.js';
5
+ import { FakePeerDoubleRatchet } from '../protocol/signal/fake-peer-double-ratchet.js';
6
+ import { FakePeerGroupRecvSession } from '../protocol/signal/fake-peer-group-recv-session.js';
7
+ import { generateFakePeerKeyBundle } from '../protocol/signal/fake-peer-key-bundle.js';
8
+ import { FakeSenderKey } from '../protocol/signal/fake-sender-key.js';
9
+ import { proto } from '../transport/protos.js';
10
+ export class FakePeer {
11
+ constructor(keyBundle, options, deps) {
12
+ this.ratchetInitiated = false;
13
+ this.nextMessageCounter = 0;
14
+ this.sentPlaintextById = new Map();
15
+ this.sentEncryptedById = new Map();
16
+ this.senderKeysByGroup = new Map();
17
+ this.groupsBootstrapped = new Set();
18
+ this.groupRecvSession = new FakePeerGroupRecvSession();
19
+ this.keyBundle = keyBundle;
20
+ this.identity = {
21
+ identityKeyPair: keyBundle.identityKeyPair,
22
+ registrationId: keyBundle.registrationId
23
+ };
24
+ this.jid = options.jid;
25
+ this.displayName = options.displayName;
26
+ this.deps = deps;
27
+ this.skipOneTimePreKey = options.skipOneTimePreKey === true;
28
+ this.replayCacheEnabled = options.enableReplayCache === true;
29
+ this.reservedOneTimePreKey =
30
+ !this.skipOneTimePreKey && deps.reserveOneTimePreKey
31
+ ? deps.reserveOneTimePreKey()
32
+ : null;
33
+ this.ratchet = new FakePeerDoubleRatchet(keyBundle);
34
+ }
35
+ static async create(options, deps) {
36
+ const keyBundle = options.keyBundle ?? (await generateFakePeerKeyBundle());
37
+ return new FakePeer(keyBundle, options, deps);
38
+ }
39
+ async sendMessage(message, options = {}) {
40
+ await this.ensureRatchetInitiated();
41
+ const plaintext = encodePlaintextWithPadding(message);
42
+ const { type, ciphertext } = await this.ratchet.encrypt(plaintext);
43
+ const finalCiphertext = options.tamperCiphertext
44
+ ? options.tamperCiphertext(ciphertext)
45
+ : ciphertext;
46
+ const enc = { type, ciphertext: finalCiphertext };
47
+ const id = options.id ?? this.nextId();
48
+ if (this.replayCacheEnabled) {
49
+ // Stash the plaintext so the bench / a test can replay this
50
+ // exact payload later in response to an incoming retry
51
+ // receipt from the lib (replaySentMessage). Also stash the
52
+ // PRISTINE ciphertext (pre-tamper) + signal type so a
53
+ // replay can put the original frame back on the wire –
54
+ // matches how a real Signal sender re-sends after a
55
+ // recipient asks for retry: same counter + ratchet key,
56
+ // just an uncorrupted body.
57
+ this.sentPlaintextById.set(id, message);
58
+ this.sentEncryptedById.set(id, { type, ciphertext });
59
+ }
60
+ const stanza = buildMessage({
61
+ id,
62
+ from: options.from ?? this.jid,
63
+ t: options.t,
64
+ type: options.type ?? 'text',
65
+ notify: this.displayName,
66
+ participant: options.participant,
67
+ enc: [enc]
68
+ });
69
+ await this.deps.pushStanza(stanza);
70
+ }
71
+ /**
72
+ * Re-encrypts and re-pushes a previously sent message identified by
73
+ * `originalMsgId`. Used by tests / benches that exercise the lib's
74
+ * incoming-retry path: after the lib emits `<receipt type="retry">`
75
+ * back to this peer, the peer is expected to re-send the same
76
+ * plaintext so the lib can decrypt it successfully on the second
77
+ * attempt.
78
+ *
79
+ * The replay reuses the original stanza id by default so the lib's
80
+ * dedup logic (and tests that wait for a specific id) see the resend
81
+ * as a continuation of the same outbound. Pass `resendId` to use a
82
+ * different id.
83
+ */
84
+ async replaySentMessage(originalMsgId, options = {}) {
85
+ const stored = this.sentEncryptedById.get(originalMsgId);
86
+ if (stored) {
87
+ // Re-send the pristine ciphertext – same Signal counter +
88
+ // ratchet key as the original send, just without the
89
+ // tamper. The lib's recv chain still has the message key
90
+ // for that counter, so this is the recovery path real
91
+ // WhatsApp peers walk after observing a retry receipt for
92
+ // a frame the recipient failed to decrypt.
93
+ const enc = { type: stored.type, ciphertext: stored.ciphertext };
94
+ const id = options.resendId ?? originalMsgId;
95
+ const stanza = buildMessage({
96
+ id,
97
+ from: this.jid,
98
+ type: 'text',
99
+ notify: this.displayName,
100
+ enc: [enc]
101
+ });
102
+ await this.deps.pushStanza(stanza);
103
+ return;
104
+ }
105
+ const message = this.sentPlaintextById.get(originalMsgId);
106
+ if (!message) {
107
+ throw new Error(`FakePeer.replaySentMessage: no sent plaintext for id ${originalMsgId}`);
108
+ }
109
+ await this.sendMessage(message, { id: options.resendId ?? originalMsgId });
110
+ }
111
+ /**
112
+ * Rotates the peer's prekey material and resets the Signal session
113
+ * state so the lib has to re-run X3DH on the next outbound. Keeps
114
+ * the identity key pair (same user) but issues a new signed prekey
115
+ * + new one-time prekey set. After this call:
116
+ * - `keyBundle` reflects the new material;
117
+ * - the underlying `FakePeerDoubleRatchet` is fresh – the next
118
+ * inbound from the lib must be a `pkmsg` (initial signal
119
+ * message), which the new `keyBundle` is wired to decrypt;
120
+ * - any cached one-time prekey reservation in the dispenser is
121
+ * dropped so subsequent retry-receipts include this peer's new
122
+ * keys directly via {@link sendRetryReceipt}.
123
+ *
124
+ * Use this to simulate the "session lost" scenario that real
125
+ * WhatsApp clients trigger when they need the sender to fully
126
+ * re-bootstrap the Signal session via the retry-receipt path.
127
+ */
128
+ async rotateForRetry() {
129
+ const refreshed = await generateFakePeerKeyBundle({
130
+ identityKeyPair: this.keyBundle.identityKeyPair,
131
+ registrationId: this.keyBundle.registrationId,
132
+ signedPreKeyId: this.keyBundle.signedPreKey.id + 1,
133
+ firstOneTimePreKeyId: (this.keyBundle.oneTimePreKeys[this.keyBundle.oneTimePreKeys.length - 1]?.id ?? 0) +
134
+ 1
135
+ });
136
+ this.keyBundle = refreshed;
137
+ this.ratchet = new FakePeerDoubleRatchet(refreshed);
138
+ this.ratchetInitiated = false;
139
+ this.reservedOneTimePreKey = null;
140
+ }
141
+ sendConversation(text, options = {}) {
142
+ return this.sendMessage({ conversation: text }, options);
143
+ }
144
+ async sendMessageEdit(input, options = {}) {
145
+ await this.sendMessage({
146
+ protocolMessage: {
147
+ type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
148
+ key: {
149
+ remoteJid: this.jid,
150
+ fromMe: false,
151
+ id: input.targetMessageId
152
+ },
153
+ timestampMs: Date.now(),
154
+ editedMessage: {
155
+ conversation: input.newText
156
+ }
157
+ }
158
+ }, options);
159
+ }
160
+ async sendMessageRevoke(input, options = {}) {
161
+ await this.sendMessage({
162
+ protocolMessage: {
163
+ type: proto.Message.ProtocolMessage.Type.REVOKE,
164
+ key: {
165
+ remoteJid: this.jid,
166
+ fromMe: false,
167
+ id: input.targetMessageId
168
+ }
169
+ }
170
+ }, options);
171
+ }
172
+ async sendReaction(input, options = {}) {
173
+ await this.sendMessage({
174
+ reactionMessage: {
175
+ key: {
176
+ remoteJid: this.jid,
177
+ fromMe: false,
178
+ id: input.targetMessageId
179
+ },
180
+ text: input.emoji,
181
+ senderTimestampMs: Date.now()
182
+ }
183
+ }, options);
184
+ }
185
+ async sendHistorySync(input = {}, options = {}) {
186
+ const message = await buildHistorySyncMessage(input);
187
+ await this.sendMessage(message, options);
188
+ }
189
+ async sendHistorySyncExternal(input, options = {}) {
190
+ const message = buildHistorySyncExternalMessage(input);
191
+ await this.sendMessage(message, options);
192
+ }
193
+ async sendImageMessage(input, options = {}) {
194
+ await this.sendMessage({
195
+ imageMessage: {
196
+ url: input.directPath,
197
+ directPath: input.directPath,
198
+ mediaKey: input.mediaKey,
199
+ fileSha256: input.fileSha256,
200
+ fileEncSha256: input.fileEncSha256,
201
+ fileLength: input.fileLength,
202
+ mimetype: input.mimetype ?? 'image/jpeg',
203
+ caption: input.caption,
204
+ width: input.width,
205
+ height: input.height,
206
+ mediaKeyTimestamp: Math.floor(Date.now() / 1000)
207
+ }
208
+ }, options);
209
+ }
210
+ async sendVideoMessage(input, options = {}) {
211
+ await this.sendMessage({
212
+ videoMessage: {
213
+ url: input.directPath,
214
+ directPath: input.directPath,
215
+ mediaKey: input.mediaKey,
216
+ fileSha256: input.fileSha256,
217
+ fileEncSha256: input.fileEncSha256,
218
+ fileLength: input.fileLength,
219
+ mimetype: input.mimetype ?? 'video/mp4',
220
+ caption: input.caption,
221
+ seconds: input.seconds,
222
+ width: input.width,
223
+ height: input.height,
224
+ mediaKeyTimestamp: Math.floor(Date.now() / 1000)
225
+ }
226
+ }, options);
227
+ }
228
+ async sendAudioMessage(input, options = {}) {
229
+ await this.sendMessage({
230
+ audioMessage: {
231
+ url: input.directPath,
232
+ directPath: input.directPath,
233
+ mediaKey: input.mediaKey,
234
+ fileSha256: input.fileSha256,
235
+ fileEncSha256: input.fileEncSha256,
236
+ fileLength: input.fileLength,
237
+ mimetype: input.mimetype ?? (input.ptt ? 'audio/ogg' : 'audio/mp4'),
238
+ seconds: input.seconds,
239
+ ptt: input.ptt,
240
+ mediaKeyTimestamp: Math.floor(Date.now() / 1000)
241
+ }
242
+ }, options);
243
+ }
244
+ async sendDocumentMessage(input, options = {}) {
245
+ await this.sendMessage({
246
+ documentMessage: {
247
+ url: input.directPath,
248
+ directPath: input.directPath,
249
+ mediaKey: input.mediaKey,
250
+ fileSha256: input.fileSha256,
251
+ fileEncSha256: input.fileEncSha256,
252
+ fileLength: input.fileLength,
253
+ mimetype: input.mimetype ?? 'application/pdf',
254
+ title: input.title,
255
+ fileName: input.fileName,
256
+ pageCount: input.pageCount,
257
+ mediaKeyTimestamp: Math.floor(Date.now() / 1000)
258
+ }
259
+ }, options);
260
+ }
261
+ async sendStickerMessage(input, options = {}) {
262
+ await this.sendMessage({
263
+ stickerMessage: {
264
+ url: input.directPath,
265
+ directPath: input.directPath,
266
+ mediaKey: input.mediaKey,
267
+ fileSha256: input.fileSha256,
268
+ fileEncSha256: input.fileEncSha256,
269
+ fileLength: input.fileLength,
270
+ mimetype: input.mimetype ?? 'image/webp',
271
+ width: input.width,
272
+ height: input.height,
273
+ isAnimated: input.isAnimated,
274
+ mediaKeyTimestamp: Math.floor(Date.now() / 1000)
275
+ }
276
+ }, options);
277
+ }
278
+ async sendAppStateSyncKeyShare(input, options = {}) {
279
+ const message = buildAppStateSyncKeyShareMessage(input);
280
+ await this.sendMessage(message, options);
281
+ }
282
+ async sendGroupConversation(groupJid, text, options = {}) {
283
+ return this.sendGroupMessage(groupJid, { conversation: text }, options);
284
+ }
285
+ async sendGroupMessage(groupJid, message, options = {}) {
286
+ let senderKey = this.senderKeysByGroup.get(groupJid);
287
+ if (!senderKey) {
288
+ senderKey = await FakeSenderKey.generate();
289
+ this.senderKeysByGroup.set(groupJid, senderKey);
290
+ }
291
+ const plaintext = encodePlaintextWithPadding(message);
292
+ const { ciphertext, distributionMessage } = await senderKey.encrypt(plaintext);
293
+ // Seed the sender key before the first skmsg for this group.
294
+ const encChildren = [];
295
+ if (!this.groupsBootstrapped.has(groupJid)) {
296
+ await this.ensureRatchetInitiated();
297
+ const bootstrapPlaintext = encodePlaintextWithPadding({
298
+ senderKeyDistributionMessage: {
299
+ groupId: groupJid,
300
+ axolotlSenderKeyDistributionMessage: distributionMessage
301
+ }
302
+ });
303
+ const { type: pkType, ciphertext: pkCt } = await this.ratchet.encrypt(bootstrapPlaintext);
304
+ encChildren.push({ type: pkType, ciphertext: pkCt });
305
+ this.groupsBootstrapped.add(groupJid);
306
+ }
307
+ encChildren.push({ type: 'skmsg', ciphertext });
308
+ const groupStanza = buildMessage({
309
+ id: options.id ?? this.nextId(),
310
+ from: groupJid,
311
+ participant: this.jid,
312
+ t: options.t,
313
+ type: options.type ?? 'text',
314
+ notify: this.displayName,
315
+ enc: encChildren
316
+ });
317
+ await this.deps.pushStanza(groupStanza);
318
+ }
319
+ /**
320
+ * Sends a `<receipt type="retry">` stanza to the lib asking it to
321
+ * re-encrypt and re-send the message identified by `originalMsgId`.
322
+ * Used by benches / tests that exercise the lib's outbound retry
323
+ * replay path.
324
+ *
325
+ * Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
326
+ * `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
327
+ *
328
+ * When `includeKeys` is true (default), the `<keys>` block carries
329
+ * the peer's current identity + signed prekey + one-time prekey.
330
+ * This forces the lib's `WaRetryCoordinator` to tear down the
331
+ * existing session and re-run X3DH against these fresh keys before
332
+ * re-encrypting the original payload, so the resend lands as a
333
+ * `pkmsg` against the peer's *new* prekey state. Pair this with
334
+ * {@link rotateForRetry} to simulate full session loss + recovery.
335
+ *
336
+ * `<device-identity>` is omitted: it's `maybeChild` in wa-web and
337
+ * also optional in the lib's parser. The fake-peer fixture doesn't
338
+ * have an ADV primary-device to sign one, and the lib's outbound
339
+ * retry replay path does not require it.
340
+ */
341
+ async sendRetryReceipt(originalMsgId, options = {}) {
342
+ const receiptId = options.receiptId ?? originalMsgId;
343
+ const retryCount = options.count ?? 1;
344
+ const t = options.t ?? Math.floor(Date.now() / 1000);
345
+ const error = options.error;
346
+ const includeKeys = options.includeKeys !== false;
347
+ const registration = new Uint8Array(4);
348
+ const regId = this.keyBundle.registrationId & 0xffffffff;
349
+ registration[0] = (regId >>> 24) & 0xff;
350
+ registration[1] = (regId >>> 16) & 0xff;
351
+ registration[2] = (regId >>> 8) & 0xff;
352
+ registration[3] = regId & 0xff;
353
+ const retryAttrs = {
354
+ v: '1',
355
+ count: String(retryCount),
356
+ id: originalMsgId,
357
+ t: String(t)
358
+ };
359
+ if (error !== undefined && error !== 0) {
360
+ retryAttrs.error = String(error);
361
+ }
362
+ const content = [
363
+ {
364
+ tag: 'retry',
365
+ attrs: retryAttrs
366
+ },
367
+ {
368
+ tag: 'registration',
369
+ attrs: {},
370
+ content: registration
371
+ }
372
+ ];
373
+ if (includeKeys) {
374
+ content.push(this.buildRetryKeysNode());
375
+ }
376
+ const receipt = {
377
+ tag: 'receipt',
378
+ attrs: {
379
+ id: receiptId,
380
+ from: this.jid,
381
+ type: 'retry',
382
+ t: String(t)
383
+ },
384
+ content
385
+ };
386
+ await this.deps.pushStanza(receipt);
387
+ }
388
+ buildRetryKeysNode() {
389
+ const skey = this.keyBundle.signedPreKey;
390
+ const oneTime = this.keyBundle.oneTimePreKeys[0];
391
+ if (!oneTime) {
392
+ throw new Error('FakePeer.sendRetryReceipt: no one-time prekey available');
393
+ }
394
+ const skeyIdBytes = encodeUint3(skey.id);
395
+ const keyIdBytes = encodeUint3(oneTime.id);
396
+ return {
397
+ tag: 'keys',
398
+ attrs: {},
399
+ content: [
400
+ {
401
+ tag: 'identity',
402
+ attrs: {},
403
+ content: this.keyBundle.identityKeyPair.pubKey
404
+ },
405
+ {
406
+ tag: 'skey',
407
+ attrs: {},
408
+ content: [
409
+ { tag: 'id', attrs: {}, content: skeyIdBytes },
410
+ { tag: 'value', attrs: {}, content: skey.keyPair.pubKey },
411
+ { tag: 'signature', attrs: {}, content: skey.signature }
412
+ ]
413
+ },
414
+ {
415
+ tag: 'key',
416
+ attrs: {},
417
+ content: [
418
+ { tag: 'id', attrs: {}, content: keyIdBytes },
419
+ { tag: 'value', attrs: {}, content: oneTime.keyPair.pubKey }
420
+ ]
421
+ }
422
+ ]
423
+ };
424
+ }
425
+ expectMessage(options = {}) {
426
+ const timeoutMs = options.timeoutMs ?? 5000;
427
+ return new Promise((resolve, reject) => {
428
+ let unsubscribe = null;
429
+ const timer = setTimeout(() => {
430
+ if (unsubscribe)
431
+ unsubscribe();
432
+ reject(new Error(`FakePeer.expectMessage timed out after ${timeoutMs}ms`));
433
+ }, timeoutMs);
434
+ unsubscribe = this.deps.subscribeInboundMessages((stanza) => {
435
+ const to = stanza.attrs.to ?? '';
436
+ if (to.endsWith('@g.us'))
437
+ return;
438
+ const enc = findEncForPeer(stanza, this.jid);
439
+ if (!enc)
440
+ return;
441
+ const encType = enc.attrs.type;
442
+ if (encType !== 'pkmsg' && encType !== 'msg')
443
+ return;
444
+ const ciphertext = enc.content;
445
+ if (!(ciphertext instanceof Uint8Array))
446
+ return;
447
+ this.decryptPairwise(encType, ciphertext)
448
+ .then((message) => {
449
+ clearTimeout(timer);
450
+ if (unsubscribe)
451
+ unsubscribe();
452
+ resolve({ message, stanza, encType });
453
+ })
454
+ .catch((error) => {
455
+ clearTimeout(timer);
456
+ if (unsubscribe)
457
+ unsubscribe();
458
+ reject(error instanceof Error ? error : new Error(String(error)));
459
+ });
460
+ });
461
+ });
462
+ }
463
+ expectGroupMessage(groupJid, options = {}) {
464
+ const timeoutMs = options.timeoutMs ?? 5000;
465
+ return new Promise((resolve, reject) => {
466
+ let unsubscribe = null;
467
+ const timer = setTimeout(() => {
468
+ if (unsubscribe)
469
+ unsubscribe();
470
+ reject(new Error(`FakePeer.expectGroupMessage timed out after ${timeoutMs}ms`));
471
+ }, timeoutMs);
472
+ unsubscribe = this.deps.subscribeInboundMessages((stanza) => {
473
+ if (stanza.attrs.to !== groupJid)
474
+ return;
475
+ const skmsg = findTopLevelEnc(stanza, 'skmsg');
476
+ if (!skmsg || !(skmsg.content instanceof Uint8Array))
477
+ return;
478
+ const senderJid = options.senderJid ?? stanza.attrs.participant;
479
+ if (!senderJid) {
480
+ return;
481
+ }
482
+ this.bootstrapAndDecryptGroup(senderJid, groupJid, stanza, skmsg.content)
483
+ .then((message) => {
484
+ clearTimeout(timer);
485
+ if (unsubscribe)
486
+ unsubscribe();
487
+ resolve({ message, stanza, encType: 'skmsg' });
488
+ })
489
+ .catch((error) => {
490
+ clearTimeout(timer);
491
+ if (unsubscribe)
492
+ unsubscribe();
493
+ reject(error instanceof Error ? error : new Error(String(error)));
494
+ });
495
+ });
496
+ });
497
+ }
498
+ async decryptPairwise(encType, ciphertext) {
499
+ const padded = encType === 'pkmsg'
500
+ ? await this.ratchet.decryptPreKeyMessage(ciphertext)
501
+ : await this.ratchet.decryptMessage(ciphertext);
502
+ return proto.Message.decode(padded);
503
+ }
504
+ async decryptStanza(stanza) {
505
+ const enc = findEncForPeer(stanza, this.jid);
506
+ if (!enc || !(enc.content instanceof Uint8Array))
507
+ return null;
508
+ const encType = enc.attrs.type;
509
+ if (encType !== 'pkmsg' && encType !== 'msg')
510
+ return null;
511
+ const message = await this.decryptPairwise(encType, enc.content);
512
+ return { message, stanza, encType };
513
+ }
514
+ async bootstrapAndDecryptGroup(senderJid, groupJid, stanza, skmsgCiphertext) {
515
+ const bootstrap = findEncForPeer(stanza, this.jid);
516
+ if (bootstrap && bootstrap.content instanceof Uint8Array) {
517
+ const bootstrapType = bootstrap.attrs.type;
518
+ if (bootstrapType === 'pkmsg' || bootstrapType === 'msg') {
519
+ const padded = bootstrapType === 'pkmsg'
520
+ ? await this.ratchet.decryptPreKeyMessage(bootstrap.content)
521
+ : await this.ratchet.decryptMessage(bootstrap.content);
522
+ const innerMessage = proto.Message.decode(padded);
523
+ const skdm = innerMessage.senderKeyDistributionMessage;
524
+ const axolotl = skdm?.axolotlSenderKeyDistributionMessage;
525
+ if (axolotl) {
526
+ this.groupRecvSession.addDistribution(groupJid, senderJid, axolotl);
527
+ }
528
+ }
529
+ }
530
+ const padded = await this.groupRecvSession.decryptGroupMessage(groupJid, senderJid, skmsgCiphertext);
531
+ return proto.Message.decode(padded);
532
+ }
533
+ async ensureRatchetInitiated() {
534
+ if (this.ratchetInitiated)
535
+ return;
536
+ if (this.ratchet.hasSendChain()) {
537
+ this.ratchetInitiated = true;
538
+ return;
539
+ }
540
+ const bundle = await this.deps.bundleResolver();
541
+ const oneTimePreKey = this.reservedOneTimePreKey ?? undefined;
542
+ await this.ratchet.initiateOutbound(bundle, {
543
+ oneTimePreKey,
544
+ skipOneTimePreKey: this.skipOneTimePreKey || !oneTimePreKey
545
+ });
546
+ this.ratchetInitiated = true;
547
+ }
548
+ nextId() {
549
+ this.nextMessageCounter += 1;
550
+ return `${this.jid}-${Date.now()}-${this.nextMessageCounter}`;
551
+ }
552
+ }
553
+ function findEncForPeer(stanza, peerJid) {
554
+ if (!Array.isArray(stanza.content))
555
+ return null;
556
+ for (const child of stanza.content) {
557
+ if (child.tag === 'enc' && stanza.attrs.to === peerJid) {
558
+ return child;
559
+ }
560
+ if (child.tag === 'participants' && Array.isArray(child.content)) {
561
+ for (const toNode of child.content) {
562
+ if (toNode.tag !== 'to' || toNode.attrs.jid !== peerJid)
563
+ continue;
564
+ if (!Array.isArray(toNode.content))
565
+ continue;
566
+ for (const inner of toNode.content) {
567
+ if (inner.tag === 'enc')
568
+ return inner;
569
+ }
570
+ }
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+ function findTopLevelEnc(stanza, type) {
576
+ if (!Array.isArray(stanza.content))
577
+ return null;
578
+ for (const child of stanza.content) {
579
+ if (child.tag === 'enc' && child.attrs.type === type) {
580
+ return child;
581
+ }
582
+ }
583
+ return null;
584
+ }
585
+ function encodeUint3(value) {
586
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffff) {
587
+ throw new Error(`encodeUint3: value out of range: ${value}`);
588
+ }
589
+ const out = new Uint8Array(3);
590
+ out[0] = (value >>> 16) & 0xff;
591
+ out[1] = (value >>> 8) & 0xff;
592
+ out[2] = value & 0xff;
593
+ return out;
594
+ }
595
+ function encodePlaintextWithPadding(message) {
596
+ const encoded = proto.Message.encode(message).finish();
597
+ const padLen = 16 - (encoded.byteLength % 16);
598
+ const out = new Uint8Array(encoded.byteLength + padLen);
599
+ out.set(encoded, 0);
600
+ for (let i = encoded.byteLength; i < out.byteLength; i += 1) {
601
+ out[i] = padLen;
602
+ }
603
+ return out;
604
+ }