zapo-js 1.1.2 → 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 (45) hide show
  1. package/dist/client/WaClient.js +1 -7
  2. package/dist/client/WaClientFactory.js +7 -4
  3. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +16 -0
  4. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +43 -6
  5. package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
  6. package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
  7. package/dist/client/coordinators/WaProfileCoordinator.d.ts +7 -0
  8. package/dist/client/coordinators/WaProfileCoordinator.js +10 -4
  9. package/dist/client/coordinators/WaRetryCoordinator.js +1 -1
  10. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
  11. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  12. package/dist/client/persistence/mailbox.d.ts +6 -0
  13. package/dist/client/persistence/mailbox.js +2 -2
  14. package/dist/client/types.d.ts +49 -0
  15. package/dist/esm/client/WaClient.js +1 -7
  16. package/dist/esm/client/WaClientFactory.js +8 -5
  17. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +44 -7
  18. package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
  19. package/dist/esm/client/coordinators/WaProfileCoordinator.js +10 -4
  20. package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -1
  21. package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  22. package/dist/esm/client/persistence/mailbox.js +2 -2
  23. package/dist/esm/message/primitives/incoming.js +99 -42
  24. package/dist/esm/protocol/jid.js +42 -0
  25. package/dist/esm/retry/reason.js +2 -2
  26. package/dist/esm/transport/node/WaNodeOrchestrator.js +6 -2
  27. package/dist/esm/transport/node/builders/message.js +3 -0
  28. package/dist/esm/transport/node/builders/presence.js +2 -1
  29. package/dist/esm/transport/node/builders/profile.js +3 -2
  30. package/dist/esm/transport/node/builders/retry.js +2 -4
  31. package/dist/index.d.ts +1 -1
  32. package/dist/message/primitives/incoming.d.ts +3 -1
  33. package/dist/message/primitives/incoming.js +98 -41
  34. package/dist/protocol/jid.d.ts +12 -0
  35. package/dist/protocol/jid.js +44 -0
  36. package/dist/retry/reason.js +2 -2
  37. package/dist/transport/node/WaNodeOrchestrator.js +6 -2
  38. package/dist/transport/node/builders/message.d.ts +1 -0
  39. package/dist/transport/node/builders/message.js +3 -0
  40. package/dist/transport/node/builders/presence.d.ts +6 -0
  41. package/dist/transport/node/builders/presence.js +2 -1
  42. package/dist/transport/node/builders/profile.d.ts +1 -1
  43. package/dist/transport/node/builders/profile.js +3 -2
  44. package/dist/transport/node/builders/retry.js +2 -4
  45. package/package.json +2 -2
@@ -31,6 +31,52 @@ function extractMessageIdentityAttrs(attrs) {
31
31
  pushName: attrs.notify
32
32
  };
33
33
  }
34
+ /**
35
+ * Self-authored 1:1 chat is the recipient, so its alternate addressing is the
36
+ * `recipient*` attrs (the `sender*` attrs describe me). Promotes `recipientAlt`
37
+ * to `remoteJidAlt` and drops the stale sender/recipient fields.
38
+ */
39
+ function promoteRecipientAddressing(identity) {
40
+ return {
41
+ ...(identity.recipientAlt ? { remoteJidAlt: identity.recipientAlt } : {}),
42
+ ...(identity.senderUsername !== undefined
43
+ ? { senderUsername: identity.senderUsername }
44
+ : {})
45
+ };
46
+ }
47
+ /**
48
+ * Resolves the {@link WaIncomingMessageKey} for a decrypted stanza. `remoteJid`
49
+ * is the chat: the `from`, or the recipient (`recipient` attr, then the
50
+ * deviceSentMessage `destinationJid`) when the message is `fromMe`.
51
+ */
52
+ function buildIncomingMessageKey(node, sender, options, destinationJid) {
53
+ const fromUserJid = node.attrs.from ? (0, jid_1.toUserJid)(node.attrs.from) : node.attrs.from;
54
+ const isGroup = fromUserJid ? (0, jid_1.isGroupJid)(fromUserJid) : false;
55
+ const isBroadcast = fromUserJid ? (0, jid_1.isBroadcastJid)(fromUserJid) : false;
56
+ const fromMe = sender
57
+ ? (0, jid_1.isOwnAccountJid)(sender.userJid, options.getMeJid?.(), options.getMeLid?.())
58
+ : false;
59
+ const selfSentChat = fromMe && !isGroup && !isBroadcast
60
+ ? (node.attrs.recipient ?? destinationJid ?? undefined)
61
+ : undefined;
62
+ const chatJid = selfSentChat ? (0, jid_1.toUserJid)(selfSentChat) : fromUserJid;
63
+ const { pushName, ...identity } = extractMessageIdentityAttrs(node.attrs);
64
+ const keyIdentity = selfSentChat ? promoteRecipientAddressing(identity) : identity;
65
+ return {
66
+ pushName,
67
+ key: {
68
+ remoteJid: chatJid ?? '',
69
+ id: node.attrs.id ?? '',
70
+ fromMe,
71
+ isGroup,
72
+ isBroadcast,
73
+ isNewsletter: false,
74
+ ...keyIdentity,
75
+ senderDevice: sender?.device ?? 0,
76
+ ...((isGroup || isBroadcast) && sender ? { participant: sender.userJid } : {})
77
+ }
78
+ };
79
+ }
34
80
  function pickNextRetryCount(node) {
35
81
  const retryNode = (0, helpers_1.findNodeChild)(node, 'retry');
36
82
  const parsed = (0, primitives_1.parseOptionalInt)(retryNode?.attrs.count);
@@ -244,26 +290,11 @@ function processMsmsgEncNode(node, encNode, senderJid, options) {
244
290
  encPayload: decoded.encPayload
245
291
  }
246
292
  };
