zapo-js 1.7.0 → 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 (97) hide show
  1. package/README.md +26 -12
  2. package/dist/appstate/crypto/WaAppStateCrypto.js +4 -6
  3. package/dist/auth/WaAuthClient.d.ts +14 -1
  4. package/dist/auth/WaAuthClient.js +22 -1
  5. package/dist/auth/credentials-flow.d.ts +3 -1
  6. package/dist/auth/credentials-flow.js +3 -0
  7. package/dist/auth/pairing/WaPairingFlow.d.ts +5 -0
  8. package/dist/auth/pairing/WaPairingFlow.js +66 -14
  9. package/dist/auth/types.d.ts +13 -0
  10. package/dist/client/WaClientFactory.js +3 -1
  11. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +35 -27
  12. package/dist/client/messaging/messages.js +16 -12
  13. package/dist/client/newsletter/discovery.js +4 -2
  14. package/dist/client/persistence/history-sync.d.ts +18 -0
  15. package/dist/client/persistence/history-sync.js +134 -42
  16. package/dist/client/plugins/types.d.ts +10 -2
  17. package/dist/crypto/core/index.d.ts +1 -1
  18. package/dist/crypto/core/index.js +2 -1
  19. package/dist/crypto/core/primitives.d.ts +19 -0
  20. package/dist/crypto/core/primitives.js +48 -0
  21. package/dist/crypto/core/xeddsa.js +19 -4
  22. package/dist/crypto/curves/X25519.js +16 -2
  23. package/dist/esm/appstate/crypto/WaAppStateCrypto.js +4 -6
  24. package/dist/esm/auth/WaAuthClient.js +22 -1
  25. package/dist/esm/auth/credentials-flow.js +3 -0
  26. package/dist/esm/auth/pairing/WaPairingFlow.js +67 -15
  27. package/dist/esm/client/WaClientFactory.js +3 -1
  28. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +37 -29
  29. package/dist/esm/client/messaging/messages.js +16 -12
  30. package/dist/esm/client/newsletter/discovery.js +4 -2
  31. package/dist/esm/client/persistence/history-sync.js +133 -43
  32. package/dist/esm/crypto/core/index.js +1 -1
  33. package/dist/esm/crypto/core/primitives.js +47 -0
  34. package/dist/esm/crypto/core/xeddsa.js +19 -4
  35. package/dist/esm/crypto/curves/X25519.js +16 -2
  36. package/dist/esm/media/crypto/WaMediaCrypto.js +38 -19
  37. package/dist/esm/message/crypto/reporting-token.js +38 -216
  38. package/dist/esm/message/encode/content.js +53 -8
  39. package/dist/esm/message/primitives/incoming.js +14 -7
  40. package/dist/esm/protocol/defaults.js +1 -0
  41. package/dist/esm/protocol/device-identity.js +10 -5
  42. package/dist/esm/protocol/jid.js +83 -36
  43. package/dist/esm/signal/group/SenderKeyChain.js +1 -2
  44. package/dist/esm/signal/group/SenderKeyManager.js +1 -7
  45. package/dist/esm/signal/session/SignalProtocol.js +22 -21
  46. package/dist/esm/signal/session/SignalRatchet.js +20 -17
  47. package/dist/esm/signal/session/encoding.js +25 -3
  48. package/dist/esm/signal/session/resolver.js +11 -5
  49. package/dist/esm/transport/binary/constants.js +19 -0
  50. package/dist/esm/transport/binary/decoder.js +17 -19
  51. package/dist/esm/transport/binary/encoder.js +61 -18
  52. package/dist/esm/transport/node/builders/global.js +20 -0
  53. package/dist/esm/transport/node/builders/message.js +32 -15
  54. package/dist/esm/transport/noise/WaClientPayload.js +43 -22
  55. package/dist/esm/transport/noise/WaFrameCodec.js +14 -4
  56. package/dist/esm/util/protoscan.js +181 -0
  57. package/dist/esm/util/runtime.js +34 -0
  58. package/dist/media/crypto/WaMediaCrypto.js +36 -17
  59. package/dist/message/crypto/reporting-token.d.ts +3 -1
  60. package/dist/message/crypto/reporting-token.js +38 -216
  61. package/dist/message/encode/content.d.ts +17 -1
  62. package/dist/message/encode/content.js +54 -8
  63. package/dist/message/primitives/incoming.js +14 -7
  64. package/dist/message/types.d.ts +10 -0
  65. package/dist/protocol/defaults.d.ts +1 -0
  66. package/dist/protocol/defaults.js +1 -0
  67. package/dist/protocol/device-identity.d.ts +14 -4
  68. package/dist/protocol/device-identity.js +9 -4
  69. package/dist/protocol/jid.d.ts +4 -1
  70. package/dist/protocol/jid.js +83 -36
  71. package/dist/signal/group/SenderKeyChain.d.ts +1 -1
  72. package/dist/signal/group/SenderKeyChain.js +1 -2
  73. package/dist/signal/group/SenderKeyManager.js +1 -7
  74. package/dist/signal/session/SignalProtocol.d.ts +13 -0
  75. package/dist/signal/session/SignalProtocol.js +22 -21
  76. package/dist/signal/session/SignalRatchet.d.ts +4 -4
  77. package/dist/signal/session/SignalRatchet.js +19 -16
  78. package/dist/signal/session/SignalSession.d.ts +8 -1
  79. package/dist/signal/session/encoding.d.ts +5 -0
  80. package/dist/signal/session/encoding.js +25 -3
  81. package/dist/signal/session/resolver.js +11 -5
  82. package/dist/transport/binary/constants.d.ts +8 -0
  83. package/dist/transport/binary/constants.js +20 -1
  84. package/dist/transport/binary/decoder.js +17 -19
  85. package/dist/transport/binary/encoder.js +60 -17
  86. package/dist/transport/node/builders/global.d.ts +8 -0
  87. package/dist/transport/node/builders/global.js +21 -0
  88. package/dist/transport/node/builders/message.js +32 -15
  89. package/dist/transport/noise/WaClientPayload.js +43 -22
  90. package/dist/transport/noise/WaFrameCodec.d.ts +1 -0
  91. package/dist/transport/noise/WaFrameCodec.js +14 -4
  92. package/dist/transport/noise/types.d.ts +17 -0
  93. package/dist/util/protoscan.d.ts +33 -0
  94. package/dist/util/protoscan.js +186 -0
  95. package/dist/util/runtime.d.ts +12 -0
  96. package/dist/util/runtime.js +35 -0
  97. package/package.json +1 -1
