zapo-js 1.1.1 → 1.1.3

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 (67) hide show
  1. package/dist/appstate/sync/WaAppStateSyncClient.d.ts +11 -1
  2. package/dist/appstate/sync/WaAppStateSyncClient.js +36 -15
  3. package/dist/auth/credentials-flow.js +3 -1
  4. package/dist/client/WaClientFactory.js +33 -10
  5. package/dist/client/connection/WaConnectionManager.js +3 -0
  6. package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
  7. package/dist/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
  8. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +6 -1
  9. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +12 -8
  10. package/dist/client/coordinators/WaPassiveTasksCoordinator.d.ts +6 -1
  11. package/dist/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
  12. package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
  13. package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
  14. package/dist/client/coordinators/WaProfileCoordinator.d.ts +18 -6
  15. package/dist/client/coordinators/WaProfileCoordinator.js +13 -4
  16. package/dist/client/coordinators/WaRetryCoordinator.d.ts +18 -0
  17. package/dist/client/coordinators/WaRetryCoordinator.js +88 -15
  18. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
  19. package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  20. package/dist/esm/appstate/sync/WaAppStateSyncClient.js +36 -15
  21. package/dist/esm/auth/credentials-flow.js +3 -1
  22. package/dist/esm/client/WaClientFactory.js +34 -11
  23. package/dist/esm/client/connection/WaConnectionManager.js +3 -0
  24. package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
  25. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -10
  26. package/dist/esm/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
  27. package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
  28. package/dist/esm/client/coordinators/WaProfileCoordinator.js +13 -4
  29. package/dist/esm/client/coordinators/WaRetryCoordinator.js +89 -16
  30. package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
  31. package/dist/esm/message/WaMessageClient.js +3 -0
  32. package/dist/esm/message/primitives/incoming.js +77 -45
  33. package/dist/esm/protocol/constants.js +1 -1
  34. package/dist/esm/protocol/jid.js +42 -0
  35. package/dist/esm/protocol/message.js +22 -0
  36. package/dist/esm/retry/reason.js +2 -2
  37. package/dist/esm/retry/replay.js +36 -2
  38. package/dist/esm/signal/session/SignalRatchet.js +2 -2
  39. package/dist/esm/transport/node/WaNodeOrchestrator.js +8 -4
  40. package/dist/esm/transport/node/builders/global.js +3 -0
  41. package/dist/esm/transport/node/builders/presence.js +2 -1
  42. package/dist/esm/transport/node/builders/profile.js +3 -2
  43. package/dist/esm/transport/node/builders/retry.js +2 -4
  44. package/dist/message/WaMessageClient.js +3 -0
  45. package/dist/message/primitives/incoming.d.ts +8 -1
  46. package/dist/message/primitives/incoming.js +76 -44
  47. package/dist/message/types.d.ts +5 -0
  48. package/dist/protocol/constants.d.ts +1 -1
  49. package/dist/protocol/constants.js +3 -2
  50. package/dist/protocol/jid.d.ts +12 -0
  51. package/dist/protocol/jid.js +44 -0
  52. package/dist/protocol/message.d.ts +22 -0
  53. package/dist/protocol/message.js +23 -1
  54. package/dist/retry/reason.js +2 -2
  55. package/dist/retry/replay.d.ts +12 -0
  56. package/dist/retry/replay.js +36 -2
  57. package/dist/signal/session/SignalRatchet.js +2 -2
  58. package/dist/transport/node/WaNodeOrchestrator.d.ts +6 -1
  59. package/dist/transport/node/WaNodeOrchestrator.js +8 -4
  60. package/dist/transport/node/builders/global.d.ts +1 -0
  61. package/dist/transport/node/builders/global.js +3 -0
  62. package/dist/transport/node/builders/presence.d.ts +6 -0
  63. package/dist/transport/node/builders/presence.js +2 -1
  64. package/dist/transport/node/builders/profile.d.ts +1 -1
  65. package/dist/transport/node/builders/profile.js +3 -2
  66. package/dist/transport/node/builders/retry.js +2 -4
  67. package/package.json +2 -2
