zapo-js 1.6.2 → 1.7.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.
Files changed (218) hide show
  1. package/README.md +26 -12
  2. package/dist/abprops-spec.d.ts +10 -0
  3. package/dist/abprops-spec.js +25 -0
  4. package/dist/appstate/crypto/WaAppStateCrypto.js +4 -6
  5. package/dist/auth/WaAuthClient.d.ts +14 -1
  6. package/dist/auth/WaAuthClient.js +22 -1
  7. package/dist/auth/credentials-flow.d.ts +3 -1
  8. package/dist/auth/credentials-flow.js +3 -0
  9. package/dist/auth/pairing/WaPairingFlow.d.ts +5 -0
  10. package/dist/auth/pairing/WaPairingFlow.js +66 -14
  11. package/dist/auth/types.d.ts +13 -0
  12. package/dist/client/WaClient.d.ts +11 -0
  13. package/dist/client/WaClient.js +60 -0
  14. package/dist/client/WaClientFactory.d.ts +2 -2
  15. package/dist/client/WaClientFactory.js +53 -13
  16. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  17. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  18. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  19. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  20. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  21. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  22. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  23. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +252 -57
  24. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  25. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  26. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  27. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  28. package/dist/client/events/dirty.d.ts +3 -0
  29. package/dist/client/events/dirty.js +12 -7
  30. package/dist/client/events/offline.d.ts +9 -0
  31. package/dist/client/events/offline.js +95 -0
  32. package/dist/client/events/privacy.d.ts +45 -0
  33. package/dist/client/events/privacy.js +130 -0
  34. package/dist/client/messaging/group-metadata.d.ts +12 -0
  35. package/dist/client/messaging/group-metadata.js +24 -8
  36. package/dist/client/messaging/link-preview.d.ts +7 -0
  37. package/dist/client/messaging/link-preview.js +27 -0
  38. package/dist/client/messaging/messages.js +16 -12
  39. package/dist/client/newsletter/content.d.ts +4 -1
  40. package/dist/client/newsletter/content.js +43 -18
  41. package/dist/client/newsletter/discovery.js +4 -2
  42. package/dist/client/newsletter/messaging.d.ts +2 -0
  43. package/dist/client/newsletter/messaging.js +2 -1
  44. package/dist/client/newsletter/mex.d.ts +2 -2
  45. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  46. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  47. package/dist/client/persistence/ephemeral-setting.js +120 -0
  48. package/dist/client/persistence/group-history.d.ts +29 -0
  49. package/dist/client/persistence/group-history.js +149 -0
  50. package/dist/client/persistence/history-blob.d.ts +25 -0
  51. package/dist/client/persistence/history-blob.js +38 -0
  52. package/dist/client/persistence/history-sync.d.ts +20 -0
  53. package/dist/client/persistence/history-sync.js +165 -71
  54. package/dist/client/plugins/types.d.ts +10 -2
  55. package/dist/client/types.d.ts +150 -16
  56. package/dist/crypto/core/index.d.ts +1 -1
  57. package/dist/crypto/core/index.js +2 -1
  58. package/dist/crypto/core/primitives.d.ts +19 -0
  59. package/dist/crypto/core/primitives.js +48 -0
  60. package/dist/crypto/core/xeddsa.js +35 -4
  61. package/dist/crypto/curves/X25519.js +29 -2
  62. package/dist/crypto/nativeBackend.d.ts +20 -0
  63. package/dist/crypto/nativeBackend.js +95 -0
  64. package/dist/esm/abprops-spec.js +15 -0
  65. package/dist/esm/appstate/crypto/WaAppStateCrypto.js +4 -6
  66. package/dist/esm/auth/WaAuthClient.js +22 -1
  67. package/dist/esm/auth/credentials-flow.js +3 -0
  68. package/dist/esm/auth/pairing/WaPairingFlow.js +67 -15
  69. package/dist/esm/client/WaClient.js +60 -0
  70. package/dist/esm/client/WaClientFactory.js +54 -14
  71. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  72. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  73. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  74. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +254 -60
  75. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  76. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  77. package/dist/esm/client/events/dirty.js +12 -7
  78. package/dist/esm/client/events/offline.js +92 -0
  79. package/dist/esm/client/events/privacy.js +123 -0
  80. package/dist/esm/client/messaging/group-metadata.js +25 -9
  81. package/dist/esm/client/messaging/link-preview.js +29 -2
  82. package/dist/esm/client/messaging/messages.js +16 -12
  83. package/dist/esm/client/newsletter/content.js +44 -19
  84. package/dist/esm/client/newsletter/discovery.js +4 -2
  85. package/dist/esm/client/newsletter/messaging.js +2 -1
  86. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  87. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  88. package/dist/esm/client/persistence/group-history.js +145 -0
  89. package/dist/esm/client/persistence/history-blob.js +34 -0
  90. package/dist/esm/client/persistence/history-sync.js +157 -65
  91. package/dist/esm/crypto/core/index.js +1 -1
  92. package/dist/esm/crypto/core/primitives.js +47 -0
  93. package/dist/esm/crypto/core/xeddsa.js +36 -5
  94. package/dist/esm/crypto/curves/X25519.js +29 -2
  95. package/dist/esm/crypto/nativeBackend.js +93 -0
  96. package/dist/esm/index.js +3 -1
  97. package/dist/esm/media/crypto/WaMediaCrypto.js +38 -19
  98. package/dist/esm/message/context-info.js +39 -0
  99. package/dist/esm/message/crypto/reporting-token.js +38 -216
  100. package/dist/esm/message/encode/content.js +73 -12
  101. package/dist/esm/message/encode/media-payload.js +3 -1
  102. package/dist/esm/message/kinds/group-history.js +36 -0
  103. package/dist/esm/message/primitives/incoming.js +32 -13
  104. package/dist/esm/protocol/abprops.js +18 -162
  105. package/dist/esm/protocol/constants.js +4 -3
  106. package/dist/esm/protocol/defaults.js +7 -0
  107. package/dist/esm/protocol/device-identity.js +10 -5
  108. package/dist/esm/protocol/jid.js +83 -36
  109. package/dist/esm/protocol/media.js +1 -0
  110. package/dist/esm/protocol/message.js +8 -0
  111. package/dist/esm/protocol/notification.js +1 -0
  112. package/dist/esm/protocol/privacy.js +70 -4
  113. package/dist/esm/signal/group/SenderKeyChain.js +1 -2
  114. package/dist/esm/signal/group/SenderKeyManager.js +1 -7
  115. package/dist/esm/signal/session/SignalProtocol.js +22 -21
  116. package/dist/esm/signal/session/SignalRatchet.js +20 -17
  117. package/dist/esm/signal/session/encoding.js +25 -3
  118. package/dist/esm/signal/session/resolver.js +11 -5
  119. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  120. package/dist/esm/store/createStore.js +19 -2
  121. package/dist/esm/store/index.js +1 -0
  122. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  123. package/dist/esm/store/noop.store.js +8 -0
  124. package/dist/esm/transport/binary/constants.js +19 -0
  125. package/dist/esm/transport/binary/decoder.js +17 -19
  126. package/dist/esm/transport/binary/encoder.js +61 -18
  127. package/dist/esm/transport/node/builders/global.js +20 -0
  128. package/dist/esm/transport/node/builders/message.js +32 -15
  129. package/dist/esm/transport/node/builders/privacy.js +63 -4
  130. package/dist/esm/transport/noise/WaClientPayload.js +43 -22
  131. package/dist/esm/transport/noise/WaFrameCodec.js +14 -4
  132. package/dist/esm/util/coercion.js +12 -4
  133. package/dist/esm/util/index.js +1 -1
  134. package/dist/esm/util/protoscan.js +181 -0
  135. package/dist/esm/util/runtime.js +34 -0
  136. package/dist/index.d.ts +11 -6
  137. package/dist/index.js +15 -2
  138. package/dist/media/crypto/WaMediaCrypto.js +36 -17
  139. package/dist/media/types.d.ts +1 -1
  140. package/dist/message/context-info.d.ts +10 -0
  141. package/dist/message/context-info.js +40 -0
  142. package/dist/message/crypto/reporting-token.d.ts +3 -1
  143. package/dist/message/crypto/reporting-token.js +38 -216
  144. package/dist/message/encode/content.d.ts +24 -4
  145. package/dist/message/encode/content.js +74 -12
  146. package/dist/message/encode/media-payload.d.ts +5 -2
  147. package/dist/message/encode/media-payload.js +3 -1
  148. package/dist/message/kinds/group-history.d.ts +30 -0
  149. package/dist/message/kinds/group-history.js +40 -0
  150. package/dist/message/primitives/incoming.d.ts +5 -0
  151. package/dist/message/primitives/incoming.js +32 -13
  152. package/dist/message/types.d.ts +10 -0
  153. package/dist/protocol/abprops.d.ts +27 -140
  154. package/dist/protocol/abprops.js +18 -163
  155. package/dist/protocol/constants.d.ts +4 -3
  156. package/dist/protocol/constants.js +10 -3
  157. package/dist/protocol/defaults.d.ts +7 -0
  158. package/dist/protocol/defaults.js +7 -0
  159. package/dist/protocol/device-identity.d.ts +14 -4
  160. package/dist/protocol/device-identity.js +9 -4
  161. package/dist/protocol/jid.d.ts +4 -1
  162. package/dist/protocol/jid.js +83 -36
  163. package/dist/protocol/media.d.ts +1 -0
  164. package/dist/protocol/media.js +1 -0
  165. package/dist/protocol/message.d.ts +6 -0
  166. package/dist/protocol/message.js +9 -0
  167. package/dist/protocol/notification.d.ts +1 -0
  168. package/dist/protocol/notification.js +1 -0
  169. package/dist/protocol/privacy.d.ts +52 -0
  170. package/dist/protocol/privacy.js +71 -5
  171. package/dist/signal/group/SenderKeyChain.d.ts +1 -1
  172. package/dist/signal/group/SenderKeyChain.js +1 -2
  173. package/dist/signal/group/SenderKeyManager.js +1 -7
  174. package/dist/signal/session/SignalProtocol.d.ts +13 -0
  175. package/dist/signal/session/SignalProtocol.js +22 -21
  176. package/dist/signal/session/SignalRatchet.d.ts +4 -4
  177. package/dist/signal/session/SignalRatchet.js +19 -16
  178. package/dist/signal/session/SignalSession.d.ts +8 -1
  179. package/dist/signal/session/encoding.d.ts +5 -0
  180. package/dist/signal/session/encoding.js +25 -3
  181. package/dist/signal/session/resolver.js +11 -5
  182. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  183. package/dist/store/contracts/chat-metadata.store.js +2 -0
  184. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  185. package/dist/store/contracts/thread.store.d.ts +6 -0
  186. package/dist/store/createStore.d.ts +10 -2
  187. package/dist/store/createStore.js +18 -1
  188. package/dist/store/index.d.ts +3 -1
  189. package/dist/store/index.js +3 -1
  190. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  191. package/dist/store/memory/chat-metadata.store.js +70 -0
  192. package/dist/store/noop.store.d.ts +2 -0
  193. package/dist/store/noop.store.js +9 -1
  194. package/dist/store/types.d.ts +190 -39
  195. package/dist/transport/binary/constants.d.ts +8 -0
  196. package/dist/transport/binary/constants.js +20 -1
  197. package/dist/transport/binary/decoder.js +17 -19
  198. package/dist/transport/binary/encoder.js +60 -17
  199. package/dist/transport/node/builders/global.d.ts +8 -0
  200. package/dist/transport/node/builders/global.js +21 -0
  201. package/dist/transport/node/builders/message.js +32 -15
  202. package/dist/transport/node/builders/privacy.d.ts +26 -2
  203. package/dist/transport/node/builders/privacy.js +63 -3
  204. package/dist/transport/noise/WaClientPayload.js +43 -22
  205. package/dist/transport/noise/WaFrameCodec.d.ts +1 -0
  206. package/dist/transport/noise/WaFrameCodec.js +14 -4
  207. package/dist/transport/noise/types.d.ts +17 -0
  208. package/dist/util/coercion.d.ts +6 -0
  209. package/dist/util/coercion.js +13 -4
  210. package/dist/util/index.d.ts +1 -1
  211. package/dist/util/index.js +2 -1
  212. package/dist/util/protoscan.d.ts +33 -0
  213. package/dist/util/protoscan.js +186 -0
  214. package/dist/util/runtime.d.ts +12 -0
  215. package/dist/util/runtime.js +35 -0
  216. package/package.json +3 -1
  217. package/spec/abprops/index.d.ts +4402 -0
  218. package/spec/abprops/index.js +4372 -0