247
- const chatJid = node.attrs.from ? (0, jid_1.toUserJid)(node.attrs.from) : node.attrs.from;
248
293
  const sender = senderJid ? (0, jid_1.parseJidFull)(senderJid) : null;
249
- const isGroup = chatJid ? (0, jid_1.isGroupJid)(chatJid) : false;
250
- const isBroadcast = chatJid ? (0, jid_1.isBroadcastJid)(chatJid) : false;
251
- const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
294
+ const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
252
295
  options.emitIncomingMessage?.({
253
296
  rawNode: buildIncomingEventRawNode(node),
254
- key: {
255
- remoteJid: chatJid ?? '',
256
- id: node.attrs.id ?? '',
257
- fromMe: false,
258
- isGroup,
259
- isBroadcast,
260
- isNewsletter: false,
261
- ...keyIdentity,
262
- ...((isGroup || isBroadcast) && sender?.userJid
263
- ? { participant: sender.userJid }
264
- : {}),
265
- senderDevice: sender?.address.device ?? 0
266
- },
297
+ key,
267
298
  stanzaType: node.attrs.type,
268
299
  offline: node.attrs.offline !== undefined,
269
300
  timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
@@ -317,29 +348,14 @@ async function decryptAndProcessEncNode(node, encNode, encType, senderJid, optio
317
348
  }
318
349
  }
319
350
  if (shouldEmitIncomingMessage(message)) {
320
- // remoteJid is the chat identity, which is deviceless: the device
321
- // lives in senderDevice (from senderAddress), so strip any `:device`
322
- // segment the `from` attr carries for 1:1 chats.
323
- const fromAttr = node.attrs.from;
324
- const chatJid = fromAttr ? (0, jid_1.toUserJid)(fromAttr) : fromAttr;
325
- const isGroup = chatJid ? (0, jid_1.isGroupJid)(chatJid) : false;
326
- const isBroadcast = chatJid ? (0, jid_1.isBroadcastJid)(chatJid) : false;
327
- const senderUserJid = `${senderAddress.user}@${senderAddress.server}`;
328
- const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
351
+ const { key, pushName } = buildIncomingMessageKey(node, {
352
+ userJid: `${senderAddress.user}@${senderAddress.server}`,
353
+ device: senderAddress.device
354
+ }, options, decodedMessage.deviceSentMessage?.destinationJid ?? undefined);
329
355
  const expirationSeconds = (0, context_info_1.pickIncomingExpirationSeconds)(message);
330
356
  options.emitIncomingMessage?.({
331
357
  rawNode: buildIncomingEventRawNode(node),
332
- key: {
333
- remoteJid: chatJid ?? '',
334
- id: node.attrs.id ?? '',
335
- fromMe: false,
336
- isGroup,
337
- isBroadcast,
338
- isNewsletter: false,
339
- ...keyIdentity,
340
- senderDevice: senderAddress.device,
341
- ...(isGroup || isBroadcast ? { participant: senderUserJid } : {})
342
- },
358
+ key,
343
359
  stanzaType: node.attrs.type,
344
360
  offline: node.attrs.offline !== undefined,
345
361
  timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
@@ -418,10 +434,15 @@ async function handleIncomingMessageAck(node, options) {
418
434
  continue;
419
435
  }
420
436
  const encType = child.attrs.type === 'msg' ? 'msg' : 'pkmsg';
421
- result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) => options.signalProtocol.decryptMessage(senderAddress, {
422
- type: encType,
423
- ciphertext
424
- }));
437
+ result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) => {
438
+ const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(senderJid, options.getMeJid?.(), options.getMeLid?.());
439
+ return options.signalProtocol.decryptMessage(sessionJid === senderJid
440
+ ? senderAddress
441
+ : (0, jid_1.parseSignalAddressFromJid)(sessionJid), {
442
+ type: encType,
443
+ ciphertext
444
+ });
445
+ });
425
446
  break;
