@zapo-js/fake-server 1.0.0 → 1.2.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.
Files changed (73) hide show
  1. package/README.md +180 -33
  2. package/dist/api/FakeMobilePrimary.d.ts +55 -0
  3. package/dist/api/FakeMobilePrimary.js +106 -0
  4. package/dist/api/FakePairingDriver.js +1 -3
  5. package/dist/api/FakePeer.js +7 -1
  6. package/dist/api/FakeServerSession.d.ts +100 -0
  7. package/dist/api/FakeServerSession.js +355 -0
  8. package/dist/api/FakeWaServer.d.ts +145 -37
  9. package/dist/api/FakeWaServer.js +345 -270
  10. package/dist/api/ServerRegistries.d.ts +7 -0
  11. package/dist/api/ServerRegistries.js +20 -5
  12. package/dist/api/iq-handlers.d.ts +20 -1
  13. package/dist/api/iq-handlers.js +119 -1
  14. package/dist/bin.js +69 -21
  15. package/dist/esm/api/FakeMobilePrimary.js +102 -0
  16. package/dist/esm/api/FakePairingDriver.js +2 -4
  17. package/dist/esm/api/FakePeer.js +7 -1
  18. package/dist/esm/api/FakeServerSession.js +349 -0
  19. package/dist/esm/api/FakeWaServer.js +344 -270
  20. package/dist/esm/api/ServerRegistries.js +20 -5
  21. package/dist/esm/api/iq-handlers.js +119 -1
  22. package/dist/esm/bin.js +67 -19
  23. package/dist/esm/index.js +9 -1
  24. package/dist/esm/infra/WaFakeConnection.js +8 -22
  25. package/dist/esm/infra/WaFakeConnectionPipeline.js +38 -14
  26. package/dist/esm/infra/WaFakeTcpServer.js +69 -0
  27. package/dist/esm/infra/WaFakeWsServer.js +2 -1
  28. package/dist/esm/infra/socket-adapters.js +79 -0
  29. package/dist/esm/protocol/auth/cert-chain.js +13 -2
  30. package/dist/esm/protocol/auth/client-payload-validate.js +77 -1
  31. package/dist/esm/protocol/auth/pair-device.js +16 -1
  32. package/dist/esm/protocol/iq/companion-host.js +104 -0
  33. package/dist/esm/protocol/iq/link-code.js +90 -0
  34. package/dist/esm/protocol/iq/router.js +9 -3
  35. package/dist/esm/protocol/push/mobile-notification.js +97 -0
  36. package/dist/esm/state/fake-companion-host.js +158 -0
  37. package/dist/esm/transport/auth.js +7 -0
  38. package/dist/esm/transport/crypto.js +1 -1
  39. package/dist/esm/transport/util.js +2 -1
  40. package/dist/index.d.ts +19 -4
  41. package/dist/index.js +29 -1
  42. package/dist/infra/WaFakeConnection.d.ts +21 -3
  43. package/dist/infra/WaFakeConnection.js +8 -22
  44. package/dist/infra/WaFakeConnectionPipeline.d.ts +11 -2
  45. package/dist/infra/WaFakeConnectionPipeline.js +38 -14
  46. package/dist/infra/WaFakeTcpServer.d.ts +29 -0
  47. package/dist/infra/WaFakeTcpServer.js +73 -0
  48. package/dist/infra/WaFakeWsServer.js +2 -1
  49. package/dist/infra/socket-adapters.d.ts +18 -0
  50. package/dist/infra/socket-adapters.js +83 -0
  51. package/dist/protocol/auth/cert-chain.d.ts +4 -0
  52. package/dist/protocol/auth/cert-chain.js +13 -2
  53. package/dist/protocol/auth/client-payload-validate.d.ts +24 -0
  54. package/dist/protocol/auth/client-payload-validate.js +77 -1
  55. package/dist/protocol/auth/pair-device.d.ts +15 -1
  56. package/dist/protocol/auth/pair-device.js +16 -0
  57. package/dist/protocol/iq/companion-host.d.ts +52 -0
  58. package/dist/protocol/iq/companion-host.js +109 -0
  59. package/dist/protocol/iq/link-code.d.ts +46 -0
  60. package/dist/protocol/iq/link-code.js +95 -0
  61. package/dist/protocol/iq/router.d.ts +21 -2
  62. package/dist/protocol/iq/router.js +9 -3
  63. package/dist/protocol/push/mobile-notification.d.ts +58 -0
  64. package/dist/protocol/push/mobile-notification.js +102 -0
  65. package/dist/state/fake-companion-host.d.ts +73 -0
  66. package/dist/state/fake-companion-host.js +163 -0
  67. package/dist/transport/auth.d.ts +8 -0
  68. package/dist/transport/auth.js +8 -0
  69. package/dist/transport/crypto.d.ts +1 -1
  70. package/dist/transport/crypto.js +2 -1
  71. package/dist/transport/util.d.ts +2 -1
  72. package/dist/transport/util.js +3 -1
  73. package/package.json +6 -1