@@ -105,19 +105,23 @@ function buildPollCreationMessage(content) {
105
105
  const options = content.options.map((opt) => ({
106
106
  optionName: typeof opt === 'string' ? opt : opt.name
107
107
  }));
108
- return {
109
- pollCreationMessageV3: {
110
- name: content.name,
111
- options,
112
- selectableOptionsCount: content.selectableCount ?? 1,
113
- ...(content.allowAddOption !== undefined
114
- ? { allowAddOption: content.allowAddOption }
115
- : {}),
116
- ...(content.hideParticipantName !== undefined
117
- ? { hideParticipantName: content.hideParticipantName }
118
- : {})
119
- }
108
+ const selectableOptionsCount = content.selectableCount ?? 1;
109
+ const allowAddOption = content.allowAddOption === true;
110
+ const hideParticipantName = content.hideParticipantName === true;
111
+ const pollBody = {
112
+ name: content.name,
113
+ options,
114
+ selectableOptionsCount,
115
+ ...(allowAddOption ? { allowAddOption: true } : {}),
116
+ ...(hideParticipantName ? { hideParticipantName: true } : {})
120
117
  };
118
+ if (allowAddOption || hideParticipantName) {
119
+ return { pollCreationMessageV6: pollBody };
120
+ }
121
+ if (selectableOptionsCount === 1) {
122
+ return { pollCreationMessageV3: pollBody };
123
+ }
124
+ return { pollCreationMessage: pollBody };
121
125
  }
122
126
  function buildEventMessage(content) {
123
127
  const eventMessage = {
@@ -17,7 +17,8 @@ function createDiscoveryOps(deps) {
17
17
  fetch_full_image: opts?.fetchFullImage ?? keyType !== 'INVITE',
18
18
  fetch_creation_time: opts?.fetchCreationTime ?? true,
19
19
  fetch_wamo_sub: opts?.fetchWamoSub ?? false,
20
- fetch_status_metadata: false
20
+ fetch_status_metadata: false,
21
+ fetch_pinned_messages: true
21
22
  });
22
23
  if (!data?.xwa2_newsletter) {
23
24
  throw new Error('newsletter fetch returned no envelope');
@@ -107,7 +108,8 @@ function createDiscoveryOps(deps) {
107
108
  type: isJid ? 'JID' : 'INVITE',
108
109
  view_role: opts?.viewRole ?? newsletter_1.WA_NEWSLETTER_VIEW_ROLES.SUBSCRIBER
109
110
  },
110
- fetch_wamo_sub: opts?.fetchWamoSub ?? false
111
+ fetch_wamo_sub: opts?.fetchWamoSub ?? false,
112
+ fetch_pinned_messages: true
111
113
  });
112
114
  return (0, parse_1.parseDehydratedMetadata)(data);
113
115
  }