426
447
  }
427
448
  case 'msmsg': {
@@ -479,6 +500,42 @@ async function handleIncomingMessageAck(node, options) {
479
500
  await options.sendNode(ackNode);
480
501
  return true;
481
502
  }
503
+ const unavailableNode = (0, helpers_1.findNodeChild)(node, 'unavailable');
504
+ if (unavailableNode) {
505
+ const kind = unavailableNode.attrs.hosted === 'true'
506
+ ? 'hosted'
507
+ : unavailableNode.attrs.type === 'view_once'
508
+ ? 'view_once'
509
+ : 'other';
510
+ const senderJid = node.attrs.participant ?? node.attrs.from;
511
+ const sender = senderJid ? (0, jid_1.parseJidFull)(senderJid) : null;
512
+ const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
513
+ options.emitUnavailableMessage?.({
514
+ rawNode: buildIncomingEventRawNode(node),
515
+ key,
516
+ kind,
517
+ stanzaType: node.attrs.type,
518
+ offline: node.attrs.offline !== undefined,
519
+ timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
520
+ pushName
521
+ });
522
+ const ackNode = (0, global_1.buildAckNode)({
523
+ kind: 'message',
524
+ node,
525
+ id,
526
+ to: from,
527
+ from: options.getMeJid?.()
528
+ });
529
+ options.logger.trace('acking unavailable incoming message', {
530
+ id,
531
+ to: from,
532
+ type: ackNode.attrs.type,
533
+ participant: ackNode.attrs.participant,
534
+ unavailableKind: kind
535
+ });
536
+ await options.sendNode(ackNode);
537
+ return true;
538
+ }
482
539
  if (!shouldSendStandardReceipt) {
483
540
  return true;
484
541
  }
@@ -66,6 +66,18 @@ export declare function toUserJid(jid: string, options?: {
66
66
  readonly canonicalizeSignalServer?: boolean;
67
67
  readonly hostDomain?: string;
68
68
  }): string;
69
+ /**
70
+ * True when `jid` is the account's own user, matching the `meJid` (pn) or
71
+ * `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
72
+ * `isMeAccount`.
73
+ */
74
+ export declare function isOwnAccountJid(jid: string, meJid: string | null | undefined, meLid: string | null | undefined): boolean;
75
+ /**
76
+ * Rewrites the account's own PN device JID to its LID equivalent so self traffic
77
+ * keys one LID session instead of forking into separate PN and LID ratchets.
78
+ * No-op for other users, already-LID/unparseable JIDs, or unknown identity.
79
+ */
80
+ export declare function canonicalizeOwnAccountJid(jid: string, meJid: string | null | undefined, meLid: string | null | undefined): string;
69
81
  /**
70
82
  * Returns the JID in its full device form. JIDs with `device === 0` lose the
71
83
  * device segment (`user@server`); all others keep `user:device@server`.
@@ -15,6 +15,8 @@ exports.parseJidFull = parseJidFull;
15
15
  exports.canonicalizeSignalServer = canonicalizeSignalServer;
16
16
  exports.canonicalizeSignalJid = canonicalizeSignalJid;
17
17
  exports.toUserJid = toUserJid;
18
+ exports.isOwnAccountJid = isOwnAccountJid;
19
+ exports.canonicalizeOwnAccountJid = canonicalizeOwnAccountJid;
18
20
  exports.normalizeDeviceJid = normalizeDeviceJid;
19
21
  exports.applyDeviceToJid = applyDeviceToJid;
20
22
  exports.isHostedDeviceId = isHostedDeviceId;
@@ -217,6 +219,48 @@ function toUserJid(jid, options = {}) {
217
219
  : address.server;
218
220
  return `${address.user}@${server}`;
219
221
  }
222
+ /**
223
+ * True when `jid` is the account's own user, matching the `meJid` (pn) or
224
+ * `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
225
+ * `isMeAccount`.
226
+ */
227
+ function isOwnAccountJid(jid, meJid, meLid) {
228
+ const candidateUser = toUserJid(jid);
229
+ return ((!!meJid && toUserJid(meJid) === candidateUser) ||
230
+ (!!meLid && toUserJid(meLid) === candidateUser));
231
+ }
232
+ /**
233
+ * Rewrites the account's own PN device JID to its LID equivalent so self traffic
234
+ * keys one LID session instead of forking into separate PN and LID ratchets.
235
+ * No-op for other users, already-LID/unparseable JIDs, or unknown identity.
236
+ */
237
+ function canonicalizeOwnAccountJid(jid, meJid, meLid) {
238
+ if (!meJid || !meLid)
239
+ return jid;
240
+ let address;
241
+ try {
242
+ address = parseSignalAddressFromJid(jid);
243
+ }
244
+ catch {
245
+ return jid;
246
+ }
247
+ if ((address.server ?? constants_1.WA_DEFAULTS.HOST_DOMAIN) !== constants_1.WA_DEFAULTS.HOST_DOMAIN)
248
+ return jid;
249
+ let mePnUser;
250
+ let meLidUser;
251
+ try {
252
+ mePnUser = parseSignalAddressFromJid(meJid).user;
253
+ meLidUser = parseSignalAddressFromJid(meLid).user;
254
+ }
255
+ catch {
256
+ return jid;
257
+ }
258
+ if (address.user !== mePnUser)
259
+ return jid;
260
+ return address.device === 0
261
+ ? `${meLidUser}@${constants_1.WA_DEFAULTS.LID_SERVER}`
262
+ : `${meLidUser}:${address.device}@${constants_1.WA_DEFAULTS.LID_SERVER}`;
263
+ }
220
264
  /**
221
265
  * Returns the JID in its full device form. JIDs with `device === 0` lose the
222
266
  * device segment (`user@server`); all others keep `user:device@server`.
@@ -20,10 +20,10 @@ const RETRY_REASON_MATCHERS = [
20
20
  },
21
21
  { matches: ['invalid signature'], code: constants_1.RETRY_REASON.SignalErrorInvalidSignature },
22
22
  {
23
- matches: ['too many messages in future', 'future message'],
23
+ matches: ['too far in future', 'too many messages in future', 'future message'],
24
24
  code: constants_1.RETRY_REASON.SignalErrorFutureMessage
25
25
  },
26
- { matches: ['invalid mac'], code: constants_1.RETRY_REASON.SignalErrorBadMac },
26
+ { matches: ['invalid message mac', 'invalid mac'], code: constants_1.RETRY_REASON.SignalErrorBadMac },
27
27
  { matches: ['invalid session'], code: constants_1.RETRY_REASON.SignalErrorInvalidSession },
28
28
  { matches: ['invalid message key'], code: constants_1.RETRY_REASON.SignalErrorInvalidMsgKey },
29
29
  {
@@ -24,8 +24,12 @@ class WaNodeOrchestrator {
24
24
  return this.pendingQueries.size > 0;
25
25
  }
26
26
  clearPending(reason) {
27
- this.logger.warn('clearing pending node queries', {
28
- count: this.pendingQueries.size,
27
+ const count = this.pendingQueries.size;
28
+ if (count === 0) {
29
+ return;
30
+ }
31
+ this.logger.debug('clearing pending node queries', {
32
+ count,
29
33
  reason: reason.message
30
34
  });
31
35
  for (const pending of this.pendingQueries.values()) {
@@ -15,6 +15,7 @@ type DirectMessageFanoutInput = {
15
15
  readonly customNodes?: readonly BinaryNode[];
16
16
  readonly mediatype?: string;
17
17
  readonly decryptFail?: string;
18
+ readonly peerRecipientPn?: string;
18
19
  readonly additionalAttributes?: Readonly<Record<string, string>>;
19
20
  };
20
21
  type GroupMessageFanoutInput = DirectMessageFanoutInput & {
@@ -39,6 +39,9 @@ function buildMessageAttrs(input) {
39
39
  if (input.addressingMode) {
40
40
  attrs.addressing_mode = input.addressingMode;
41
41
  }
42
+ if (input.peerRecipientPn) {
43
+ attrs.peer_recipient_pn = input.peerRecipientPn;
44
+ }
42
45
  if (input.additionalAttributes) {
43
46
  Object.assign(attrs, input.additionalAttributes);
44
47
  }
@@ -9,5 +9,11 @@ export interface BuildPresenceSubscribeNodeInput {
9
9
  readonly jid: string;
10
10
  readonly name?: string;
11
11
  readonly context?: string;
12
+ /**
13
+ * Receiver-mode `<tctoken>` node echoed back to prove this account is a
14
+ * trusted contact, gating the target's presence/last-seen visibility.
15
+ * Attached as a child of the `<presence>` stanza when present.
16
+ */
17
+ readonly privacyTokenNode?: BinaryNode;
12
18
  }
