@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.
- package/README.md +3 -3
- 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 +201 -1
- package/dist/api/FakeWaServer.d.ts +11 -1
- package/dist/api/FakeWaServer.js +34 -2
- 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 +0 -1
- package/dist/api/iq-handlers.js +0 -1
- package/dist/{cli.d.ts → bin.d.ts} +0 -1
- package/dist/{cli.js → bin.js} +1 -2
- package/dist/esm/api/AppStateSyncManager.js +146 -0
- package/dist/esm/api/FakePairingDriver.js +31 -0
- package/dist/esm/api/FakePeer.js +604 -0
- package/dist/esm/api/FakeWaServer.js +680 -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 +492 -0
- package/dist/esm/bin.js +246 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/infra/WaFakeConnection.js +49 -0
- package/dist/esm/infra/WaFakeConnectionPipeline.js +223 -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 +39 -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 +111 -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 +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/WaFakeConnection.d.ts +0 -1
- package/dist/infra/WaFakeConnection.js +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +0 -1
- package/dist/infra/WaFakeConnectionPipeline.js +27 -24
- 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 +0 -1
- package/dist/protocol/auth/cert-chain.js +0 -1
- 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 +0 -1
- package/dist/protocol/iq/router.js +0 -1
- 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 +15 -0
- package/package.json +21 -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/dist/esm/bin.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Standalone CLI for `@zapo-js/fake-server`. */
|
|
3
|
+
import { FakeWaServer } from './api/FakeWaServer.js';
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
let host = '127.0.0.1';
|
|
6
|
+
let port = 0;
|
|
7
|
+
let path = '/ws/chat';
|
|
8
|
+
const peerJids = [];
|
|
9
|
+
const groupSpecs = [];
|
|
10
|
+
let log = false;
|
|
11
|
+
let quiet = false;
|
|
12
|
+
let json = false;
|
|
13
|
+
let help = false;
|
|
14
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
15
|
+
const arg = argv[index];
|
|
16
|
+
const next = () => {
|
|
17
|
+
const value = argv[index + 1];
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
throw new Error(`missing value for ${arg}`);
|
|
20
|
+
}
|
|
21
|
+
index += 1;
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
switch (arg) {
|
|
25
|
+
case '-h':
|
|
26
|
+
case '--help':
|
|
27
|
+
help = true;
|
|
28
|
+
break;
|
|
29
|
+
case '--host':
|
|
30
|
+
host = next();
|
|
31
|
+
break;
|
|
32
|
+
case '--port':
|
|
33
|
+
port = Number.parseInt(next(), 10);
|
|
34
|
+
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
35
|
+
throw new Error(`invalid --port: ${argv[index]}`);
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
case '--path':
|
|
39
|
+
path = next();
|
|
40
|
+
break;
|
|
41
|
+
case '--peer':
|
|
42
|
+
peerJids.push(next());
|
|
43
|
+
break;
|
|
44
|
+
case '--group':
|
|
45
|
+
groupSpecs.push(next());
|
|
46
|
+
break;
|
|
47
|
+
case '--log':
|
|
48
|
+
log = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--quiet':
|
|
51
|
+
quiet = true;
|
|
52
|
+
break;
|
|
53
|
+
case '--json':
|
|
54
|
+
json = true;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { host, port, path, peerJids, groupSpecs, log, quiet, json, help };
|
|
61
|
+
}
|
|
62
|
+
function printHelp() {
|
|
63
|
+
process.stdout.write(`@zapo-js/fake-server – standalone fake WhatsApp Web server
|
|
64
|
+
|
|
65
|
+
USAGE
|
|
66
|
+
npx @zapo-js/fake-server [flags]
|
|
67
|
+
|
|
68
|
+
FLAGS
|
|
69
|
+
--host <host> Bind host (default: 127.0.0.1)
|
|
70
|
+
--port <port> WebSocket listener port (default: random)
|
|
71
|
+
--path <path> WebSocket upgrade path (default: /ws/chat)
|
|
72
|
+
--peer <jid> Pre-create a fake peer; can be repeated
|
|
73
|
+
--group <spec> Pre-create a fake group; spec format
|
|
74
|
+
<group-jid>=<peer-jid>,<peer-jid>,...
|
|
75
|
+
All peers must already be passed via --peer.
|
|
76
|
+
--log Print every captured inbound stanza
|
|
77
|
+
--quiet Suppress the startup info banner
|
|
78
|
+
--json Print connection info as JSON
|
|
79
|
+
-h | --help Show this help
|
|
80
|
+
|
|
81
|
+
The server stays up until SIGINT (Ctrl+C).
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
function parseGroupSpec(spec) {
|
|
85
|
+
const eqIdx = spec.indexOf('=');
|
|
86
|
+
if (eqIdx < 0) {
|
|
87
|
+
throw new Error(`invalid --group spec '${spec}': expected '<group-jid>=<peer-jid>,<peer-jid>,...'`);
|
|
88
|
+
}
|
|
89
|
+
const groupJid = spec.slice(0, eqIdx).trim();
|
|
90
|
+
const participantsRaw = spec.slice(eqIdx + 1);
|
|
91
|
+
const participantJids = participantsRaw
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((jid) => jid.trim())
|
|
94
|
+
.filter((jid) => jid.length > 0);
|
|
95
|
+
if (!groupJid || participantJids.length === 0) {
|
|
96
|
+
throw new Error(`invalid --group spec '${spec}': empty group jid or participant list`);
|
|
97
|
+
}
|
|
98
|
+
return { groupJid, participantJids };
|
|
99
|
+
}
|
|
100
|
+
async function main() {
|
|
101
|
+
let args;
|
|
102
|
+
try {
|
|
103
|
+
args = parseArgs(process.argv.slice(2));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
107
|
+
process.stderr.write('run with --help to see usage.\n');
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
if (args.help) {
|
|
111
|
+
printHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
const parsedGroups = args.groupSpecs.map(parseGroupSpec);
|
|
115
|
+
for (const group of parsedGroups) {
|
|
116
|
+
for (const jid of group.participantJids) {
|
|
117
|
+
if (!args.peerJids.includes(jid)) {
|
|
118
|
+
process.stderr.write(`error: --group '${group.groupJid}' references peer '${jid}' that was not declared via --peer\n`);
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const server = await FakeWaServer.start({
|
|
124
|
+
host: args.host,
|
|
125
|
+
port: args.port,
|
|
126
|
+
path: args.path
|
|
127
|
+
});
|
|
128
|
+
let setupComplete = false;
|
|
129
|
+
const setupPromise = new Promise((resolve, reject) => {
|
|
130
|
+
server.onAuthenticatedPipeline(async (pipeline) => {
|
|
131
|
+
if (setupComplete)
|
|
132
|
+
return;
|
|
133
|
+
setupComplete = true;
|
|
134
|
+
try {
|
|
135
|
+
const peers = new Map();
|
|
136
|
+
for (const jid of args.peerJids) {
|
|
137
|
+
const peer = await server.createFakePeer({ jid }, pipeline);
|
|
138
|
+
peers.set(jid, peer);
|
|
139
|
+
process.stdout.write(`[fake-server] created peer ${jid}\n`);
|
|
140
|
+
}
|
|
141
|
+
for (const group of parsedGroups) {
|
|
142
|
+
const participants = group.participantJids.map((jid) => {
|
|
143
|
+
const peer = peers.get(jid);
|
|
144
|
+
if (!peer) {
|
|
145
|
+
throw new Error(`internal: peer ${jid} not found for group ${group.groupJid}`);
|
|
146
|
+
}
|
|
147
|
+
return peer;
|
|
148
|
+
});
|
|
149
|
+
server.createFakeGroup({
|
|
150
|
+
groupJid: group.groupJid,
|
|
151
|
+
participants
|
|
152
|
+
});
|
|
153
|
+
process.stdout.write(`[fake-server] created group ${group.groupJid} with ${participants.length} participants\n`);
|
|
154
|
+
}
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
setupPromise.catch((error) => {
|
|
163
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
});
|
|
166
|
+
if (args.log) {
|
|
167
|
+
server.onPipeline((pipeline) => {
|
|
168
|
+
pipeline.setEvents({
|
|
169
|
+
onStanza: (node) => {
|
|
170
|
+
process.stdout.write(`[wire] ${node.tag}${node.attrs.id ? ` id=${node.attrs.id}` : ''}${node.attrs.type ? ` type=${node.attrs.type}` : ''}\n`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!args.quiet) {
|
|
176
|
+
const noiseRoot = server.noiseRootCa;
|
|
177
|
+
if (args.json) {
|
|
178
|
+
process.stdout.write(JSON.stringify({
|
|
179
|
+
url: server.url,
|
|
180
|
+
host: server.host,
|
|
181
|
+
port: server.port,
|
|
182
|
+
path: args.path,
|
|
183
|
+
noiseRootCa: {
|
|
184
|
+
serial: noiseRoot.serial,
|
|
185
|
+
publicKeyHex: bytesToHex(noiseRoot.publicKey)
|
|
186
|
+
},
|
|
187
|
+
peers: args.peerJids,
|
|
188
|
+
groups: parsedGroups
|
|
189
|
+
}, null, 2) + '\n');
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const banner = [
|
|
193
|
+
'',
|
|
194
|
+
'┌─────────────────────────────────────────────────────────────',
|
|
195
|
+
'│ @zapo-js/fake-server is up',
|
|
196
|
+
'├─────────────────────────────────────────────────────────────',
|
|
197
|
+
`│ url ${server.url}`,
|
|
198
|
+
`│ host ${server.host}`,
|
|
199
|
+
`│ port ${server.port}`,
|
|
200
|
+
`│ path ${args.path}`,
|
|
201
|
+
`│ noise root ca serial=${noiseRoot.serial} pub=${bytesToHex(noiseRoot.publicKey).slice(0, 32)}…`,
|
|
202
|
+
`│ peers ${args.peerJids.length === 0 ? '(none)' : args.peerJids.join(', ')}`,
|
|
203
|
+
`│ groups ${parsedGroups.length === 0 ? '(none)' : parsedGroups.map((g) => g.groupJid).join(', ')}`,
|
|
204
|
+
'├─────────────────────────────────────────────────────────────',
|
|
205
|
+
'│ Wire your WaClient with:',
|
|
206
|
+
'│ chatSocketUrls: [server.url]',
|
|
207
|
+
'│ testHooks: { noiseRootCa: <hex above as Uint8Array> }',
|
|
208
|
+
'│ Hit Ctrl+C to stop.',
|
|
209
|
+
'└─────────────────────────────────────────────────────────────',
|
|
210
|
+
''
|
|
211
|
+
];
|
|
212
|
+
process.stdout.write(banner.join('\n'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let stopping = false;
|
|
216
|
+
const shutdown = async (signal) => {
|
|
217
|
+
if (stopping)
|
|
218
|
+
return;
|
|
219
|
+
stopping = true;
|
|
220
|
+
process.stdout.write(`\n[fake-server] received ${signal}, shutting down…\n`);
|
|
221
|
+
try {
|
|
222
|
+
await server.stop();
|
|
223
|
+
process.stdout.write('[fake-server] stopped cleanly\n');
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
process.stderr.write(`error during shutdown: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
232
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
233
|
+
await new Promise(() => undefined);
|
|
234
|
+
}
|
|
235
|
+
function bytesToHex(bytes) {
|
|
236
|
+
let out = '';
|
|
237
|
+
for (let index = 0; index < bytes.byteLength; index += 1) {
|
|
238
|
+
const value = bytes[index];
|
|
239
|
+
out += value < 16 ? `0${value.toString(16)}` : value.toString(16);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
main().catch((error) => {
|
|
244
|
+
process.stderr.write(`fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { FakePairingDriver } from './api/FakePairingDriver.js';
|
|
2
|
+
export { FakePeer } from './api/FakePeer.js';
|
|
3
|
+
export { FakeWaServer } from './api/FakeWaServer.js';
|
|
4
|
+
export { IqExpectation, Scenario } from './api/Scenario.js';
|
|
5
|
+
export { WaFakeConnection } from './infra/WaFakeConnection.js';
|
|
6
|
+
export { buildAppStateSyncFullResult, buildAppStateSyncResult, buildServerSyncNotification, parseAppStateSyncRequest } from './protocol/iq/appstate-sync.js';
|
|
7
|
+
export { buildIqError, buildIqResult } from './protocol/iq/router.js';
|
|
8
|
+
export { buildCall, buildFailure } from './protocol/push/call-failure.js';
|
|
9
|
+
export { buildChatstate } from './protocol/push/chatstate.js';
|
|
10
|
+
export { buildIncomingErrorStanza } from './protocol/push/error-stanza.js';
|
|
11
|
+
export { buildAppStateSyncKeyShareMessage } from './protocol/push/app-state-key-share.js';
|
|
12
|
+
export { buildHistorySyncExternalMessage, buildHistorySyncMessage, encodeHistorySyncPlaintext } from './protocol/push/history-sync.js';
|
|
13
|
+
export { APP_STATE_EMPTY_LT_HASH, FakeAppStateCrypto } from './protocol/signal/fake-app-state-crypto.js';
|
|
14
|
+
export { FakeAppStateCollection } from './state/fake-app-state-collection.js';
|
|
15
|
+
export { FakeMediaStore } from './state/fake-media-store.js';
|
|
16
|
+
export { buildMessage } from './protocol/push/message.js';
|
|
17
|
+
export { buildGroupNotification, buildNotification } from './protocol/push/notification.js';
|
|
18
|
+
export { buildIncomingPresence } from './protocol/push/presence.js';
|
|
19
|
+
export { buildReceipt } from './protocol/push/receipt.js';
|
|
20
|
+
export { generateFakePeerIdentity } from './protocol/signal/fake-peer-identity.js';
|
|
21
|
+
export { FakePeerDoubleRatchet } from './protocol/signal/fake-peer-double-ratchet.js';
|
|
22
|
+
export { FakeSenderKey } from './protocol/signal/fake-sender-key.js';
|
|
23
|
+
export { buildAdvSignedDeviceIdentity, generateFakePrimaryDevice } from './protocol/auth/fake-primary-device.js';
|
|
24
|
+
export { buildPairDeviceIq, buildPairSuccessIq, parsePairingQrString } from './protocol/auth/pair-device.js';
|
|
25
|
+
export { parsePreKeyUploadIq, PreKeyUploadParseError } from './protocol/signal/prekey-upload.js';
|
|
26
|
+
export { buildStreamErrorAck, buildStreamErrorCode, buildStreamErrorDeviceRemoved, buildStreamErrorReplaced, buildStreamErrorXmlNotWellFormed } from './protocol/stream/stream-error.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class WaFakeConnection {
|
|
2
|
+
constructor(id, socket) {
|
|
3
|
+
this.handlers = {};
|
|
4
|
+
this.currentState = 'open';
|
|
5
|
+
this.id = id;
|
|
6
|
+
this.socket = socket;
|
|
7
|
+
this.bindSocketEvents();
|
|
8
|
+
}
|
|
9
|
+
get state() {
|
|
10
|
+
return this.currentState;
|
|
11
|
+
}
|
|
12
|
+
setHandlers(handlers) {
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
}
|
|
15
|
+
sendFrame(frame) {
|
|
16
|
+
if (this.currentState !== 'open') {
|
|
17
|
+
throw new Error(`cannot send frame on connection in state "${this.currentState}"`);
|
|
18
|
+
}
|
|
19
|
+
this.socket.send(frame);
|
|
20
|
+
}
|
|
21
|
+
close(code = 1000, reason = '') {
|
|
22
|
+
if (this.currentState === 'closed' || this.currentState === 'closing') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.currentState = 'closing';
|
|
26
|
+
this.socket.close(code, reason);
|
|
27
|
+
}
|
|
28
|
+
bindSocketEvents() {
|
|
29
|
+
this.socket.on('message', (data, isBinary) => {
|
|
30
|
+
if (!isBinary) {
|
|
31
|
+
this.handlers.onError?.(new Error('received unexpected text frame'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const frame = data instanceof Uint8Array
|
|
35
|
+
? data
|
|
36
|
+
: Array.isArray(data)
|
|
37
|
+
? new Uint8Array(Buffer.concat(data))
|
|
38
|
+
: new Uint8Array(data);
|
|
39
|
+
this.handlers.onFrame?.(frame);
|
|
40
|
+
});
|
|
41
|
+
this.socket.on('close', (code, reasonBuf) => {
|
|
42
|
+
this.currentState = 'closed';
|
|
43
|
+
this.handlers.onClose?.({ code, reason: reasonBuf.toString('utf8') });
|
|
44
|
+
});
|
|
45
|
+
this.socket.on('error', (error) => {
|
|
46
|
+
this.handlers.onError?.(error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/** Per-connection lifecycle pipeline (Noise handshake + stanza loop). */
|
|
2
|
+
import { buildFakeCertChain } from '../protocol/auth/cert-chain.js';
|
|
3
|
+
import { parseClientPayload } from '../protocol/auth/client-payload-validate.js';
|
|
4
|
+
import { buildSuccessNode } from '../protocol/auth/success-node.js';
|
|
5
|
+
import { decodeBinaryNodeStanza, encodeBinaryNodeStanza } from '../transport/codec.js';
|
|
6
|
+
import { X25519 } from '../transport/crypto.js';
|
|
7
|
+
import { proto } from '../transport/protos.js';
|
|
8
|
+
import { WaFakeFrameSocket } from './WaFakeFrameSocket.js';
|
|
9
|
+
import { WaFakeNoiseHandshake } from './WaFakeNoiseHandshake.js';
|
|
10
|
+
import { WaFakeTransport } from './WaFakeTransport.js';
|
|
11
|
+
const NOISE_XX_NAME = new TextEncoder().encode('Noise_XX_25519_AESGCM_SHA256\0\0\0\0');
|
|
12
|
+
const NOISE_IK_NAME = new TextEncoder().encode('Noise_IK_25519_AESGCM_SHA256\0\0\0\0');
|
|
13
|
+
const PROLOGUE = new Uint8Array([0x57, 0x41, 0x06, 0x03]);
|
|
14
|
+
export class WaFakeConnectionPipeline {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.events = {};
|
|
17
|
+
this.state = { kind: 'awaiting_prologue' };
|
|
18
|
+
this.chain = Promise.resolve();
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.frameSocket = new WaFakeFrameSocket(config.connection);
|
|
21
|
+
this.frameSocket.setHandlers({
|
|
22
|
+
onPrologue: () => this.onPrologue(),
|
|
23
|
+
onFrame: (frame) => this.scheduleFrame(frame),
|
|
24
|
+
onClose: (info) => {
|
|
25
|
+
this.state = { kind: 'closed' };
|
|
26
|
+
this.events.onClose?.(info);
|
|
27
|
+
},
|
|
28
|
+
onError: (error) => this.events.onError?.(error)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
setEvents(events) {
|
|
32
|
+
this.events = events;
|
|
33
|
+
}
|
|
34
|
+
isAuthenticated() {
|
|
35
|
+
return this.state.kind === 'authenticated';
|
|
36
|
+
}
|
|
37
|
+
async sendStanza(node) {
|
|
38
|
+
if (this.state.kind !== 'authenticated') {
|
|
39
|
+
throw new Error(`cannot send stanza while pipeline is in state "${this.state.kind}"`);
|
|
40
|
+
}
|
|
41
|
+
const stanza = encodeBinaryNodeStanza(node);
|
|
42
|
+
// Keep outbound encrypted frames in FIFO order across concurrent callers.
|
|
43
|
+
const ciphertext = await this.state.transport.encryptFrame(stanza);
|
|
44
|
+
this.frameSocket.sendFrame(ciphertext);
|
|
45
|
+
}
|
|
46
|
+
onPrologue() {
|
|
47
|
+
if (this.state.kind !== 'awaiting_prologue') {
|
|
48
|
+
this.events.onError?.(new Error('received second prologue from client'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.state = { kind: 'awaiting_client_hello' };
|
|
52
|
+
}
|
|
53
|
+
scheduleFrame(frame) {
|
|
54
|
+
this.chain = this.chain.then(() => this.handleFrame(frame));
|
|
55
|
+
}
|
|
56
|
+
async handleFrame(frame) {
|
|
57
|
+
try {
|
|
58
|
+
switch (this.state.kind) {
|
|
59
|
+
case 'awaiting_client_hello':
|
|
60
|
+
await this.handleClientHello(frame);
|
|
61
|
+
return;
|
|
62
|
+
case 'awaiting_client_finish':
|
|
63
|
+
await this.handleClientFinish(frame, this.state.handshake, this.state.serverEphemeralKeyPair);
|
|
64
|
+
return;
|
|
65
|
+
case 'authenticated':
|
|
66
|
+
await this.handleAuthenticatedFrame(frame, this.state.transport);
|
|
67
|
+
return;
|
|
68
|
+
default:
|
|
69
|
+
this.events.onError?.(new Error(`unexpected frame in state "${this.state.kind}"`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.fail(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async handleClientHello(frame) {
|
|
77
|
+
const parsed = proto.HandshakeMessage.decode(frame);
|
|
78
|
+
const clientHello = parsed.clientHello;
|
|
79
|
+
if (!clientHello?.ephemeral) {
|
|
80
|
+
throw new Error('ClientHello missing ephemeral');
|
|
81
|
+
}
|
|
82
|
+
// IK includes encrypted static+payload in ClientHello; XX does not.
|
|
83
|
+
if (clientHello.static && clientHello.payload) {
|
|
84
|
+
await this.handleIkClientHello(clientHello);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await this.handleXxClientHello(clientHello.ephemeral);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async handleXxClientHello(clientEphemeralPub) {
|
|
91
|
+
const handshake = new WaFakeNoiseHandshake();
|
|
92
|
+
handshake.start(NOISE_XX_NAME, PROLOGUE);
|
|
93
|
+
handshake.authenticate(clientEphemeralPub);
|
|
94
|
+
const serverEphemeral = await X25519.generateKeyPair();
|
|
95
|
+
handshake.authenticate(serverEphemeral.pubKey);
|
|
96
|
+
handshake.mixIntoKey(await X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub));
|
|
97
|
+
const encryptedServerStatic = handshake.encrypt(this.config.serverStaticKeyPair.pubKey);
|
|
98
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
|
|
99
|
+
const certChain = await buildFakeCertChain({
|
|
100
|
+
root: this.config.rootCa,
|
|
101
|
+
leafKey: this.config.serverStaticKeyPair.pubKey
|
|
102
|
+
});
|
|
103
|
+
const encryptedCertPayload = handshake.encrypt(certChain.encoded);
|
|
104
|
+
const serverHello = proto.HandshakeMessage.encode({
|
|
105
|
+
serverHello: {
|
|
106
|
+
ephemeral: serverEphemeral.pubKey,
|
|
107
|
+
static: encryptedServerStatic,
|
|
108
|
+
payload: encryptedCertPayload
|
|
109
|
+
}
|
|
110
|
+
}).finish();
|
|
111
|
+
this.frameSocket.sendFrame(serverHello);
|
|
112
|
+
this.state = {
|
|
113
|
+
kind: 'awaiting_client_finish',
|
|
114
|
+
handshake,
|
|
115
|
+
serverEphemeralKeyPair: serverEphemeral,
|
|
116
|
+
clientEphemeralPub
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Server-side IK path used when the client already cached our static key. */
|
|
120
|
+
async handleIkClientHello(clientHello) {
|
|
121
|
+
if (!clientHello.ephemeral || !clientHello.static || !clientHello.payload) {
|
|
122
|
+
throw new Error('IK ClientHello missing ephemeral/static/payload');
|
|
123
|
+
}
|
|
124
|
+
const clientEphemeralPub = clientHello.ephemeral;
|
|
125
|
+
const encryptedClientStatic = clientHello.static;
|
|
126
|
+
const encryptedClientPayload = clientHello.payload;
|
|
127
|
+
const handshake = new WaFakeNoiseHandshake();
|
|
128
|
+
handshake.start(NOISE_IK_NAME, PROLOGUE);
|
|
129
|
+
handshake.authenticate(this.config.serverStaticKeyPair.pubKey);
|
|
130
|
+
handshake.authenticate(clientEphemeralPub);
|
|
131
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
|
|
132
|
+
const clientStaticKey = handshake.decrypt(encryptedClientStatic);
|
|
133
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientStaticKey));
|
|
134
|
+
const clientPayloadBytes = handshake.decrypt(encryptedClientPayload);
|
|
135
|
+
const clientPayload = parseClientPayload(clientPayloadBytes);
|
|
136
|
+
const serverEphemeral = await X25519.generateKeyPair();
|
|
137
|
+
handshake.authenticate(serverEphemeral.pubKey);
|
|
138
|
+
const [dhEphem, dhStatic] = await Promise.all([
|
|
139
|
+
X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub),
|
|
140
|
+
X25519.scalarMult(serverEphemeral.privKey, clientStaticKey)
|
|
141
|
+
]);
|
|
142
|
+
handshake.mixIntoKey(dhEphem);
|
|
143
|
+
handshake.mixIntoKey(dhStatic);
|
|
144
|
+
const certChain = await buildFakeCertChain({
|
|
145
|
+
root: this.config.rootCa,
|
|
146
|
+
leafKey: this.config.serverStaticKeyPair.pubKey
|
|
147
|
+
});
|
|
148
|
+
const encryptedCertPayload = handshake.encrypt(certChain.encoded);
|
|
149
|
+
const serverHello = proto.HandshakeMessage.encode({
|
|
150
|
+
serverHello: {
|
|
151
|
+
ephemeral: serverEphemeral.pubKey,
|
|
152
|
+
payload: encryptedCertPayload
|
|
153
|
+
}
|
|
154
|
+
}).finish();
|
|
155
|
+
this.frameSocket.sendFrame(serverHello);
|
|
156
|
+
const keys = handshake.finish();
|
|
157
|
+
const transport = new WaFakeTransport({
|
|
158
|
+
recvKey: keys.recvKey,
|
|
159
|
+
sendKey: keys.sendKey
|
|
160
|
+
});
|
|
161
|
+
this.state = { kind: 'authenticated', transport };
|
|
162
|
+
await this.sendStanza(buildSuccessNode(this.config.successNodeAttributes));
|
|
163
|
+
this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
|
|
164
|
+
}
|
|
165
|
+
async handleClientFinish(frame, handshake, serverEphemeralKeyPair) {
|
|
166
|
+
const parsed = proto.HandshakeMessage.decode(frame);
|
|
167
|
+
const clientFinish = parsed.clientFinish;
|
|
168
|
+
if (!clientFinish?.static || !clientFinish.payload) {
|
|
169
|
+
throw new Error('ClientFinish missing static/payload');
|
|
170
|
+
}
|
|
171
|
+
const clientStaticKey = handshake.decrypt(clientFinish.static);
|
|
172
|
+
handshake.mixIntoKey(await X25519.scalarMult(serverEphemeralKeyPair.privKey, clientStaticKey));
|
|
173
|
+
const clientPayloadBytes = handshake.decrypt(clientFinish.payload);
|
|
174
|
+
const clientPayload = parseClientPayload(clientPayloadBytes);
|
|
175
|
+
const keys = handshake.finish();
|
|
176
|
+
const transport = new WaFakeTransport({
|
|
177
|
+
recvKey: keys.recvKey,
|
|
178
|
+
sendKey: keys.sendKey
|
|
179
|
+
});
|
|
180
|
+
this.state = { kind: 'authenticated', transport };
|
|
181
|
+
await this.sendStanza(buildSuccessNode(this.config.successNodeAttributes));
|
|
182
|
+
this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
|
|
183
|
+
}
|
|
184
|
+
async handleAuthenticatedFrame(frame, transport) {
|
|
185
|
+
const stanzaBytes = await transport.decryptFrame(frame);
|
|
186
|
+
const node = await decodeBinaryNodeStanza(stanzaBytes);
|
|
187
|
+
this.events.onStanza?.(node);
|
|
188
|
+
if (node.tag === 'iq') {
|
|
189
|
+
const response = await this.config.iqRouter.route(node);
|
|
190
|
+
if (response !== null) {
|
|
191
|
+
await this.sendStanza(response);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
this.events.onUnhandledStanza?.(node);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Mirror the minimal ack shape that resolves the lib pending publish.
|
|
199
|
+
if (node.tag === 'message' && node.attrs.id) {
|
|
200
|
+
const messageType = node.attrs.type ?? 'text';
|
|
201
|
+
const from = node.attrs.to ?? 's.whatsapp.net';
|
|
202
|
+
const ackAttrs = {
|
|
203
|
+
id: node.attrs.id,
|
|
204
|
+
class: 'receipt',
|
|
205
|
+
type: messageType,
|
|
206
|
+
from
|
|
207
|
+
};
|
|
208
|
+
if (node.attrs.participant) {
|
|
209
|
+
ackAttrs.participant = node.attrs.participant;
|
|
210
|
+
}
|
|
211
|
+
await this.sendStanza({
|
|
212
|
+
tag: 'ack',
|
|
213
|
+
attrs: ackAttrs
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
fail(error) {
|
|
218
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
219
|
+
this.events.onError?.(err);
|
|
220
|
+
this.state = { kind: 'closed' };
|
|
221
|
+
this.config.connection.close(1011, err.message.slice(0, 120));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** WhatsApp frame codec (prologue + 3-byte length-prefixed frames). */
|
|
2
|
+
const PROLOGUE_W = 0x57;
|
|
3
|
+
const PROLOGUE_A = 0x41;
|
|
4
|
+
const ROUTING_MAGIC = 0xed;
|
|
5
|
+
const VERSION_HEADER_LENGTH = 4;
|
|
6
|
+
const FRAME_LENGTH_PREFIX_BYTES = 3;
|
|
7
|
+
const MAX_FRAME_LENGTH = 1 << 24;
|
|
8
|
+
export class WaFakeFrameSocket {
|
|
9
|
+
constructor(connection) {
|
|
10
|
+
this.handlers = {};
|
|
11
|
+
this.inboundBuffer = new Uint8Array(0);
|
|
12
|
+
this.prologueParsed = false;
|
|
13
|
+
this.connection = connection;
|
|
14
|
+
this.connection.setHandlers({
|
|
15
|
+
onFrame: (bytes) => this.handleInboundBytes(bytes),
|
|
16
|
+
onClose: (info) => this.handlers.onClose?.(info),
|
|
17
|
+
onError: (error) => this.handlers.onError?.(error)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
setHandlers(handlers) {
|
|
21
|
+
this.handlers = handlers;
|
|
22
|
+
}
|
|
23
|
+
sendFrame(body) {
|
|
24
|
+
if (body.byteLength >= MAX_FRAME_LENGTH) {
|
|
25
|
+
throw new Error(`frame too large: ${body.byteLength} bytes`);
|
|
26
|
+
}
|
|
27
|
+
const out = new Uint8Array(FRAME_LENGTH_PREFIX_BYTES + body.byteLength);
|
|
28
|
+
out[0] = (body.byteLength >> 16) & 0xff;
|
|
29
|
+
out[1] = (body.byteLength >> 8) & 0xff;
|
|
30
|
+
out[2] = body.byteLength & 0xff;
|
|
31
|
+
out.set(body, FRAME_LENGTH_PREFIX_BYTES);
|
|
32
|
+
this.connection.sendFrame(out);
|
|
33
|
+
}
|
|
34
|
+
handleInboundBytes(chunk) {
|
|
35
|
+
try {
|
|
36
|
+
this.inboundBuffer = concatBytes(this.inboundBuffer, chunk);
|
|
37
|
+
if (!this.prologueParsed) {
|
|
38
|
+
if (!this.tryParsePrologue()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.drainFrames();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
tryParsePrologue() {
|
|
49
|
+
const buffer = this.inboundBuffer;
|
|
50
|
+
let offset = 0;
|
|
51
|
+
let routingInfo = null;
|
|
52
|
+
if (buffer.byteLength === 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (buffer[0] === ROUTING_MAGIC) {
|
|
56
|
+
if (buffer.byteLength < 4) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const routingLen = (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
|
|
60
|
+
if (buffer.byteLength < 4 + routingLen + VERSION_HEADER_LENGTH) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
routingInfo = buffer.slice(4, 4 + routingLen);
|
|
64
|
+
offset = 4 + routingLen;
|
|
65
|
+
}
|
|
66
|
+
else if (buffer.byteLength < VERSION_HEADER_LENGTH) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const versionStart = offset;
|
|
70
|
+
const w = buffer[versionStart];
|
|
71
|
+
const a = buffer[versionStart + 1];
|
|
72
|
+
if (w !== PROLOGUE_W || a !== PROLOGUE_A) {
|
|
73
|
+
throw new Error(`invalid prologue magic: expected "WA", got ${hex(w)} ${hex(a)}`);
|
|
74
|
+
}
|
|
75
|
+
const protocolVersion = buffer[versionStart + 2];
|
|
76
|
+
const dictVersion = buffer[versionStart + 3];
|
|
77
|
+
offset = versionStart + VERSION_HEADER_LENGTH;
|
|
78
|
+
this.inboundBuffer = buffer.slice(offset);
|
|
79
|
+
this.prologueParsed = true;
|
|
80
|
+
this.handlers.onPrologue?.({ protocolVersion, dictVersion, routingInfo });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
drainFrames() {
|
|
84
|
+
while (true) {
|
|
85
|
+
const buffer = this.inboundBuffer;
|
|
86
|
+
if (buffer.byteLength < FRAME_LENGTH_PREFIX_BYTES) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const length = (buffer[0] << 16) | (buffer[1] << 8) | buffer[2];
|
|
90
|
+
if (length >= MAX_FRAME_LENGTH) {
|
|
91
|
+
throw new Error(`frame too large: ${length} bytes`);
|
|
92
|
+
}
|
|
93
|
+
const total = FRAME_LENGTH_PREFIX_BYTES + length;
|
|
94
|
+
if (buffer.byteLength < total) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const body = buffer.slice(FRAME_LENGTH_PREFIX_BYTES, total);
|
|
98
|
+
this.inboundBuffer = buffer.slice(total);
|
|
99
|
+
this.handlers.onFrame?.(body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function concatBytes(a, b) {
|
|
104
|
+
if (a.byteLength === 0)
|
|
105
|
+
return b;
|
|
106
|
+
if (b.byteLength === 0)
|
|
107
|
+
return a;
|
|
108
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
109
|
+
out.set(a, 0);
|
|
110
|
+
out.set(b, a.byteLength);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function hex(byte) {
|
|
114
|
+
if (byte === undefined)
|
|
115
|
+
return 'undefined';
|
|
116
|
+
return `0x${byte.toString(16).padStart(2, '0')}`;
|
|
117
|
+
}
|