@@ -0,0 +1,158 @@
1
+ /** Per-session account state for a mobile primary hosting companion devices. */
2
+ import { proto } from '../transport/protos.js';
3
+ export class FakeCompanionHostState {
4
+ constructor() {
5
+ this.primaryIdentity = null;
6
+ this.companions = new Map();
7
+ this.reservedDeviceIds = new Set();
8
+ this.keyIndexList = null;
9
+ this.linkListeners = new Set();
10
+ this.revokeListeners = new Set();
11
+ }
12
+ /** The mobile primary bound to this session, or `null` for a web session. */
13
+ get primary() {
14
+ return this.primaryIdentity;
15
+ }
16
+ /**
17
+ * Records the primary at login. The first account to bind owns the session
18
+ * for good: a later login under a different number would otherwise mint
19
+ * device jids for one account while the companions of another are still
20
+ * tracked here.
21
+ */
22
+ bindPrimary(identity) {
23
+ if (this.primaryIdentity) {
24
+ return;
25
+ }
26
+ this.primaryIdentity = identity;
27
+ }
28
+ /** Companions linked from this account, in link order. */
29
+ linkedCompanions() {
30
+ return [...this.companions.values()];
31
+ }
32
+ /** Last key-index list the primary published, or `null` if it never did. */
33
+ publishedKeyIndexList() {
34
+ return this.keyIndexList;
35
+ }
36
+ /**
37
+ * Reserves the next free device slot. The primary owns device 0, so
38
+ * companions start at 1 and a revoked slot is reused only once no live
39
+ * companion holds it. The slot is held from here until the link is either
40
+ * recorded or released, because a link spans an await and two overlapping
41
+ * pairings would otherwise be handed the same jid.
42
+ */
43
+ allocateDeviceId() {
44
+ const taken = new Set([0, ...this.reservedDeviceIds]);
45
+ for (const companion of this.companions.values()) {
46
+ taken.add(companion.deviceId);
47
+ }
48
+ let candidate = 1;
49
+ while (taken.has(candidate)) {
50
+ candidate += 1;
51
+ }
52
+ this.reservedDeviceIds.add(candidate);
53
+ return candidate;
54
+ }
55
+ /** Frees a slot whose link never completed. */
56
+ releaseDeviceId(deviceId) {
57
+ this.reservedDeviceIds.delete(deviceId);
58
+ }
59
+ recordCompanion(companion) {
60
+ this.reservedDeviceIds.delete(companion.deviceId);
61
+ this.companions.set(companion.deviceJid, companion);
62
+ for (const listener of this.linkListeners) {
63
+ try {
64
+ listener(companion);
65
+ }
66
+ catch (error) {
67
+ void error;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Drops companions and notifies listeners. Returns the jids actually
73
+ * removed, so a revoke of an untracked device is observable as a no-op.
74
+ */
75
+ removeCompanions(deviceJids) {
76
+ const removed = [];
77
+ for (const deviceJid of deviceJids) {
78
+ if (this.companions.delete(deviceJid)) {
79
+ removed.push(deviceJid);
80
+ }
81
+ }
82
+ if (removed.length > 0) {
83
+ for (const listener of this.revokeListeners) {
84
+ try {
85
+ listener(removed);
86
+ }
87
+ catch (error) {
88
+ void error;
89
+ }
90
+ }
91
+ }
92
+ return removed;
93
+ }
94
+ removeAllCompanions() {
95
+ return this.removeCompanions([...this.companions.keys()]);
96
+ }
97
+ recordKeyIndexList(bytes, timestampSeconds) {
98
+ const decoded = decodeKeyIndexList(bytes);
99
+ this.keyIndexList = {
100
+ bytes,
101
+ timestampSeconds,
102
+ validIndexes: decoded.validIndexes,
103
+ currentIndex: decoded.currentIndex
104
+ };
105
+ }
106
+ onCompanionLinked(listener) {
107
+ this.linkListeners.add(listener);
108
+ return () => {
109
+ this.linkListeners.delete(listener);
110
+ };
111
+ }
112
+ onCompanionsRevoked(listener) {
113
+ this.revokeListeners.add(listener);
114
+ return () => {
115
+ this.revokeListeners.delete(listener);
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * Reads the ADV key index out of the `ADVSignedDeviceIdentityHMAC` the primary
121
+ * uploaded. Returns 0 when the payload does not decode, which keeps a
122
+ * malformed upload observable in the recorded companion instead of throwing
123
+ * inside the IQ handler.
124
+ */
125
+ export function readCompanionKeyIndex(deviceIdentityBytes) {
126
+ try {
127
+ const hmacWrapper = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityBytes);
128
+ if (!hmacWrapper.details) {
129
+ return 0;
130
+ }
131
+ const signed = proto.ADVSignedDeviceIdentity.decode(hmacWrapper.details);
132
+ if (!signed.details) {
133
+ return 0;
134
+ }
135
+ return proto.ADVDeviceIdentity.decode(signed.details).keyIndex ?? 0;
136
+ }
137
+ catch (error) {
138
+ void error;
139
+ return 0;
140
+ }
141
+ }
142
+ function decodeKeyIndexList(bytes) {
143
+ try {
144
+ const signed = proto.ADVSignedKeyIndexList.decode(bytes);
145
+ if (!signed.details) {
146
+ return { validIndexes: [], currentIndex: 0 };
147
+ }
148
+ const details = proto.ADVKeyIndexList.decode(signed.details);
149
+ return {
150
+ validIndexes: details.validIndexes ?? [],
151
+ currentIndex: details.currentIndex ?? 0
152
+ };
153
+ }
154
+ catch (error) {
155
+ void error;
156
+ return { validIndexes: [], currentIndex: 0 };
157
+ }
158
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Layer 1 – credential/store type wrapper.
3
+ *
4
+ * Re-exports the auth and store types from zapo-js so the fixture layer can
5
+ * seed a registered session without importing zapo-js directly.
6
+ */
7
+ export {};
@@ -4,5 +4,5 @@
4
4
  * Re-exports bit-exact cryptographic primitives from zapo-js. No protocol
5
5
  * interpretation, so reusing them does not create a test-vs-impl tautology.
6
6
  */
7
- export { aesCbcDecrypt, aesCbcEncrypt, aesGcmDecrypt, aesGcmEncrypt, Ed25519, hkdf, hkdfSplit, hmacSha256Sign, hmacSha512Sign, prependVersion, randomBytesAsync, readVersionedContent, sha256, sha512, toRawPubKey, toSerializedPubKey, X25519, xeddsaSign, xeddsaVerify } from 'zapo-js/crypto';
7
+ export { aesCbcDecrypt, aesCbcEncrypt, aesGcmDecrypt, aesGcmEncrypt, Ed25519, hkdf, hkdfSplit, hmacSha256Sign, hmacSha512Sign, prependVersion, randomBytesAsync, randomIntAsync, readVersionedContent, sha256, sha512, toRawPubKey, toSerializedPubKey, X25519, xeddsaSign, xeddsaVerify } from 'zapo-js/crypto';
8
8
  export { WaMediaCrypto } from 'zapo-js/media';
@@ -4,5 +4,6 @@
4
4
  * Re-exports bit-exact byte helpers from zapo-js so Layer 2/3 of the fake
5
5
  * server can use them without importing zapo-js directly.
6
6
  */
7
- export { bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url } from 'zapo-js/util';
7
+ export { bytesToBase64, bytesToBase64UrlSafe, bytesToHex, decodeBase64Url, toError } from 'zapo-js/util';
8
8
  export const TEXT_ENCODER = new TextEncoder();
9
+ export const TEXT_DECODER = new TextDecoder();
package/dist/index.d.ts CHANGED
@@ -1,14 +1,29 @@
1
+ export { createFakeMobilePrimary, seedFakeMobilePrimary } from './api/FakeMobilePrimary';
2
+ export type { FakeMobilePrimary, SeedFakeMobilePrimaryOptions } from './api/FakeMobilePrimary';
1
3
  export { FakePairingDriver } from './api/FakePairingDriver';
2
4
  export type { CompanionPairingMaterial, FakePairingDriverDeps, FakePairingDriverOptions } from './api/FakePairingDriver';
3
5
  export { FakePeer } from './api/FakePeer';
4
6
  export type { CreateFakePeerOptions, ExpectGroupMessageOptions, ExpectMessageOptions, ReceivedMessage, SendMessageOptions } from './api/FakePeer';
5
- export { FakeWaServer } from './api/FakeWaServer';
6
- export type { BinaryNode, ExpectIqOptions, ExpectStanzaOptions, FakeWaServerNoiseRootCa, FakeWaServerOptions, FakeWaServerPipelineListener, StanzaMatcher, WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from './api/FakeWaServer';
7
+ export { FakeServerSession, FakeWaServer } from './api/FakeWaServer';
8
+ export type { BinaryNode, ExpectIqOptions, ExpectStanzaOptions, FakeSessionKeyInfo, FakeWaServerNoiseRootCa, FakeWaServerOptions, FakeWaServerPipelineListener, StanzaMatcher, WaFakeAuthenticatedInfo, WaFakeConnectionPipeline } from './api/FakeWaServer';
7
9
  export { IqExpectation, Scenario } from './api/Scenario';
8
10
  export type { AuthenticatedPipelineListener, ScenarioServer } from './api/Scenario';
9
11
  export { WaFakeConnection } from './infra/WaFakeConnection';
10
- export type { WaFakeConnectionHandlers, WaFakeConnectionState } from './infra/WaFakeConnection';
11
- export type { WaFakeIqHandler, WaFakeIqMatcher, WaFakeIqResponder, WaFakeIqRouterEvents, WaFakeIqType } from './protocol/iq/router';
12
+ export type { WaFakeConnectionHandlers, WaFakeConnectionState, WaFakeSocketEvents, WaFakeSocketLike } from './infra/WaFakeConnection';
13
+ export { WaFakeTcpServer } from './infra/WaFakeTcpServer';
14
+ export type { WaFakeTcpServerListenInfo, WaFakeTcpServerOptions } from './infra/WaFakeTcpServer';
15
+ export { createTcpSocketAdapter, createWebSocketAdapter } from './infra/socket-adapters';
16
+ export { ClientPayloadValidationError, parseClientPayload } from './protocol/auth/client-payload-validate';
17
+ export type { ClientPayloadFlavor, LoginPayload, MobileLoginDetails, ParsedClientPayload, RegistrationPayload } from './protocol/auth/client-payload-validate';
18
+ export type { WaFakeIqConnection, WaFakeIqContext, WaFakeIqHandler, WaFakeIqMatcher, WaFakeIqResponder, WaFakeIqRouterEvents, WaFakeIqType } from './protocol/iq/router';
19
+ export { parseKeyIndexListPublish, parsePairDeviceUpload, parseRemoveCompanionDevice } from './protocol/iq/companion-host';
20
+ export type { ParsedKeyIndexListPublish, ParsedPairDeviceUpload, ParsedRemoveCompanionDevice } from './protocol/iq/companion-host';
21
+ export { buildCompanionHelloResultContent, buildLinkCodeNotification, parseLinkCodeStanza } from './protocol/iq/link-code';
22
+ export type { BuildLinkCodeNotificationInput, LinkCodeStage, ParsedLinkCodeStanza } from './protocol/iq/link-code';
23
+ export { buildAccountSyncDevicesNotification, buildAccountTakeoverNotice, buildRegistrationCodeNotification } from './protocol/push/mobile-notification';
24
+ export type { BuildAccountSyncDevicesInput, BuildAccountTakeoverNoticeInput, BuildRegistrationCodeNotificationInput, FakeAccountDevice } from './protocol/push/mobile-notification';
25
+ export { FakeCompanionHostState, readCompanionKeyIndex } from './state/fake-companion-host';
26
+ export type { FakeLinkedCompanion, FakeMobilePrimaryIdentity, FakePublishedKeyIndexList } from './state/fake-companion-host';
12
27
  export { buildAppStateSyncFullResult, buildAppStateSyncResult, buildServerSyncNotification, parseAppStateSyncRequest } from './protocol/iq/appstate-sync';
13
28
  export type { BuildAppStateSyncFullResultInput, BuildAppStateSyncResultInput, BuildServerSyncNotificationInput, FakeAppStateCollectionName, FakeAppStateCollectionPayload } from './protocol/iq/appstate-sync';
14
29
  export { buildIqError, buildIqResult } from './protocol/iq/router';
package/dist/index.js CHANGED
@@ -1,17 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildStreamErrorXmlNotWellFormed = exports.buildStreamErrorReplaced = exports.buildStreamErrorDeviceRemoved = exports.buildStreamErrorCode = exports.buildStreamErrorAck = exports.PreKeyUploadParseError = exports.parsePreKeyUploadIq = exports.parsePairingQrString = exports.buildPairSuccessIq = exports.buildPairDeviceIq = exports.generateFakePrimaryDevice = exports.buildAdvSignedDeviceIdentity = exports.FakeSenderKey = exports.FakePeerDoubleRatchet = exports.generateFakePeerIdentity = exports.buildReceipt = exports.buildIncomingPresence = exports.buildNotification = exports.buildGroupNotification = exports.buildMessage = exports.FakeMediaStore = exports.FakeAppStateCollection = exports.FakeAppStateCrypto = exports.APP_STATE_EMPTY_LT_HASH = exports.encodeHistorySyncPlaintext = exports.buildHistorySyncMessage = exports.buildHistorySyncExternalMessage = exports.buildAppStateSyncKeyShareMessage = exports.buildIncomingErrorStanza = exports.buildChatstate = exports.buildFailure = exports.buildCall = exports.buildIqResult = exports.buildIqError = exports.parseAppStateSyncRequest = exports.buildServerSyncNotification = exports.buildAppStateSyncResult = exports.buildAppStateSyncFullResult = exports.WaFakeConnection = exports.Scenario = exports.IqExpectation = exports.FakeWaServer = exports.FakePeer = exports.FakePairingDriver = void 0;
3
+ exports.FakePeerDoubleRatchet = exports.generateFakePeerIdentity = exports.buildReceipt = exports.buildIncomingPresence = exports.buildNotification = exports.buildGroupNotification = exports.buildMessage = exports.FakeMediaStore = exports.FakeAppStateCollection = exports.FakeAppStateCrypto = exports.APP_STATE_EMPTY_LT_HASH = exports.encodeHistorySyncPlaintext = exports.buildHistorySyncMessage = exports.buildHistorySyncExternalMessage = exports.buildAppStateSyncKeyShareMessage = exports.buildIncomingErrorStanza = exports.buildChatstate = exports.buildFailure = exports.buildCall = exports.buildIqResult = exports.buildIqError = exports.parseAppStateSyncRequest = exports.buildServerSyncNotification = exports.buildAppStateSyncResult = exports.buildAppStateSyncFullResult = exports.readCompanionKeyIndex = exports.FakeCompanionHostState = exports.buildRegistrationCodeNotification = exports.buildAccountTakeoverNotice = exports.buildAccountSyncDevicesNotification = exports.parseLinkCodeStanza = exports.buildLinkCodeNotification = exports.buildCompanionHelloResultContent = exports.parseRemoveCompanionDevice = exports.parsePairDeviceUpload = exports.parseKeyIndexListPublish = exports.parseClientPayload = exports.ClientPayloadValidationError = exports.createWebSocketAdapter = exports.createTcpSocketAdapter = exports.WaFakeTcpServer = exports.WaFakeConnection = exports.Scenario = exports.IqExpectation = exports.FakeWaServer = exports.FakeServerSession = exports.FakePeer = exports.FakePairingDriver = exports.seedFakeMobilePrimary = exports.createFakeMobilePrimary = void 0;
4
+ exports.buildStreamErrorXmlNotWellFormed = exports.buildStreamErrorReplaced = exports.buildStreamErrorDeviceRemoved = exports.buildStreamErrorCode = exports.buildStreamErrorAck = exports.PreKeyUploadParseError = exports.parsePreKeyUploadIq = exports.parsePairingQrString = exports.buildPairSuccessIq = exports.buildPairDeviceIq = exports.generateFakePrimaryDevice = exports.buildAdvSignedDeviceIdentity = exports.FakeSenderKey = void 0;
5
+ var FakeMobilePrimary_1 = require("./api/FakeMobilePrimary");
6
+ Object.defineProperty(exports, "createFakeMobilePrimary", { enumerable: true, get: function () { return FakeMobilePrimary_1.createFakeMobilePrimary; } });
7
+ Object.defineProperty(exports, "seedFakeMobilePrimary", { enumerable: true, get: function () { return FakeMobilePrimary_1.seedFakeMobilePrimary; } });
4
8
  var FakePairingDriver_1 = require("./api/FakePairingDriver");
5
9
  Object.defineProperty(exports, "FakePairingDriver", { enumerable: true, get: function () { return FakePairingDriver_1.FakePairingDriver; } });
6
10
  var FakePeer_1 = require("./api/FakePeer");
7
11
  Object.defineProperty(exports, "FakePeer", { enumerable: true, get: function () { return FakePeer_1.FakePeer; } });
8
12
  var FakeWaServer_1 = require("./api/FakeWaServer");
13
+ Object.defineProperty(exports, "FakeServerSession", { enumerable: true, get: function () { return FakeWaServer_1.FakeServerSession; } });
9
14
  Object.defineProperty(exports, "FakeWaServer", { enumerable: true, get: function () { return FakeWaServer_1.FakeWaServer; } });
10
15
  var Scenario_1 = require("./api/Scenario");
11
16
  Object.defineProperty(exports, "IqExpectation", { enumerable: true, get: function () { return Scenario_1.IqExpectation; } });
12
17
  Object.defineProperty(exports, "Scenario", { enumerable: true, get: function () { return Scenario_1.Scenario; } });
13
18
  var WaFakeConnection_1 = require("./infra/WaFakeConnection");
14
19
  Object.defineProperty(exports, "WaFakeConnection", { enumerable: true, get: function () { return WaFakeConnection_1.WaFakeConnection; } });
20
+ var WaFakeTcpServer_1 = require("./infra/WaFakeTcpServer");
21
+ Object.defineProperty(exports, "WaFakeTcpServer", { enumerable: true, get: function () { return WaFakeTcpServer_1.WaFakeTcpServer; } });
22
+ var socket_adapters_1 = require("./infra/socket-adapters");
23
+ Object.defineProperty(exports, "createTcpSocketAdapter", { enumerable: true, get: function () { return socket_adapters_1.createTcpSocketAdapter; } });
24
+ Object.defineProperty(exports, "createWebSocketAdapter", { enumerable: true, get: function () { return socket_adapters_1.createWebSocketAdapter; } });
25
+ var client_payload_validate_1 = require("./protocol/auth/client-payload-validate");
26
+ Object.defineProperty(exports, "ClientPayloadValidationError", { enumerable: true, get: function () { return client_payload_validate_1.ClientPayloadValidationError; } });
27
+ Object.defineProperty(exports, "parseClientPayload", { enumerable: true, get: function () { return client_payload_validate_1.parseClientPayload; } });
28
+ var companion_host_1 = require("./protocol/iq/companion-host");
29
+ Object.defineProperty(exports, "parseKeyIndexListPublish", { enumerable: true, get: function () { return companion_host_1.parseKeyIndexListPublish; } });
30
+ Object.defineProperty(exports, "parsePairDeviceUpload", { enumerable: true, get: function () { return companion_host_1.parsePairDeviceUpload; } });
31
+ Object.defineProperty(exports, "parseRemoveCompanionDevice", { enumerable: true, get: function () { return companion_host_1.parseRemoveCompanionDevice; } });
32
+ var link_code_1 = require("./protocol/iq/link-code");
33
+ Object.defineProperty(exports, "buildCompanionHelloResultContent", { enumerable: true, get: function () { return link_code_1.buildCompanionHelloResultContent; } });
34
+ Object.defineProperty(exports, "buildLinkCodeNotification", { enumerable: true, get: function () { return link_code_1.buildLinkCodeNotification; } });
35
+ Object.defineProperty(exports, "parseLinkCodeStanza", { enumerable: true, get: function () { return link_code_1.parseLinkCodeStanza; } });
36
+ var mobile_notification_1 = require("./protocol/push/mobile-notification");
37
+ Object.defineProperty(exports, "buildAccountSyncDevicesNotification", { enumerable: true, get: function () { return mobile_notification_1.buildAccountSyncDevicesNotification; } });
38
+ Object.defineProperty(exports, "buildAccountTakeoverNotice", { enumerable: true, get: function () { return mobile_notification_1.buildAccountTakeoverNotice; } });
39
+ Object.defineProperty(exports, "buildRegistrationCodeNotification", { enumerable: true, get: function () { return mobile_notification_1.buildRegistrationCodeNotification; } });
40
+ var fake_companion_host_1 = require("./state/fake-companion-host");
41
+ Object.defineProperty(exports, "FakeCompanionHostState", { enumerable: true, get: function () { return fake_companion_host_1.FakeCompanionHostState; } });
42
+ Object.defineProperty(exports, "readCompanionKeyIndex", { enumerable: true, get: function () { return fake_companion_host_1.readCompanionKeyIndex; } });
15
43
  var appstate_sync_1 = require("./protocol/iq/appstate-sync");
16
44
  Object.defineProperty(exports, "buildAppStateSyncFullResult", { enumerable: true, get: function () { return appstate_sync_1.buildAppStateSyncFullResult; } });
17
45
  Object.defineProperty(exports, "buildAppStateSyncResult", { enumerable: true, get: function () { return appstate_sync_1.buildAppStateSyncResult; } });
@@ -1,4 +1,3 @@
1
- import type { WebSocket } from 'ws';
2
1
  export type WaFakeConnectionState = 'open' | 'closing' | 'closed';
3
2
  export interface WaFakeConnectionHandlers {
4
3
  readonly onFrame?: (frame: Uint8Array) => void;
@@ -8,15 +7,34 @@ export interface WaFakeConnectionHandlers {
8
7
  }) => void;
9
8
  readonly onError?: (error: Error) => void;
10
9
  }
10
+ /** Events a carrier adapter pushes into the connection it backs. */
11
+ export interface WaFakeSocketEvents {
12
+ readonly onFrame: (frame: Uint8Array) => void;
13
+ readonly onClose: (info: {
14
+ readonly code: number;
15
+ readonly reason: string;
16
+ }) => void;
17
+ readonly onError: (error: Error) => void;
18
+ }
19
+ /**
20
+ * Carrier a connection speaks over. Each transport adapts its own quirks (the
21
+ * WebSocket fragment array, the absence of close codes on a TCP stream) behind
22
+ * this interface, so every layer above stays carrier-agnostic: the WebSocket
23
+ * listener serves companions, the TCP listener serves the mobile transport.
24
+ */
25
+ export interface WaFakeSocketLike {
26
+ readonly send: (frame: Uint8Array) => void;
27
+ readonly close: (code: number, reason: string) => void;
28
+ readonly listen: (events: WaFakeSocketEvents) => void;
29
+ }
11
30
  export declare class WaFakeConnection {
12
31
  readonly id: string;
13
32
  private readonly socket;
14
33
  private handlers;
15
34
  private currentState;
16
- constructor(id: string, socket: WebSocket);
35
+ constructor(id: string, socket: WaFakeSocketLike);
17
36
  get state(): WaFakeConnectionState;
18
37
  setHandlers(handlers: WaFakeConnectionHandlers): void;
19
38
  sendFrame(frame: Uint8Array): void;
20
39
  close(code?: number, reason?: string): void;
21
- private bindSocketEvents;
22
40
  }
@@ -7,7 +7,14 @@ class WaFakeConnection {
7
7
  this.currentState = 'open';
8
8
  this.id = id;
9
9
  this.socket = socket;
10
- this.bindSocketEvents();
10
+ this.socket.listen({
11
+ onFrame: (frame) => this.handlers.onFrame?.(frame),
12
+ onClose: (info) => {
13
+ this.currentState = 'closed';
14
+ this.handlers.onClose?.(info);
15
+ },
16
+ onError: (error) => this.handlers.onError?.(error)
17
+ });
11
18
  }
12
19
  get state() {
13
20
  return this.currentState;
@@ -28,26 +35,5 @@ class WaFakeConnection {
28
35
  this.currentState = 'closing';
29
36
  this.socket.close(code, reason);
30
37
  }
31
- bindSocketEvents() {
32
- this.socket.on('message', (data, isBinary) => {
33
- if (!isBinary) {
34
- this.handlers.onError?.(new Error('received unexpected text frame'));
35
- return;
36
- }
37
- const frame = data instanceof Uint8Array
38
- ? data
39
- : Array.isArray(data)
40
- ? new Uint8Array(Buffer.concat(data))
41
- : new Uint8Array(data);
42
- this.handlers.onFrame?.(frame);
43
- });
44
- this.socket.on('close', (code, reasonBuf) => {
45
- this.currentState = 'closed';
46
- this.handlers.onClose?.({ code, reason: reasonBuf.toString('utf8') });
47
- });
48
- this.socket.on('error', (error) => {
49
- this.handlers.onError?.(error);
50
- });
51
- }
52
38
  }
53
39
  exports.WaFakeConnection = WaFakeConnection;
@@ -2,7 +2,6 @@
2
2
  import { type FakeNoiseRootCa } from '../protocol/auth/cert-chain';
3
3
  import { type ParsedClientPayload } from '../protocol/auth/client-payload-validate';
4
4
  import { buildSuccessNode } from '../protocol/auth/success-node';
5
- import { type WaFakeIqRouter } from '../protocol/iq/router';
6
5
  import { type BinaryNode } from '../transport/codec';
7
6
  import { type SignalKeyPair } from '../transport/crypto';
8
7
  import type { WaFakeConnection } from './WaFakeConnection';
@@ -10,7 +9,12 @@ export interface WaFakeConnectionPipelineConfig {
10
9
  readonly connection: WaFakeConnection;
11
10
  readonly rootCa: FakeNoiseRootCa;
12
11
  readonly serverStaticKeyPair: SignalKeyPair;
13
- readonly iqRouter: WaFakeIqRouter;
12
+ /**
13
+ * Routes an inbound IQ against the session this connection is bound to.
14
+ * The server resolves the session per connection, so the pipeline stays
15
+ * unaware of session identity.
16
+ */
17
+ readonly routeIq: (iq: BinaryNode) => Promise<BinaryNode | null>;
14
18
  readonly successNodeAttributes?: Parameters<typeof buildSuccessNode>[0];
15
19
  }
16
20
  export interface WaFakeConnectionPipelineEvents {
@@ -33,9 +37,13 @@ export declare class WaFakeConnectionPipeline {
33
37
  private events;
34
38
  private state;
35
39
  private chain;
40
+ private authenticatedPayload;
41
+ private clientPrologue;
36
42
  constructor(config: WaFakeConnectionPipelineConfig);
37
43
  setEvents(events: WaFakeConnectionPipelineEvents): void;
38
44
  isAuthenticated(): boolean;
45
+ /** Parsed ClientPayload of the authenticated connection; null before auth. */
46
+ get clientPayload(): ParsedClientPayload | null;
39
47
  sendStanza(node: BinaryNode): Promise<void>;
40
48
  private onPrologue;
41
49
  private scheduleFrame;
@@ -45,6 +53,7 @@ export declare class WaFakeConnectionPipeline {
45
53
  /** Server-side IK path used when the client already cached our static key. */
46
54
  private handleIkClientHello;
47
55
  private handleClientFinish;
56
+ private completeAuthentication;
48
57
  private handleAuthenticatedFrame;
49
58
  private fail;
50
59
  }
@@ -13,16 +13,20 @@ const WaFakeNoiseHandshake_1 = require("./WaFakeNoiseHandshake");
13
13
  const WaFakeTransport_1 = require("./WaFakeTransport");
14
14
  const NOISE_XX_NAME = new TextEncoder().encode('Noise_XX_25519_AESGCM_SHA256\0\0\0\0');
15
15
  const NOISE_IK_NAME = new TextEncoder().encode('Noise_IK_25519_AESGCM_SHA256\0\0\0\0');
16
- const PROLOGUE = new Uint8Array([0x57, 0x41, 0x06, 0x03]);
16
+ const PROLOGUE_MAGIC_W = 0x57;
17
+ const PROLOGUE_MAGIC_A = 0x41;
18
+ const DEFAULT_PROLOGUE = new Uint8Array([PROLOGUE_MAGIC_W, PROLOGUE_MAGIC_A, 0x06, 0x03]);
17
19
  class WaFakeConnectionPipeline {
18
20
  constructor(config) {
19
21
  this.events = {};
20
22
  this.state = { kind: 'awaiting_prologue' };
21
23
  this.chain = Promise.resolve();
24
+ this.authenticatedPayload = null;
25
+ this.clientPrologue = DEFAULT_PROLOGUE;
22
26
  this.config = config;
23
27
  this.frameSocket = new WaFakeFrameSocket_1.WaFakeFrameSocket(config.connection);
24
28
  this.frameSocket.setHandlers({
25
- onPrologue: () => this.onPrologue(),
29
+ onPrologue: (prologue) => this.onPrologue(prologue),
26
30
  onFrame: (frame) => this.scheduleFrame(frame),
27
31
  onClose: (info) => {
28
32
  this.state = { kind: 'closed' };
@@ -37,6 +41,10 @@ class WaFakeConnectionPipeline {
37
41
  isAuthenticated() {
38
42
  return this.state.kind === 'authenticated';
39
43
  }
44
+ /** Parsed ClientPayload of the authenticated connection; null before auth. */
45
+ get clientPayload() {
46
+ return this.authenticatedPayload;
47
+ }
40
48
  async sendStanza(node) {
41
49
  if (this.state.kind !== 'authenticated') {
42
50
  throw new Error(`cannot send stanza while pipeline is in state "${this.state.kind}"`);
@@ -46,11 +54,20 @@ class WaFakeConnectionPipeline {
46
54
  const ciphertext = await this.state.transport.encryptFrame(stanza);
47
55
  this.frameSocket.sendFrame(ciphertext);
48
56
  }
49
- onPrologue() {
57
+ onPrologue(prologue) {
50
58
  if (this.state.kind !== 'awaiting_prologue') {
51
59
  this.events.onError?.(new Error('received second prologue from client'));
52
60
  return;
53
61
  }
62
+ // Mix back the header the client actually sent instead of a fixed one:
63
+ // it is hashed into the handshake, and a client is free to advertise a
64
+ // protocol/dict version other than the web client's 6/3.
65
+ this.clientPrologue = new Uint8Array([
66
+ PROLOGUE_MAGIC_W,
67
+ PROLOGUE_MAGIC_A,
68
+ prologue.protocolVersion,
69
+ prologue.dictVersion
70
+ ]);
54
71
  this.state = { kind: 'awaiting_client_hello' };
55
72
  }
56
73
  scheduleFrame(frame) {
@@ -92,7 +109,7 @@ class WaFakeConnectionPipeline {
92
109
  }
93
110
  async handleXxClientHello(clientEphemeralPub) {
94
111
  const handshake = new WaFakeNoiseHandshake_1.WaFakeNoiseHandshake();
95
- handshake.start(NOISE_XX_NAME, PROLOGUE);
112
+ handshake.start(NOISE_XX_NAME, this.clientPrologue);
96
113
  handshake.authenticate(clientEphemeralPub);
97
114
  const serverEphemeral = await crypto_1.X25519.generateKeyPair();
98
115
  handshake.authenticate(serverEphemeral.pubKey);
@@ -128,7 +145,7 @@ class WaFakeConnectionPipeline {
128
145
  const encryptedClientStatic = clientHello.static;
129
146
  const encryptedClientPayload = clientHello.payload;
130
147
  const handshake = new WaFakeNoiseHandshake_1.WaFakeNoiseHandshake();
131
- handshake.start(NOISE_IK_NAME, PROLOGUE);
148
+ handshake.start(NOISE_IK_NAME, this.clientPrologue);
132
149
  handshake.authenticate(this.config.serverStaticKeyPair.pubKey);
133
150
  handshake.authenticate(clientEphemeralPub);
134
151
  handshake.mixIntoKey(await crypto_1.X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
@@ -156,14 +173,7 @@ class WaFakeConnectionPipeline {
156
173
  }
157
174
  }).finish();
158
175
  this.frameSocket.sendFrame(serverHello);
159
- const keys = handshake.finish();
160
- const transport = new WaFakeTransport_1.WaFakeTransport({
161
- recvKey: keys.recvKey,
162
- sendKey: keys.sendKey
163
- });
164
- this.state = { kind: 'authenticated', transport };
165
- await this.sendStanza((0, success_node_1.buildSuccessNode)(this.config.successNodeAttributes));
166
- this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
176
+ await this.completeAuthentication(handshake, clientPayload, clientStaticKey);
167
177
  }
168
178
  async handleClientFinish(frame, handshake, serverEphemeralKeyPair) {
169
179
  const parsed = protos_1.proto.HandshakeMessage.decode(frame);
@@ -175,13 +185,27 @@ class WaFakeConnectionPipeline {
175
185
  handshake.mixIntoKey(await crypto_1.X25519.scalarMult(serverEphemeralKeyPair.privKey, clientStaticKey));
176
186
  const clientPayloadBytes = handshake.decrypt(clientFinish.payload);
177
187
  const clientPayload = (0, client_payload_validate_1.parseClientPayload)(clientPayloadBytes);
188
+ await this.completeAuthentication(handshake, clientPayload, clientStaticKey);
189
+ }
190
+ async completeAuthentication(handshake, clientPayload, clientStaticKey) {
178
191
  const keys = handshake.finish();
179
192
  const transport = new WaFakeTransport_1.WaFakeTransport({
180
193
  recvKey: keys.recvKey,
181
194
  sendKey: keys.sendKey
182
195
  });
183
196
  this.state = { kind: 'authenticated', transport };
197
+ this.authenticatedPayload = clientPayload;
184
198
  await this.sendStanza((0, success_node_1.buildSuccessNode)(this.config.successNodeAttributes));
199
+ if (clientPayload.kind === 'login') {
200
+ // Real WA drains offline messages after login and closes the
201
+ // drain with this bulletin. Baileys-family clients buffer their
202
+ // events until it arrives, so send it eagerly with count=0.
203
+ await this.sendStanza({
204
+ tag: 'ib',
205
+ attrs: { from: 's.whatsapp.net' },
206
+ content: [{ tag: 'offline', attrs: { count: '0' } }]
207
+ });
208
+ }
185
209
  this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
186
210
  }
187
211
  async handleAuthenticatedFrame(frame, transport) {
@@ -189,7 +213,7 @@ class WaFakeConnectionPipeline {
189
213
  const node = await (0, codec_1.decodeBinaryNodeStanza)(stanzaBytes);
190
214
  this.events.onStanza?.(node);
191
215
  if (node.tag === 'iq') {
192
- const response = await this.config.iqRouter.route(node);
216
+ const response = await this.config.routeIq(node);
193
217
  if (response !== null) {
194
218
  await this.sendStanza(response);
195
219
  }
@@ -0,0 +1,29 @@
1
+ import { WaFakeConnection } from './WaFakeConnection';
2
+ export interface WaFakeTcpServerOptions {
3
+ readonly host?: string;
4
+ readonly port?: number;
5
+ }
6
+ export interface WaFakeTcpServerListenInfo {
7
+ readonly host: string;
8
+ readonly port: number;
9
+ /** `tcp://host:port` – feed straight into `mobileTransport.tcpUrl`. */
10
+ readonly url: string;
11
+ }
12
+ export type WaFakeTcpServerConnectionListener = (connection: WaFakeConnection) => void;
13
+ /**
14
+ * Raw-TCP listener for the WhatsApp Mobile transport. The mobile client dials
15
+ * `tcp://host:port` instead of upgrading to a WebSocket, but speaks the exact
16
+ * same prologue + length-prefixed frames on top, so every layer above the
17
+ * carrier is shared with {@link WaFakeWsServer}.
18
+ */
19
+ export declare class WaFakeTcpServer {
20
+ private readonly options;
21
+ private server;
22
+ private connectionListener;
23
+ private readonly sockets;
24
+ private nextConnectionId;
25
+ constructor(options?: WaFakeTcpServerOptions);
26
+ onConnection(listener: WaFakeTcpServerConnectionListener): void;
27
+ listen(): Promise<WaFakeTcpServerListenInfo>;
28
+ close(): Promise<void>;
29
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WaFakeTcpServer = void 0;
4
+ const node_net_1 = require("node:net");
5
+ const socket_adapters_1 = require("./socket-adapters");
6
+ const WaFakeConnection_1 = require("./WaFakeConnection");
7
+ /**
8
+ * Raw-TCP listener for the WhatsApp Mobile transport. The mobile client dials
9
+ * `tcp://host:port` instead of upgrading to a WebSocket, but speaks the exact
10
+ * same prologue + length-prefixed frames on top, so every layer above the
11
+ * carrier is shared with {@link WaFakeWsServer}.
12
+ */
13
+ class WaFakeTcpServer {
14
+ constructor(options = {}) {
15
+ this.server = null;
16
+ this.connectionListener = null;
17
+ this.sockets = new Set();
18
+ this.nextConnectionId = 0;
19
+ this.options = {
20
+ host: options.host ?? '127.0.0.1',
21
+ port: options.port ?? 0
22
+ };
23
+ }
24
+ onConnection(listener) {
25
+ this.connectionListener = listener;
26
+ }
27
+ async listen() {
28
+ if (this.server) {
29
+ throw new Error('fake tcp server is already listening');
30
+ }
31
+ const server = (0, node_net_1.createServer)((socket) => {
32
+ this.sockets.add(socket);
33
+ socket.on('close', () => this.sockets.delete(socket));
34
+ const id = `t${this.nextConnectionId++}`;
35
+ this.connectionListener?.(new WaFakeConnection_1.WaFakeConnection(id, (0, socket_adapters_1.createTcpSocketAdapter)(socket)));
36
+ });
37
+ await new Promise((resolve, reject) => {
38
+ const onError = (error) => {
39
+ server.off('listening', onListening);
40
+ reject(error);
41
+ };
42
+ const onListening = () => {
43
+ server.off('error', onError);
44
+ resolve();
45
+ };
46
+ server.once('error', onError);
47
+ server.once('listening', onListening);
48
+ server.listen(this.options.port, this.options.host);
49
+ });
50
+ this.server = server;
51
+ const address = server.address();
52
+ return {
53
+ host: address.address,
54
+ port: address.port,
55
+ url: `tcp://${address.address}:${address.port}`
56
+ };
57
+ }
58
+ async close() {
59
+ const server = this.server;
60
+ if (!server) {
61
+ return;
62
+ }
63
+ for (const socket of this.sockets) {
64
+ socket.destroy();
65
+ }
66
+ this.sockets.clear();
67
+ await new Promise((resolve, reject) => {
68
+ server.close((error) => (error ? reject(error) : resolve()));
69
+ });
70
+ this.server = null;
71
+ }
72
+ }
73
+ exports.WaFakeTcpServer = WaFakeTcpServer;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WaFakeWsServer = void 0;
4
4
  const node_http_1 = require("node:http");
5
5
  const ws_1 = require("ws");
6
+ const socket_adapters_1 = require("./socket-adapters");
6
7
  const WaFakeConnection_1 = require("./WaFakeConnection");
7
8
  class WaFakeWsServer {
8
9
  constructor(options = {}) {
@@ -52,7 +53,7 @@ class WaFakeWsServer {
52
53
  const wsServer = new ws_1.WebSocketServer({ server: httpServer, path: this.options.path });
53
54
  wsServer.on('connection', (socket) => {
54
55
  const id = `c${this.nextConnectionId++}`;
55
- const connection = new WaFakeConnection_1.WaFakeConnection(id, socket);
56
+ const connection = new WaFakeConnection_1.WaFakeConnection(id, (0, socket_adapters_1.createWebSocketAdapter)(socket));
56
57
  this.connectionListener?.(connection);
57
58
  });
58
59
  await new Promise((resolve, reject) => {