13
19
  export declare function buildPresenceSubscribeNode(input: BuildPresenceSubscribeNodeInput): BinaryNode;
@@ -38,6 +38,7 @@ function buildPresenceSubscribeNode(input) {
38
38
  }
39
39
  return {
40
40
  tag: nodes_1.WA_NODE_TAGS.PRESENCE,
41
- attrs
41
+ attrs,
42
+ ...(input.privacyTokenNode ? { content: [input.privacyTokenNode] } : {})
42
43
  };
43
44
  }
@@ -1,6 +1,6 @@
1
1
  import type { BinaryNode } from '../../types';
2
2
  export type WaProfilePictureType = 'preview' | 'image';
3
- export declare function buildGetProfilePictureIq(targetJid: string, type?: WaProfilePictureType, existingId?: string): BinaryNode;
3
+ export declare function buildGetProfilePictureIq(targetJid: string, type?: WaProfilePictureType, existingId?: string, privacyTokenNode?: BinaryNode): BinaryNode;
4
4
  export declare function buildSetProfilePictureIq(imageBytes: Uint8Array, targetJid?: string): BinaryNode;
5
5
  export declare function buildDeleteProfilePictureIq(targetJid?: string): BinaryNode;
6
6
  export declare function buildSetStatusIq(text: string): BinaryNode;