@@ -21,6 +21,25 @@ export declare function aesGcmEncrypt(key: AesKey, nonce: Uint8Array, plaintext:
21
21
  export declare function aesGcmDecrypt(key: AesKey, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
22
22
  /** AES-256-CBC encrypt with PKCS#7 padding (default Node behavior). */
23
23
  export declare function aesCbcEncrypt(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
24
+ /**
25
+ * AES-256-CBC encrypt into a buffer that reserves `trailerBytes` of spare room
26
+ * after the ciphertext, for a caller that has to append a MAC to it.
27
+ *
28
+ * Appending to the result of {@link aesCbcEncrypt} means copying the whole
29
+ * ciphertext a second time, which on a media payload is a copy of the entire
30
+ * file to make room for ten bytes. Sizing the destination from the PKCS#7
31
+ * output length up front removes that copy, and feeding the cipher in bounded
32
+ * chunks keeps the transient buffer Node hands back to a chunk rather than a
33
+ * second full-size ciphertext, so peak memory stays near one copy of the
34
+ * payload instead of two.
35
+ *
36
+ * The ciphertext occupies `result.length - trailerBytes`; the reserved tail is
37
+ * left zeroed for the caller to fill.
38
+ *
39
+ * @throws when `trailerBytes` is not a non-negative safe integer, or when the
40
+ * cipher yields a length other than the PKCS#7 size the destination was cut to.
41
+ */
42
+ export declare function aesCbcEncryptWithTrailer(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array, trailerBytes: number): Uint8Array;
24
43
  /** AES-256-CBC decrypt with PKCS#7 padding. */
25
44
  export declare function aesCbcDecrypt(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array;
26
45
  /** AES-256-CTR encrypt with the given 16-byte counter. */
@@ -7,6 +7,7 @@ exports.md5Bytes = md5Bytes;
7
7
  exports.aesGcmEncrypt = aesGcmEncrypt;
8
8
  exports.aesGcmDecrypt = aesGcmDecrypt;
9
9
  exports.aesCbcEncrypt = aesCbcEncrypt;
10
+ exports.aesCbcEncryptWithTrailer = aesCbcEncryptWithTrailer;
10
11
  exports.aesCbcDecrypt = aesCbcDecrypt;
11
12
  exports.aesCtrEncrypt = aesCtrEncrypt;
12
13
  exports.aesCtrDecrypt = aesCtrDecrypt;
@@ -17,6 +18,9 @@ const node_crypto_1 = require("node:crypto");
17
18
  const node_util_1 = require("node:util");
18
19
  const bytes_1 = require("../../util/bytes");
19
20
  const AES_GCM_TAG_LENGTH = 16;
21
+ const AES_CBC_BLOCK_SIZE = 16;
22
+ /** Bounds the transient buffer `cipher.update` returns per call. */
23
+ const AES_CBC_CHUNK_SIZE = 262144;
20
24
  const pbkdf2Async = (0, node_util_1.promisify)(node_crypto_1.pbkdf2);
21
25
  function feed(target, input) {
22
26
  if (Array.isArray(input)) {
@@ -96,6 +100,50 @@ function aesCbcEncrypt(key, iv, plaintext) {
96
100
  }
97
101
  return (0, bytes_1.concatBytes)([head, tail]);
98
102
  }
103
+ /**
104
+ * AES-256-CBC encrypt into a buffer that reserves `trailerBytes` of spare room
105
+ * after the ciphertext, for a caller that has to append a MAC to it.
106
+ *
107
+ * Appending to the result of {@link aesCbcEncrypt} means copying the whole
108
+ * ciphertext a second time, which on a media payload is a copy of the entire
109
+ * file to make room for ten bytes. Sizing the destination from the PKCS#7
110
+ * output length up front removes that copy, and feeding the cipher in bounded
111
+ * chunks keeps the transient buffer Node hands back to a chunk rather than a
112
+ * second full-size ciphertext, so peak memory stays near one copy of the
113
+ * payload instead of two.
114
+ *
115
+ * The ciphertext occupies `result.length - trailerBytes`; the reserved tail is
116
+ * left zeroed for the caller to fill.
117
+ *
118
+ * @throws when `trailerBytes` is not a non-negative safe integer, or when the
119
+ * cipher yields a length other than the PKCS#7 size the destination was cut to.
120
+ */
121
+ function aesCbcEncryptWithTrailer(key, iv, plaintext, trailerBytes) {
122
+ if (!Number.isSafeInteger(trailerBytes) || trailerBytes < 0) {
123
+ throw new Error(`invalid trailer length ${trailerBytes}`);
124
+ }
125
+ const cipherLength = (Math.floor(plaintext.length / AES_CBC_BLOCK_SIZE) + 1) * AES_CBC_BLOCK_SIZE;
126
+ const out = new Uint8Array(cipherLength + trailerBytes);
127
+ const cipher = (0, node_crypto_1.createCipheriv)('aes-256-cbc', key, iv);
128
+ let offset = 0;
129
+ for (let start = 0; start < plaintext.length; start += AES_CBC_CHUNK_SIZE) {
130
+ const end = Math.min(start + AES_CBC_CHUNK_SIZE, plaintext.length);
131
+ const block = cipher.update(plaintext.subarray(start, end));
132
+ if (block.length > 0) {
133
+ out.set(block, offset);
134
+ offset += block.length;
135
+ }
136
+ }
137
+ const tail = cipher.final();
138
+ if (tail.length > 0) {
139
+ out.set(tail, offset);
140
+ offset += tail.length;
141
+ }
142
+ if (offset !== cipherLength) {
143
+ throw new Error(`aes-cbc produced ${offset} bytes, expected ${cipherLength}`);
144
+ }
145
+ return out;
146
+ }
99
147
  /** AES-256-CBC decrypt with PKCS#7 padding. */
100
148
  function aesCbcDecrypt(key, iv, ciphertext) {
101
149
  const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-cbc', key, iv);
@@ -9,17 +9,30 @@ const X25519_1 = require("../curves/X25519");
9
9
  const edwards_1 = require("../math/edwards");
10
10
  const le_1 = require("../math/le");
11
11
  const mod_1 = require("../math/mod");
12
+ const nativeBackend_1 = require("../nativeBackend");
12
13
  const bytes_1 = require("../../util/bytes");
13
14
  const PREFIX_SIGNATURE_RANDOM = new Uint8Array([
14
15
  0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
15
16
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
16
17
  ]);
18
+ const nativeBinding = (() => {
19
+ if (process.env.ZAPO_XEDDSA_FORCE_JS)
20
+ return null;
21
+ const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
22
+ if (mod && typeof mod.xeddsaSign === 'function' && typeof mod.xeddsaVerify === 'function') {
23
+ return { xeddsaSign: mod.xeddsaSign, xeddsaVerify: mod.xeddsaVerify };
24
+ }
25
+ return null;
26
+ })();
17
27
  /**
18
28
  * Verifies an XEdDSA signature over `message` against an X25519 (Montgomery)
19
29
  * public key, converting to the Edwards form internally. Returns `false`
20
30
  * on shape mismatch instead of throwing.
21
31
  */
22
32
  async function xeddsaVerify(curvePublicKey, message, signature) {
33
+ if (nativeBinding) {
34
+ return nativeBinding.xeddsaVerify(curvePublicKey, message, signature);
35
+ }
23
36
  if (signature.length !== 64) {
24
37
  return false;
25
38
  }
@@ -38,16 +51,34 @@ async function xeddsaVerify(curvePublicKey, message, signature) {
38
51
  signature[lastByteIndex] = originalLastByte;
39
52
  }
40
53
  }
54
+ const publicDerivationCache = new WeakMap();
55
+ function derivePublicForSigning(privateKey) {
56
+ const cached = publicDerivationCache.get(privateKey);
57
+ if (cached) {
58
+ return cached;
59
+ }
60
+ const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
61
+ const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
62
+ const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
63
+ const derivation = {
64
+ privateScalar,
65
+ encodedPublic,
66
+ pubKeySignBit: encodedPublic[31] & 0x80
67
+ };
68
+ publicDerivationCache.set(privateKey, derivation);
69
+ return derivation;
70
+ }
41
71
  /**
42
72
  * Signs `message` with an X25519 (Montgomery) private key using the XEdDSA
43
73
  * construction. Returns a 64-byte signature.
44
74
  */
45
75
  async function xeddsaSign(privateKey, message) {
46
76
  (0, bytes_1.assertByteLength)(privateKey, 32, `invalid curve25519 private key length ${privateKey.length}`);
47
- const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
48
- const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
49
- const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
50
- const pubKeySignBit = encodedPublic[31] & 0x80;
77
+ if (nativeBinding) {
78
+ return (0, bytes_1.toBytesView)(nativeBinding.xeddsaSign(privateKey, message));
79
+ }
80
+ const { privateScalar, encodedPublic, pubKeySignBit } = derivePublicForSigning(privateKey);
81
+ const clampedPrivateKey = privateKey;
51
82
  const randomSuffix = await (0, random_1.randomBytesAsync)(64);
52
83
  const r = (0, mod_1.modGroup)((0, le_1.bytesToBigIntLE)((0, primitives_1.sha512)([PREFIX_SIGNATURE_RANDOM, clampedPrivateKey, message, randomSuffix])));
53
84
  const encodedR = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(r));
@@ -10,7 +10,17 @@ const constants_1 = require("../curves/constants");
10
10
  const types_1 = require("../curves/types");
11
11
  const constants_2 = require("../math/constants");
12
12
  const fe_1 = require("../math/fe");
13
+ const nativeBackend_1 = require("../nativeBackend");
13
14
  const bytes_1 = require("../../util/bytes");
15
+ const nativeX25519ScalarMult = (() => {
16
+ if (process.env.ZAPO_X25519_FORCE_JS)
17
+ return null;
18
+ const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
19
+ if (mod && typeof mod.x25519ScalarMult === 'function') {
20
+ return mod.x25519ScalarMult;
21
+ }
22
+ return null;
23
+ })();
14
24
  const diffieHellmanWithCallback = node_crypto_1.diffieHellman;
15
25
  let diffieHellmanAsync = null;
16
26
  let diffieHellmanAsyncProbed = false;
@@ -76,19 +86,33 @@ function montgomeryToEdwardsPublic(curvePublicKey, signBit) {
76
86
  encoded[31] = (encoded[31] & 0x7f) | (signBit & 0x80);
77
87
  return encoded;
78
88
  }
89
+ const privateKeyObjectCache = new WeakMap();
90
+ const publicKeyObjectCache = new WeakMap();
79
91
  function x25519PrivateKeyObject(privKey) {
80
- return (0, node_crypto_1.createPrivateKey)({
92
+ const cached = privateKeyObjectCache.get(privKey);
93
+ if (cached) {
94
+ return cached;
95
+ }
96
+ const keyObject = (0, node_crypto_1.createPrivateKey)({
81
97
  key: (0, types_1.pkcs8FromRawPrivate)(constants_1.X25519_PKCS8_PREFIX, privKey),
82
98
  format: 'der',
83
99
  type: 'pkcs8'
84
100
  });
101
+ privateKeyObjectCache.set(privKey, keyObject);
102
+ return keyObject;
85
103
  }
86
104
  function x25519PublicKeyObject(pubKey) {
87
- return (0, node_crypto_1.createPublicKey)({
105
+ const cached = publicKeyObjectCache.get(pubKey);
106
+ if (cached) {
107
+ return cached;
108
+ }
109
+ const keyObject = (0, node_crypto_1.createPublicKey)({
88
110
  key: (0, bytes_1.concatBytes)([constants_1.X25519_SPKI_PREFIX, pubKey]),
89
111
  format: 'der',
90
112
  type: 'spki'
91
113
  });
114
+ publicKeyObjectCache.set(pubKey, keyObject);
115
+ return keyObject;
92
116
  }
93
117
  /**
94
118
  * X25519 key-pair generation and Diffie-Hellman scalar multiplication
@@ -120,6 +144,9 @@ class X25519 {
120
144
  static async scalarMult(privKey, pubKey) {
121
145
  (0, bytes_1.assertByteLength)(privKey, 32, 'x25519 private key must be 32 bytes');
122
146
  (0, bytes_1.assertByteLength)(pubKey, 32, 'x25519 public key must be 32 bytes');
147
+ if (nativeX25519ScalarMult) {
148
+ return (0, bytes_1.toBytesView)(nativeX25519ScalarMult(privKey, pubKey));
149
+ }
123
150
  const opts = {
124
151
  privateKey: x25519PrivateKeyObject(privKey),
125
152
  publicKey: x25519PublicKeyObject(pubKey)
@@ -0,0 +1,20 @@
1
+ export interface NativeCryptoModule {
2
+ readonly x25519ScalarMult?: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array;
3
+ readonly xeddsaSign?: (privateKey: Uint8Array, message: Uint8Array) => Uint8Array;
4
+ readonly xeddsaVerify?: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => boolean;
5
+ }
6
+ declare const ABSENCE_TOKENS: {
7
+ readonly napi: readonly ["'@zapo-js/native'", "binding.js", "Cannot find native binding"];
8
+ readonly wasm: readonly ["'@zapo-js/native'", "zapo_native_wasm.js"];
9
+ };
10
+ export type NativeBackendKind = keyof typeof ABSENCE_TOKENS;
11
+ /**
12
+ * Classifies a backend load failure: `true` means the backend is simply not
13
+ * available on this install (missing optional package, entry file absent,
14
+ * no prebuilt binary, ESM build without `require`) and the fallback should
15
+ * stay silent; `false` means the backend is present but broken and the
16
+ * degradation deserves a process warning.
17
+ */
18
+ export declare function isBackendAbsence(backend: NativeBackendKind, error: unknown): boolean;
19
+ export declare function resolveNativeCryptoBackend(): NativeCryptoModule | null;
20
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isBackendAbsence = isBackendAbsence;
4
+ exports.resolveNativeCryptoBackend = resolveNativeCryptoBackend;
5
+ /**
6
+ * Resolves the optional native crypto accelerator backend shared by the
7
+ * X25519 and XEdDSA paths. Selected via `ZAPO_NATIVE_BACKEND`:
8
+ *
9
+ * - `auto` (default) – try the NAPI addon first, then the WASM build,
10
+ * then no accelerator.
11
+ * - `napi` – only the compiled Rust NAPI addon (`@zapo-js/native`).
12
+ * - `wasm` – only the wasm-bindgen build of the same Rust core.
13
+ * The shipped artifact targets the web (ESM glue +
14
+ * explicit init) so one build serves browsers and
15
+ * Node; loading it here relies on `require(esm)`,
16
+ * available unflagged on Node 20.19+ / 22.12+.
17
+ * - `js` / `none` – no accelerator; callers fall back to the pure-JS
18
+ * (and `node:crypto`) implementations.
19
+ *
20
+ * The module is resolved once and memoised. A failed backend resolves to
21
+ * `null` (or, under `auto`, falls through to the next one) so callers
22
+ * transparently fall back to JS. An absent backend stays silent - that is
23
+ * the expected shape of an optional accelerator - but a present-yet-broken
24
+ * one (corrupt binary, ABI mismatch, unsupported runtime) emits a process
25
+ * warning with code `ZAPO_NATIVE_LOAD_FAILED` so the degradation is
26
+ * visible. The per-primitive `ZAPO_X25519_FORCE_JS` / `ZAPO_XEDDSA_FORCE_JS`
27
+ * escape hatches are honoured by the callers, not here.
28
+ */
29
+ const node_fs_1 = require("node:fs");
30
+ const primitives_1 = require("../util/primitives");
31
+ const WASM_MODULE_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
32
+ const WASM_BINARY_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
33
+ function loadWasmBackend() {
34
+ const mod = require(WASM_MODULE_SPECIFIER);
35
+ const wasmBytes = (0, node_fs_1.readFileSync)(require.resolve(WASM_BINARY_SPECIFIER));
36
+ mod.initSync({ module: wasmBytes });
37
+ return mod;
38
+ }
39
+ let cached;
40
+ const ABSENCE_TOKENS = {
41
+ napi: ["'@zapo-js/native'", 'binding.js', 'Cannot find native binding'],
42
+ wasm: ["'@zapo-js/native'", 'zapo_native_wasm.js']
43
+ };
44
+ /**
45
+ * Classifies a backend load failure: `true` means the backend is simply not
46
+ * available on this install (missing optional package, entry file absent,
47
+ * no prebuilt binary, ESM build without `require`) and the fallback should
48
+ * stay silent; `false` means the backend is present but broken and the
49
+ * degradation deserves a process warning.
50
+ */
51
+ function isBackendAbsence(backend, error) {
52
+ if (error instanceof ReferenceError) {
53
+ return error.message.includes('require is not defined');
54
+ }
55
+ const { code, message } = (error ?? {});
56
+ if (typeof message !== 'string')
57
+ return false;
58
+ const notFound = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
59
+ if (!notFound && !message.includes('Cannot find native binding'))
60
+ return false;
61
+ return ABSENCE_TOKENS[backend].some((token) => message.includes(token));
62
+ }
63
+ function warnLoadFailure(backend, error) {
64
+ process.emitWarning('failed to load native crypto accelerator, falling back', {
65
+ code: 'ZAPO_NATIVE_LOAD_FAILED',
66
+ detail: `${backend}: ${(0, primitives_1.toError)(error).message}`
67
+ });
68
+ }
69
+ function resolveNativeCryptoBackend() {
70
+ if (cached !== undefined)
71
+ return cached;
72
+ const backend = (process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
73
+ cached = null;
74
+ if (backend === 'js' || backend === 'none')
75
+ return cached;
76
+ if (backend === 'napi' || backend === 'auto') {
77
+ try {
78
+ cached = require('@zapo-js/native');
79
+ }
80
+ catch (error) {
81
+ if (!isBackendAbsence('napi', error))
82
+ warnLoadFailure('napi', error);
83
+ }
84
+ }
85
+ if (cached === null && (backend === 'wasm' || backend === 'auto')) {
86
+ try {
87
+ cached = loadWasmBackend();
88
+ }
89
+ catch (error) {
90
+ if (!isBackendAbsence('wasm', error))
91
+ warnLoadFailure('wasm', error);
92
+ }
93
+ }
94
+ return cached;
95
+ }
@@ -0,0 +1,15 @@
1
+ import { WA_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
2
+ export { WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
3
+ // The reverse map is emitted with numeric literal keys, so it cannot be indexed
4
+ // with a plain `number`. Widening it keeps the lookup a single property read and
5
+ // avoids allocating a parallel Map with 2000+ entries at module init.
6
+ const AB_PROP_NAME_BY_CODE = WA_ABPROPS_BY_CODE;
7
+ /**
8
+ * Resolves the client-side prop name for a wire config code.
9
+ *
10
+ * Returns `undefined` for codes this build does not know about - the server
11
+ * advertises props from newer bundles, and WA Web drops those rows too.
12
+ */
13
+ export function resolveAbPropNameByCode(configCode) {
14
+ return AB_PROP_NAME_BY_CODE[configCode];
15
+ }
@@ -174,12 +174,10 @@ export class WaAppStateCrypto {
174
174
  if (out.byteLength !== left.byteLength) {
175
175
  throw new Error('lt hash output length mismatch');
176
176
  }
177
- const leftView = new DataView(left.buffer, left.byteOffset, left.byteLength);
178
- const rightView = new DataView(right.buffer, right.byteOffset, right.byteLength);
179
- const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
180
177
  for (let offset = 0; offset < left.byteLength; offset += APP_STATE_POINT_SIZE) {
181
- const value = combine(leftView.getUint16(offset, true), rightView.getUint16(offset, true));
182
- outView.setUint16(offset, value & 0xffff, true);
178
+ const value = combine(left[offset] | (left[offset + 1] << 8), right[offset] | (right[offset + 1] << 8));
179
+ out[offset] = value & 0xff;
180
+ out[offset + 1] = (value >>> 8) & 0xff;
183
181
  }
184
182
  return out;
185
183
  }
@@ -196,7 +194,7 @@ export class WaAppStateCrypto {
196
194
  generateValueMac(valueMacHmacKey, associatedData, iv, cipherText) {
197
195
  const octetLength = new Uint8Array(APP_STATE_MAC_OCTET_LENGTH);
198
196
  octetLength[octetLength.length - 1] = associatedData.byteLength & 0xff;
199
- const full = hmacSha512Sign(valueMacHmacKey, concatBytes([associatedData, iv, cipherText, octetLength]));
197
+ const full = hmacSha512Sign(valueMacHmacKey, [associatedData, iv, cipherText, octetLength]);
200
198
  return full.subarray(0, APP_STATE_VALUE_MAC_LENGTH);
201
199
  }
202
200
  touchDerivedKeysCacheEntry(cacheKey, keys) {
@@ -20,11 +20,18 @@ export class WaAuthClient {
20
20
  constructor(options, deps) {
21
21
  this.versionOverride = null;
22
22
  this.mobileAppVersionOverride = null;
23
+ /**
24
+ * Successful logins made by this instance, advertised as
25
+ * `connectAttemptCount`. Process-local by design – the persisted
26
+ * counterpart is `credentials.loginCounter`.
27
+ */
28
+ this.connectAttemptCount = 0;
23
29
  const device = resolveWaDeviceIdentity(options);
24
30
  this.options = Object.freeze({
25
31
  ...options,
26
32
  deviceBrowser: device.browser,
27
33
  deviceOsDisplayName: device.osDisplayName,
34
+ deviceOsVersion: device.osVersion ?? undefined,
28
35
  devicePlatform: device.platform,
29
36
  requireFullSync: options.requireFullSync
30
37
  });
@@ -122,8 +129,10 @@ export class WaAuthClient {
122
129
  const mobileAppVersionOverride = this.mobileAppVersionOverride;
123
130
  this.mobileAppVersionOverride = null;
124
131
  return buildCommsConfig(this.logger, this.requireCredentials(), socketOptions, {
132
+ connectAttemptCount: this.connectAttemptCount,
125
133
  deviceBrowser: this.options.deviceBrowser,
126
134
  deviceOsDisplayName: this.options.deviceOsDisplayName,
135
+ deviceOsVersion: this.options.deviceOsVersion,
127
136
  requireFullSync: this.options.requireFullSync,
128
137
  version: override ?? this.options.version,
129
138
  mobileTransport: this.options.mobileTransport,
@@ -236,10 +245,19 @@ export class WaAuthClient {
236
245
  * Persists the per-connection success attributes from the server (LID,
237
246
  * display name, companion key, last-success ts, props versions, ...). Only
238
247
  * persists when at least one attribute actually changed.
248
+ *
249
+ * Pass `countsAsLogin` only for the server `success` node: it advances the
250
+ * login counters advertised as `lc` / `connectAttemptCount`. Callers that
251
+ * merely reuse this to persist a single attribute (a push-name change, for
252
+ * instance) must leave it unset.
239
253
  */
240
- async persistSuccessAttributes(attributes) {
254
+ async persistSuccessAttributes(attributes, { countsAsLogin = false } = {}) {
255
+ if (countsAsLogin) {
256
+ this.connectAttemptCount += 1;
257
+ }
241
258
  let persistDiff;
242
259
  const computeDiff = (current, next) => ({
260
+ loginCounterChanged: next.loginCounter !== current.loginCounter,
243
261
  lidChanged: next.meLid !== current.meLid,
244
262
  displayNameChanged: next.meDisplayName !== current.meDisplayName,
245
263
  companionChanged: (current.companionEncStatic === undefined) !==
@@ -259,6 +277,9 @@ export class WaAuthClient {
259
277
  meDisplayName: attributes.meDisplayName ?? credentials.meDisplayName,
260
278
  companionEncStatic: attributes.companionEncStatic ?? credentials.companionEncStatic,
261
279
  lastSuccessTs: attributes.lastSuccessTs ?? credentials.lastSuccessTs,
280
+ loginCounter: countsAsLogin
281
+ ? (credentials.loginCounter ?? 0) + 1
282
+ : credentials.loginCounter,
262
283
  propsVersion: attributes.propsVersion ?? credentials.propsVersion,
263
284
  abPropsVersion: attributes.abPropsVersion ?? credentials.abPropsVersion,
264
285
  connectionLocation: attributes.connectionLocation ?? credentials.connectionLocation,
@@ -174,6 +174,8 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
174
174
  ? {
175
175
  username: loginIdentity.username,
176
176
  device: loginIdentity.device,
177
+ loginCounter: credentials.loginCounter ?? 0,
178
+ connectAttemptCount: clientOptions.connectAttemptCount ?? 0,
177
179
  deviceBrowser: clientOptions.deviceBrowser,
178
180
  deviceOsDisplayName: clientOptions.deviceOsDisplayName,
179
181
  versionBase
@@ -185,6 +187,7 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
185
187
  signedPreKey: credentials.signedPreKey,
186
188
  deviceBrowser: clientOptions.deviceBrowser,
187
189
  deviceOsDisplayName: clientOptions.deviceOsDisplayName,
190
+ deviceOsVersion: clientOptions.deviceOsVersion,
188
191
  requireFullSync: clientOptions.requireFullSync,
189
192
  versionBase
190
193
  }
@@ -5,13 +5,18 @@ import { getWaBrowserDisplayName } from '../../protocol/browser.js';
5
5
  import { WA_DEFAULTS, WA_IQ_TYPES, WA_NODE_TAGS, WA_SIGNALING } from '../../protocol/constants.js';
6
6
  import { parsePhoneJid } from '../../protocol/jid.js';
7
7
  import { ADV_PREFIX_HOSTED_ACCOUNT_SIGNATURE, computeAdvIdentityHmac, generateDeviceSignature, verifyDeviceIdentityAccountSignature } from '../../signal/attestation/WaAdvSignature.js';
8
- import { buildAckNode, buildIqResultNode } from '../../transport/node/builders/global.js';
8
+ import { buildAckNode, buildIqErrorNode, buildIqResultNode } from '../../transport/node/builders/global.js';
9
9
  import { buildCompanionFinishRequestNode, buildCompanionHelloRequestNode, buildGetCountryCodeRequestNode } from '../../transport/node/builders/pairing.js';
10
10
  import { decodeNodeContentUtf8OrBytes, findNodeChild, findNodeChildrenByTags, getFirstNodeChild, getNodeChildrenNonEmptyUtf8ByTag, hasNodeChild } from '../../transport/node/helpers.js';
11
11
  import { assertIqResult } from '../../transport/node/query.js';
12
12
  import { concatBytes, decodeProtoBytes, uint8Equal, uint8TimingSafeEqual } from '../../util/bytes.js';
13
+ import { toError } from '../../util/primitives.js';
14
+ /** Server-enforced bounds on the `pair-success` `device-identity` payload. */
15
+ const PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES = 1;
16
+ const PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES = 500;
13
17
  export class WaPairingFlow {
14
18
  constructor(options) {
19
+ this.pairSuccessInFlight = false;
15
20
  this.opts = options;
16
21
  this.pairingSession = null;
17
22
  }
@@ -30,6 +35,7 @@ export class WaPairingFlow {
30
35
  const normalizedCustomCode = customCode !== undefined ? normalizeCustomPairingCode(customCode) : undefined;
31
36
  const credentials = this.requireCredentials();
32
37
  const phoneJid = parsePhoneJid(phoneNumber);
38
+ const createdAtSeconds = Math.floor(Date.now() / 1000);
33
39
  const [companionHello, refreshedCredentials] = await Promise.all([
34
40
  createCompanionHello({ customCode: normalizedCustomCode }),
35
41
  this.rotateAdvSecret(credentials)
@@ -61,7 +67,7 @@ export class WaPairingFlow {
61
67
  pairingCode: companionHello.pairingCode,
62
68
  phoneJid,
63
69
  ref,
64
- createdAtSeconds: Math.floor(Date.now() / 1000),
70
+ createdAtSeconds,
65
71
  companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
66
72
  attempts: 0,
67
73
  finished: false
@@ -163,6 +169,23 @@ export class WaPairingFlow {
163
169
  this.opts.logger.debug('pair-device refs updated', { refsCount: refs.length });
164
170
  }
165
171
  async handlePairSuccess(iqNode, pairSuccessNode) {
172
+ if (this.pairSuccessInFlight) {
173
+ this.opts.logger.debug('pair-success ignored: already processing');
174
+ return;
175
+ }
176
+ if (this.opts.auth.getCredentials()?.meJid) {
177
+ this.opts.logger.debug('pair-success ignored: session already registered');
178
+ return;
179
+ }
180
+ this.pairSuccessInFlight = true;
181
+ try {
182
+ await this.completePairSuccess(iqNode, pairSuccessNode);
183
+ }
184
+ finally {
185
+ this.pairSuccessInFlight = false;
186
+ }
187
+ }
188
+ async completePairSuccess(iqNode, pairSuccessNode) {
166
189
  this.opts.logger.debug('processing pair-success node');
167
190
  const credentials = this.requireCredentials();
168
191
  const [deviceIdentityNode, deviceNode, platformNode] = findNodeChildrenByTags(pairSuccessNode, [WA_NODE_TAGS.DEVICE_IDENTITY, 'device', WA_NODE_TAGS.PLATFORM]);
@@ -174,7 +197,12 @@ export class WaPairingFlow {
174
197
  });
175
198
  throw new Error('pair-success stanza is missing required nodes');
176
199
  }
177
- const wrappedIdentity = proto.ADVSignedDeviceIdentityHMAC.decode(decodeNodeContentUtf8OrBytes(deviceIdentityNode.content, 'pair-success.device-identity'));
200
+ const deviceIdentityBytes = decodeNodeContentUtf8OrBytes(deviceIdentityNode.content, 'pair-success.device-identity');
201
+ if (deviceIdentityBytes.length < PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES ||
202
+ deviceIdentityBytes.length > PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES) {
203
+ throw new Error(`pair-success device-identity must be ${PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES}-${PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES} bytes, got ${deviceIdentityBytes.length}`);
204
+ }
205
+ const wrappedIdentity = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityBytes);
178
206
  const wrappedDetails = decodeProtoBytes(wrappedIdentity.details, 'ADVSignedDeviceIdentityHMAC.details');
179
207
  const wrappedHmac = decodeProtoBytes(wrappedIdentity.hmac, 'ADVSignedDeviceIdentityHMAC.hmac');
180
208
  const accountType = wrappedIdentity.accountType ?? proto.ADVEncryptionType.E2EE;
@@ -191,16 +219,23 @@ export class WaPairingFlow {
191
219
  platform: platformNode.attrs.name,
192
220
  isHosted
193
221
  });
222
+ await this.sendPairSuccessValidationError(iqNode);
194
223
  throw new Error('pair-success HMAC validation failed');
195
224
  }
196
225
  }
197
- const { signedIdentity, keyIndex, responseIdentityBytes } = await this.buildPairSuccessResponseIdentity(credentials, wrappedDetails);
226
+ const built = await this.buildPairSuccessResponseIdentity(credentials, wrappedDetails);
227
+ if (!built) {
228
+ await this.sendPairSuccessValidationError(iqNode);
229
+ throw new Error('pair-success account signature validation failed');
230
+ }
231
+ const { signedIdentity, keyIndex, responseIdentityBytes } = built;
198
232
  const nextCredentials = {
199
233
  ...credentials,
200
234
  signedIdentity,
201
235
  meJid: deviceNode.attrs.jid,
202
236
  meLid: deviceNode.attrs.lid,
203
- platform: platformNode.attrs.name
237
+ platform: platformNode.attrs.name,
238
+ loginCounter: 0
204
239
  };
205
240
  await this.opts.auth.updateCredentials(nextCredentials);
206
241
  this.opts.logger.info('pair-success credentials updated', {
@@ -235,6 +270,7 @@ export class WaPairingFlow {
235
270
  this.opts.callbacks.emitPaired(nextCredentials);
236
271
  this.opts.logger.debug('pair-success completed and paired event emitted');
237
272
  }
273
+ /** Returns `null` when the primary's account signature does not verify. */
238
274
  async buildPairSuccessResponseIdentity(credentials, wrappedDetails) {
239
275
  const signedIdentity = proto.ADVSignedDeviceIdentity.decode(wrappedDetails);
240
276
  const details = decodeProtoBytes(signedIdentity.details, 'ADVSignedDeviceIdentity.details');
@@ -250,7 +286,7 @@ export class WaPairingFlow {
250
286
  keyIndex: advDeviceIdentity.keyIndex ?? 0,
251
287
  isDeviceHosted
252
288
  });
253
- throw new Error('pair-success account signature validation failed');
289
+ return null;
254
290
  }
255
291
  }
256
292
  signedIdentity.deviceSignature = await generateDeviceSignature(details, localIdentity, accountSignatureKey, false);
@@ -265,19 +301,35 @@ export class WaPairingFlow {
265
301
  responseIdentityBytes
266
302
  };
267
303
  }
304
+ /** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
305
+ async sendPairSuccessValidationError(iqNode) {
306
+ try {
307
+ await this.opts.socket.sendNode(buildIqErrorNode(iqNode, { code: 401, text: 'not-authorized' }));
308
+ }
309
+ catch (error) {
310
+ this.opts.logger.warn('failed to send pair-success error response', {
311
+ message: toError(error).message
312
+ });
313
+ }
314
+ }
268
315
  async handlePrimaryHello(linkCodeNode) {
269
- const credentials = this.requireCredentials();
316
+ let credentials = this.requireCredentials();
270
317
  const pairingSession = this.pairingSession;
271
- if (!pairingSession || pairingSession.finished) {
318
+ if (!pairingSession) {
272
319
  this.opts.logger.trace('primary_hello ignored: no active session');
273
320
  return;
274
321
  }
275
322
  pairingSession.attempts += 1;
276
323
  this.opts.logger.debug('processing primary_hello', {
277
- attempts: pairingSession.attempts
324
+ attempts: pairingSession.attempts,
325
+ finished: pairingSession.finished
278
326
  });
279
- if (pairingSession.attempts > 3) {
280
- throw new Error('pairing code exceeded maximum primary hello attempts');
327
+ if (pairingSession.finished) {
328
+ if (pairingSession.attempts > WA_DEFAULTS.PAIRING_CODE_MAX_PRIMARY_HELLOS) {
329
+ throw new Error('pairing code exceeded maximum primary hello attempts');
330
+ }
331
+ credentials = await this.rotateAdvSecret(credentials);
332
+ pairingSession.finished = false;
281
333
  }
282
334
  const [refNode, wrappedPrimaryNode, primaryIdentityNode] = findNodeChildrenByTags(linkCodeNode, [
283
335
  WA_NODE_TAGS.LINK_CODE_PAIRING_REF,
@@ -306,6 +358,10 @@ export class WaPairingFlow {
306
358
  companionEphemeralPrivKey: pairingSession.companionEphemeralKeyPair.privKey,
307
359
  registrationIdentityKeyPair: credentials.registrationInfo.identityKeyPair
308
360
  });
361
+ await this.opts.auth.updateCredentials({
362
+ ...credentials,
363
+ advSecretKey: finish.advSecret
364
+ });
309
365
  const result = await this.opts.socket.query(buildCompanionFinishRequestNode({
310
366
  phoneJid: pairingSession.phoneJid,
311
367
  wrappedKeyBundle: finish.wrappedKeyBundle,
@@ -315,10 +371,6 @@ export class WaPairingFlow {
315
371
  if (result.attrs.type === WA_IQ_TYPES.ERROR) {
316
372
  throw new Error('companion_finish returned error');
317
373
  }
318
- await this.opts.auth.updateCredentials({
319
- ...credentials,
320
- advSecretKey: finish.advSecret
321
- });
322
374
  pairingSession.finished = true;
323
375
  this.opts.logger.debug('primary_hello completed with companion_finish success');
324
376
  }