@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
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { Agent as HttpsAgent } from 'node:https';
|
|
2
|
+
import { WaFakeConnectionPipeline } from '../infra/WaFakeConnectionPipeline.js';
|
|
3
|
+
import { WaFakeMediaHttpsServer } from '../infra/WaFakeMediaHttpsServer.js';
|
|
4
|
+
import { WaFakeWsServer } from '../infra/WaFakeWsServer.js';
|
|
5
|
+
import { generateFakeNoiseRootCa } from '../protocol/auth/cert-chain.js';
|
|
6
|
+
import { FakeMediaStore } from '../state/fake-media-store.js';
|
|
7
|
+
import { X25519 } from '../transport/crypto.js';
|
|
8
|
+
import { FakePairingDriver } from './FakePairingDriver.js';
|
|
9
|
+
import { FakePeer } from './FakePeer.js';
|
|
10
|
+
import { FakeServerSession } from './FakeServerSession.js';
|
|
11
|
+
import { Scenario } from './Scenario.js';
|
|
12
|
+
export { FakeServerSession } from './FakeServerSession.js';
|
|
13
|
+
export class FakeWaServer {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.pipelines = new Set();
|
|
16
|
+
this.pipelineSessions = new WeakMap();
|
|
17
|
+
this.authenticatedListeners = new Set();
|
|
18
|
+
this.sessions = new Map();
|
|
19
|
+
this.globalIqHandlers = new Set();
|
|
20
|
+
this.rootCa = null;
|
|
21
|
+
this.serverStaticKeyPair = null;
|
|
22
|
+
this.listenInfo = null;
|
|
23
|
+
this.pipelineListeners = new Set();
|
|
24
|
+
this.rejectMode = null;
|
|
25
|
+
this.mediaStore = new FakeMediaStore();
|
|
26
|
+
this.mediaHttpsServer = new WaFakeMediaHttpsServer();
|
|
27
|
+
this.capturedMediaUploads = [];
|
|
28
|
+
this.nextUploadCounter = 0;
|
|
29
|
+
this.cachedMediaProxyAgent = null;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.wsServer = new WaFakeWsServer(options);
|
|
32
|
+
this.wsServer.onConnection((connection) => this.handleConnection(connection));
|
|
33
|
+
this.defaultSession = this.createSession('__default__');
|
|
34
|
+
}
|
|
35
|
+
/** State of the single default session (the only session for one client). */
|
|
36
|
+
get registries() {
|
|
37
|
+
return this.defaultSession.registries;
|
|
38
|
+
}
|
|
39
|
+
get preKeyDispenser() {
|
|
40
|
+
return this.defaultSession.preKeyDispenser;
|
|
41
|
+
}
|
|
42
|
+
get appStateSync() {
|
|
43
|
+
return this.defaultSession.appStateSync;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the isolated session for `id`, creating it on first use. Every
|
|
47
|
+
* session gets its own registries, prekey dispenser, app-state, IQ router,
|
|
48
|
+
* and stanza capture. Use with the `sessionKey` option to run several
|
|
49
|
+
* `WaClient`s against one server without them sharing any state.
|
|
50
|
+
*/
|
|
51
|
+
session(id) {
|
|
52
|
+
const existing = this.sessions.get(id);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
return this.createSession(id);
|
|
57
|
+
}
|
|
58
|
+
/** The session a connection is bound to (default before authentication). */
|
|
59
|
+
sessionFor(pipeline) {
|
|
60
|
+
return this.pipelineSessions.get(pipeline) ?? this.defaultSession;
|
|
61
|
+
}
|
|
62
|
+
createSession(id) {
|
|
63
|
+
const session = new FakeServerSession(id, { requireMediaHttpsInfo: () => this.requireMediaHttpsInfo() }, { defaultIqHandlers: this.options.defaultIqHandlers !== false });
|
|
64
|
+
this.sessions.set(id, session);
|
|
65
|
+
// Server-level custom handlers apply to every session, present and future.
|
|
66
|
+
for (const handler of this.globalIqHandlers) {
|
|
67
|
+
handler.unregisterBySession.set(session, session.registerIqHandler(handler.matcher, handler.respond, handler.label));
|
|
68
|
+
}
|
|
69
|
+
return session;
|
|
70
|
+
}
|
|
71
|
+
registerIqHandler(matcher, respond, label) {
|
|
72
|
+
const entry = {
|
|
73
|
+
matcher,
|
|
74
|
+
respond,
|
|
75
|
+
label,
|
|
76
|
+
unregisterBySession: new Map()
|
|
77
|
+
};
|
|
78
|
+
for (const session of this.sessions.values()) {
|
|
79
|
+
entry.unregisterBySession.set(session, session.registerIqHandler(matcher, respond, label));
|
|
80
|
+
}
|
|
81
|
+
this.globalIqHandlers.add(entry);
|
|
82
|
+
return () => {
|
|
83
|
+
for (const unregister of entry.unregisterBySession.values()) {
|
|
84
|
+
unregister();
|
|
85
|
+
}
|
|
86
|
+
entry.unregisterBySession.clear();
|
|
87
|
+
this.globalIqHandlers.delete(entry);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async routeIqForTest(iq) {
|
|
91
|
+
return this.defaultSession.routeIq(iq);
|
|
92
|
+
}
|
|
93
|
+
onOutboundGroupOp(listener) {
|
|
94
|
+
return this.registries.onOutboundGroupOp(listener);
|
|
95
|
+
}
|
|
96
|
+
onOutboundPrivacySet(listener) {
|
|
97
|
+
return this.registries.onOutboundPrivacySet(listener);
|
|
98
|
+
}
|
|
99
|
+
onOutboundBlocklistChange(listener) {
|
|
100
|
+
return this.registries.onOutboundBlocklistChange(listener);
|
|
101
|
+
}
|
|
102
|
+
onOutboundProfilePictureSet(listener) {
|
|
103
|
+
return this.registries.onOutboundProfilePictureSet(listener);
|
|
104
|
+
}
|
|
105
|
+
onOutboundStatusSet(listener) {
|
|
106
|
+
return this.registries.onOutboundStatusSet(listener);
|
|
107
|
+
}
|
|
108
|
+
onLogout(listener) {
|
|
109
|
+
return this.registries.onLogout(listener);
|
|
110
|
+
}
|
|
111
|
+
onOutboundPrivacyTokenIssue(listener) {
|
|
112
|
+
return this.registries.onOutboundPrivacyTokenIssue(listener);
|
|
113
|
+
}
|
|
114
|
+
onOutboundDirtyBitsClear(listener) {
|
|
115
|
+
return this.registries.onOutboundDirtyBitsClear(listener);
|
|
116
|
+
}
|
|
117
|
+
privacyTokensIssuedSnapshot() {
|
|
118
|
+
return this.registries.privacyTokensIssuedSnapshot();
|
|
119
|
+
}
|
|
120
|
+
privacySettingsSnapshot() {
|
|
121
|
+
return this.registries.privacySettingsSnapshot();
|
|
122
|
+
}
|
|
123
|
+
blocklistSnapshot() {
|
|
124
|
+
return this.registries.blocklistSnapshot();
|
|
125
|
+
}
|
|
126
|
+
latestStatusSnapshot() {
|
|
127
|
+
return this.registries.latestStatusSnapshot();
|
|
128
|
+
}
|
|
129
|
+
groupRegistrySnapshot() {
|
|
130
|
+
return this.registries.groupRegistrySnapshot();
|
|
131
|
+
}
|
|
132
|
+
setAbProps(input) {
|
|
133
|
+
this.registries.setAbProps(input);
|
|
134
|
+
}
|
|
135
|
+
setPrivacyDisallowedList(category, jids) {
|
|
136
|
+
this.registries.setPrivacyDisallowedList(category, jids);
|
|
137
|
+
}
|
|
138
|
+
setProfilePictureRecord(jid, picture) {
|
|
139
|
+
this.registries.setProfilePictureRecord(jid, picture);
|
|
140
|
+
}
|
|
141
|
+
setBusinessProfileRecord(jid, profile) {
|
|
142
|
+
this.registries.setBusinessProfileRecord(jid, profile);
|
|
143
|
+
}
|
|
144
|
+
createFakeGroup(input) {
|
|
145
|
+
return this.registries.createFakeGroup(input);
|
|
146
|
+
}
|
|
147
|
+
async triggerPreKeyUpload(pipeline, options = {}) {
|
|
148
|
+
return this.sessionFor(pipeline).preKeyDispenser.triggerPreKeyUpload(pipeline, options);
|
|
149
|
+
}
|
|
150
|
+
awaitPreKeyBundle(timeoutMs = 15000) {
|
|
151
|
+
return this.preKeyDispenser.awaitPreKeyBundle(timeoutMs);
|
|
152
|
+
}
|
|
153
|
+
capturedPreKeyBundleSnapshot() {
|
|
154
|
+
return this.preKeyDispenser.capturedPreKeyBundleSnapshot();
|
|
155
|
+
}
|
|
156
|
+
dispenseOneTimePreKey() {
|
|
157
|
+
return this.preKeyDispenser.dispenseOneTimePreKey();
|
|
158
|
+
}
|
|
159
|
+
preKeyDispenserMissesSnapshot() {
|
|
160
|
+
return this.preKeyDispenser.preKeyDispenserMissesSnapshot();
|
|
161
|
+
}
|
|
162
|
+
preKeysAvailable() {
|
|
163
|
+
return this.preKeyDispenser.preKeysAvailable();
|
|
164
|
+
}
|
|
165
|
+
registerAppStateSyncKey(keyId, keyData) {
|
|
166
|
+
this.appStateSync.registerAppStateSyncKey(keyId, keyData);
|
|
167
|
+
}
|
|
168
|
+
onOutboundAppStateMutation(listener) {
|
|
169
|
+
return this.appStateSync.onOutboundAppStateMutation(listener);
|
|
170
|
+
}
|
|
171
|
+
expectAppStateMutation(predicate, timeoutMs = 15000) {
|
|
172
|
+
return this.appStateSync.expectAppStateMutation(predicate, timeoutMs);
|
|
173
|
+
}
|
|
174
|
+
provideAppStateCollection(name, provider) {
|
|
175
|
+
return this.appStateSync.provideAppStateCollection(name, provider);
|
|
176
|
+
}
|
|
177
|
+
async pushServerSyncNotification(pipeline, input) {
|
|
178
|
+
return this.sessionFor(pipeline).appStateSync.pushServerSyncNotification(pipeline, input);
|
|
179
|
+
}
|
|
180
|
+
onAuthenticatedPipeline(listener) {
|
|
181
|
+
this.authenticatedListeners.add(listener);
|
|
182
|
+
return () => this.authenticatedListeners.delete(listener);
|
|
183
|
+
}
|
|
184
|
+
scenario(configure) {
|
|
185
|
+
configure(new Scenario(this));
|
|
186
|
+
}
|
|
187
|
+
expectIq(matcher, options = {}) {
|
|
188
|
+
return this.defaultSession.expectIq(matcher, options);
|
|
189
|
+
}
|
|
190
|
+
capturedStanzaSnapshot() {
|
|
191
|
+
return this.defaultSession.capturedStanzaSnapshot();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Subscribes to every stanza captured from the client side (the lib) of
|
|
195
|
+
* the default session. The listener is called synchronously for each new
|
|
196
|
+
* stanza as it arrives. Returns an unsubscribe function. Useful for
|
|
197
|
+
* benches that need to count or react to a stream of receipts/messages
|
|
198
|
+
* without paying the O(N²) cost of polling `capturedStanzaSnapshot()` or
|
|
199
|
+
* queuing one `expectStanza(...)` per iteration.
|
|
200
|
+
*/
|
|
201
|
+
onCapturedStanza(listener) {
|
|
202
|
+
return this.defaultSession.onCapturedStanza(listener);
|
|
203
|
+
}
|
|
204
|
+
expectStanza(matcher, options = {}) {
|
|
205
|
+
return this.defaultSession.expectStanza(matcher, options);
|
|
206
|
+
}
|
|
207
|
+
async broadcastStanza(node) {
|
|
208
|
+
const tasks = [];
|
|
209
|
+
for (const pipeline of this.pipelines) {
|
|
210
|
+
tasks.push(pipeline.sendStanza(node).catch(() => undefined));
|
|
211
|
+
}
|
|
212
|
+
await Promise.all(tasks);
|
|
213
|
+
return tasks.length;
|
|
214
|
+
}
|
|
215
|
+
waitForAuthenticatedPipeline(timeoutMs = 60000) {
|
|
216
|
+
for (const pipeline of this.pipelines) {
|
|
217
|
+
if (pipeline.isAuthenticated()) {
|
|
218
|
+
return Promise.resolve(pipeline);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return this.waitForNextAuthenticatedPipeline(timeoutMs);
|
|
222
|
+
}
|
|
223
|
+
waitForNextAuthenticatedPipeline(timeoutMs = 60000) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const timer = setTimeout(() => reject(new Error(`waitForNextAuthenticatedPipeline timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
226
|
+
const unregister = this.onAuthenticatedPipeline((pipeline) => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
unregister();
|
|
229
|
+
resolve(pipeline);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async publishMediaBlob(input) {
|
|
234
|
+
return this.mediaStore.publish(input);
|
|
235
|
+
}
|
|
236
|
+
mediaUrl(path) {
|
|
237
|
+
const info = this.requireMediaHttpsInfo();
|
|
238
|
+
const normalized = path.startsWith('/') ? path : `/${path}`;
|
|
239
|
+
return `https://${info.host}:${info.port}${normalized}`;
|
|
240
|
+
}
|
|
241
|
+
requireMediaHttpsInfo() {
|
|
242
|
+
const info = this.mediaHttpsServer.info;
|
|
243
|
+
if (!info) {
|
|
244
|
+
throw new Error('fake media https server is not listening');
|
|
245
|
+
}
|
|
246
|
+
return info;
|
|
247
|
+
}
|
|
248
|
+
get mediaProxyAgent() {
|
|
249
|
+
if (!this.cachedMediaProxyAgent) {
|
|
250
|
+
// keepAlive is required for representative perf measurements
|
|
251
|
+
// and matches what real-world WhatsApp clients do – every
|
|
252
|
+
// media upload/download would otherwise pay a full TLS
|
|
253
|
+
// handshake. The `src/media/__tests__/media.test.ts` fixtures
|
|
254
|
+
// and a production-quality `proxy.mediaUpload` configuration
|
|
255
|
+
// all set this; mirroring it here keeps benches in line.
|
|
256
|
+
this.cachedMediaProxyAgent = new HttpsAgent({
|
|
257
|
+
rejectUnauthorized: false,
|
|
258
|
+
keepAlive: true
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return this.cachedMediaProxyAgent;
|
|
262
|
+
}
|
|
263
|
+
capturedMediaUploadSnapshot() {
|
|
264
|
+
return this.capturedMediaUploads.slice();
|
|
265
|
+
}
|
|
266
|
+
async createFakePeer(options, pipeline) {
|
|
267
|
+
const session = this.sessionFor(pipeline);
|
|
268
|
+
const peer = await FakePeer.create(options, this.buildFakePeerDeps(pipeline, session));
|
|
269
|
+
session.registries.registerPeer(peer);
|
|
270
|
+
return peer;
|
|
271
|
+
}
|
|
272
|
+
async createFakePeerWithDevices(input, pipeline) {
|
|
273
|
+
if (input.deviceIds.length === 0) {
|
|
274
|
+
throw new Error('createFakePeerWithDevices requires at least one deviceId');
|
|
275
|
+
}
|
|
276
|
+
const atIdx = input.userJid.indexOf('@');
|
|
277
|
+
if (atIdx < 0) {
|
|
278
|
+
throw new Error(`invalid userJid ${input.userJid}`);
|
|
279
|
+
}
|
|
280
|
+
const userPart = input.userJid.slice(0, atIdx);
|
|
281
|
+
const server = input.userJid.slice(atIdx + 1);
|
|
282
|
+
const session = this.sessionFor(pipeline);
|
|
283
|
+
const peers = [];
|
|
284
|
+
for (const deviceId of input.deviceIds) {
|
|
285
|
+
const deviceJid = deviceId === 0 ? input.userJid : `${userPart}:${deviceId}@${server}`;
|
|
286
|
+
const peer = await FakePeer.create({
|
|
287
|
+
jid: deviceJid,
|
|
288
|
+
displayName: input.displayName,
|
|
289
|
+
skipOneTimePreKey: input.skipOneTimePreKey
|
|
290
|
+
}, this.buildFakePeerDeps(pipeline, session));
|
|
291
|
+
session.registries.registerPeer(peer);
|
|
292
|
+
peers.push(peer);
|
|
293
|
+
}
|
|
294
|
+
return peers;
|
|
295
|
+
}
|
|
296
|
+
buildFakePeerDeps(pipeline, session) {
|
|
297
|
+
return {
|
|
298
|
+
bundleResolver: () => session.preKeyDispenser.awaitPreKeyBundle(),
|
|
299
|
+
reserveOneTimePreKey: () => session.preKeyDispenser.dispenseOneTimePreKey(),
|
|
300
|
+
pushStanza: (stanza) => pipeline.sendStanza(stanza),
|
|
301
|
+
subscribeInboundMessages: (listener) => session.subscribeInboundMessages(listener)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async runPairing(pipeline, options, companionMaterialResolver) {
|
|
305
|
+
const session = this.sessionFor(pipeline);
|
|
306
|
+
const driver = new FakePairingDriver(options, {
|
|
307
|
+
pipeline,
|
|
308
|
+
companionMaterialResolver,
|
|
309
|
+
waitForPairDeviceAck: async (pairDeviceIqId) => {
|
|
310
|
+
await session.expectIq({
|
|
311
|
+
id: pairDeviceIqId
|
|
312
|
+
}, {
|
|
313
|
+
timeoutMs: 15000
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
await driver.run();
|
|
318
|
+
}
|
|
319
|
+
setRejectMode(info) {
|
|
320
|
+
if (info === null) {
|
|
321
|
+
this.rejectMode = null;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.rejectMode = {
|
|
325
|
+
code: info.code ?? 1011,
|
|
326
|
+
reason: info.reason ?? 'fake-server reject mode'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
static async start(options = {}) {
|
|
330
|
+
const server = new FakeWaServer(options);
|
|
331
|
+
await server.listen();
|
|
332
|
+
return server;
|
|
333
|
+
}
|
|
334
|
+
get url() {
|
|
335
|
+
return this.requireListening().url;
|
|
336
|
+
}
|
|
337
|
+
get host() {
|
|
338
|
+
return this.requireListening().host;
|
|
339
|
+
}
|
|
340
|
+
get port() {
|
|
341
|
+
return this.requireListening().port;
|
|
342
|
+
}
|
|
343
|
+
get noiseRootCa() {
|
|
344
|
+
const root = this.requireRootCa();
|
|
345
|
+
return { publicKey: root.publicKey, serial: root.serial };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Subscribes to every new connection pipeline (pre-auth). Listeners
|
|
349
|
+
* fan out; returns an unsubscribe function.
|
|
350
|
+
*/
|
|
351
|
+
onPipeline(listener) {
|
|
352
|
+
this.pipelineListeners.add(listener);
|
|
353
|
+
return () => {
|
|
354
|
+
this.pipelineListeners.delete(listener);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async listen() {
|
|
358
|
+
if (this.listenInfo) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
;
|
|
362
|
+
[this.rootCa, this.serverStaticKeyPair] = await Promise.all([
|
|
363
|
+
generateFakeNoiseRootCa(),
|
|
364
|
+
X25519.generateKeyPair()
|
|
365
|
+
]);
|
|
366
|
+
const mediaHandler = this.buildMediaRequestHandler();
|
|
367
|
+
this.wsServer.setHttpRequestHandler(mediaHandler);
|
|
368
|
+
this.mediaHttpsServer.setRequestHandler(mediaHandler);
|
|
369
|
+
this.listenInfo = await this.wsServer.listen();
|
|
370
|
+
await this.mediaHttpsServer.listen('127.0.0.1');
|
|
371
|
+
}
|
|
372
|
+
async stop() {
|
|
373
|
+
this.pipelines.clear();
|
|
374
|
+
await this.wsServer.close();
|
|
375
|
+
await this.mediaHttpsServer.close();
|
|
376
|
+
if (this.cachedMediaProxyAgent) {
|
|
377
|
+
this.cachedMediaProxyAgent.destroy();
|
|
378
|
+
this.cachedMediaProxyAgent = null;
|
|
379
|
+
}
|
|
380
|
+
this.listenInfo = null;
|
|
381
|
+
this.rootCa = null;
|
|
382
|
+
this.serverStaticKeyPair = null;
|
|
383
|
+
}
|
|
384
|
+
handleConnection(connection) {
|
|
385
|
+
if (this.rejectMode) {
|
|
386
|
+
connection.close(this.rejectMode.code, this.rejectMode.reason);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!this.rootCa || !this.serverStaticKeyPair) {
|
|
390
|
+
connection.close(1011, 'fake server not initialized');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const pipeline = new WaFakeConnectionPipeline({
|
|
394
|
+
connection,
|
|
395
|
+
rootCa: this.rootCa,
|
|
396
|
+
serverStaticKeyPair: this.serverStaticKeyPair,
|
|
397
|
+
routeIq: (node) => this.sessionFor(pipeline).routeIq(node),
|
|
398
|
+
...(this.options.successNodeAttributes !== undefined
|
|
399
|
+
? { successNodeAttributes: this.options.successNodeAttributes }
|
|
400
|
+
: {})
|
|
401
|
+
});
|
|
402
|
+
this.pipelines.add(pipeline);
|
|
403
|
+
this.pipelineSessions.set(pipeline, this.defaultSession);
|
|
404
|
+
pipeline.setEvents({
|
|
405
|
+
onAuthenticated: () => {
|
|
406
|
+
this.bindPipelineSession(pipeline);
|
|
407
|
+
for (const listener of this.authenticatedListeners) {
|
|
408
|
+
try {
|
|
409
|
+
void Promise.resolve(listener(pipeline)).catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* ignore */
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
onStanza: (node) => this.sessionFor(pipeline).handleCapturedStanza(node),
|
|
417
|
+
onClose: () => this.pipelines.delete(pipeline)
|
|
418
|
+
});
|
|
419
|
+
for (const listener of this.pipelineListeners) {
|
|
420
|
+
try {
|
|
421
|
+
void Promise.resolve(listener(pipeline)).catch(() => undefined);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
/* ignore */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Rebinds a connection to its keyed session once authenticated. Runs
|
|
430
|
+
* before user `onAuthenticated` listeners and before any client stanza, so
|
|
431
|
+
* captures and IQ routing land in the right session from the first frame.
|
|
432
|
+
* No-op without a `sessionKey` resolver (single default session).
|
|
433
|
+
*/
|
|
434
|
+
bindPipelineSession(pipeline) {
|
|
435
|
+
const resolveKey = this.options.sessionKey;
|
|
436
|
+
const clientPayload = pipeline.clientPayload;
|
|
437
|
+
if (!resolveKey || !clientPayload) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const id = resolveKey({ clientPayload, pipeline });
|
|
441
|
+
this.pipelineSessions.set(pipeline, this.session(id));
|
|
442
|
+
}
|
|
443
|
+
buildMediaRequestHandler() {
|
|
444
|
+
return (req, res) => {
|
|
445
|
+
const rawUrl = req.url ?? '';
|
|
446
|
+
const [path, query] = rawUrl.split('?');
|
|
447
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
448
|
+
if (method === 'POST') {
|
|
449
|
+
this.handleMediaUpload(req, res, path, query);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const blob = this.mediaStore.get(path);
|
|
453
|
+
if (!blob) {
|
|
454
|
+
res.statusCode = 404;
|
|
455
|
+
res.end();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
res.statusCode = 200;
|
|
459
|
+
res.setHeader('content-type', 'application/octet-stream');
|
|
460
|
+
res.setHeader('content-length', String(blob.encryptedBytes.byteLength));
|
|
461
|
+
res.end(Buffer.from(blob.encryptedBytes));
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
handleMediaUpload(req, res, path, query) {
|
|
465
|
+
const chunks = [];
|
|
466
|
+
req.on('data', (chunk) => {
|
|
467
|
+
chunks.push(chunk);
|
|
468
|
+
});
|
|
469
|
+
req.on('end', () => {
|
|
470
|
+
const encryptedBytes = new Uint8Array(Buffer.concat(chunks));
|
|
471
|
+
const segments = path.split('/').filter(Boolean);
|
|
472
|
+
const mediaType = segments[1] ?? 'unknown';
|
|
473
|
+
const auth = parseQueryParam(query, 'auth');
|
|
474
|
+
const upload = {
|
|
475
|
+
path,
|
|
476
|
+
mediaType,
|
|
477
|
+
encryptedBytes,
|
|
478
|
+
contentType: req.headers['content-type'],
|
|
479
|
+
auth,
|
|
480
|
+
receivedAtMs: Date.now()
|
|
481
|
+
};
|
|
482
|
+
this.capturedMediaUploads.push(upload);
|
|
483
|
+
this.mediaStore.setRaw(path, encryptedBytes, mediaType);
|
|
484
|
+
this.nextUploadCounter += 1;
|
|
485
|
+
const downloadUrl = this.mediaUrl(path);
|
|
486
|
+
const responseBody = JSON.stringify({
|
|
487
|
+
url: downloadUrl,
|
|
488
|
+
direct_path: path
|
|
489
|
+
});
|
|
490
|
+
res.statusCode = 200;
|
|
491
|
+
res.setHeader('content-type', 'application/json');
|
|
492
|
+
res.setHeader('content-length', String(Buffer.byteLength(responseBody)));
|
|
493
|
+
res.end(responseBody);
|
|
494
|
+
});
|
|
495
|
+
req.on('error', (error) => {
|
|
496
|
+
if (!res.headersSent) {
|
|
497
|
+
res.statusCode = 500;
|
|
498
|
+
}
|
|
499
|
+
res.end(error.message);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
requireListening() {
|
|
503
|
+
if (!this.listenInfo) {
|
|
504
|
+
throw new Error('fake server is not listening');
|
|
505
|
+
}
|
|
506
|
+
return this.listenInfo;
|
|
507
|
+
}
|
|
508
|
+
requireRootCa() {
|
|
509
|
+
if (!this.rootCa) {
|
|
510
|
+
throw new Error('fake server is not listening');
|
|
511
|
+
}
|
|
512
|
+
return this.rootCa;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function parseQueryParam(query, name) {
|
|
516
|
+
if (!query)
|
|
517
|
+
return undefined;
|
|
518
|
+
for (const pair of query.split('&')) {
|
|
519
|
+
const eq = pair.indexOf('=');
|
|
520
|
+
if (eq < 0)
|
|
521
|
+
continue;
|
|
522
|
+
const key = decodeURIComponent(pair.slice(0, eq));
|
|
523
|
+
if (key !== name)
|
|
524
|
+
continue;
|
|
525
|
+
return decodeURIComponent(pair.slice(eq + 1));
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { buildNotification } from '../protocol/push/notification.js';
|
|
2
|
+
export class PreKeyDispenser {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.capturedPreKeyBundle = null;
|
|
5
|
+
this.preKeyDispenserCursor = 0;
|
|
6
|
+
this.preKeyDispenserMisses = 0;
|
|
7
|
+
this.nextPreKeyNotificationId = 1;
|
|
8
|
+
this.preKeyBundleWaiters = new Set();
|
|
9
|
+
}
|
|
10
|
+
captureBundle(bundle) {
|
|
11
|
+
this.capturedPreKeyBundle = bundle;
|
|
12
|
+
this.preKeyDispenserCursor = 0;
|
|
13
|
+
this.preKeyDispenserMisses = 0;
|
|
14
|
+
for (const waiter of this.preKeyBundleWaiters) {
|
|
15
|
+
waiter.resolve(bundle);
|
|
16
|
+
}
|
|
17
|
+
this.preKeyBundleWaiters.clear();
|
|
18
|
+
}
|
|
19
|
+
awaitPreKeyBundle(timeoutMs = 15000) {
|
|
20
|
+
if (this.capturedPreKeyBundle) {
|
|
21
|
+
return Promise.resolve(this.capturedPreKeyBundle);
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const waiter = {
|
|
25
|
+
resolve: (bundle) => {
|
|
26
|
+
clearTimeout(waiter.timer);
|
|
27
|
+
this.preKeyBundleWaiters.delete(waiter);
|
|
28
|
+
resolve(bundle);
|
|
29
|
+
},
|
|
30
|
+
reject: (error) => {
|
|
31
|
+
clearTimeout(waiter.timer);
|
|
32
|
+
this.preKeyBundleWaiters.delete(waiter);
|
|
33
|
+
reject(error);
|
|
34
|
+
},
|
|
35
|
+
timer: setTimeout(() => {
|
|
36
|
+
this.preKeyBundleWaiters.delete(waiter);
|
|
37
|
+
reject(new Error(`awaitPreKeyBundle timed out after ${timeoutMs}ms`));
|
|
38
|
+
}, timeoutMs)
|
|
39
|
+
};
|
|
40
|
+
this.preKeyBundleWaiters.add(waiter);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
capturedPreKeyBundleSnapshot() {
|
|
44
|
+
return this.capturedPreKeyBundle;
|
|
45
|
+
}
|
|
46
|
+
dispenseOneTimePreKey() {
|
|
47
|
+
const bundle = this.capturedPreKeyBundle;
|
|
48
|
+
if (!bundle) {
|
|
49
|
+
this.preKeyDispenserMisses += 1;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (this.preKeyDispenserCursor >= bundle.preKeys.length) {
|
|
53
|
+
this.preKeyDispenserMisses += 1;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const entry = bundle.preKeys[this.preKeyDispenserCursor];
|
|
57
|
+
this.preKeyDispenserCursor += 1;
|
|
58
|
+
return { keyId: entry.keyId, publicKey: entry.publicKey };
|
|
59
|
+
}
|
|
60
|
+
preKeysAvailable() {
|
|
61
|
+
const bundle = this.capturedPreKeyBundle;
|
|
62
|
+
if (!bundle)
|
|
63
|
+
return 0;
|
|
64
|
+
return Math.max(0, bundle.preKeys.length - this.preKeyDispenserCursor);
|
|
65
|
+
}
|
|
66
|
+
preKeyDispenserMissesSnapshot() {
|
|
67
|
+
return this.preKeyDispenserMisses;
|
|
68
|
+
}
|
|
69
|
+
async triggerPreKeyUpload(pipeline, options = {}) {
|
|
70
|
+
const opts = typeof options === 'number' ? { timeoutMs: options } : options;
|
|
71
|
+
const timeoutMs = opts.timeoutMs ?? 15000;
|
|
72
|
+
if (!opts.force && this.capturedPreKeyBundle) {
|
|
73
|
+
return this.capturedPreKeyBundle;
|
|
74
|
+
}
|
|
75
|
+
if (opts.force) {
|
|
76
|
+
this.capturedPreKeyBundle = null;
|
|
77
|
+
this.preKeyDispenserCursor = 0;
|
|
78
|
+
}
|
|
79
|
+
const bundlePromise = this.awaitPreKeyBundle(timeoutMs);
|
|
80
|
+
const id = `prekey-low-${this.nextPreKeyNotificationId++}`;
|
|
81
|
+
await pipeline.sendStanza(buildNotification({
|
|
82
|
+
id,
|
|
83
|
+
type: 'encrypt',
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
tag: 'count',
|
|
87
|
+
attrs: { value: '0' }
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}));
|
|
91
|
+
return bundlePromise;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class Scenario {
|
|
2
|
+
constructor(server) {
|
|
3
|
+
this.server = server;
|
|
4
|
+
}
|
|
5
|
+
onIq(matcher) {
|
|
6
|
+
return new IqExpectation(this.server, matcher);
|
|
7
|
+
}
|
|
8
|
+
afterAuth(listener) {
|
|
9
|
+
this.server.onAuthenticatedPipeline(listener);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class IqExpectation {
|
|
13
|
+
constructor(server, matcher) {
|
|
14
|
+
this.server = server;
|
|
15
|
+
this.matcher = matcher;
|
|
16
|
+
}
|
|
17
|
+
respondWith(response) {
|
|
18
|
+
this.server.registerIqHandler(this.matcher, () => response);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
respond(responder) {
|
|
22
|
+
this.server.registerIqHandler(this.matcher, responder);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
respondOnce(response) {
|
|
26
|
+
const unregister = this.server.registerIqHandler(this.matcher, async (iq) => {
|
|
27
|
+
unregister();
|
|
28
|
+
return typeof response === 'function' ? response(iq) : response;
|
|
29
|
+
});
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
}
|