@@ -11,7 +11,7 @@ exports.buildGetUsernameUsyncQueryNode = buildGetUsernameUsyncQueryNode;
11
11
  exports.buildGetStatusUsyncQueryNodes = buildGetStatusUsyncQueryNodes;
12
12
  const constants_1 = require("../../../protocol/constants");
13
13
  const query_1 = require("../../node/query");
14
- function buildGetProfilePictureIq(targetJid, type = 'preview', existingId) {
14
+ function buildGetProfilePictureIq(targetJid, type = 'preview', existingId, privacyTokenNode) {
15
15
  const pictureAttrs = {
16
16
  type,
17
17
  query: 'url'
@@ -22,7 +22,8 @@ function buildGetProfilePictureIq(targetJid, type = 'preview', existingId) {
22
22
  return (0, query_1.buildIqNode)(constants_1.WA_IQ_TYPES.GET, constants_1.WA_DEFAULTS.HOST_DOMAIN, constants_1.WA_XMLNS.PROFILE_PICTURE, [
23
23
  {
24
24
  tag: constants_1.WA_NODE_TAGS.PICTURE,
25
- attrs: pictureAttrs
25
+ attrs: pictureAttrs,
26
+ ...(privacyTokenNode ? { content: [privacyTokenNode] } : {})
26
27
  }
27
28
  ], {
28
29
  target: targetJid
@@ -76,11 +76,9 @@ function buildRetryReceiptNode(input) {
76
76
  v: constants_2.RETRY_RECEIPT_VERSION,
77
77
  count: String(input.retryCount),
78
78
  id: input.originalMsgId,
79
- t: input.t
79
+ t: input.t,
80
+ error: String(input.error ?? 0)
80
81
  };
81
- if (input.error !== undefined && input.error !== 0) {
82
- retryAttrs.error = String(input.error);
83
- }
84
82
  const content = [
85
83
  {
86
84
  tag: 'retry',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapo-js",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "High-performance WhatsApp Web TypeScript library",
5
5
  "license": "MIT",
6
6
  "author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",
@@ -124,7 +124,7 @@
124
124
  },
125
125
  "scripts": {
126
126
  "preinstall": "node ./scripts/check-node-version.cjs",
127
- "prepack": "npm run build",
127
+ "prepare": "npm run build",
128
128
  "changeset": "changeset",
129
129
  "changeset:status": "changeset status --verbose",
130
130
  "version:packages": "changeset version",