@@ -25,7 +25,12 @@ interface WaAppStateSyncClientOptions {
25
25
  readonly defaultTimeoutMs?: number;
26
26
  readonly onMissingKeys?: (event: WaAppStateMissingKeysEvent) => Promise<void>;
27
27
  readonly skipMacVerification?: boolean;
28
- readonly mobilePrimary?: boolean;
28
+ /**
29
+ * Resolved per sync operation (post-connect, after credentials load) so a
30
+ * registered mobile-primary session reconnecting without an explicit
31
+ * `mobileTransport` option still drives the primary-authoritative sync path.
32
+ */
33
+ readonly mobilePrimary?: () => boolean;
29
34
  readonly isOwnAccountDevice?: (deviceJid: string) => boolean;
30
35
  readonly sendKeyShare?: (toDeviceJid: string, keys: readonly WaAppStateSyncKey[], missingKeyIds: readonly Uint8Array[]) => Promise<void>;
31
36
  readonly triggerSync?: () => Promise<void>;
@@ -55,6 +60,11 @@ export declare class WaAppStateSyncClient {
55
60
  /**
56
61
  * Returns the active app-state sync key, generating and persisting a new
57
62
  * one when the store is empty (used during initial setup).
63
+ *
64
+ * The key id mirrors the primary device layout: 2 big-endian bytes of
65
+ * device id followed by a 4 big-endian byte epoch. `keyEpoch` and
66
+ * `pickActiveSyncKey` read that structure, so a shorter id would make the
67
+ * generated key invisible to active-key selection.
58
68
  */
59
69
  ensureInitialSyncKey(): Promise<WaAppStateSyncKey>;
60
70
  /** Imports peer-shared sync keys into the store; returns the count actually added. */
@@ -38,20 +38,33 @@ class WaAppStateSyncClient {
38
38
  this.sendKeyShare = options.sendKeyShare;
39
39
  this.triggerSync = options.triggerSync;
40
40
  this.crypto = new WaAppStateCrypto_1.WaAppStateCrypto(undefined, options.skipMacVerification === true);
41
- this.mobilePrimary = options.mobilePrimary ?? false;
41
+ this.mobilePrimary = options.mobilePrimary ?? (() => false);
42
42
  this.syncContext = null;
43
43
  this.syncPromise = null;
44
44
  }
45
45
  /**
46
46
  * Returns the active app-state sync key, generating and persisting a new
47
47
  * one when the store is empty (used during initial setup).
48
+ *
49
+ * The key id mirrors the primary device layout: 2 big-endian bytes of
50
+ * device id followed by a 4 big-endian byte epoch. `keyEpoch` and
51
+ * `pickActiveSyncKey` read that structure, so a shorter id would make the
52
+ * generated key invisible to active-key selection.
48
53
  */
49
54
  async ensureInitialSyncKey() {
50
55
  const existing = await this.store.getActiveSyncKey();
51
56
  if (existing) {
52
57
  return existing;
53
58
  }
54
- const keyIdBytes = await (0, _crypto_1.randomBytesAsync)(2);
59
+ const deviceId = this.resolveDeviceIndex() ?? 0;
60
+ const epoch = await (0, _crypto_1.randomIntAsync)(1, 65537);
61
+ const keyIdBytes = new Uint8Array(6);
62
+ keyIdBytes[0] = (deviceId >>> 8) & 0xff;
63
+ keyIdBytes[1] = deviceId & 0xff;
64
+ keyIdBytes[2] = (epoch >>> 24) & 0xff;
65
+ keyIdBytes[3] = (epoch >>> 16) & 0xff;
66
+ keyIdBytes[4] = (epoch >>> 8) & 0xff;
67
+ keyIdBytes[5] = epoch & 0xff;
55
68
  const keyData = await (0, _crypto_1.randomBytesAsync)(32);
56
69
  const rawId = await (0, _crypto_1.randomIntAsync)(0, 4294967295);
57
70
  const key = {
@@ -64,6 +77,7 @@ class WaAppStateSyncClient {
64
77
  this.crypto.clearCache();
65
78
  this.logger.info('app-state initial sync key generated (mobile primary)', {
66
79
  keyId: (0, bytes_1.bytesToHex)(keyIdBytes),
80
+ epoch,
67
81
  rawId
68
82
  });
69
83
  return key;
@@ -389,17 +403,18 @@ class WaAppStateSyncClient {
389
403
  async buildCollectionSyncRequest(collection, pendingByCollection, activeSyncKey) {
390
404
  const collectionState = await this.getCollectionState(collection);
391
405
  const hasPersistedState = collectionState.initialized;
406
+ const requestSnapshot = !this.mobilePrimary() && !hasPersistedState;
392
407
  const attrs = {
393
408
  name: collection,
394
409
  version: String(hasPersistedState ? collectionState.version : constants_1.APP_STATE_DEFAULT_COLLECTION_VERSION),
395
- return_snapshot: hasPersistedState ? 'false' : 'true'
410
+ return_snapshot: requestSnapshot ? 'true' : 'false'
396
411
  };
397
412
  const children = [];
398
413
  const pendingMutations = pendingByCollection.get(collection) ?? [];
399
414
  let outgoingContext;
400
415
  let skippedUpload = false;
401
416
  if (pendingMutations.length > 0) {
402
- if (!hasPersistedState) {
417
+ if (!hasPersistedState && !this.mobilePrimary()) {
403
418
  skippedUpload = true;
404
419
  this.logger.debug('app-state skipped outgoing patch upload until snapshot bootstrap', {
405
420
  collection,
@@ -438,7 +453,7 @@ class WaAppStateSyncClient {
438
453
  content: [
439
454
  {
440
455
  tag: constants_2.WA_NODE_TAGS.SYNC,
441
- attrs: this.mobilePrimary ? { data_namespace: '3' } : {},
456
+ attrs: this.mobilePrimary() ? { data_namespace: '3' } : {},
442
457
  content: collectionNodes
443
458
  }
444
459
  ]
@@ -473,20 +488,19 @@ class WaAppStateSyncClient {
473
488
  return this.createCollectionOutcome(collection, payload.state, payload.version);
474
489
  }
475
490
  const pendingMutationsCount = pendingByCollection.get(collection)?.length ?? 0;
476
- if (payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT ||
477
- payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE) {
491
+ const isConflict = payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT ||
492
+ payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE;
493
+ if (isConflict) {
478
494
  shouldRefetch =
479
495
  payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE ||
480
496
  pendingMutationsCount > 0;
481
- return this.createCollectionOutcome(collection, payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT
482
- ? pendingMutationsCount > 0
483
- ? constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT
484
- : constants_2.WA_APP_STATE_COLLECTION_STATES.SUCCESS
485
- : payload.state, payload.version, shouldRefetch);
486
497
  }
487
498
  try {
488
499
  let appliedMutations = [];
489
- if (payload.snapshotReference) {
500
+ if (payload.snapshotReference && this.mobilePrimary()) {
501
+ collectionLogger.debug('app-state ignoring server snapshot on primary device');
502
+ }
503
+ else if (payload.snapshotReference) {
490
504
  const downloader = options.downloadExternalBlob;
491
505
  if (!downloader) {
492
506
  throw new Error(`snapshot for ${payload.collection} requires external blob downloader`);
@@ -525,12 +539,19 @@ class WaAppStateSyncClient {
525
539
  payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.SUCCESS_HAS_MORE ||
526
540
  (payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.SUCCESS &&
527
541
  skippedUploadCollections.has(collection));
542
+ const resolvedState = !isConflict
543
+ ? payload.state
544
+ : payload.state === constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT
545
+ ? pendingMutationsCount > 0
546
+ ? constants_2.WA_APP_STATE_COLLECTION_STATES.CONFLICT
547
+ : constants_2.WA_APP_STATE_COLLECTION_STATES.SUCCESS
548
+ : payload.state;
528
549
  collectionLogger.debug('app-state collection processed', {
529
- state: payload.state,
550
+ state: resolvedState,
530
551
  version: payload.version,
531
552
  appliedMutations: appliedMutations.length
532
553
  });
533
- return this.createCollectionOutcome(collection, payload.state, payload.version, shouldRefetch, collectionStateChanged, appliedMutations);
554
+ return this.createCollectionOutcome(collection, resolvedState, payload.version, shouldRefetch, collectionStateChanged, appliedMutations);
534
555
  }
535
556
  catch (error) {
536
557
  if (error instanceof WaAppStateMissingKeyError) {
@@ -35,6 +35,8 @@ async function loadOrCreateCredentials(args) {
35
35
  }
36
36
  await restoreSignalStore(args.signalStore, args.preKeyStore, existing);
37
37
  args.logger.trace('auth credentials restored into signal store');
38
+ // A mobile primary has no self-signed device-identity and no key-index-list:
39
+ // both are companion-only (set at pairing). Do not re-add them here.
38
40
  return existing;
39
41
  }
40
42
  async function persistCredentials(args, credentials) {
@@ -134,7 +136,7 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
134
136
  noise: {
135
137
  clientStaticKeyPair: credentials.noiseKeyPair,
136
138
  isRegistered: registered,
137
- serverStaticKey: credentials.serverStaticKey,
139
+ serverStaticKey: registered ? credentials.serverStaticKey : undefined,
138
140
  routingInfo: credentials.routingInfo,
139
141
  trustedRootCa: clientOptions.noiseTrustedRootCa,
140
142
  verifyCertificateChain: clientOptions.disableNoiseCertificateChainVerification
@@ -214,7 +214,7 @@ function buildWaClientDependencies(input) {
214
214
  logger,
215
215
  defaultTimeoutMs: options.nodeQueryTimeoutMs,
216
216
  hostDomain: constants_1.WA_DEFAULTS.HOST_DOMAIN,
217
- mobileIqIdFormat: options.mobileTransport !== undefined
217
+ mobileIqIdFormat: () => isMobilePrimary()
218
218
  });
219
219
  const keepAlive = new WaKeepAlive_1.WaKeepAlive({
220
220
  logger,
@@ -356,6 +356,7 @@ function buildWaClientDependencies(input) {
356
356
  }
357
357
  });
358
358
  const getCurrentCredentials = authClient.getCurrentCredentials.bind(authClient);
359
+ const isMobilePrimary = () => options.mobileTransport !== undefined || Boolean(getCurrentCredentials()?.deviceInfo);
359
360
  const groupCoordinator = (0, WaGroupCoordinator_1.createGroupCoordinator)({
360
361
  queryWithContext: runtime.queryWithContext,
361
362
  mexSocket: { query: runtime.query }
@@ -481,12 +482,13 @@ function buildWaClientDependencies(input) {
481
482
  additionalAttributes: sendOptions.additionalAttributes
482
483
  }),
483
484
  getIcdcHashLength: () => abPropsCoordinator.getConfigValue('md_icdc_hash_length'),
484
- mobileMessageIdFormat: options.mobileTransport !== undefined,
485
+ mobileMessageIdFormat: isMobilePrimary,
485
486
  serverClock
486
487
  });
487
488
  const presenceCoordinator = (0, WaPresenceCoordinator_1.createPresenceCoordinator)({
488
489
  sendNode: (node) => nodeOrchestrator.sendNode(node, false),
489
- getCurrentCredentials
490
+ getCurrentCredentials,
491
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
490
492
  });
491
493
  const peerDataOperation = (0, peer_data_operation_1.createPeerDataOperationRequester)({
492
494
  logger,
@@ -521,12 +523,15 @@ function buildWaClientDependencies(input) {
521
523
  sendNode: runtime.sendNode,
522
524
  getCurrentCredentials,
523
525
  resolveUserIcdc: (userJid) => messageDispatch.resolveUserIcdc(userJid),
526
+ resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
527
+ // Placeholder resend asks the primary phone (a peer) for the plaintext.
524
528
  peerDataOperation,
525
529
  emitIncomingMessage: (event) => {
526
530
  void runtime
527
531
  .handleIncomingMessageEvent(event)
528
532
  .catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
529
- }
533
+ },
534
+ isMobilePrimary
530
535
  });
531
536
  const botCoordinator = (0, WaBotCoordinator_1.createBotCoordinator)({
532
537
  logger,
@@ -550,20 +555,30 @@ function buildWaClientDependencies(input) {
550
555
  await messageDispatch.requestAppStateSyncKeys(keyIds);
551
556
  },
552
557
  skipMacVerification: options.dangerous?.disableAppStateMacVerification,
553
- mobilePrimary: options.mobileTransport !== undefined,
558
+ mobilePrimary: isMobilePrimary,
554
559
  isOwnAccountDevice: (deviceJid) => {
555
560
  const credentials = getCurrentCredentials();
556
561
  if (!credentials)
557
562
  return false;
558
- const candidateUser = (0, jid_1.toUserJid)(deviceJid);
559
- return ((!!credentials.meJid && (0, jid_1.toUserJid)(credentials.meJid) === candidateUser) ||
560
- (!!credentials.meLid && (0, jid_1.toUserJid)(credentials.meLid) === candidateUser));
563
+ return (0, jid_1.isOwnAccountJid)(deviceJid, credentials.meJid, credentials.meLid);
561
564
  },
562
565
  sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
563
566
  triggerSync: async () => {
564
567
  await runtime.syncAppState();
565
568
  }
566
569
  });
570
+ // Persists a pushName change and re-broadcasts presence carrying it (how the
571
+ // name reaches peers on primary connections). No-op when unchanged, which
572
+ // also collapses the app-state echo of our own SettingPushName write.
573
+ const applyOwnPushName = async (name) => {
574
+ if (getCurrentCredentials()?.meDisplayName === name) {
575
+ return;
576
+ }
577
+ await authClient.persistSuccessAttributes({ meDisplayName: name });
578
+ if (connectionManager?.isConnected()) {
579
+ await presenceCoordinator.send();
580
+ }
581
+ };
567
582
  const appStateMutations = new WaAppStateMutationCoordinator_1.WaAppStateMutationCoordinator({
568
583
  logger,
569
584
  messageStore: sessionStore.messages,
@@ -574,7 +589,12 @@ function buildWaClientDependencies(input) {
574
589
  emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
575
590
  emitMutation: (event) => runtime.emitEvent('mutation', event),
576
591
  nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
577
- contactSink: runtime.persistContact
592
+ contactSink: runtime.persistContact,
593
+ pushNameSink: (name) => {
594
+ void applyOwnPushName(name).catch((error) => logger.debug('apply own pushName from app-state sync failed', {
595
+ message: (0, primitives_1.toError)(error).message
596
+ }));
597
+ }
578
598
  });
579
599
  const profileCoordinator = (0, WaProfileCoordinator_1.createProfileCoordinator)({
580
600
  queryWithContext: runtime.queryWithContext,
@@ -582,6 +602,8 @@ function buildWaClientDependencies(input) {
582
602
  mexSocket: { query: runtime.query },
583
603
  queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
584
604
  mutations: appStateMutations,
605
+ applyOwnPushName,
606
+ resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
585
607
  logger
586
608
  });
587
609
  const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
@@ -662,6 +684,7 @@ function buildWaClientDependencies(input) {
662
684
  logger,
663
685
  sendNode: runtime.sendNode,
664
686
  getMeJid: () => getCurrentCredentials()?.meJid,
687
+ getMeLid: () => getCurrentCredentials()?.meLid,
665
688
  signalProtocol,
666
689
  senderKeyManager,
667
690
  onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
@@ -979,7 +1002,7 @@ function buildWaClientDependencies(input) {
979
1002
  abPropsCoordinator,
980
1003
  markOnlineOnConnect: options.markOnlineOnConnect ?? false
981
1004
  }),
982
- mobilePrimary: options.mobileTransport !== undefined,
1005
+ mobilePrimary: isMobilePrimary,
983
1006
  appStateSync
984
1007
  });
985
1008
  const lowLevelCoordinator = (0, WaLowLevelCoordinator_1.createLowLevelCoordinator)({
@@ -249,6 +249,9 @@ class WaConnectionManager {
249
249
  if (!serverStaticKey) {
250
250
  this.logger.trace('no server static key available to persist');
251
251
  }
252
+ else if (!credentials.meJid) {
253
+ this.logger.trace('skipping server static key persist while unregistered');
254
+ }
252
255
  else {
253
256
  await this.authClient.persistServerStaticKey(serverStaticKey);
254
257
  this.assertLifecycleCurrent(lifecycleGeneration, 'start comms');
@@ -42,6 +42,12 @@ interface WaAppStateMutationCoordinatorOptions {
42
42
  * bootstrapped at pair-time regardless of `emitSnapshotMutations`.
43
43
  */
44
44
  readonly contactSink?: (record: WaStoredContactRecord) => void;
45
+ /**
46
+ * Sink for applied `SettingPushName` mutations (the account's own display
47
+ * name). Invoked on every winning mutation including snapshot ones, so the
48
+ * local display name is bootstrapped at pair-time.
49
+ */
50
+ readonly pushNameSink?: (name: string) => void;
45
51
  }
46
52
  export interface WaSetStatusPrivacyInput {
47
53
  readonly mode: StatusDistributionModeKey | StatusDistributionMode;
@@ -76,6 +82,7 @@ export declare class WaAppStateMutationCoordinator {
76
82
  private readonly emitSnapshotMutations;
77
83
  private readonly nctSaltSink?;
78
84
  private readonly contactSink?;
85
+ private readonly pushNameSink?;
79
86
  private readonly pendingMutations;
80
87
  private flushPromise;
81
88
  constructor(options: WaAppStateMutationCoordinatorOptions);
@@ -96,6 +103,7 @@ export declare class WaAppStateMutationCoordinator {
96
103
  emitEventsFromSyncResult(syncResult: WaAppStateSyncResult): void;
97
104
  private handleNctSaltMutation;
98
105
  private handleContactMutation;
106
+ private handlePushNameMutation;
99
107
  /**
100
108
  * Mutes or unmutes a chat. `muteEndTimestampMs` is required when
101
109
  * `muted` is `true` and must be a non-negative safe-integer epoch.
@@ -183,6 +183,7 @@ class WaAppStateMutationCoordinator {
183
183
  this.emitSnapshotMutations = options.emitSnapshotMutations === true;
184
184
  this.nctSaltSink = options.nctSaltSink;
185
185
  this.contactSink = options.contactSink;
186
+ this.pushNameSink = options.pushNameSink;
186
187
  this.pendingMutations = new Map();
187
188
  this.flushPromise = null;
188
189
  }
@@ -230,11 +231,10 @@ class WaAppStateMutationCoordinator {
230
231
  emitEventsFromSyncResult(syncResult) {
231
232
  for (const collectionResult of syncResult.collections) {
232
233
  const mutations = collectionResult.mutations ?? [];
233
- // Persistence sinks (contact store, ...): run on the last-wins
234
- // mutation per key INCLUDING snapshot sources, so pair-time
235
- // bootstrap of the address book always lands in the store even
236
- // when public events are suppressed for snapshot mutations.
237
- if (this.contactSink) {
234
+ // Persistence sinks (contact store, own pushName): run on the
235
+ // last-wins mutation per key INCLUDING snapshot sources, so
236
+ // pair-time bootstrap lands even when snapshot events are suppressed.
237
+ if (this.contactSink || this.pushNameSink) {
238
238
  const sinkLastIndex = new Map();
239
239
  for (let i = 0; i < mutations.length; i += 1) {
240
240
  const m = mutations[i];
@@ -255,6 +255,16 @@ class WaAppStateMutationCoordinator {
255
255
  message: (0, primitives_1.toError)(error).message
256
256
  });
257
257
  }
258
+ try {
259
+ this.handlePushNameMutation(m);
260
+ }
261
+ catch (error) {
262
+ this.logger.debug('pushName sink failed', {
263
+ collection: m.collection,
264
+ index: m.index,
265
+ message: (0, primitives_1.toError)(error).message
266
+ });
267
+ }
258
268
  }
259
269
  }
260
270
  const lastMutationIndexByKey = new Map();
@@ -359,6 +369,20 @@ class WaAppStateMutationCoordinator {
359
369
  lastUpdatedMs
360
370
  });
361
371
  }
372
+ handlePushNameMutation(mutation) {
373
+ if (!this.pushNameSink)
374
+ return;
375
+ // A `set` under the literal index ["setting_pushName"]; cheap reject
376
+ // before reading the value.
377
+ if (mutation.operation !== 'set')
378
+ return;
379
+ if (!mutation.index.includes('setting_pushName'))
380
+ return;
381
+ const name = mutation.value?.pushNameSetting?.name;
382
+ if (typeof name !== 'string')
383
+ return;
384
+ this.pushNameSink(name);
385
+ }
362
386
  /**
363
387
  * Mutes or unmutes a chat. `muteEndTimestampMs` is required when
364
388
  * `muted` is `true` and must be a non-negative safe-integer epoch.
@@ -50,7 +50,12 @@ interface WaMessageDispatchCoordinatorOptions {
50
50
  readonly onDirectMessageSent: (recipientJid: string) => void;
51
51
  readonly sendNewsletterMessage?: (newsletterJid: string, content: WaSendMessageContent, options: WaSendMessageOptions, contextInfo: WaSendContextInfo | null) => Promise<WaMessagePublishResult>;
52
52
  readonly getIcdcHashLength?: () => number;
53
- readonly mobileMessageIdFormat?: boolean;
53
+ /**
54
+ * Resolved per outgoing message (post-connect, after credentials load) so a
55
+ * registered mobile session reconnecting without an explicit
56
+ * `mobileTransport` option still emits the mobile message-id format.
57
+ */
58
+ readonly mobileMessageIdFormat?: () => boolean;
54
59
  readonly serverClock: ServerClock;
55
60
  }
56
61
  export declare class WaMessageDispatchCoordinator {
@@ -26,7 +26,7 @@ class WaMessageDispatchCoordinator {
26
26
  this.privacyTokenDedup = new PromiseDedup_1.PromiseDedup();
27
27
  this.distributionDedup = new PromiseDedup_1.PromiseDedup();
28
28
  this.deps = options;
29
- this.mobileMessageIdFormat = options.mobileMessageIdFormat ?? false;
29
+ this.mobileMessageIdFormat = options.mobileMessageIdFormat ?? (() => false);
30
30
  this.serverClock = options.serverClock;
31
31
  }
32
32
  async publishMessageNode(node, options = {}) {
@@ -68,17 +68,20 @@ class WaMessageDispatchCoordinator {
68
68
  }
69
69
  async publishSignalMessage(input, options = {}) {
70
70
  this.requireCurrentMeJid('publishSignalMessage');
71
- const address = (0, jid_1.parseSignalAddressFromJid)(input.to);
71
+ const credentials = this.deps.getCurrentCredentials();
72
+ const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(input.to, credentials?.meJid, credentials?.meLid);
73
+ const address = (0, jid_1.parseSignalAddressFromJid)(sessionJid);
72
74
  if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
73
75
  throw new Error('publishSignalMessage currently supports only direct chats; use sender-key flow for groups');
74
76
  }
75
77
  this.deps.logger.trace('wa client publish signal message', {
76
78
  to: input.to,
79
+ sessionJid,
77
80
  type: input.type
78
81
  });
79
82
  const [paddedPlaintext] = await Promise.all([
80
83
  (0, padding_1.writeRandomPadMax16)(input.plaintext),
81
- this.deps.sessionResolver.ensureSession(address, input.to, input.expectedIdentity)
84
+ this.deps.sessionResolver.ensureSession(address, sessionJid, input.expectedIdentity)
82
85
  ]);
83
86
  const encrypted = await this.deps.signalProtocol.encryptMessage(address, paddedPlaintext, input.expectedIdentity);
84
87
  const messageType = input.type ?? 'text';
@@ -325,7 +328,8 @@ class WaMessageDispatchCoordinator {
325
328
  await this.deps.messageClient.sendReceipt(input);
326
329
  }
327
330
  async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
328
- const meJid = this.deps.getCurrentCredentials()?.meJid;
331
+ const credentials = this.deps.getCurrentCredentials();
332
+ const meJid = credentials?.meJid;
329
333
  const meParsed = meJid ? (0, jid_1.parseJidFull)(meJid) : undefined;
330
334
  const meUserJid = meParsed?.userJid;
331
335
  let senderIcdc = null;
@@ -657,7 +661,7 @@ class WaMessageDispatchCoordinator {
657
661
  const serverAddressingMode = result.ack.addressingMode;
658
662
  const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
659
663
  const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
660
- const hasAddressingError = ackError === 421;
664
+ const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
661
665
  if (!retryContext.retried &&
662
666
  (hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
663
667
  this.deps.logger.warn('group direct publish acknowledged with mismatch metadata', {
@@ -832,7 +836,7 @@ class WaMessageDispatchCoordinator {
832
836
  const serverAddressingMode = result.ack.addressingMode;
833
837
  const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
834
838
  const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
835
- const hasAddressingError = ackError === 421;
839
+ const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
836
840
  if (!retryContext.retried &&
837
841
  (hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
838
842
  this.deps.logger.warn('group message publish acknowledged with mismatch metadata', {
@@ -1194,7 +1198,7 @@ class WaMessageDispatchCoordinator {
1194
1198
  const meUserJid = (0, jid_1.toUserJid)(this.requireCurrentMeJid('sendMessage'));
1195
1199
  const timestampBytes = new Uint8Array(8);
1196
1200
  const dv = new DataView(timestampBytes.buffer, timestampBytes.byteOffset, timestampBytes.byteLength);
1197
- if (this.mobileMessageIdFormat) {
1201
+ if (this.mobileMessageIdFormat()) {
1198
1202
  dv.setBigUint64(0, BigInt(Date.now()), false);
1199
1203
  const digest = (0, primitives_1.md5Bytes)([
1200
1204
  timestampBytes,
@@ -1216,7 +1220,7 @@ class WaMessageDispatchCoordinator {
1216
1220
  this.deps.logger.warn('failed to generate message id, falling back to random', {
1217
1221
  message: (0, primitives_2.toError)(error).message
1218
1222
  });
1219
- if (this.mobileMessageIdFormat) {
1223
+ if (this.mobileMessageIdFormat()) {
1220
1224
  const bytes = await (0, _crypto_1.randomBytesAsync)(16);
1221
1225
  bytes[0] = 0xac;
1222
1226
  return (0, bytes_1.bytesToHex)(bytes).toUpperCase();
@@ -40,7 +40,12 @@ export declare class WaPassiveTasksCoordinator {
40
40
  readonly signedPreKeyRotationIntervalMs?: number;
41
41
  readonly signedPreKeyServerErrorBackoffMs?: number;
42
42
  readonly runtime: WaPassiveTasksRuntime;
43
- readonly mobilePrimary?: boolean;
43
+ /**
44
+ * Resolved when passive tasks run (post-connect, after credentials
45
+ * load) so a registered mobile-primary session reconnecting without an
46
+ * explicit `mobileTransport` option still bootstraps the primary path.
47
+ */
48
+ readonly mobilePrimary?: () => boolean;
44
49
  readonly appStateSync?: WaAppStateSyncClient;
45
50
  });
46
51
  startPassiveTasksAfterConnect(): void;
@@ -19,7 +19,7 @@ class WaPassiveTasksCoordinator {
19
19
  this.signedPreKeyServerErrorBackoffMs =
20
20
  options.signedPreKeyServerErrorBackoffMs ?? constants_2.SIGNAL_SIGNED_PREKEY_SERVER_ERROR_BACKOFF_MS;
21
21
  this.runtime = options.runtime;
22
- this.mobilePrimary = options.mobilePrimary ?? false;
22
+ this.mobilePrimary = options.mobilePrimary ?? (() => false);
23
23
  this.appStateSync = options.appStateSync;
24
24
  this.passiveTasksPromise = null;
25
25
  }
@@ -62,7 +62,7 @@ class WaPassiveTasksCoordinator {
62
62
  return;
63
63
  }
64
64
  this.runtime.syncAbProps();
65
- if (this.mobilePrimary && this.appStateSync) {
65
+ if (this.mobilePrimary() && this.appStateSync) {
66
66
  await this.appStateSync.ensureInitialSyncKey().catch((error) => {
67
67
  this.logger.warn('app-state initial key generation failed', {
68
68
  message: (0, primitives_1.toError)(error).message
@@ -149,7 +149,7 @@ class WaPassiveTasksCoordinator {
149
149
  const response = await this.runtime.queryWithContext('prekeys.upload', uploadNode, constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS, {
150
150
  count: preKeys.length,
151
151
  lastPreKeyId
152
- }, this.mobilePrimary ? { useSystemId: true } : undefined);
152
+ }, this.mobilePrimary() ? { useSystemId: true } : undefined);
153
153
  if (response.attrs.type === constants_1.WA_IQ_TYPES.RESULT) {
154
154
  // Mark uploaded key first so the serverHasPreKeys flag never commits ahead of local key progress.
155
155
  await this.preKeyStore.markKeyAsUploaded(lastPreKeyId);
@@ -21,6 +21,12 @@ export interface WaPresenceCoordinator {
21
21
  interface WaPresenceCoordinatorOptions {
22
22
  readonly sendNode: (node: BinaryNode) => Promise<void>;
23
23
  readonly getCurrentCredentials: () => WaAuthCredentials | null;
24
+ /**
25
+ * Resolves the receiver-mode `<tctoken>` node for a contact, echoed back
26
+ * on a user presence subscription to unlock the target's presence
27
+ * visibility. Returns `null` when no valid token is held.
28
+ */
29
+ readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
24
30
  }
25
31
  /** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
26
32
  export declare function createPresenceCoordinator(options: WaPresenceCoordinatorOptions): WaPresenceCoordinator;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createPresenceCoordinator = createPresenceCoordinator;
4
+ const jid_1 = require("../../protocol/jid");
4
5
  const chatstate_1 = require("../../transport/node/builders/chatstate");
5
6
  const presence_1 = require("../../transport/node/builders/presence");
6
7
  /** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
7
8
  function createPresenceCoordinator(options) {
8
- const { sendNode, getCurrentCredentials } = options;
9
+ const { sendNode, getCurrentCredentials, resolvePrivacyTokenNode } = options;
9
10
  return {
10
11
  send: async (type) => {
11
12
  const credentials = getCurrentCredentials();
@@ -15,7 +16,12 @@ function createPresenceCoordinator(options) {
15
16
  await sendNode((0, chatstate_1.buildChatstateNode)({ jid, ...opts }));
16
17
  },
17
18
  subscribe: async (jid, opts) => {
18
- await sendNode((0, presence_1.buildPresenceSubscribeNode)({ jid, ...opts }));
19
+ const privacyTokenNode = (0, jid_1.isGroupJid)(jid) ? null : await resolvePrivacyTokenNode(jid);
20
+ await sendNode((0, presence_1.buildPresenceSubscribeNode)({
21
+ jid,
22
+ ...opts,
23
+ ...(privacyTokenNode ? { privacyTokenNode } : {})
24
+ }));
19
25
  }
20
26
  };
21
27
  }
@@ -79,12 +79,11 @@ export interface WaProfileCoordinator {
79
79
  readonly setStatus: (text: string) => Promise<void>;
80
80
  /**
81
81
  * Sets the account's pushName - the display name broadcast to other
82
- * users in chats and group participant lists. WhatsApp Web applies the
83
- * change via a `SettingPushName` app-state mutation (collection
84
- * `critical_block`); the new value reaches peers as their next
85
- * incoming-message envelope from this account carries the updated
86
- * `notify` attr. Empty strings are accepted but reset the display name
87
- * to the device fingerprint default.
82
+ * users in chats and group participant lists. Writes a `SettingPushName`
83
+ * app-state mutation to sync the account's other devices, persists the
84
+ * name locally, and re-broadcasts an available presence carrying it (the
85
+ * step that propagates the name on primary connections, where no phone
86
+ * re-broadcasts on this device's behalf). Empty strings clear the name.
88
87
  */
89
88
  readonly setPushName: (name: string) => Promise<void>;
90
89
  /** Batched usync fetch of picture id + status for many JIDs. */
@@ -144,6 +143,19 @@ interface WaProfileCoordinatorOptions {
144
143
  * mutations.
145
144
  */
146
145
  readonly mutations: WaAppStateMutationCoordinator;
146
+ /**
147
+ * Applies a pushName change locally: persists the display name and
148
+ * re-broadcasts an available presence carrying it. Expected to be
149
+ * idempotent (a no-op when the name already matches).
150
+ */
151
+ readonly applyOwnPushName: (name: string) => Promise<void>;
152
+ /**
153
+ * Resolves the receiver-mode `<tctoken>` node for a contact, echoed back on
154
+ * privacy-gated profile queries (picture get, about/status usync) to prove
155
+ * this account is a trusted contact. Returns `null` when no valid token is
156
+ * held for the JID.
157
+ */
158
+ readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
147
159
  readonly logger: Logger;
148
160
  }
149
161
  /** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */