@zapo-js/fake-server 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -8
- package/dist/api/AppStateSyncManager.d.ts +0 -1
- package/dist/api/AppStateSyncManager.js +0 -1
- package/dist/api/FakePairingDriver.d.ts +0 -1
- package/dist/api/FakePairingDriver.js +0 -1
- package/dist/api/FakePeer.d.ts +77 -4
- package/dist/api/FakePeer.js +208 -2
- package/dist/api/FakeServerSession.d.ts +67 -0
- package/dist/api/FakeServerSession.js +293 -0
- package/dist/api/FakeWaServer.d.ts +77 -31
- package/dist/api/FakeWaServer.js +140 -259
- package/dist/api/PreKeyDispenser.d.ts +0 -1
- package/dist/api/PreKeyDispenser.js +0 -1
- package/dist/api/Scenario.d.ts +0 -1
- package/dist/api/Scenario.js +0 -1
- package/dist/api/ServerRegistries.d.ts +0 -1
- package/dist/api/ServerRegistries.js +0 -1
- package/dist/api/iq-handlers.d.ts +1 -1
- package/dist/api/iq-handlers.js +23 -1
- package/dist/{cli.d.ts → bin.d.ts} +0 -1
- package/dist/bin.js +296 -0
- package/dist/esm/api/AppStateSyncManager.js +146 -0
- package/dist/esm/api/FakePairingDriver.js +31 -0
- package/dist/esm/api/FakePeer.js +610 -0
- package/dist/esm/api/FakeServerSession.js +287 -0
- package/dist/esm/api/FakeWaServer.js +528 -0
- package/dist/esm/api/PreKeyDispenser.js +93 -0
- package/dist/esm/api/Scenario.js +32 -0
- package/dist/esm/api/ServerRegistries.js +261 -0
- package/dist/esm/api/iq-handlers.js +515 -0
- package/dist/{cli.js → esm/bin.js} +70 -25
- package/dist/esm/index.js +26 -0
- package/dist/esm/infra/WaFakeConnection.js +49 -0
- package/dist/esm/infra/WaFakeConnectionPipeline.js +235 -0
- package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
- package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
- package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
- package/dist/esm/infra/WaFakeTransport.js +31 -0
- package/dist/esm/infra/WaFakeWsServer.js +96 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/protocol/auth/cert-chain.js +50 -0
- package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
- package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
- package/dist/esm/protocol/auth/pair-device.js +87 -0
- package/dist/esm/protocol/auth/success-node.js +26 -0
- package/dist/esm/protocol/iq/abprops.js +30 -0
- package/dist/esm/protocol/iq/appstate-sync.js +146 -0
- package/dist/esm/protocol/iq/business.js +59 -0
- package/dist/esm/protocol/iq/dirty-bits.js +19 -0
- package/dist/esm/protocol/iq/group-ops.js +154 -0
- package/dist/esm/protocol/iq/newsletter.js +14 -0
- package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
- package/dist/esm/protocol/iq/privacy-token.js +26 -0
- package/dist/esm/protocol/iq/privacy.js +123 -0
- package/dist/esm/protocol/iq/profile.js +72 -0
- package/dist/esm/protocol/iq/router.js +117 -0
- package/dist/esm/protocol/iq/usync.js +64 -0
- package/dist/esm/protocol/push/app-state-key-share.js +26 -0
- package/dist/esm/protocol/push/call-failure.js +27 -0
- package/dist/esm/protocol/push/chatstate.js +23 -0
- package/dist/esm/protocol/push/error-stanza.js +13 -0
- package/dist/esm/protocol/push/history-sync.js +111 -0
- package/dist/esm/protocol/push/message.js +38 -0
- package/dist/esm/protocol/push/notification.js +29 -0
- package/dist/esm/protocol/push/presence.js +14 -0
- package/dist/esm/protocol/push/receipt.js +21 -0
- package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
- package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
- package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
- package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
- package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
- package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
- package/dist/esm/protocol/signal/prekey-upload.js +107 -0
- package/dist/esm/protocol/stream/stream-error.js +56 -0
- package/dist/esm/state/fake-app-state-collection.js +134 -0
- package/dist/esm/state/fake-media-store.js +53 -0
- package/dist/esm/transport/codec.js +8 -0
- package/dist/esm/transport/crypto.js +8 -0
- package/dist/esm/transport/protos.js +7 -0
- package/dist/esm/transport/util.js +8 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -2
- package/dist/infra/WaFakeConnection.d.ts +0 -1
- package/dist/infra/WaFakeConnection.js +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +10 -3
- package/dist/infra/WaFakeConnectionPipeline.js +47 -32
- package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
- package/dist/infra/WaFakeFrameSocket.js +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
- package/dist/infra/WaFakeNoiseHandshake.js +19 -24
- package/dist/infra/WaFakeTransport.d.ts +2 -4
- package/dist/infra/WaFakeTransport.js +2 -1
- package/dist/infra/WaFakeWsServer.d.ts +0 -1
- package/dist/infra/WaFakeWsServer.js +0 -1
- package/dist/protocol/auth/cert-chain.d.ts +4 -1
- package/dist/protocol/auth/cert-chain.js +13 -3
- package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
- package/dist/protocol/auth/client-payload-validate.js +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
- package/dist/protocol/auth/fake-primary-device.js +1 -3
- package/dist/protocol/auth/pair-device.d.ts +0 -1
- package/dist/protocol/auth/pair-device.js +4 -16
- package/dist/protocol/auth/success-node.d.ts +0 -1
- package/dist/protocol/auth/success-node.js +0 -1
- package/dist/protocol/iq/abprops.d.ts +0 -1
- package/dist/protocol/iq/abprops.js +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts +0 -1
- package/dist/protocol/iq/appstate-sync.js +0 -1
- package/dist/protocol/iq/business.d.ts +0 -1
- package/dist/protocol/iq/business.js +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts +0 -1
- package/dist/protocol/iq/dirty-bits.js +0 -1
- package/dist/protocol/iq/group-ops.d.ts +0 -1
- package/dist/protocol/iq/group-ops.js +0 -1
- package/dist/protocol/iq/newsletter.d.ts +0 -1
- package/dist/protocol/iq/newsletter.js +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
- package/dist/protocol/iq/prekey-fetch.js +0 -1
- package/dist/protocol/iq/privacy-token.d.ts +0 -1
- package/dist/protocol/iq/privacy-token.js +0 -1
- package/dist/protocol/iq/privacy.d.ts +0 -1
- package/dist/protocol/iq/privacy.js +15 -16
- package/dist/protocol/iq/profile.d.ts +0 -1
- package/dist/protocol/iq/profile.js +0 -1
- package/dist/protocol/iq/router.d.ts +7 -2
- package/dist/protocol/iq/router.js +8 -3
- package/dist/protocol/iq/usync.d.ts +0 -1
- package/dist/protocol/iq/usync.js +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts +0 -1
- package/dist/protocol/push/app-state-key-share.js +0 -1
- package/dist/protocol/push/call-failure.d.ts +0 -1
- package/dist/protocol/push/call-failure.js +0 -1
- package/dist/protocol/push/chatstate.d.ts +0 -1
- package/dist/protocol/push/chatstate.js +0 -1
- package/dist/protocol/push/error-stanza.d.ts +0 -1
- package/dist/protocol/push/error-stanza.js +0 -1
- package/dist/protocol/push/history-sync.d.ts +0 -1
- package/dist/protocol/push/history-sync.js +0 -1
- package/dist/protocol/push/message.d.ts +0 -1
- package/dist/protocol/push/message.js +0 -1
- package/dist/protocol/push/notification.d.ts +0 -1
- package/dist/protocol/push/notification.js +0 -1
- package/dist/protocol/push/presence.d.ts +0 -1
- package/dist/protocol/push/presence.js +0 -1
- package/dist/protocol/push/receipt.d.ts +0 -1
- package/dist/protocol/push/receipt.js +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
- package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
- package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-identity.js +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
- package/dist/protocol/signal/fake-sender-key.js +5 -9
- package/dist/protocol/signal/prekey-upload.d.ts +0 -1
- package/dist/protocol/signal/prekey-upload.js +0 -1
- package/dist/protocol/stream/stream-error.d.ts +0 -1
- package/dist/protocol/stream/stream-error.js +0 -1
- package/dist/state/fake-app-state-collection.d.ts +0 -1
- package/dist/state/fake-app-state-collection.js +0 -1
- package/dist/state/fake-media-store.d.ts +0 -1
- package/dist/state/fake-media-store.js +2 -2
- package/dist/transport/codec.d.ts +2 -3
- package/dist/transport/codec.js +2 -3
- package/dist/transport/crypto.d.ts +5 -7
- package/dist/transport/crypto.js +6 -11
- package/dist/transport/protos.d.ts +2 -4
- package/dist/transport/protos.js +2 -4
- package/dist/transport/util.d.ts +8 -0
- package/dist/transport/util.js +16 -0
- package/package.json +26 -6
- package/dist/api/AppStateSyncManager.d.ts.map +0 -1
- package/dist/api/AppStateSyncManager.js.map +0 -1
- package/dist/api/FakePairingDriver.d.ts.map +0 -1
- package/dist/api/FakePairingDriver.js.map +0 -1
- package/dist/api/FakePeer.d.ts.map +0 -1
- package/dist/api/FakePeer.js.map +0 -1
- package/dist/api/FakeWaServer.d.ts.map +0 -1
- package/dist/api/FakeWaServer.js.map +0 -1
- package/dist/api/PreKeyDispenser.d.ts.map +0 -1
- package/dist/api/PreKeyDispenser.js.map +0 -1
- package/dist/api/Scenario.d.ts.map +0 -1
- package/dist/api/Scenario.js.map +0 -1
- package/dist/api/ServerRegistries.d.ts.map +0 -1
- package/dist/api/ServerRegistries.js.map +0 -1
- package/dist/api/iq-handlers.d.ts.map +0 -1
- package/dist/api/iq-handlers.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/WaFakeConnection.d.ts.map +0 -1
- package/dist/infra/WaFakeConnection.js.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
- package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
- package/dist/infra/WaFakeFrameSocket.js.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
- package/dist/infra/WaFakeTransport.d.ts.map +0 -1
- package/dist/infra/WaFakeTransport.js.map +0 -1
- package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeWsServer.js.map +0 -1
- package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
- package/dist/protocol/auth/cert-chain.js.map +0 -1
- package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
- package/dist/protocol/auth/client-payload-validate.js.map +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
- package/dist/protocol/auth/fake-primary-device.js.map +0 -1
- package/dist/protocol/auth/pair-device.d.ts.map +0 -1
- package/dist/protocol/auth/pair-device.js.map +0 -1
- package/dist/protocol/auth/success-node.d.ts.map +0 -1
- package/dist/protocol/auth/success-node.js.map +0 -1
- package/dist/protocol/iq/abprops.d.ts.map +0 -1
- package/dist/protocol/iq/abprops.js.map +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
- package/dist/protocol/iq/appstate-sync.js.map +0 -1
- package/dist/protocol/iq/business.d.ts.map +0 -1
- package/dist/protocol/iq/business.js.map +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
- package/dist/protocol/iq/dirty-bits.js.map +0 -1
- package/dist/protocol/iq/group-ops.d.ts.map +0 -1
- package/dist/protocol/iq/group-ops.js.map +0 -1
- package/dist/protocol/iq/newsletter.d.ts.map +0 -1
- package/dist/protocol/iq/newsletter.js.map +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
- package/dist/protocol/iq/prekey-fetch.js.map +0 -1
- package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
- package/dist/protocol/iq/privacy-token.js.map +0 -1
- package/dist/protocol/iq/privacy.d.ts.map +0 -1
- package/dist/protocol/iq/privacy.js.map +0 -1
- package/dist/protocol/iq/profile.d.ts.map +0 -1
- package/dist/protocol/iq/profile.js.map +0 -1
- package/dist/protocol/iq/router.d.ts.map +0 -1
- package/dist/protocol/iq/router.js.map +0 -1
- package/dist/protocol/iq/usync.d.ts.map +0 -1
- package/dist/protocol/iq/usync.js.map +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
- package/dist/protocol/push/app-state-key-share.js.map +0 -1
- package/dist/protocol/push/call-failure.d.ts.map +0 -1
- package/dist/protocol/push/call-failure.js.map +0 -1
- package/dist/protocol/push/chatstate.d.ts.map +0 -1
- package/dist/protocol/push/chatstate.js.map +0 -1
- package/dist/protocol/push/error-stanza.d.ts.map +0 -1
- package/dist/protocol/push/error-stanza.js.map +0 -1
- package/dist/protocol/push/history-sync.d.ts.map +0 -1
- package/dist/protocol/push/history-sync.js.map +0 -1
- package/dist/protocol/push/message.d.ts.map +0 -1
- package/dist/protocol/push/message.js.map +0 -1
- package/dist/protocol/push/notification.d.ts.map +0 -1
- package/dist/protocol/push/notification.js.map +0 -1
- package/dist/protocol/push/presence.d.ts.map +0 -1
- package/dist/protocol/push/presence.js.map +0 -1
- package/dist/protocol/push/receipt.d.ts.map +0 -1
- package/dist/protocol/push/receipt.js.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
- package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
- package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
- package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-session.js +0 -207
- package/dist/protocol/signal/fake-peer-session.js.map +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
- package/dist/protocol/signal/fake-sender-key.js.map +0 -1
- package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
- package/dist/protocol/signal/prekey-upload.js.map +0 -1
- package/dist/protocol/stream/stream-error.d.ts.map +0 -1
- package/dist/protocol/stream/stream-error.js.map +0 -1
- package/dist/state/fake-app-state-collection.d.ts.map +0 -1
- package/dist/state/fake-app-state-collection.js.map +0 -1
- package/dist/state/fake-media-store.d.ts.map +0 -1
- package/dist/state/fake-media-store.js.map +0 -1
- package/dist/transport/codec.d.ts.map +0 -1
- package/dist/transport/codec.js.map +0 -1
- package/dist/transport/crypto.d.ts.map +0 -1
- package/dist/transport/crypto.js.map +0 -1
- package/dist/transport/index.d.ts +0 -4
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js +0 -20
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/protos.d.ts.map +0 -1
- package/dist/transport/protos.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# @zapo-js/fake-server
|
|
2
2
|
|
|
3
|
-
In-process fake WhatsApp Web server that drives the real `zapo-js` `WaClient` end-to-end
|
|
3
|
+
In-process fake WhatsApp Web server that drives the real `zapo-js` `WaClient` end-to-end - full Noise XX/IK handshake, QR pairing, Signal Protocol (X3DH + Double Ratchet), SenderKey for groups, media upload/download over self-signed HTTPS, app-state sync - all without touching WhatsApp servers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -D @zapo-js/fake-server zapo-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`zapo-js` is a peer dependency - the fake server reuses the core's binary codec, crypto, and protos. Everything below works from any project; nothing requires this monorepo.
|
|
4
12
|
|
|
5
13
|
## Quick start
|
|
6
14
|
|
|
@@ -29,8 +37,8 @@ await server.stop()
|
|
|
29
37
|
```text
|
|
30
38
|
src/
|
|
31
39
|
├── api/ # Public-facing API
|
|
32
|
-
│ ├── FakeWaServer.ts # Main facade
|
|
33
|
-
│ ├── FakePeer.ts # Simulated WhatsApp peer
|
|
40
|
+
│ ├── FakeWaServer.ts # Main facade – WS server, IQ router, registries, lifecycle
|
|
41
|
+
│ ├── FakePeer.ts # Simulated WhatsApp peer – Signal crypto, send/recv, groups
|
|
34
42
|
│ ├── FakePairingDriver.ts # QR pairing flow orchestrator
|
|
35
43
|
│ └── Scenario.ts # Declarative test scenario DSL
|
|
36
44
|
├── protocol/ # Protocol-layer builders, parsers, crypto
|
|
@@ -50,7 +58,7 @@ src/
|
|
|
50
58
|
│ ├── fake-media-store.ts # In-memory media blob store
|
|
51
59
|
│ └── fake-app-state-collection.ts # App-state patch/snapshot provider
|
|
52
60
|
├── transport/ # Re-exports from zapo-js (codec, crypto, protos)
|
|
53
|
-
└── __tests__/ # Cross-check test suite
|
|
61
|
+
└── __tests__/ # Cross-check test suite
|
|
54
62
|
└── helpers/ # Shared test utilities (zapo-client factory)
|
|
55
63
|
|
|
56
64
|
bench/
|
|
@@ -63,14 +71,63 @@ bench/
|
|
|
63
71
|
|
|
64
72
|
### FakeWaServer
|
|
65
73
|
|
|
66
|
-
Central facade. Manages the WS listener, noise handshake, IQ router, and all state registries
|
|
74
|
+
Central facade. Manages the WS listener, noise handshake, IQ router, and all state registries.
|
|
75
|
+
|
|
76
|
+
Constructor options (`FakeWaServer.start({...})` / `new FakeWaServer({...})`):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const server = await FakeWaServer.start({
|
|
80
|
+
host: '127.0.0.1', // default
|
|
81
|
+
port: 5222, // default: random free port
|
|
82
|
+
path: '/ws/chat', // default
|
|
83
|
+
successNodeAttributes: {
|
|
84
|
+
// stamped on every post-handshake <success/>
|
|
85
|
+
lid: '5511999@lid',
|
|
86
|
+
displayName: 'Fake Display'
|
|
87
|
+
},
|
|
88
|
+
defaultIqHandlers: false // start with an empty IQ router (default: true)
|
|
89
|
+
})
|
|
90
|
+
```
|
|
67
91
|
|
|
68
92
|
- **Peer registry** (`peerRegistry`): maps device JIDs → `FakePeer` instances. The global `usync` + `prekey-fetch` IQ handlers consult this.
|
|
93
|
+
- **Sessions**: state is grouped per session (see below). A single-client server uses one default session, and the `server.registries` / `server.expectIq(...)` / `server.preKeyDispenser` surface delegates straight to it.
|
|
69
94
|
- **Group registry** (`groupRegistry`): maps group JIDs → group metadata + participants. The `w:g2` handler serves it.
|
|
70
|
-
- **IQ router** (`WaFakeIqRouter`): first-match-wins stanza dispatcher with `{ xmlns, type, childTag }` matchers. ~
|
|
95
|
+
- **IQ router** (`WaFakeIqRouter`): first-match-wins stanza dispatcher with `{ xmlns, type, childTag }` matchers. ~27 global handlers registered in the constructor cover every IQ the lib emits during normal operation (disable them with `defaultIqHandlers: false`). `registerIqHandler(matcher, respond)` registers at high priority and shadows the defaults; a responder that returns `null` falls through to the next matching handler, so you can observe an IQ (capture, assert) and still let the default answer it.
|
|
71
96
|
- **Prekey dispenser**: hands out unique one-time prekeys from the lib's upload to FakePeers. Resets on each forced refill (`triggerPreKeyUpload({ force: true })`).
|
|
72
97
|
- **Listener fan-outs**: `onOutboundGroupOp`, `onOutboundPrivacySet`, `onOutboundBlocklistChange`, `onOutboundProfilePictureSet`, `onOutboundStatusSet`, `onLogout`, `onOutboundPrivacyTokenIssue`, `onOutboundDirtyBitsClear`.
|
|
73
98
|
|
|
99
|
+
### Sessions (multiple clients, one server)
|
|
100
|
+
|
|
101
|
+
By default every connection shares one session, so a single `WaClient` sees the whole `FakeWaServer` surface. To run **several clients against one server without them sharing any state**, pass a `sessionKey` resolver: each authenticated connection is bound to an isolated session keyed by the returned id. Sessions never share peers, groups, prekeys, app-state, or captured stanzas.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const server = await FakeWaServer.start({
|
|
105
|
+
// Key each connection by its login username; pre-login (registration)
|
|
106
|
+
// connections fall under one transient bucket.
|
|
107
|
+
sessionKey: ({ clientPayload }) =>
|
|
108
|
+
clientPayload.kind === 'login' ? String(clientPayload.username) : 'pending'
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// After a client authenticates, reach its isolated state through the pipeline:
|
|
112
|
+
const session = server.sessionFor(pipeline) // FakeServerSession
|
|
113
|
+
await session.expectIq({ xmlns: 'passive', type: 'set' })
|
|
114
|
+
session.registries.peerRegistry // this client's peers only
|
|
115
|
+
session.preKeyDispenser.capturedPreKeyBundleSnapshot() // this client's prekeys
|
|
116
|
+
|
|
117
|
+
// Or address a session by id directly:
|
|
118
|
+
const alice = server.session('5511111111111')
|
|
119
|
+
|
|
120
|
+
// Peer creation routes to the connection's session automatically:
|
|
121
|
+
const peer = await server.createFakePeer({ jid: peerJid }, pipeline)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
|
|
126
|
+
- The resolver runs once per connection, right after authentication (so `clientPayload` is available). Connections that resolve to the same id reuse one session.
|
|
127
|
+
- `server.registerIqHandler(...)` applies to every session (present and future); `session.registerIqHandler(...)` scopes to one session.
|
|
128
|
+
- The media HTTPS store is process-global and shared across sessions; isolation covers peers/groups/prekeys/app-state/captures, not uploaded media blobs.
|
|
129
|
+
- QR pairing writes to the connection's session too, so pair clients **sequentially** when keying registration connections into a shared bucket.
|
|
130
|
+
|
|
74
131
|
### FakePeer
|
|
75
132
|
|
|
76
133
|
Simulated WhatsApp peer with real Signal Protocol crypto:
|
|
@@ -101,8 +158,10 @@ Every outbound IQ the lib sends during normal operation is handled:
|
|
|
101
158
|
| `w:p` / `urn:xmpp:ping` get | `whatsapp-ping` / `xmpp-ping` | Ack |
|
|
102
159
|
| `encrypt` set | `prekey-upload` | Captures bundle, resets dispenser |
|
|
103
160
|
| `encrypt` get `<digest>` | `signal-digest` | Returns 404 → forces upload |
|
|
161
|
+
| `encrypt` get `<count>` | `prekey-count` | Serves remaining dispenser prekey count |
|
|
104
162
|
| `encrypt` set `<rotate>` | `signed-prekey-rotate` | Ack |
|
|
105
163
|
| `encrypt` get `<key>` | `prekey-fetch` | Serves peer bundles from registry |
|
|
164
|
+
| `passive` set | `passive-mode` | Ack |
|
|
106
165
|
| `usync` get | `usync` | Resolves device IDs from registry |
|
|
107
166
|
| `w:m` set `<media_conn>` | `media-conn` | Points lib at fake HTTPS server |
|
|
108
167
|
| `w:sync:app:state` set | `app-state-sync` | Serves patches/snapshots from providers |
|
|
@@ -119,6 +178,16 @@ Every outbound IQ the lib sends during normal operation is handled:
|
|
|
119
178
|
| `newsletter` get `<my_addons>` | `newsletter-my-addons` | Ack |
|
|
120
179
|
| `urn:xmpp:whatsapp:dirty` set | `dirty-bits-clear` | Captures cleared bits |
|
|
121
180
|
|
|
181
|
+
## Using with non-zapo-js clients
|
|
182
|
+
|
|
183
|
+
The fake server speaks the real wire protocol, so other WhatsApp Web libraries (Baileys forks, whatsmeow, ...) can connect to it too. The recipe:
|
|
184
|
+
|
|
185
|
+
1. **Point the client's WebSocket URL at `server.url`** (the upgrade path is `/ws/chat`, same as production).
|
|
186
|
+
2. **Trust the fake root CA.** The server signs its Noise cert chain with a per-instance random root; clients that verify the cert signature against WhatsApp's pinned production key must be told to trust `server.noiseRootCa.publicKey` instead (exposed via the CLI's `--json` as `noiseRootCa.publicKeyHex`). The chain carries a valid `notBefore`/`notAfter` window, so clients that enforce validity work out of the box.
|
|
187
|
+
3. **Post-connect IQs are answered by default**: the `passive` set IQ and the `encrypt` get `<count>` prekey-count query, which Baileys-family and whatsmeow clients block on before reporting the connection as open.
|
|
188
|
+
4. **Offline drain bulletin**: after every login the server sends `<ib><offline count="0"/></ib>`, which Baileys-family clients require before flushing their buffered events.
|
|
189
|
+
5. **QR pairing**: in-process, use `server.runPairing(...)` with the client's `advSecretKey` + identity public key; from the standalone CLI, use `--pair <device-jid>` and paste the QR payload the client displays when prompted.
|
|
190
|
+
|
|
122
191
|
## Benchmarking
|
|
123
192
|
|
|
124
193
|
```bash
|
|
@@ -161,16 +230,35 @@ node --expose-gc --import tsx packages/fake-server/bench/messaging.bench.ts --cp
|
|
|
161
230
|
|
|
162
231
|
## CLI
|
|
163
232
|
|
|
233
|
+
The package ships a `fake-wa-server` bin, so the standalone server works from any project (or no project at all):
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# One-off via npx
|
|
237
|
+
npx @zapo-js/fake-server --port 5222 --peer 5511888@s.whatsapp.net --log
|
|
238
|
+
|
|
239
|
+
# Installed as a dev dependency
|
|
240
|
+
npx fake-wa-server --port 5222 --peer 5511888@s.whatsapp.net --log
|
|
241
|
+
|
|
242
|
+
# First-time QR pairing: prompts on stdin for the QR payload the client displays
|
|
243
|
+
npx fake-wa-server --port 5222 --pair 5511999:1@s.whatsapp.net
|
|
244
|
+
|
|
245
|
+
# Print connection info (url + noise root CA hex) as JSON, for scripting
|
|
246
|
+
npx fake-wa-server --json
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
From inside this monorepo (runs the source, no build needed):
|
|
250
|
+
|
|
164
251
|
```bash
|
|
165
|
-
# Standalone server for manual testing
|
|
166
252
|
npm --workspace=@zapo-js/fake-server run cli -- --port 5222 --peer 5511888@s.whatsapp.net --log
|
|
167
253
|
```
|
|
168
254
|
|
|
255
|
+
Run with `--help` for the full flag list.
|
|
256
|
+
|
|
169
257
|
## Test suite
|
|
170
258
|
|
|
171
259
|
```bash
|
|
172
260
|
npm --workspace=@zapo-js/fake-server test
|
|
173
|
-
#
|
|
261
|
+
# 164 tests, --test-concurrency=1
|
|
174
262
|
```
|
|
175
263
|
|
|
176
264
|
Cross-check tests drive a real `WaClient` against the fake server and assert on both sides (lib emits correct events, peer decrypts correctly). Unit tests validate protocol builders/parsers in isolation.
|
|
@@ -24,4 +24,3 @@ export declare class AppStateSyncManager {
|
|
|
24
24
|
pushServerSyncNotification(pipeline: WaFakeConnectionPipeline, input: BuildServerSyncNotificationInput): Promise<void>;
|
|
25
25
|
consumeOutboundAppStatePatches(iq: BinaryNode): Promise<void>;
|
|
26
26
|
}
|
|
27
|
-
//# sourceMappingURL=AppStateSyncManager.d.ts.map
|
package/dist/api/FakePeer.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface CreateFakePeerOptions {
|
|
|
12
12
|
readonly displayName?: string;
|
|
13
13
|
readonly keyBundle?: FakePeerKeyBundle;
|
|
14
14
|
readonly skipOneTimePreKey?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Stash every outbound (id → plaintext + pristine ciphertext) so
|
|
17
|
+
* tests / benches can drive replays via {@link FakePeer.replaySentMessage}.
|
|
18
|
+
* Off by default to avoid inflating the fixture's heap on long
|
|
19
|
+
* memory-focused runs that never replay.
|
|
20
|
+
*/
|
|
21
|
+
readonly enableReplayCache?: boolean;
|
|
15
22
|
}
|
|
16
23
|
export interface SendMessageOptions {
|
|
17
24
|
readonly id?: string;
|
|
@@ -45,20 +52,57 @@ interface FakePeerDeps {
|
|
|
45
52
|
export declare class FakePeer {
|
|
46
53
|
readonly jid: string;
|
|
47
54
|
readonly displayName?: string;
|
|
48
|
-
|
|
55
|
+
keyBundle: FakePeerKeyBundle;
|
|
49
56
|
readonly identity: FakePeerIdentity;
|
|
50
57
|
private readonly deps;
|
|
51
|
-
private
|
|
58
|
+
private ratchet;
|
|
52
59
|
private readonly skipOneTimePreKey;
|
|
53
|
-
private
|
|
60
|
+
private reservedOneTimePreKey;
|
|
54
61
|
private ratchetInitiated;
|
|
55
62
|
private nextMessageCounter;
|
|
63
|
+
private readonly replayCacheEnabled;
|
|
64
|
+
private readonly sentPlaintextById;
|
|
65
|
+
private readonly sentEncryptedById;
|
|
56
66
|
private readonly senderKeysByGroup;
|
|
57
67
|
private readonly groupsBootstrapped;
|
|
58
68
|
private readonly groupRecvSession;
|
|
59
69
|
private constructor();
|
|
60
70
|
static create(options: CreateFakePeerOptions, deps: FakePeerDeps): Promise<FakePeer>;
|
|
61
71
|
sendMessage(message: proto.IMessage, options?: SendMessageOptions): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Re-encrypts and re-pushes a previously sent message identified by
|
|
74
|
+
* `originalMsgId`. Used by tests / benches that exercise the lib's
|
|
75
|
+
* incoming-retry path: after the lib emits `<receipt type="retry">`
|
|
76
|
+
* back to this peer, the peer is expected to re-send the same
|
|
77
|
+
* plaintext so the lib can decrypt it successfully on the second
|
|
78
|
+
* attempt.
|
|
79
|
+
*
|
|
80
|
+
* The replay reuses the original stanza id by default so the lib's
|
|
81
|
+
* dedup logic (and tests that wait for a specific id) see the resend
|
|
82
|
+
* as a continuation of the same outbound. Pass `resendId` to use a
|
|
83
|
+
* different id.
|
|
84
|
+
*/
|
|
85
|
+
replaySentMessage(originalMsgId: string, options?: {
|
|
86
|
+
readonly resendId?: string;
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Rotates the peer's prekey material and resets the Signal session
|
|
90
|
+
* state so the lib has to re-run X3DH on the next outbound. Keeps
|
|
91
|
+
* the identity key pair (same user) but issues a new signed prekey
|
|
92
|
+
* + new one-time prekey set. After this call:
|
|
93
|
+
* - `keyBundle` reflects the new material;
|
|
94
|
+
* - the underlying `FakePeerDoubleRatchet` is fresh – the next
|
|
95
|
+
* inbound from the lib must be a `pkmsg` (initial signal
|
|
96
|
+
* message), which the new `keyBundle` is wired to decrypt;
|
|
97
|
+
* - any cached one-time prekey reservation in the dispenser is
|
|
98
|
+
* dropped so subsequent retry-receipts include this peer's new
|
|
99
|
+
* keys directly via {@link sendRetryReceipt}.
|
|
100
|
+
*
|
|
101
|
+
* Use this to simulate the "session lost" scenario that real
|
|
102
|
+
* WhatsApp clients trigger when they need the sender to fully
|
|
103
|
+
* re-bootstrap the Signal session via the retry-receipt path.
|
|
104
|
+
*/
|
|
105
|
+
rotateForRetry(): Promise<void>;
|
|
62
106
|
sendConversation(text: string, options?: SendMessageOptions): Promise<void>;
|
|
63
107
|
sendMessageEdit(input: {
|
|
64
108
|
readonly targetMessageId: string;
|
|
@@ -131,6 +175,36 @@ export declare class FakePeer {
|
|
|
131
175
|
sendAppStateSyncKeyShare(input: BuildAppStateSyncKeyShareInput, options?: SendMessageOptions): Promise<void>;
|
|
132
176
|
sendGroupConversation(groupJid: string, text: string, options?: SendMessageOptions): Promise<void>;
|
|
133
177
|
sendGroupMessage(groupJid: string, message: proto.IMessage, options?: SendMessageOptions): Promise<void>;
|
|
178
|
+
/**
|
|
179
|
+
* Sends a `<receipt type="retry">` stanza to the lib asking it to
|
|
180
|
+
* re-encrypt and re-send the message identified by `originalMsgId`.
|
|
181
|
+
* Used by benches / tests that exercise the lib's outbound retry
|
|
182
|
+
* replay path.
|
|
183
|
+
*
|
|
184
|
+
* Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
|
|
185
|
+
* `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
|
|
186
|
+
*
|
|
187
|
+
* When `includeKeys` is true (default), the `<keys>` block carries
|
|
188
|
+
* the peer's current identity + signed prekey + one-time prekey.
|
|
189
|
+
* This forces the lib's `WaRetryCoordinator` to tear down the
|
|
190
|
+
* existing session and re-run X3DH against these fresh keys before
|
|
191
|
+
* re-encrypting the original payload, so the resend lands as a
|
|
192
|
+
* `pkmsg` against the peer's *new* prekey state. Pair this with
|
|
193
|
+
* {@link rotateForRetry} to simulate full session loss + recovery.
|
|
194
|
+
*
|
|
195
|
+
* `<device-identity>` is omitted: it's `maybeChild` in wa-web and
|
|
196
|
+
* also optional in the lib's parser. The fake-peer fixture doesn't
|
|
197
|
+
* have an ADV primary-device to sign one, and the lib's outbound
|
|
198
|
+
* retry replay path does not require it.
|
|
199
|
+
*/
|
|
200
|
+
sendRetryReceipt(originalMsgId: string, options?: {
|
|
201
|
+
readonly count?: number;
|
|
202
|
+
readonly receiptId?: string;
|
|
203
|
+
readonly t?: number;
|
|
204
|
+
readonly error?: number;
|
|
205
|
+
readonly includeKeys?: boolean;
|
|
206
|
+
}): Promise<void>;
|
|
207
|
+
private buildRetryKeysNode;
|
|
134
208
|
expectMessage(options?: ExpectMessageOptions): Promise<ReceivedMessage>;
|
|
135
209
|
expectGroupMessage(groupJid: string, options?: ExpectGroupMessageOptions): Promise<ReceivedMessage>;
|
|
136
210
|
private decryptPairwise;
|
|
@@ -140,4 +214,3 @@ export declare class FakePeer {
|
|
|
140
214
|
private nextId;
|
|
141
215
|
}
|
|
142
216
|
export {};
|
|
143
|
-
//# sourceMappingURL=FakePeer.d.ts.map
|
package/dist/api/FakePeer.js
CHANGED
|
@@ -14,6 +14,8 @@ class FakePeer {
|
|
|
14
14
|
constructor(keyBundle, options, deps) {
|
|
15
15
|
this.ratchetInitiated = false;
|
|
16
16
|
this.nextMessageCounter = 0;
|
|
17
|
+
this.sentPlaintextById = new Map();
|
|
18
|
+
this.sentEncryptedById = new Map();
|
|
17
19
|
this.senderKeysByGroup = new Map();
|
|
18
20
|
this.groupsBootstrapped = new Set();
|
|
19
21
|
this.groupRecvSession = new fake_peer_group_recv_session_1.FakePeerGroupRecvSession();
|
|
@@ -26,6 +28,7 @@ class FakePeer {
|
|
|
26
28
|
this.displayName = options.displayName;
|
|
27
29
|
this.deps = deps;
|
|
28
30
|
this.skipOneTimePreKey = options.skipOneTimePreKey === true;
|
|
31
|
+
this.replayCacheEnabled = options.enableReplayCache === true;
|
|
29
32
|
this.reservedOneTimePreKey =
|
|
30
33
|
!this.skipOneTimePreKey && deps.reserveOneTimePreKey
|
|
31
34
|
? deps.reserveOneTimePreKey()
|
|
@@ -45,6 +48,18 @@ class FakePeer {
|
|
|
45
48
|
: ciphertext;
|
|
46
49
|
const enc = { type, ciphertext: finalCiphertext };
|
|
47
50
|
const id = options.id ?? this.nextId();
|
|
51
|
+
if (this.replayCacheEnabled) {
|
|
52
|
+
// Stash the plaintext so the bench / a test can replay this
|
|
53
|
+
// exact payload later in response to an incoming retry
|
|
54
|
+
// receipt from the lib (replaySentMessage). Also stash the
|
|
55
|
+
// PRISTINE ciphertext (pre-tamper) + signal type so a
|
|
56
|
+
// replay can put the original frame back on the wire –
|
|
57
|
+
// matches how a real Signal sender re-sends after a
|
|
58
|
+
// recipient asks for retry: same counter + ratchet key,
|
|
59
|
+
// just an uncorrupted body.
|
|
60
|
+
this.sentPlaintextById.set(id, message);
|
|
61
|
+
this.sentEncryptedById.set(id, { type, ciphertext });
|
|
62
|
+
}
|
|
48
63
|
const stanza = (0, message_1.buildMessage)({
|
|
49
64
|
id,
|
|
50
65
|
from: options.from ?? this.jid,
|
|
@@ -56,6 +71,76 @@ class FakePeer {
|
|
|
56
71
|
});
|
|
57
72
|
await this.deps.pushStanza(stanza);
|
|
58
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Re-encrypts and re-pushes a previously sent message identified by
|
|
76
|
+
* `originalMsgId`. Used by tests / benches that exercise the lib's
|
|
77
|
+
* incoming-retry path: after the lib emits `<receipt type="retry">`
|
|
78
|
+
* back to this peer, the peer is expected to re-send the same
|
|
79
|
+
* plaintext so the lib can decrypt it successfully on the second
|
|
80
|
+
* attempt.
|
|
81
|
+
*
|
|
82
|
+
* The replay reuses the original stanza id by default so the lib's
|
|
83
|
+
* dedup logic (and tests that wait for a specific id) see the resend
|
|
84
|
+
* as a continuation of the same outbound. Pass `resendId` to use a
|
|
85
|
+
* different id.
|
|
86
|
+
*/
|
|
87
|
+
async replaySentMessage(originalMsgId, options = {}) {
|
|
88
|
+
const stored = this.sentEncryptedById.get(originalMsgId);
|
|
89
|
+
if (stored) {
|
|
90
|
+
// Re-send the pristine ciphertext – same Signal counter +
|
|
91
|
+
// ratchet key as the original send, just without the
|
|
92
|
+
// tamper. The lib's recv chain still has the message key
|
|
93
|
+
// for that counter, so this is the recovery path real
|
|
94
|
+
// WhatsApp peers walk after observing a retry receipt for
|
|
95
|
+
// a frame the recipient failed to decrypt.
|
|
96
|
+
const enc = { type: stored.type, ciphertext: stored.ciphertext };
|
|
97
|
+
const id = options.resendId ?? originalMsgId;
|
|
98
|
+
const stanza = (0, message_1.buildMessage)({
|
|
99
|
+
id,
|
|
100
|
+
from: this.jid,
|
|
101
|
+
type: 'text',
|
|
102
|
+
notify: this.displayName,
|
|
103
|
+
enc: [enc]
|
|
104
|
+
});
|
|
105
|
+
await this.deps.pushStanza(stanza);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const message = this.sentPlaintextById.get(originalMsgId);
|
|
109
|
+
if (!message) {
|
|
110
|
+
throw new Error(`FakePeer.replaySentMessage: no sent plaintext for id ${originalMsgId}`);
|
|
111
|
+
}
|
|
112
|
+
await this.sendMessage(message, { id: options.resendId ?? originalMsgId });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Rotates the peer's prekey material and resets the Signal session
|
|
116
|
+
* state so the lib has to re-run X3DH on the next outbound. Keeps
|
|
117
|
+
* the identity key pair (same user) but issues a new signed prekey
|
|
118
|
+
* + new one-time prekey set. After this call:
|
|
119
|
+
* - `keyBundle` reflects the new material;
|
|
120
|
+
* - the underlying `FakePeerDoubleRatchet` is fresh – the next
|
|
121
|
+
* inbound from the lib must be a `pkmsg` (initial signal
|
|
122
|
+
* message), which the new `keyBundle` is wired to decrypt;
|
|
123
|
+
* - any cached one-time prekey reservation in the dispenser is
|
|
124
|
+
* dropped so subsequent retry-receipts include this peer's new
|
|
125
|
+
* keys directly via {@link sendRetryReceipt}.
|
|
126
|
+
*
|
|
127
|
+
* Use this to simulate the "session lost" scenario that real
|
|
128
|
+
* WhatsApp clients trigger when they need the sender to fully
|
|
129
|
+
* re-bootstrap the Signal session via the retry-receipt path.
|
|
130
|
+
*/
|
|
131
|
+
async rotateForRetry() {
|
|
132
|
+
const refreshed = await (0, fake_peer_key_bundle_1.generateFakePeerKeyBundle)({
|
|
133
|
+
identityKeyPair: this.keyBundle.identityKeyPair,
|
|
134
|
+
registrationId: this.keyBundle.registrationId,
|
|
135
|
+
signedPreKeyId: this.keyBundle.signedPreKey.id + 1,
|
|
136
|
+
firstOneTimePreKeyId: (this.keyBundle.oneTimePreKeys[this.keyBundle.oneTimePreKeys.length - 1]?.id ?? 0) +
|
|
137
|
+
1
|
|
138
|
+
});
|
|
139
|
+
this.keyBundle = refreshed;
|
|
140
|
+
this.ratchet = new fake_peer_double_ratchet_1.FakePeerDoubleRatchet(refreshed);
|
|
141
|
+
this.ratchetInitiated = false;
|
|
142
|
+
this.reservedOneTimePreKey = null;
|
|
143
|
+
}
|
|
59
144
|
sendConversation(text, options = {}) {
|
|
60
145
|
return this.sendMessage({ conversation: text }, options);
|
|
61
146
|
}
|
|
@@ -234,6 +319,112 @@ class FakePeer {
|
|
|
234
319
|
});
|
|
235
320
|
await this.deps.pushStanza(groupStanza);
|
|
236
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Sends a `<receipt type="retry">` stanza to the lib asking it to
|
|
324
|
+
* re-encrypt and re-send the message identified by `originalMsgId`.
|
|
325
|
+
* Used by benches / tests that exercise the lib's outbound retry
|
|
326
|
+
* replay path.
|
|
327
|
+
*
|
|
328
|
+
* Shape matches WhatsApp Web's `WAWebRetryRequestParser`:
|
|
329
|
+
* `<receipt id type=retry from t><retry id count?/><registration>4B</registration>[<keys>...</keys>]</receipt>`.
|
|
330
|
+
*
|
|
331
|
+
* When `includeKeys` is true (default), the `<keys>` block carries
|
|
332
|
+
* the peer's current identity + signed prekey + one-time prekey.
|
|
333
|
+
* This forces the lib's `WaRetryCoordinator` to tear down the
|
|
334
|
+
* existing session and re-run X3DH against these fresh keys before
|
|
335
|
+
* re-encrypting the original payload, so the resend lands as a
|
|
336
|
+
* `pkmsg` against the peer's *new* prekey state. Pair this with
|
|
337
|
+
* {@link rotateForRetry} to simulate full session loss + recovery.
|
|
338
|
+
*
|
|
339
|
+
* `<device-identity>` is omitted: it's `maybeChild` in wa-web and
|
|
340
|
+
* also optional in the lib's parser. The fake-peer fixture doesn't
|
|
341
|
+
* have an ADV primary-device to sign one, and the lib's outbound
|
|
342
|
+
* retry replay path does not require it.
|
|
343
|
+
*/
|
|
344
|
+
async sendRetryReceipt(originalMsgId, options = {}) {
|
|
345
|
+
const receiptId = options.receiptId ?? originalMsgId;
|
|
346
|
+
const retryCount = options.count ?? 1;
|
|
347
|
+
const t = options.t ?? Math.floor(Date.now() / 1000);
|
|
348
|
+
const error = options.error;
|
|
349
|
+
const includeKeys = options.includeKeys !== false;
|
|
350
|
+
const registration = new Uint8Array(4);
|
|
351
|
+
const regId = this.keyBundle.registrationId & 0xffffffff;
|
|
352
|
+
registration[0] = (regId >>> 24) & 0xff;
|
|
353
|
+
registration[1] = (regId >>> 16) & 0xff;
|
|
354
|
+
registration[2] = (regId >>> 8) & 0xff;
|
|
355
|
+
registration[3] = regId & 0xff;
|
|
356
|
+
const retryAttrs = {
|
|
357
|
+
v: '1',
|
|
358
|
+
count: String(retryCount),
|
|
359
|
+
id: originalMsgId,
|
|
360
|
+
t: String(t)
|
|
361
|
+
};
|
|
362
|
+
if (error !== undefined && error !== 0) {
|
|
363
|
+
retryAttrs.error = String(error);
|
|
364
|
+
}
|
|
365
|
+
const content = [
|
|
366
|
+
{
|
|
367
|
+
tag: 'retry',
|
|
368
|
+
attrs: retryAttrs
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
tag: 'registration',
|
|
372
|
+
attrs: {},
|
|
373
|
+
content: registration
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
if (includeKeys) {
|
|
377
|
+
content.push(this.buildRetryKeysNode());
|
|
378
|
+
}
|
|
379
|
+
const receipt = {
|
|
380
|
+
tag: 'receipt',
|
|
381
|
+
attrs: {
|
|
382
|
+
id: receiptId,
|
|
383
|
+
from: this.jid,
|
|
384
|
+
type: 'retry',
|
|
385
|
+
t: String(t)
|
|
386
|
+
},
|
|
387
|
+
content
|
|
388
|
+
};
|
|
389
|
+
await this.deps.pushStanza(receipt);
|
|
390
|
+
}
|
|
391
|
+
buildRetryKeysNode() {
|
|
392
|
+
const skey = this.keyBundle.signedPreKey;
|
|
393
|
+
const oneTime = this.keyBundle.oneTimePreKeys[0];
|
|
394
|
+
if (!oneTime) {
|
|
395
|
+
throw new Error('FakePeer.sendRetryReceipt: no one-time prekey available');
|
|
396
|
+
}
|
|
397
|
+
const skeyIdBytes = encodeUint3(skey.id);
|
|
398
|
+
const keyIdBytes = encodeUint3(oneTime.id);
|
|
399
|
+
return {
|
|
400
|
+
tag: 'keys',
|
|
401
|
+
attrs: {},
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
tag: 'identity',
|
|
405
|
+
attrs: {},
|
|
406
|
+
content: this.keyBundle.identityKeyPair.pubKey
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
tag: 'skey',
|
|
410
|
+
attrs: {},
|
|
411
|
+
content: [
|
|
412
|
+
{ tag: 'id', attrs: {}, content: skeyIdBytes },
|
|
413
|
+
{ tag: 'value', attrs: {}, content: skey.keyPair.pubKey },
|
|
414
|
+
{ tag: 'signature', attrs: {}, content: skey.signature }
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
tag: 'key',
|
|
419
|
+
attrs: {},
|
|
420
|
+
content: [
|
|
421
|
+
{ tag: 'id', attrs: {}, content: keyIdBytes },
|
|
422
|
+
{ tag: 'value', attrs: {}, content: oneTime.keyPair.pubKey }
|
|
423
|
+
]
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
237
428
|
expectMessage(options = {}) {
|
|
238
429
|
const timeoutMs = options.timeoutMs ?? 5000;
|
|
239
430
|
return new Promise((resolve, reject) => {
|
|
@@ -359,7 +550,13 @@ class FakePeer {
|
|
|
359
550
|
}
|
|
360
551
|
nextId() {
|
|
361
552
|
this.nextMessageCounter += 1;
|
|
362
|
-
|
|
553
|
+
// WA-style uppercase hex, no '@': strict binary decoders tokenize an
|
|
554
|
+
// id containing '@' as a JID and reject the stanza. The registration
|
|
555
|
+
// id keeps ids from colliding across peers created in the same ms.
|
|
556
|
+
const peerPart = this.keyBundle.registrationId.toString(16).padStart(4, '0');
|
|
557
|
+
const timePart = Date.now().toString(16);
|
|
558
|
+
const counterPart = this.nextMessageCounter.toString(16).padStart(4, '0');
|
|
559
|
+
return `3EB0${peerPart}${timePart}${counterPart}`.toUpperCase();
|
|
363
560
|
}
|
|
364
561
|
}
|
|
365
562
|
exports.FakePeer = FakePeer;
|
|
@@ -395,6 +592,16 @@ function findTopLevelEnc(stanza, type) {
|
|
|
395
592
|
}
|
|
396
593
|
return null;
|
|
397
594
|
}
|
|
595
|
+
function encodeUint3(value) {
|
|
596
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffff) {
|
|
597
|
+
throw new Error(`encodeUint3: value out of range: ${value}`);
|
|
598
|
+
}
|
|
599
|
+
const out = new Uint8Array(3);
|
|
600
|
+
out[0] = (value >>> 16) & 0xff;
|
|
601
|
+
out[1] = (value >>> 8) & 0xff;
|
|
602
|
+
out[2] = value & 0xff;
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
398
605
|
function encodePlaintextWithPadding(message) {
|
|
399
606
|
const encoded = protos_1.proto.Message.encode(message).finish();
|
|
400
607
|
const padLen = 16 - (encoded.byteLength % 16);
|
|
@@ -405,4 +612,3 @@ function encodePlaintextWithPadding(message) {
|
|
|
405
612
|
}
|
|
406
613
|
return out;
|
|
407
614
|
}
|
|
408
|
-
//# sourceMappingURL=FakePeer.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Per-session isolated server state: registries, prekeys, app-state, IQ routing, capture. */
|
|
2
|
+
import type { BuildAbPropsResultInput } from '../protocol/iq/abprops';
|
|
3
|
+
import { type WaFakeIqMatcher, type WaFakeIqResponder, WaFakeIqRouter } from '../protocol/iq/router';
|
|
4
|
+
import type { ClientPreKeyBundle } from '../protocol/signal/prekey-upload';
|
|
5
|
+
import { type BinaryNode } from '../transport/codec';
|
|
6
|
+
import { AppStateSyncManager } from './AppStateSyncManager';
|
|
7
|
+
import { PreKeyDispenser } from './PreKeyDispenser';
|
|
8
|
+
import { ServerRegistries } from './ServerRegistries';
|
|
9
|
+
export interface ExpectIqOptions {
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ExpectStanzaOptions {
|
|
13
|
+
readonly timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface StanzaMatcher {
|
|
16
|
+
readonly tag?: string;
|
|
17
|
+
readonly type?: string;
|
|
18
|
+
readonly xmlns?: string;
|
|
19
|
+
readonly from?: string;
|
|
20
|
+
readonly to?: string;
|
|
21
|
+
readonly childTag?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared server-side dependencies a session needs but does not own. The media
|
|
25
|
+
* HTTPS server is process-global (one listener), so its address is injected
|
|
26
|
+
* rather than duplicated per session.
|
|
27
|
+
*/
|
|
28
|
+
export interface FakeServerSessionHost {
|
|
29
|
+
requireMediaHttpsInfo(): {
|
|
30
|
+
readonly host: string;
|
|
31
|
+
readonly port: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One isolated logical device's worth of server state. Every `WaClient`
|
|
36
|
+
* connected to a `FakeWaServer` is bound to exactly one session; peers,
|
|
37
|
+
* groups, privacy, prekeys, app-state, and captured stanzas never leak across
|
|
38
|
+
* sessions. A single-client server uses one default session, which is why the
|
|
39
|
+
* legacy `FakeWaServer` surface delegates straight through to it.
|
|
40
|
+
*/
|
|
41
|
+
export declare class FakeServerSession {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly registries: ServerRegistries;
|
|
44
|
+
readonly preKeyDispenser: PreKeyDispenser;
|
|
45
|
+
readonly appStateSync: AppStateSyncManager;
|
|
46
|
+
readonly iqRouter: WaFakeIqRouter;
|
|
47
|
+
private readonly capturedStanzas;
|
|
48
|
+
private readonly pendingIqExpectations;
|
|
49
|
+
private readonly pendingStanzaExpectations;
|
|
50
|
+
private readonly capturedStanzaListeners;
|
|
51
|
+
private readonly inboundStanzaListeners;
|
|
52
|
+
constructor(id: string, host: FakeServerSessionHost, options: {
|
|
53
|
+
readonly defaultIqHandlers: boolean;
|
|
54
|
+
});
|
|
55
|
+
registerIqHandler(matcher: WaFakeIqMatcher, respond: WaFakeIqResponder, label?: string): () => void;
|
|
56
|
+
routeIq(iq: BinaryNode): Promise<BinaryNode | null>;
|
|
57
|
+
handleCapturedStanza(node: BinaryNode): void;
|
|
58
|
+
subscribeInboundMessages(listener: (node: BinaryNode) => void): () => void;
|
|
59
|
+
onCapturedStanza(listener: (node: BinaryNode) => void): () => void;
|
|
60
|
+
capturedStanzaSnapshot(): readonly BinaryNode[];
|
|
61
|
+
expectIq(matcher: WaFakeIqMatcher, options?: ExpectIqOptions): Promise<BinaryNode>;
|
|
62
|
+
expectStanza(matcher: StanzaMatcher, options?: ExpectStanzaOptions): Promise<BinaryNode>;
|
|
63
|
+
private buildIqHandlerDeps;
|
|
64
|
+
}
|
|
65
|
+
export declare function matchesIq(iq: BinaryNode, matcher: WaFakeIqMatcher): boolean;
|
|
66
|
+
export declare function matchesStanza(node: BinaryNode, matcher: StanzaMatcher): boolean;
|
|
67
|
+
export type { ClientPreKeyBundle, BuildAbPropsResultInput };
|