@@ -4,6 +4,24 @@ import type { Logger } from '../../infra/log/types';
4
4
  import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
5
5
  import { type Proto } from '../../proto';
6
6
  import type { WaChatMetadataStore } from '../../store/contracts/chat-metadata.store';
7
+ interface ByteRange {
8
+ readonly start: number;
9
+ readonly end: number;
10
+ }
11
+ interface HistorySyncScan {
12
+ readonly conversationRanges: readonly ByteRange[];
13
+ readonly pushnames: readonly Proto.IPushname[];
14
+ readonly inlineContacts: readonly Proto.IInlineContact[];
15
+ readonly phoneNumberToLidMappings: readonly Proto.IPhoneNumberToLIDMapping[];
16
+ readonly nctSalt: Uint8Array | null;
17
+ readonly chunkOrder: number | null;
18
+ readonly progress: number | null;
19
+ }
20
+ export declare function scanHistorySyncBlob(bytes: Uint8Array): HistorySyncScan;
21
+ export declare function scanConversationJidPair(bytes: Uint8Array, range: ByteRange): {
22
+ readonly pnJid: string | null;
23
+ readonly lidJid: string | null;
24
+ };
7
25
  interface WaHistorySyncDeps {
8
26
  readonly logger: Logger;
9
27
  readonly mediaTransfer: WaMediaTransferClient;
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanHistorySyncBlob = scanHistorySyncBlob;
4
+ exports.scanConversationJidPair = scanConversationJidPair;
3
5
  exports.runHistorySyncNotification = runHistorySyncNotification;
4
6
  exports.processHistorySyncNotification = processHistorySyncNotification;
5
7
  const node_util_1 = require("node:util");
@@ -10,6 +12,7 @@ const jid_1 = require("../../protocol/jid");
10
12
  const message_1 = require("../../protocol/message");
11
13
  const bytes_1 = require("../../util/bytes");
12
14
  const primitives_1 = require("../../util/primitives");
15
+ const protoscan_1 = require("../../util/protoscan");
13
16
  const unzipAsync = (0, node_util_1.promisify)(node_zlib_1.unzip);
14
17
  const HANDLED_SYNC_TYPES = new Set([
15
18
  _proto_1.proto.Message.HistorySyncType.INITIAL_BOOTSTRAP,
@@ -20,6 +23,90 @@ const HANDLED_SYNC_TYPES = new Set([
20
23
  _proto_1.proto.Message.HistorySyncType.NON_BLOCKING_DATA
21
24
  ]);
22
25
  const HISTORY_SYNC_MAX_PENDING_WRITES = 1024;
26
+ const HISTORY_SYNC_FIELDS = Object.freeze({
27
+ CONVERSATIONS: 2,
28
+ CHUNK_ORDER: 5,
29
+ PROGRESS: 6,
30
+ PUSHNAMES: 7,
31
+ PHONE_NUMBER_TO_LID_MAPPINGS: 15,
32
+ NCT_SALT: 19,
33
+ INLINE_CONTACTS: 20
34
+ });
35
+ const CONVERSATION_JID_FIELDS = Object.freeze({
36
+ PN_JID: 39,
37
+ LID_JID: 42,
38
+ ACCOUNT_LID: 49
39
+ });
40
+ function scanHistorySyncBlob(bytes) {
41
+ const conversationRanges = [];
42
+ const pushnames = [];
43
+ const inlineContacts = [];
44
+ const phoneNumberToLidMappings = [];
45
+ let nctSalt = null;
46
+ let chunkOrder = null;
47
+ let progress = null;
48
+ (0, protoscan_1.scanProtoFields)(bytes, 0, bytes.length, (field) => {
49
+ if (field.wireType === protoscan_1.PROTO_WIRE_TYPES.LEN) {
50
+ if (field.fieldNumber === HISTORY_SYNC_FIELDS.CONVERSATIONS) {
51
+ conversationRanges[conversationRanges.length] = {
52
+ start: field.valueStart,
53
+ end: field.valueEnd
54
+ };
55
+ }
56
+ else if (field.fieldNumber === HISTORY_SYNC_FIELDS.PUSHNAMES) {
57
+ pushnames[pushnames.length] = _proto_1.proto.Pushname.decode(bytes.subarray(field.valueStart, field.valueEnd));
58
+ }
59
+ else if (field.fieldNumber === HISTORY_SYNC_FIELDS.PHONE_NUMBER_TO_LID_MAPPINGS) {
60
+ phoneNumberToLidMappings[phoneNumberToLidMappings.length] =
61
+ _proto_1.proto.PhoneNumberToLIDMapping.decode(bytes.subarray(field.valueStart, field.valueEnd));
62
+ }
63
+ else if (field.fieldNumber === HISTORY_SYNC_FIELDS.INLINE_CONTACTS) {
64
+ inlineContacts[inlineContacts.length] = _proto_1.proto.InlineContact.decode(bytes.subarray(field.valueStart, field.valueEnd));
65
+ }
66
+ else if (field.fieldNumber === HISTORY_SYNC_FIELDS.NCT_SALT) {
67
+ nctSalt = bytes.slice(field.valueStart, field.valueEnd);
68
+ }
69
+ return;
70
+ }
71
+ if (field.wireType === protoscan_1.PROTO_WIRE_TYPES.VARINT) {
72
+ if (field.fieldNumber === HISTORY_SYNC_FIELDS.CHUNK_ORDER) {
73
+ chunkOrder = field.varintValue;
74
+ }
75
+ else if (field.fieldNumber === HISTORY_SYNC_FIELDS.PROGRESS) {
76
+ progress = field.varintValue;
77
+ }
78
+ }
79
+ });
80
+ return {
81
+ conversationRanges,
82
+ pushnames,
83
+ inlineContacts,
84
+ phoneNumberToLidMappings,
85
+ nctSalt,
86
+ chunkOrder,
87
+ progress
88
+ };
89
+ }
90
+ function scanConversationJidPair(bytes, range) {
91
+ let pnJid = null;
92
+ let lidJid = null;
93
+ let accountLid = null;
94
+ (0, protoscan_1.scanProtoFields)(bytes, range.start, range.end, (field) => {
95
+ if (field.wireType !== protoscan_1.PROTO_WIRE_TYPES.LEN) {
96
+ return;
97
+ }
98
+ if (field.fieldNumber === CONVERSATION_JID_FIELDS.PN_JID) {
99
+ pnJid = bytes_1.TEXT_DECODER.decode(bytes.subarray(field.valueStart, field.valueEnd));
100
+ }
101
+ else if (field.fieldNumber === CONVERSATION_JID_FIELDS.LID_JID) {
102
+ lidJid = bytes_1.TEXT_DECODER.decode(bytes.subarray(field.valueStart, field.valueEnd));
103
+ }
104
+ else if (field.fieldNumber === CONVERSATION_JID_FIELDS.ACCOUNT_LID) {
105
+ accountLid = bytes_1.TEXT_DECODER.decode(bytes.subarray(field.valueStart, field.valueEnd));
106
+ }
107
+ });
108
+ return { pnJid, lidJid: lidJid ?? accountLid };
109
+ }
23
110
  async function runHistorySyncNotification(deps, notification) {
24
111
  try {
25
112
  await processHistorySyncNotification(deps, notification);
@@ -49,14 +136,17 @@ async function processHistorySyncNotification(deps, notification) {
49
136
  }
50
137
  const blob = await downloadHistorySyncBlob(deps, notification);
51
138
  const decompressed = (0, bytes_1.toBytesView)(await unzipAsync(blob));
52
- const historySync = _proto_1.proto.HistorySync.decode(decompressed);
139
+ // Conversations are decoded one record at a time in the loop below so the
140
+ // full message graph never coexists in memory; the scan only records their
141
+ // byte ranges and decodes the small metadata fields.
142
+ const scan = scanHistorySyncBlob(decompressed);
53
143
  deps.logger.info('decoded history sync chunk', {
54
144
  syncType,
55
- chunkOrder: historySync.chunkOrder,
56
- progress: historySync.progress,
57
- conversations: historySync.conversations.length,
58
- pushnames: historySync.pushnames.length,
59
- inlineContacts: historySync.inlineContacts?.length ?? 0
145
+ chunkOrder: scan.chunkOrder,
146
+ progress: scan.progress,
147
+ conversations: scan.conversationRanges.length,
148
+ pushnames: scan.pushnames.length,
149
+ inlineContacts: scan.inlineContacts.length
60
150
  });
61
151
  const nowMs = Date.now();
62
152
  const pendingWrites = [];
@@ -65,24 +155,23 @@ async function processHistorySyncNotification(deps, notification) {
65
155
  // pushnames and mappings land on a single canonical (LID-form) contact
66
156
  // row instead of two mirror rows (one keyed by PN, one keyed by LID).
67
157
  const pnToLid = new Map();
68
- for (const map of historySync.phoneNumberToLidMappings ?? []) {
158
+ for (const map of scan.phoneNumberToLidMappings) {
69
159
  if (map.pnJid && map.lidJid) {
70
160
  pnToLid.set(map.pnJid, map.lidJid);
71
161
  }
72
162
  }
73
- for (const c of historySync.inlineContacts ?? []) {
163
+ for (const c of scan.inlineContacts) {
74
164
  if (c.pnJid && c.lidJid) {
75
165
  pnToLid.set(c.pnJid, c.lidJid);
76
166
  }
77
167
  }
78
- for (const conversation of historySync.conversations) {
79
- const pnJid = conversation.pnJid;
80
- const lidJid = conversation.lidJid ?? conversation.accountLid;
81
- if (pnJid && lidJid) {
82
- pnToLid.set(pnJid, lidJid);
168
+ for (const range of scan.conversationRanges) {
169
+ const pair = scanConversationJidPair(decompressed, range);
170
+ if (pair.pnJid && pair.lidJid) {
171
+ pnToLid.set(pair.pnJid, pair.lidJid);
83
172
  }
84
173
  }
85
- for (const pn of historySync.pushnames) {
174
+ for (const pn of scan.pushnames) {
86
175
  if (!pn.id) {
87
176
  continue;
88
177
  }
@@ -103,7 +192,7 @@ async function processHistorySyncNotification(deps, notification) {
103
192
  await (0, history_blob_1.flushPendingWrites)(pendingWrites);
104
193
  }
105
194
  }
106
- for (const c of historySync.inlineContacts ?? []) {
195
+ for (const c of scan.inlineContacts) {
107
196
  const jid = c.lidJid ?? c.pnJid;
108
197
  if (!jid) {
109
198
  continue;
@@ -123,12 +212,32 @@ async function processHistorySyncNotification(deps, notification) {
123
212
  }
124
213
  }
125
214
  let messagesCount = 0;
126
- for (const conversation of historySync.conversations) {
215
+ const tokenConversations = [];
216
+ for (const range of scan.conversationRanges) {
217
+ let conversation;
218
+ try {
219
+ conversation = _proto_1.proto.Conversation.decode(decompressed.subarray(range.start, range.end));
220
+ }
221
+ catch (error) {
222
+ await Promise.allSettled(pendingWrites);
223
+ throw (0, primitives_1.toError)(error);
224
+ }
127
225
  const threadJid = conversation.id;
128
226
  if (!threadJid) {
129
227
  deps.logger.debug('skipping history sync conversation without thread jid');
130
228
  continue;
131
229
  }
230
+ if (deps.onPrivacyTokens &&
231
+ (conversation.tcToken ||
232
+ conversation.tcTokenTimestamp ||
233
+ conversation.tcTokenSenderTimestamp)) {
234
+ tokenConversations[tokenConversations.length] = {
235
+ jid: threadJid,
236
+ tcToken: conversation.tcToken ? new Uint8Array(conversation.tcToken) : undefined,
237
+ tcTokenTimestamp: (0, primitives_1.longToNumber)(conversation.tcTokenTimestamp) || undefined,
238
+ tcTokenSenderTimestamp: (0, primitives_1.longToNumber)(conversation.tcTokenSenderTimestamp) || undefined
239
+ };
240
+ }
132
241
  const ephemeralExpiration = conversation.ephemeralExpiration ?? undefined;
133
242
  const ephemeralSettingTimestamp = (0, message_1.normalizeEphemeralSettingSeconds)((0, primitives_1.longToNumber)(conversation.ephemeralSettingTimestamp)) || undefined;
134
243
  if (deps.chatMetadataStore && ephemeralExpiration !== undefined) {
@@ -216,37 +325,20 @@ async function processHistorySyncNotification(deps, notification) {
216
325
  await (0, history_blob_1.flushPendingWrites)(pendingWrites);
217
326
  }
218
327
  }
219
- if (deps.onPrivacyTokens) {
220
- const tokenConversations = [];
221
- for (const conversation of historySync.conversations) {
222
- if (!conversation.id)
223
- continue;
224
- if (conversation.tcToken ||
225
- conversation.tcTokenTimestamp ||
226
- conversation.tcTokenSenderTimestamp) {
227
- tokenConversations[tokenConversations.length] = {
228
- jid: conversation.id,
229
- tcToken: conversation.tcToken,
230
- tcTokenTimestamp: (0, primitives_1.longToNumber)(conversation.tcTokenTimestamp) || undefined,
231
- tcTokenSenderTimestamp: (0, primitives_1.longToNumber)(conversation.tcTokenSenderTimestamp) || undefined
232
- };
233
- }
234
- }
235
- if (tokenConversations.length > 0) {
236
- pendingWrites[pendingWrites.length] = deps.onPrivacyTokens(tokenConversations);
237
- }
328
+ if (deps.onPrivacyTokens && tokenConversations.length > 0) {
329
+ pendingWrites[pendingWrites.length] = deps.onPrivacyTokens(tokenConversations);
238
330
  }
239
- if (deps.onNctSalt && historySync.nctSalt) {
240
- pendingWrites[pendingWrites.length] = deps.onNctSalt(historySync.nctSalt);
331
+ if (deps.onNctSalt && scan.nctSalt) {
332
+ pendingWrites[pendingWrites.length] = deps.onNctSalt(scan.nctSalt);
241
333
  }
242
334
  const event = {
243
335
  syncType,
244
336
  messagesCount,
245
- conversationsCount: historySync.conversations.length,
246
- pushnamesCount: historySync.pushnames.length,
247
- inlineContactsCount: historySync.inlineContacts?.length ?? 0,
248
- chunkOrder: historySync.chunkOrder ?? undefined,
249
- progress: historySync.progress ?? undefined
337
+ conversationsCount: scan.conversationRanges.length,
338
+ pushnamesCount: scan.pushnames.length,
339
+ inlineContactsCount: scan.inlineContacts.length,
340
+ chunkOrder: scan.chunkOrder ?? undefined,
341
+ progress: scan.progress ?? undefined
250
342
  };
251
343
  await (0, history_blob_1.flushPendingWrites)(pendingWrites);
252
344
  deps.emitEvent('history_sync_chunk', event);
@@ -63,10 +63,18 @@ type ExposedOf<P> = P extends {
63
63
  * `{ readonly voip: WaVoipCoordinator }`.
64
64
  */
65
65
  export type WaClientExposedFromPlugins<P extends readonly unknown[]> = UnionToIntersection<ExposedOf<P[number]>>;
66
- /** Event map a plugin contributes, carried as a phantom marker on its definition. */
66
+ /**
67
+ * Event map a plugin contributes, carried as a phantom marker on its definition.
68
+ *
69
+ * The marker is optional (it never exists at runtime), so under
70
+ * `exactOptionalPropertyTypes` the inferred `E` keeps the `| undefined` the
71
+ * declaration emits. `Exclude` drops it before the union reaches
72
+ * {@link UnionToIntersection}, which would otherwise collapse
73
+ * `Events | undefined` to `never` and type every client listener as `never`.
74
+ */
67
75
  type EventsOf<P> = P extends {
68
76
  readonly __pluginEvents?: infer E;
69
- } ? unknown extends E ? {} : E : {};
77
+ } ? unknown extends E ? {} : Exclude<E, undefined> : {};
70
78
  /**
71
79
  * Client events contributed by a tuple of plugin definitions, derived from the
72
80
  * plugin values (no global augmentation). Threaded into the client's
@@ -7,5 +7,5 @@ export { hkdf, hkdfSplit } from '../core/hkdf';
7
7
  export { toSerializedPubKey, toRawPubKey, prependVersion, readVersionedContent } from '../core/keys';
8
8
  export { writeNonceCounter } from '../core/nonce';
9
9
  export { randomBytesAsync, randomFillAsync, randomIntAsync } from '../core/random';
10
- export { sha1, sha256, sha512, md5Bytes, aesGcmEncrypt, aesGcmDecrypt, aesCbcEncrypt, aesCbcDecrypt, aesCtrEncrypt, aesCtrDecrypt, hmacSha256Sign, hmacSha512Sign, pbkdf2Sha256 } from '../core/primitives';
10
+ export { sha1, sha256, sha512, md5Bytes, aesGcmEncrypt, aesGcmDecrypt, aesCbcEncrypt, aesCbcEncryptWithTrailer, aesCbcDecrypt, aesCtrEncrypt, aesCtrDecrypt, hmacSha256Sign, hmacSha512Sign, pbkdf2Sha256 } from '../core/primitives';
11
11
  export { xeddsaSign, xeddsaVerify } from '../core/xeddsa';
@@ -3,7 +3,7 @@
3
3
  * Cryptographic utilities
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.xeddsaVerify = exports.xeddsaSign = exports.pbkdf2Sha256 = exports.hmacSha512Sign = exports.hmacSha256Sign = exports.aesCtrDecrypt = exports.aesCtrEncrypt = exports.aesCbcDecrypt = exports.aesCbcEncrypt = exports.aesGcmDecrypt = exports.aesGcmEncrypt = exports.md5Bytes = exports.sha512 = exports.sha256 = exports.sha1 = exports.randomIntAsync = exports.randomFillAsync = exports.randomBytesAsync = exports.writeNonceCounter = exports.readVersionedContent = exports.prependVersion = exports.toRawPubKey = exports.toSerializedPubKey = exports.hkdfSplit = exports.hkdf = exports.X25519 = exports.Ed25519 = void 0;
6
+ exports.xeddsaVerify = exports.xeddsaSign = exports.pbkdf2Sha256 = exports.hmacSha512Sign = exports.hmacSha256Sign = exports.aesCtrDecrypt = exports.aesCtrEncrypt = exports.aesCbcDecrypt = exports.aesCbcEncryptWithTrailer = exports.aesCbcEncrypt = exports.aesGcmDecrypt = exports.aesGcmEncrypt = exports.md5Bytes = exports.sha512 = exports.sha256 = exports.sha1 = exports.randomIntAsync = exports.randomFillAsync = exports.randomBytesAsync = exports.writeNonceCounter = exports.readVersionedContent = exports.prependVersion = exports.toRawPubKey = exports.toSerializedPubKey = exports.hkdfSplit = exports.hkdf = exports.X25519 = exports.Ed25519 = void 0;
7
7
  var Ed25519_1 = require("../curves/Ed25519");
8
8
  Object.defineProperty(exports, "Ed25519", { enumerable: true, get: function () { return Ed25519_1.Ed25519; } });
9
9
  var X25519_1 = require("../curves/X25519");
@@ -30,6 +30,7 @@ Object.defineProperty(exports, "md5Bytes", { enumerable: true, get: function ()
30
30
  Object.defineProperty(exports, "aesGcmEncrypt", { enumerable: true, get: function () { return primitives_1.aesGcmEncrypt; } });
31
31
  Object.defineProperty(exports, "aesGcmDecrypt", { enumerable: true, get: function () { return primitives_1.aesGcmDecrypt; } });
32
32
  Object.defineProperty(exports, "aesCbcEncrypt", { enumerable: true, get: function () { return primitives_1.aesCbcEncrypt; } });
33
+ Object.defineProperty(exports, "aesCbcEncryptWithTrailer", { enumerable: true, get: function () { return primitives_1.aesCbcEncryptWithTrailer; } });
33
34
  Object.defineProperty(exports, "aesCbcDecrypt", { enumerable: true, get: function () { return primitives_1.aesCbcDecrypt; } });
34
35
  Object.defineProperty(exports, "aesCtrEncrypt", { enumerable: true, get: function () { return primitives_1.aesCtrEncrypt; } });
35
36
  Object.defineProperty(exports, "aesCtrDecrypt", { enumerable: true, get: function () { return primitives_1.aesCtrDecrypt; } });
@@ -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);
@@ -51,6 +51,23 @@ async function xeddsaVerify(curvePublicKey, message, signature) {
51
51
  signature[lastByteIndex] = originalLastByte;
52
52
  }
53
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
+ }
54
71
  /**
55
72
  * Signs `message` with an X25519 (Montgomery) private key using the XEdDSA
56
73
  * construction. Returns a 64-byte signature.
@@ -60,10 +77,8 @@ async function xeddsaSign(privateKey, message) {
60
77
  if (nativeBinding) {
61
78
  return (0, bytes_1.toBytesView)(nativeBinding.xeddsaSign(privateKey, message));
62
79
  }
63
- const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
64
- const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
65
- const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
66
- const pubKeySignBit = encodedPublic[31] & 0x80;
80
+ const { privateScalar, encodedPublic, pubKeySignBit } = derivePublicForSigning(privateKey);
81
+ const clampedPrivateKey = privateKey;
67
82
  const randomSuffix = await (0, random_1.randomBytesAsync)(64);
68
83
  const r = (0, mod_1.modGroup)((0, le_1.bytesToBigIntLE)((0, primitives_1.sha512)([PREFIX_SIGNATURE_RANDOM, clampedPrivateKey, message, randomSuffix])));
69
84
  const encodedR = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(r));
@@ -86,19 +86,33 @@ function montgomeryToEdwardsPublic(curvePublicKey, signBit) {
86
86
  encoded[31] = (encoded[31] & 0x7f) | (signBit & 0x80);
87
87
  return encoded;
88
88
  }
89
+ const privateKeyObjectCache = new WeakMap();
90
+ const publicKeyObjectCache = new WeakMap();
89
91
  function x25519PrivateKeyObject(privKey) {
90
- 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)({
91
97
  key: (0, types_1.pkcs8FromRawPrivate)(constants_1.X25519_PKCS8_PREFIX, privKey),
92
98
  format: 'der',
93
99
  type: 'pkcs8'
94
100
  });
101
+ privateKeyObjectCache.set(privKey, keyObject);
102
+ return keyObject;
95
103
  }
96
104
  function x25519PublicKeyObject(pubKey) {
97
- 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)({
98
110
  key: (0, bytes_1.concatBytes)([constants_1.X25519_SPKI_PREFIX, pubKey]),
99
111
  format: 'der',
100
112
  type: 'spki'
101
113
  });
114
+ publicKeyObjectCache.set(pubKey, keyObject);
115
+ return keyObject;
102
116
  }
103
117
  /**
104
118
  * X25519 key-pair generation and Diffie-Hellman scalar multiplication
@@ -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
  }