@zapo-js/fake-server 1.0.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.
@@ -1,127 +1,98 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FakeWaServer = void 0;
3
+ exports.FakeWaServer = exports.FakeServerSession = void 0;
4
4
  const node_https_1 = require("node:https");
5
5
  const WaFakeConnectionPipeline_1 = require("../infra/WaFakeConnectionPipeline");
6
6
  const WaFakeMediaHttpsServer_1 = require("../infra/WaFakeMediaHttpsServer");
7
7
  const WaFakeWsServer_1 = require("../infra/WaFakeWsServer");
8
8
  const cert_chain_1 = require("../protocol/auth/cert-chain");
9
- const router_1 = require("../protocol/iq/router");
10
9
  const fake_media_store_1 = require("../state/fake-media-store");
11
10
  const crypto_1 = require("../transport/crypto");
12
- const AppStateSyncManager_1 = require("./AppStateSyncManager");
13
11
  const FakePairingDriver_1 = require("./FakePairingDriver");
14
12
  const FakePeer_1 = require("./FakePeer");
15
- const iq_handlers_1 = require("./iq-handlers");
16
- const PreKeyDispenser_1 = require("./PreKeyDispenser");
13
+ const FakeServerSession_1 = require("./FakeServerSession");
17
14
  const Scenario_1 = require("./Scenario");
18
- const ServerRegistries_1 = require("./ServerRegistries");
15
+ var FakeServerSession_2 = require("./FakeServerSession");
16
+ Object.defineProperty(exports, "FakeServerSession", { enumerable: true, get: function () { return FakeServerSession_2.FakeServerSession; } });
19
17
  class FakeWaServer {
20
18
  constructor(options = {}) {
21
- this.registries = new ServerRegistries_1.ServerRegistries();
22
- this.preKeyDispenser = new PreKeyDispenser_1.PreKeyDispenser();
23
- this.appStateSync = new AppStateSyncManager_1.AppStateSyncManager();
24
19
  this.pipelines = new Set();
25
- this.iqRouter = new router_1.WaFakeIqRouter();
26
- this.capturedStanzas = [];
27
- this.pendingIqExpectations = new Set();
28
- this.pendingStanzaExpectations = new Set();
20
+ this.pipelineSessions = new WeakMap();
29
21
  this.authenticatedListeners = new Set();
30
- this.inboundStanzaListeners = new Set();
31
- this.capturedStanzaListeners = new Set();
22
+ this.sessions = new Map();
23
+ this.globalIqHandlers = new Set();
32
24
  this.rootCa = null;
33
25
  this.serverStaticKeyPair = null;
34
26
  this.listenInfo = null;
35
- this.pipelineListener = null;
27
+ this.pipelineListeners = new Set();
36
28
  this.rejectMode = null;
37
29
  this.mediaStore = new fake_media_store_1.FakeMediaStore();
38
30
  this.mediaHttpsServer = new WaFakeMediaHttpsServer_1.WaFakeMediaHttpsServer();
39
31
  this.capturedMediaUploads = [];
40
32
  this.nextUploadCounter = 0;
41
33
  this.cachedMediaProxyAgent = null;
34
+ this.options = options;
42
35
  this.wsServer = new WaFakeWsServer_1.WaFakeWsServer(options);
43
36
  this.wsServer.onConnection((connection) => this.handleConnection(connection));
44
- (0, iq_handlers_1.registerDefaultIqHandlers)(this.iqRouter, this.buildIqHandlerDeps());
37
+ this.defaultSession = this.createSession('__default__');
45
38
  }
46
- buildIqHandlerDeps() {
47
- const reg = this.registries;
48
- const preKey = this.preKeyDispenser;
49
- const appState = this.appStateSync;
50
- return {
51
- get peerRegistry() {
52
- return reg.peerRegistry;
53
- },
54
- get groupRegistry() {
55
- return reg.groupRegistry;
56
- },
57
- get privacySettings() {
58
- return reg.privacySettings;
59
- },
60
- get blocklistJids() {
61
- return reg.blocklistJids;
62
- },
63
- get profilePicturesByJid() {
64
- return reg.profilePicturesByJid;
65
- },
66
- get businessProfilesByJid() {
67
- return reg.businessProfilesByJid;
68
- },
69
- get abPropsInput() {
70
- return reg.abPropsInput;
71
- },
72
- get issuedPrivacyTokens() {
73
- return reg.issuedPrivacyTokens;
74
- },
75
- get latestStatusText() {
76
- return reg.latestStatusText;
77
- },
78
- setLatestStatusText: (text) => {
79
- reg.latestStatusText = text;
80
- },
81
- lookupDeviceIdsForUser: (userJid) => reg.lookupDeviceIdsForUser(userJid),
82
- notifyGroupOp: (op) => reg.notifyGroupOp(op),
83
- mutatePrivacySettings: (category, value) => reg.mutatePrivacySettings(category, value),
84
- mutateBlocklist: (action, jid) => reg.mutateBlocklist(action, jid),
85
- notifyProfilePictureSet: (op) => reg.notifyProfilePictureSet(op),
86
- handleProfilePictureSet: (targetJid, newId) => reg.handleProfilePictureSet(targetJid, newId),
87
- notifyStatusSet: (text) => reg.notifyStatusSet(text),
88
- notifyLogout: () => reg.notifyLogout(),
89
- notifyPrivacyTokenIssue: (token) => reg.notifyPrivacyTokenIssue(token),
90
- notifyDirtyBitsClear: (op) => reg.notifyDirtyBitsClear(op),
91
- notifyPrivacySet: (change) => {
92
- for (const listener of reg.privacySetListeners) {
93
- try {
94
- listener(change);
95
- }
96
- catch (error) {
97
- void error;
98
- }
99
- }
100
- },
101
- notifyBlocklistChange: (change) => {
102
- for (const listener of reg.blocklistChangeListeners) {
103
- try {
104
- listener(change);
105
- }
106
- catch (error) {
107
- void error;
108
- }
109
- }
110
- },
111
- capturePreKeyBundle: (bundle) => preKey.captureBundle(bundle),
112
- consumeOutboundAppStatePatches: (iq) => appState.consumeOutboundAppStatePatches(iq),
113
- get appStateCollectionProviders() {
114
- return appState.appStateCollectionProviders;
115
- },
116
- requireMediaHttpsInfo: () => this.requireMediaHttpsInfo()
117
- };
39
+ /** State of the single default session (the only session for one client). */
40
+ get registries() {
41
+ return this.defaultSession.registries;
42
+ }
43
+ get preKeyDispenser() {
44
+ return this.defaultSession.preKeyDispenser;
45
+ }
46
+ get appStateSync() {
47
+ return this.defaultSession.appStateSync;
48
+ }
49
+ /**
50
+ * Returns the isolated session for `id`, creating it on first use. Every
51
+ * session gets its own registries, prekey dispenser, app-state, IQ router,
52
+ * and stanza capture. Use with the `sessionKey` option to run several
53
+ * `WaClient`s against one server without them sharing any state.
54
+ */
55
+ session(id) {
56
+ const existing = this.sessions.get(id);
57
+ if (existing) {
58
+ return existing;
59
+ }
60
+ return this.createSession(id);
61
+ }
62
+ /** The session a connection is bound to (default before authentication). */
63
+ sessionFor(pipeline) {
64
+ return this.pipelineSessions.get(pipeline) ?? this.defaultSession;
65
+ }
66
+ createSession(id) {
67
+ const session = new FakeServerSession_1.FakeServerSession(id, { requireMediaHttpsInfo: () => this.requireMediaHttpsInfo() }, { defaultIqHandlers: this.options.defaultIqHandlers !== false });
68
+ this.sessions.set(id, session);
69
+ // Server-level custom handlers apply to every session, present and future.
70
+ for (const handler of this.globalIqHandlers) {
71
+ handler.unregisterBySession.set(session, session.registerIqHandler(handler.matcher, handler.respond, handler.label));
72
+ }
73
+ return session;
118
74
  }
119
75
  registerIqHandler(matcher, respond, label) {
120
- const handler = { matcher, respond, label };
121
- return this.iqRouter.register(handler, { priority: 'high' });
76
+ const entry = {
77
+ matcher,
78
+ respond,
79
+ label,
80
+ unregisterBySession: new Map()
81
+ };
82
+ for (const session of this.sessions.values()) {
83
+ entry.unregisterBySession.set(session, session.registerIqHandler(matcher, respond, label));
84
+ }
85
+ this.globalIqHandlers.add(entry);
86
+ return () => {
87
+ for (const unregister of entry.unregisterBySession.values()) {
88
+ unregister();
89
+ }
90
+ entry.unregisterBySession.clear();
91
+ this.globalIqHandlers.delete(entry);
92
+ };
122
93
  }
123
94
  async routeIqForTest(iq) {
124
- return this.iqRouter.route(iq);
95
+ return this.defaultSession.routeIq(iq);
125
96
  }
126
97
  onOutboundGroupOp(listener) {
127
98
  return this.registries.onOutboundGroupOp(listener);
@@ -178,7 +149,7 @@ class FakeWaServer {
178
149
  return this.registries.createFakeGroup(input);
179
150
  }
180
151
  async triggerPreKeyUpload(pipeline, options = {}) {
181
- return this.preKeyDispenser.triggerPreKeyUpload(pipeline, options);
152
+ return this.sessionFor(pipeline).preKeyDispenser.triggerPreKeyUpload(pipeline, options);
182
153
  }
183
154
  awaitPreKeyBundle(timeoutMs = 15000) {
184
155
  return this.preKeyDispenser.awaitPreKeyBundle(timeoutMs);
@@ -208,7 +179,7 @@ class FakeWaServer {
208
179
  return this.appStateSync.provideAppStateCollection(name, provider);
209
180
  }
210
181
  async pushServerSyncNotification(pipeline, input) {
211
- return this.appStateSync.pushServerSyncNotification(pipeline, input);
182
+ return this.sessionFor(pipeline).appStateSync.pushServerSyncNotification(pipeline, input);
212
183
  }
213
184
  onAuthenticatedPipeline(listener) {
214
185
  this.authenticatedListeners.add(listener);
@@ -218,78 +189,24 @@ class FakeWaServer {
218
189
  configure(new Scenario_1.Scenario(this));
219
190
  }
220
191
  expectIq(matcher, options = {}) {
221
- const timeoutMs = options.timeoutMs ?? 2000;
222
- for (const captured of this.capturedStanzas) {
223
- if (matchesIq(captured, matcher)) {
224
- return Promise.resolve(captured);
225
- }
226
- }
227
- return new Promise((resolve, reject) => {
228
- const expectation = {
229
- matcher,
230
- resolve: (iq) => {
231
- this.pendingIqExpectations.delete(expectation);
232
- clearTimeout(expectation.timer);
233
- resolve(iq);
234
- },
235
- reject: (error) => {
236
- this.pendingIqExpectations.delete(expectation);
237
- clearTimeout(expectation.timer);
238
- reject(error);
239
- },
240
- timer: setTimeout(() => {
241
- this.pendingIqExpectations.delete(expectation);
242
- reject(new Error(`expectIq timed out after ${timeoutMs}ms (${describeMatcher(matcher)})`));
243
- }, timeoutMs)
244
- };
245
- this.pendingIqExpectations.add(expectation);
246
- });
192
+ return this.defaultSession.expectIq(matcher, options);
247
193
  }
248
194
  capturedStanzaSnapshot() {
249
- return this.capturedStanzas.slice();
195
+ return this.defaultSession.capturedStanzaSnapshot();
250
196
  }
251
197
  /**
252
- * Subscribes to every stanza captured from the client side (the lib).
253
- * The listener is called synchronously for each new stanza as it
254
- * arrives, in addition to `capturedStanzas` being appended. Returns
255
- * an unsubscribe function. Useful for benches that need to count or
256
- * react to a stream of receipts/messages without paying the O(N²)
257
- * cost of polling `capturedStanzaSnapshot()` or queuing one
258
- * `expectStanza(...)` per iteration.
198
+ * Subscribes to every stanza captured from the client side (the lib) of
199
+ * the default session. The listener is called synchronously for each new
200
+ * stanza as it arrives. Returns an unsubscribe function. Useful for
201
+ * benches that need to count or react to a stream of receipts/messages
202
+ * without paying the O(N²) cost of polling `capturedStanzaSnapshot()` or
203
+ * queuing one `expectStanza(...)` per iteration.
259
204
  */
260
205
  onCapturedStanza(listener) {
261
- this.capturedStanzaListeners.add(listener);
262
- return () => {
263
- this.capturedStanzaListeners.delete(listener);
264
- };
206
+ return this.defaultSession.onCapturedStanza(listener);
265
207
  }
266
208
  expectStanza(matcher, options = {}) {
267
- const timeoutMs = options.timeoutMs ?? 2000;
268
- for (const captured of this.capturedStanzas) {
269
- if (matchesStanza(captured, matcher)) {
270
- return Promise.resolve(captured);
271
- }
272
- }
273
- return new Promise((resolve, reject) => {
274
- const expectation = {
275
- matcher,
276
- resolve: (node) => {
277
- this.pendingStanzaExpectations.delete(expectation);
278
- clearTimeout(expectation.timer);
279
- resolve(node);
280
- },
281
- reject: (error) => {
282
- this.pendingStanzaExpectations.delete(expectation);
283
- clearTimeout(expectation.timer);
284
- reject(error);
285
- },
286
- timer: setTimeout(() => {
287
- this.pendingStanzaExpectations.delete(expectation);
288
- reject(new Error(`expectStanza timed out after ${timeoutMs}ms (${describeStanzaMatcher(matcher)})`));
289
- }, timeoutMs)
290
- };
291
- this.pendingStanzaExpectations.add(expectation);
292
- });
209
+ return this.defaultSession.expectStanza(matcher, options);
293
210
  }
294
211
  async broadcastStanza(node) {
295
212
  const tasks = [];
@@ -351,8 +268,9 @@ class FakeWaServer {
351
268
  return this.capturedMediaUploads.slice();
352
269
  }
353
270
  async createFakePeer(options, pipeline) {
354
- const peer = await FakePeer_1.FakePeer.create(options, this.buildFakePeerDeps(pipeline));
355
- this.registries.registerPeer(peer);
271
+ const session = this.sessionFor(pipeline);
272
+ const peer = await FakePeer_1.FakePeer.create(options, this.buildFakePeerDeps(pipeline, session));
273
+ session.registries.registerPeer(peer);
356
274
  return peer;
357
275
  }
358
276
  async createFakePeerWithDevices(input, pipeline) {
@@ -365,6 +283,7 @@ class FakeWaServer {
365
283
  }
366
284
  const userPart = input.userJid.slice(0, atIdx);
367
285
  const server = input.userJid.slice(atIdx + 1);
286
+ const session = this.sessionFor(pipeline);
368
287
  const peers = [];
369
288
  for (const deviceId of input.deviceIds) {
370
289
  const deviceJid = deviceId === 0 ? input.userJid : `${userPart}:${deviceId}@${server}`;
@@ -372,36 +291,27 @@ class FakeWaServer {
372
291
  jid: deviceJid,
373
292
  displayName: input.displayName,
374
293
  skipOneTimePreKey: input.skipOneTimePreKey
375
- }, this.buildFakePeerDeps(pipeline));
376
- this.registries.registerPeer(peer);
294
+ }, this.buildFakePeerDeps(pipeline, session));
295
+ session.registries.registerPeer(peer);
377
296
  peers.push(peer);
378
297
  }
379
298
  return peers;
380
299
  }
381
- buildFakePeerDeps(pipeline) {
300
+ buildFakePeerDeps(pipeline, session) {
382
301
  return {
383
- bundleResolver: () => this.preKeyDispenser.awaitPreKeyBundle(),
384
- reserveOneTimePreKey: () => this.preKeyDispenser.dispenseOneTimePreKey(),
302
+ bundleResolver: () => session.preKeyDispenser.awaitPreKeyBundle(),
303
+ reserveOneTimePreKey: () => session.preKeyDispenser.dispenseOneTimePreKey(),
385
304
  pushStanza: (stanza) => pipeline.sendStanza(stanza),
386
- subscribeInboundMessages: (listener) => {
387
- const wrapped = (node) => {
388
- if (node.tag !== 'message')
389
- return;
390
- listener(node);
391
- };
392
- this.inboundStanzaListeners.add(wrapped);
393
- return () => {
394
- this.inboundStanzaListeners.delete(wrapped);
395
- };
396
- }
305
+ subscribeInboundMessages: (listener) => session.subscribeInboundMessages(listener)
397
306
  };
398
307
  }
399
308
  async runPairing(pipeline, options, companionMaterialResolver) {
309
+ const session = this.sessionFor(pipeline);
400
310
  const driver = new FakePairingDriver_1.FakePairingDriver(options, {
401
311
  pipeline,
402
312
  companionMaterialResolver,
403
313
  waitForPairDeviceAck: async (pairDeviceIqId) => {
404
- await this.expectIq({
314
+ await session.expectIq({
405
315
  id: pairDeviceIqId
406
316
  }, {
407
317
  timeoutMs: 15000
@@ -438,8 +348,15 @@ class FakeWaServer {
438
348
  const root = this.requireRootCa();
439
349
  return { publicKey: root.publicKey, serial: root.serial };
440
350
  }
351
+ /**
352
+ * Subscribes to every new connection pipeline (pre-auth). Listeners
353
+ * fan out; returns an unsubscribe function.
354
+ */
441
355
  onPipeline(listener) {
442
- this.pipelineListener = listener;
356
+ this.pipelineListeners.add(listener);
357
+ return () => {
358
+ this.pipelineListeners.delete(listener);
359
+ };
443
360
  }
444
361
  async listen() {
445
362
  if (this.listenInfo) {
@@ -481,53 +398,51 @@ class FakeWaServer {
481
398
  connection,
482
399
  rootCa: this.rootCa,
483
400
  serverStaticKeyPair: this.serverStaticKeyPair,
484
- iqRouter: this.iqRouter
401
+ routeIq: (node) => this.sessionFor(pipeline).routeIq(node),
402
+ ...(this.options.successNodeAttributes !== undefined
403
+ ? { successNodeAttributes: this.options.successNodeAttributes }
404
+ : {})
485
405
  });
486
406
  this.pipelines.add(pipeline);
407
+ this.pipelineSessions.set(pipeline, this.defaultSession);
487
408
  pipeline.setEvents({
488
409
  onAuthenticated: () => {
410
+ this.bindPipelineSession(pipeline);
489
411
  for (const listener of this.authenticatedListeners) {
490
- void listener(pipeline);
412
+ try {
413
+ void Promise.resolve(listener(pipeline)).catch(() => undefined);
414
+ }
415
+ catch {
416
+ /* ignore */
417
+ }
491
418
  }
492
419
  },
493
- onStanza: (node) => this.handleCapturedStanza(node),
420
+ onStanza: (node) => this.sessionFor(pipeline).handleCapturedStanza(node),
494
421
  onClose: () => this.pipelines.delete(pipeline)
495
422
  });
496
- this.pipelineListener?.(pipeline);
497
- }
498
- handleCapturedStanza(node) {
499
- this.capturedStanzas.push(node);
500
- for (const listener of this.capturedStanzaListeners) {
423
+ for (const listener of this.pipelineListeners) {
501
424
  try {
502
- listener(node);
425
+ void Promise.resolve(listener(pipeline)).catch(() => undefined);
503
426
  }
504
- catch (error) {
505
- void error;
427
+ catch {
428
+ /* ignore */
506
429
  }
507
430
  }
508
- for (const listener of this.inboundStanzaListeners) {
509
- try {
510
- listener(node);
511
- }
512
- catch (error) {
513
- void error;
514
- }
515
- }
516
- for (const expectation of this.pendingStanzaExpectations) {
517
- if (matchesStanza(node, expectation.matcher)) {
518
- expectation.resolve(node);
519
- break;
520
- }
521
- }
522
- if (node.tag !== 'iq') {
431
+ }
432
+ /**
433
+ * Rebinds a connection to its keyed session once authenticated. Runs
434
+ * before user `onAuthenticated` listeners and before any client stanza, so
435
+ * captures and IQ routing land in the right session from the first frame.
436
+ * No-op without a `sessionKey` resolver (single default session).
437
+ */
438
+ bindPipelineSession(pipeline) {
439
+ const resolveKey = this.options.sessionKey;
440
+ const clientPayload = pipeline.clientPayload;
441
+ if (!resolveKey || !clientPayload) {
523
442
  return;
524
443
  }
525
- for (const expectation of this.pendingIqExpectations) {
526
- if (matchesIq(node, expectation.matcher)) {
527
- expectation.resolve(node);
528
- return;
529
- }
530
- }
444
+ const id = resolveKey({ clientPayload, pipeline });
445
+ this.pipelineSessions.set(pipeline, this.session(id));
531
446
  }
532
447
  buildMediaRequestHandler() {
533
448
  return (req, res) => {
@@ -616,69 +531,3 @@ function parseQueryParam(query, name) {
616
531
  }
617
532
  return undefined;
618
533
  }
619
- function matchesIq(iq, matcher) {
620
- if (iq.tag !== 'iq')
621
- return false;
622
- if (matcher.id !== undefined && iq.attrs.id !== matcher.id)
623
- return false;
624
- if (matcher.type !== undefined && iq.attrs.type !== matcher.type)
625
- return false;
626
- if (matcher.xmlns !== undefined && iq.attrs.xmlns !== matcher.xmlns)
627
- return false;
628
- if (matcher.childTag !== undefined) {
629
- const children = Array.isArray(iq.content) ? iq.content : null;
630
- if (!children || children.length === 0)
631
- return false;
632
- if (children[0].tag !== matcher.childTag)
633
- return false;
634
- }
635
- return true;
636
- }
637
- function describeMatcher(matcher) {
638
- const parts = [];
639
- if (matcher.id !== undefined)
640
- parts.push(`id=${matcher.id}`);
641
- if (matcher.type !== undefined)
642
- parts.push(`type=${matcher.type}`);
643
- if (matcher.xmlns !== undefined)
644
- parts.push(`xmlns=${matcher.xmlns}`);
645
- if (matcher.childTag !== undefined)
646
- parts.push(`childTag=${matcher.childTag}`);
647
- return parts.length > 0 ? parts.join(', ') : 'any iq';
648
- }
649
- function matchesStanza(node, matcher) {
650
- if (matcher.tag !== undefined && node.tag !== matcher.tag)
651
- return false;
652
- if (matcher.type !== undefined && node.attrs.type !== matcher.type)
653
- return false;
654
- if (matcher.xmlns !== undefined && node.attrs.xmlns !== matcher.xmlns)
655
- return false;
656
- if (matcher.from !== undefined && node.attrs.from !== matcher.from)
657
- return false;
658
- if (matcher.to !== undefined && node.attrs.to !== matcher.to)
659
- return false;
660
- if (matcher.childTag !== undefined) {
661
- const children = Array.isArray(node.content) ? node.content : null;
662
- if (!children || children.length === 0)
663
- return false;
664
- if (children[0].tag !== matcher.childTag)
665
- return false;
666
- }
667
- return true;
668
- }
669
- function describeStanzaMatcher(matcher) {
670
- const parts = [];
671
- if (matcher.tag !== undefined)
672
- parts.push(`tag=${matcher.tag}`);
673
- if (matcher.type !== undefined)
674
- parts.push(`type=${matcher.type}`);
675
- if (matcher.xmlns !== undefined)
676
- parts.push(`xmlns=${matcher.xmlns}`);
677
- if (matcher.from !== undefined)
678
- parts.push(`from=${matcher.from}`);
679
- if (matcher.to !== undefined)
680
- parts.push(`to=${matcher.to}`);
681
- if (matcher.childTag !== undefined)
682
- parts.push(`childTag=${matcher.childTag}`);
683
- return parts.length > 0 ? parts.join(', ') : 'any stanza';
684
- }
@@ -34,6 +34,7 @@ export interface IqHandlerDeps {
34
34
  notifyPrivacySet(change: CapturedPrivacySet): void;
35
35
  notifyBlocklistChange(change: CapturedBlocklistChange): void;
36
36
  capturePreKeyBundle(bundle: ClientPreKeyBundle): void;
37
+ countServerPreKeys(): number;
37
38
  consumeOutboundAppStatePatches(iq: BinaryNode): Promise<void>;
38
39
  readonly appStateCollectionProviders: ReadonlyMap<string, () => Promise<FakeAppStateCollectionPayload | null> | FakeAppStateCollectionPayload | null>;
39
40
  requireMediaHttpsInfo(): {
@@ -98,6 +98,29 @@ function registerDefaultIqHandlers(router, deps) {
98
98
  matcher: { xmlns: 'encrypt', type: 'get', childTag: 'digest' },
99
99
  respond: (iq) => (0, router_1.buildIqError)(iq, { code: 404, text: 'item-not-found' })
100
100
  });
101
+ // Baileys-family and whatsmeow clients block on this query before
102
+ // reporting the connection as open; zapo-js uses <digest> instead.
103
+ router.register({
104
+ label: 'prekey-count',
105
+ matcher: { xmlns: 'encrypt', type: 'get', childTag: 'count' },
106
+ respond: (iq) => {
107
+ const result = (0, router_1.buildIqResult)(iq);
108
+ return {
109
+ ...result,
110
+ content: [
111
+ {
112
+ tag: 'count',
113
+ attrs: { value: String(deps.countServerPreKeys()) }
114
+ }
115
+ ]
116
+ };
117
+ }
118
+ });
119
+ router.register({
120
+ label: 'passive-mode',
121
+ matcher: { xmlns: 'passive', type: 'set' },
122
+ respond: (iq) => (0, router_1.buildIqResult)(iq)
123
+ });
101
124
  router.register({
102
125
  label: 'media-conn',
103
126
  matcher: { xmlns: 'w:m', type: 'set', childTag: 'media_conn' },