@zapo-js/fake-server 1.2.0 → 1.3.1

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.
@@ -15,6 +15,7 @@ export interface CompanionPairingMaterial {
15
15
  export interface FakePairingDriverDeps {
16
16
  readonly pipeline: WaFakeConnectionPipeline;
17
17
  readonly waitForPairDeviceAck?: (pairDeviceIqId: string) => Promise<void>;
18
+ readonly waitForPairSuccessAck?: (pairSuccessIqId: string) => Promise<void>;
18
19
  readonly companionMaterialResolver: () => Promise<CompanionPairingMaterial>;
19
20
  }
20
21
  export declare class FakePairingDriver {
@@ -4,6 +4,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.FakePairingDriver = void 0;
5
5
  const fake_primary_device_1 = require("../protocol/auth/fake-primary-device");
6
6
  const pair_device_1 = require("../protocol/auth/pair-device");
7
+ const stream_error_1 = require("../protocol/stream/stream-error");
8
+ /** `stream:error` code the server sends once the companion has signed its identity. */
9
+ const FORCE_LOGIN_CODE = 515;
7
10
  class FakePairingDriver {
8
11
  constructor(options, deps) {
9
12
  this.primary = null;
@@ -22,12 +25,17 @@ class FakePairingDriver {
22
25
  companionIdentityPublicKey: material.identityPublicKey,
23
26
  companionDeviceId: this.options.companionDeviceId ?? 1
24
27
  });
25
- await this.deps.pipeline.sendStanza((0, pair_device_1.buildPairSuccessIq)({
28
+ const pairSuccessIq = (0, pair_device_1.buildPairSuccessIq)({
26
29
  deviceJid: this.options.deviceJid,
27
30
  deviceLid: this.options.deviceLid,
28
31
  platform: this.options.platform ?? 'IOS',
29
32
  deviceIdentityBytes
30
- }));
33
+ });
34
+ void this.deps
35
+ .waitForPairSuccessAck?.(pairSuccessIq.attrs.id)
36
+ .then(() => this.deps.pipeline.sendStanza((0, stream_error_1.buildStreamErrorCode)(FORCE_LOGIN_CODE)))
37
+ .catch(() => undefined);
38
+ await this.deps.pipeline.sendStanza(pairSuccessIq);
31
39
  }
32
40
  }
33
41
  exports.FakePairingDriver = FakePairingDriver;
@@ -1,5 +1,6 @@
1
1
  import { Agent as HttpsAgent } from 'node:https';
2
2
  import { type WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from '../infra/WaFakeConnectionPipeline';
3
+ import { type FakeNoiseRootCa } from '../protocol/auth/cert-chain';
3
4
  import type { ParsedClientPayload } from '../protocol/auth/client-payload-validate';
4
5
  import type { BuildSuccessNodeInput } from '../protocol/auth/success-node';
5
6
  import type { BuildAbPropsResultInput } from '../protocol/iq/abprops';
@@ -63,6 +64,22 @@ export interface FakeWaServerOptions {
63
64
  readonly host?: string;
64
65
  readonly port?: number;
65
66
  };
67
+ /**
68
+ * Root of the Noise certificate chain the server presents. Defaults to a
69
+ * fresh random CA per {@link FakeWaServer.listen}, read back from
70
+ * {@link FakeWaServer.noiseRootCa}.
71
+ *
72
+ * A client in a separate process cannot read it back: it has to pin the
73
+ * anchor before it dials, while the server is not listening yet. Pass a CA
74
+ * both sides derive from a shared seed to make it knowable ahead of time.
75
+ *
76
+ * Carries the signing half, unlike the public-only
77
+ * {@link FakeWaServerNoiseRootCa} this server exposes. It signs a chain no
78
+ * real client trusts, so it is test material rather than a secret — but a
79
+ * seed committed beside the tests is the intended source, not one shared
80
+ * with anything that matters.
81
+ */
82
+ readonly noiseRootCa?: FakeNoiseRootCa;
66
83
  }
67
84
  export interface FakeSessionKeyInfo {
68
85
  readonly clientPayload: ParsedClientPayload;
@@ -372,6 +372,9 @@ class FakeWaServer {
372
372
  }, {
373
373
  timeoutMs: 15000
374
374
  });
375
+ },
376
+ waitForPairSuccessAck: async (pairSuccessIqId) => {
377
+ await session.expectIq({ id: pairSuccessIqId }, { timeoutMs: 15000 });
375
378
  }
376
379
  });
377
380
  await driver.run();
@@ -432,7 +435,7 @@ class FakeWaServer {
432
435
  }
433
436
  ;
434
437
  [this.rootCa, this.serverStaticKeyPair] = await Promise.all([
435
- (0, cert_chain_1.generateFakeNoiseRootCa)(),
438
+ this.options.noiseRootCa ?? (0, cert_chain_1.generateFakeNoiseRootCa)(),
436
439
  crypto_1.X25519.generateKeyPair()
437
440
  ]);
438
441
  const mediaHandler = this.buildMediaRequestHandler();
@@ -1,6 +1,9 @@
1
1
  /** Runs the server side of the QR pairing flow. */
2
2
  import { buildAdvSignedDeviceIdentity, generateFakePrimaryDevice } from '../protocol/auth/fake-primary-device.js';
3
3
  import { buildPairDeviceIq, buildPairSuccessIq, mintPairingRefs } from '../protocol/auth/pair-device.js';
4
+ import { buildStreamErrorCode } from '../protocol/stream/stream-error.js';
5
+ /** `stream:error` code the server sends once the companion has signed its identity. */
6
+ const FORCE_LOGIN_CODE = 515;
4
7
  export class FakePairingDriver {
5
8
  constructor(options, deps) {
6
9
  this.primary = null;
@@ -19,11 +22,16 @@ export class FakePairingDriver {
19
22
  companionIdentityPublicKey: material.identityPublicKey,
20
23
  companionDeviceId: this.options.companionDeviceId ?? 1
21
24
  });
22
- await this.deps.pipeline.sendStanza(buildPairSuccessIq({
25
+ const pairSuccessIq = buildPairSuccessIq({
23
26
  deviceJid: this.options.deviceJid,
24
27
  deviceLid: this.options.deviceLid,
25
28
  platform: this.options.platform ?? 'IOS',
26
29
  deviceIdentityBytes
27
- }));
30
+ });
31
+ void this.deps
32
+ .waitForPairSuccessAck?.(pairSuccessIq.attrs.id)
33
+ .then(() => this.deps.pipeline.sendStanza(buildStreamErrorCode(FORCE_LOGIN_CODE)))
34
+ .catch(() => undefined);
35
+ await this.deps.pipeline.sendStanza(pairSuccessIq);
28
36
  }
29
37
  }
@@ -368,6 +368,9 @@ export class FakeWaServer {
368
368
  }, {
369
369
  timeoutMs: 15000
370
370
  });
371
+ },
372
+ waitForPairSuccessAck: async (pairSuccessIqId) => {
373
+ await session.expectIq({ id: pairSuccessIqId }, { timeoutMs: 15000 });
371
374
  }
372
375
  });
373
376
  await driver.run();
@@ -428,7 +431,7 @@ export class FakeWaServer {
428
431
  }
429
432
  ;
430
433
  [this.rootCa, this.serverStaticKeyPair] = await Promise.all([
431
- generateFakeNoiseRootCa(),
434
+ this.options.noiseRootCa ?? generateFakeNoiseRootCa(),
432
435
  X25519.generateKeyPair()
433
436
  ]);
434
437
  const mediaHandler = this.buildMediaRequestHandler();
package/dist/index.d.ts CHANGED
@@ -6,6 +6,12 @@ export { FakePeer } from './api/FakePeer';
6
6
  export type { CreateFakePeerOptions, ExpectGroupMessageOptions, ExpectMessageOptions, ReceivedMessage, SendMessageOptions } from './api/FakePeer';
7
7
  export { FakeServerSession, FakeWaServer } from './api/FakeWaServer';
8
8
  export type { BinaryNode, ExpectIqOptions, ExpectStanzaOptions, FakeSessionKeyInfo, FakeWaServerNoiseRootCa, FakeWaServerOptions, FakeWaServerPipelineListener, StanzaMatcher, WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from './api/FakeWaServer';
9
+ /**
10
+ * Signing half of the Noise root, needed only to pin
11
+ * {@link FakeWaServerOptions.noiseRootCa}. Readers of `server.noiseRootCa` get
12
+ * the public-only {@link FakeWaServerNoiseRootCa} instead.
13
+ */
14
+ export type { FakeNoiseRootCa } from './protocol/auth/cert-chain';
9
15
  export { IqExpectation, Scenario } from './api/Scenario';
10
16
  export type { AuthenticatedPipelineListener, ScenarioServer } from './api/Scenario';
11
17
  export { WaFakeConnection } from './infra/WaFakeConnection';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapo-js/fake-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Fake WhatsApp Web server used to test zapo-js",
5
5
  "license": "MIT",
6
6
  "author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",