zapo-js 1.1.0 → 1.1.2

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 (62) hide show
  1. package/README.md +5 -1
  2. package/dist/appstate/sync/WaAppStateSyncClient.d.ts +11 -1
  3. package/dist/appstate/sync/WaAppStateSyncClient.js +36 -15
  4. package/dist/auth/credentials-flow.js +3 -1
  5. package/dist/auth/pairing/WaPairingFlow.js +2 -0
  6. package/dist/client/WaClient.js +26 -20
  7. package/dist/client/WaClientFactory.js +28 -6
  8. package/dist/client/connection/WaConnectionManager.js +3 -0
  9. package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
  10. package/dist/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
  11. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +1 -1
  12. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +6 -1
  13. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +5 -5
  14. package/dist/client/coordinators/WaPassiveTasksCoordinator.d.ts +6 -1
  15. package/dist/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
  16. package/dist/client/coordinators/WaProfileCoordinator.d.ts +11 -6
  17. package/dist/client/coordinators/WaProfileCoordinator.js +4 -1
  18. package/dist/client/coordinators/WaRetryCoordinator.d.ts +18 -1
  19. package/dist/client/coordinators/WaRetryCoordinator.js +83 -30
  20. package/dist/client/messaging/ignore-key.js +4 -2
  21. package/dist/client/types.d.ts +13 -10
  22. package/dist/esm/appstate/sync/WaAppStateSyncClient.js +36 -15
  23. package/dist/esm/auth/credentials-flow.js +3 -1
  24. package/dist/esm/auth/pairing/WaPairingFlow.js +2 -0
  25. package/dist/esm/client/WaClient.js +26 -20
  26. package/dist/esm/client/WaClientFactory.js +28 -6
  27. package/dist/esm/client/connection/WaConnectionManager.js +3 -0
  28. package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
  29. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +1 -1
  30. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +6 -6
  31. package/dist/esm/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
  32. package/dist/esm/client/coordinators/WaProfileCoordinator.js +4 -1
  33. package/dist/esm/client/coordinators/WaRetryCoordinator.js +84 -31
  34. package/dist/esm/client/messaging/ignore-key.js +4 -2
  35. package/dist/esm/message/WaMessageClient.js +3 -0
  36. package/dist/esm/message/primitives/incoming.js +20 -5
  37. package/dist/esm/protocol/constants.js +1 -1
  38. package/dist/esm/protocol/message.js +22 -0
  39. package/dist/esm/retry/replay.js +36 -2
  40. package/dist/esm/signal/session/SignalRatchet.js +2 -2
  41. package/dist/esm/transport/WaComms.js +4 -0
  42. package/dist/esm/transport/keepalive/WaKeepAlive.js +4 -0
  43. package/dist/esm/transport/node/WaNodeOrchestrator.js +2 -2
  44. package/dist/esm/transport/node/builders/global.js +3 -0
  45. package/dist/message/WaMessageClient.js +3 -0
  46. package/dist/message/primitives/incoming.d.ts +7 -1
  47. package/dist/message/primitives/incoming.js +20 -5
  48. package/dist/message/types.d.ts +5 -0
  49. package/dist/protocol/constants.d.ts +1 -1
  50. package/dist/protocol/constants.js +3 -2
  51. package/dist/protocol/message.d.ts +22 -0
  52. package/dist/protocol/message.js +23 -1
  53. package/dist/retry/replay.d.ts +12 -0
  54. package/dist/retry/replay.js +36 -2
  55. package/dist/signal/session/SignalRatchet.js +2 -2
  56. package/dist/transport/WaComms.js +4 -0
  57. package/dist/transport/keepalive/WaKeepAlive.js +4 -0
  58. package/dist/transport/node/WaNodeOrchestrator.d.ts +6 -1
  59. package/dist/transport/node/WaNodeOrchestrator.js +2 -2
  60. package/dist/transport/node/builders/global.d.ts +1 -0
  61. package/dist/transport/node/builders/global.js +3 -0
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/vinikjkkj/zapo/master/.github/assets/logo.png" alt="zapo" width="400" />
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vinikjkkj/zapo/master/.github/assets/logo.png" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vinikjkkj/zapo/master/.github/assets/logo-light.png" />
5
+ <img src="https://raw.githubusercontent.com/vinikjkkj/zapo/master/.github/assets/logo-light.png" alt="zapo" width="400" />
6
+ </picture>
3
7
  </p>
4
8
 
5
9
  <p align="center">
@@ -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
@@ -11,6 +11,7 @@ const WaAdvSignature_1 = require("../../signal/attestation/WaAdvSignature");
11
11
  const global_1 = require("../../transport/node/builders/global");
12
12
  const pairing_1 = require("../../transport/node/builders/pairing");
13
13
  const helpers_1 = require("../../transport/node/helpers");
14
+ const query_1 = require("../../transport/node/query");
14
15
  const bytes_1 = require("../../util/bytes");
15
16
  class WaPairingFlow {
16
17
  constructor(options) {
@@ -49,6 +50,7 @@ class WaPairingFlow {
49
50
  responseTag: response.tag,
50
51
  responseType: response.attrs.type
51
52
  });
53
+ (0, query_1.assertIqResult)(response, 'companion hello');
52
54
  const linkCodeNode = (0, helpers_1.findNodeChild)(response, constants_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
53
55
  if (!linkCodeNode) {
54
56
  throw new Error('companion hello response missing link_code_companion_reg');
@@ -258,10 +258,28 @@ class WaClient extends node_events_1.EventEmitter {
258
258
  return;
259
259
  }
260
260
  if (protocolType === _proto_1.proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION) {
261
- if (this.options.history?.enabled !== false &&
262
- protocolMessage.historySyncNotification) {
263
- const peerRemoteJid = event.key.remoteJid;
264
- const peerStanzaId = event.key.id;
261
+ if (!protocolMessage.historySyncNotification) {
262
+ return;
263
+ }
264
+ const peerRemoteJid = event.key.remoteJid;
265
+ const peerStanzaId = event.key.id;
266
+ const sendHistSyncReceipt = peerRemoteJid && peerStanzaId
267
+ ? async () => {
268
+ try {
269
+ await this.message.sendReceipt(peerRemoteJid, peerStanzaId, {
270
+ type: constants_1.WA_MESSAGE_TYPES.RECEIPT_TYPE_HISTORY_SYNC
271
+ });
272
+ }
273
+ catch (err) {
274
+ this.logger.warn('failed to send hist_sync receipt', {
275
+ id: peerStanzaId,
276
+ to: peerRemoteJid,
277
+ message: (0, primitives_1.toError)(err).message
278
+ });
279
+ }
280
+ }
281
+ : undefined;
282
+ if (this.options.history?.enabled !== false) {
265
283
  await (0, history_sync_1.runHistorySyncNotification)({
266
284
  logger: this.logger,
267
285
  mediaTransfer: this.mediaTransfer,
@@ -269,24 +287,12 @@ class WaClient extends node_events_1.EventEmitter {
269
287
  emitEvent: this.emit.bind(this),
270
288
  onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
271
289
  onNctSalt: (salt) => this.deps.trustedContactToken.hydrateNctSaltFromHistorySync(salt),
272
- onProcessed: peerRemoteJid && peerStanzaId
273
- ? async () => {
274
- try {
275
- await this.message.sendReceipt(peerRemoteJid, peerStanzaId, {
276
- type: constants_1.WA_MESSAGE_TYPES.RECEIPT_TYPE_HISTORY_SYNC
277
- });
278
- }
279
- catch (err) {
280
- this.logger.warn('failed to send hist_sync receipt', {
281
- id: peerStanzaId,
282
- to: peerRemoteJid,
283
- message: (0, primitives_1.toError)(err).message
284
- });
285
- }
286
- }
287
- : undefined
290
+ onProcessed: sendHistSyncReceipt
288
291
  }, protocolMessage.historySyncNotification);
289
292
  }
293
+ else if (sendHistSyncReceipt) {
294
+ await sendHistSyncReceipt();
295
+ }
290
296
  return;
291
297
  }
292
298
  if (SYNC_RELATED_PROTOCOL_TYPES.has(protocolType)) {
@@ -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,7 +482,7 @@ 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)({
@@ -521,12 +522,15 @@ function buildWaClientDependencies(input) {
521
522
  sendNode: runtime.sendNode,
522
523
  getCurrentCredentials,
523
524
  resolveUserIcdc: (userJid) => messageDispatch.resolveUserIcdc(userJid),
525
+ resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
526
+ // Placeholder resend asks the primary phone (a peer) for the plaintext.
524
527
  peerDataOperation,
525
528
  emitIncomingMessage: (event) => {
526
529
  void runtime
527
530
  .handleIncomingMessageEvent(event)
528
531
  .catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
529
- }
532
+ },
533
+ isMobilePrimary
530
534
  });
531
535
  const botCoordinator = (0, WaBotCoordinator_1.createBotCoordinator)({
532
536
  logger,
@@ -550,7 +554,7 @@ function buildWaClientDependencies(input) {
550
554
  await messageDispatch.requestAppStateSyncKeys(keyIds);
551
555
  },
552
556
  skipMacVerification: options.dangerous?.disableAppStateMacVerification,
553
- mobilePrimary: options.mobileTransport !== undefined,
557
+ mobilePrimary: isMobilePrimary,
554
558
  isOwnAccountDevice: (deviceJid) => {
555
559
  const credentials = getCurrentCredentials();
556
560
  if (!credentials)
@@ -564,6 +568,18 @@ function buildWaClientDependencies(input) {
564
568
  await runtime.syncAppState();
565
569
  }
566
570
  });
571
+ // Persists a pushName change and re-broadcasts presence carrying it (how the
572
+ // name reaches peers on primary connections). No-op when unchanged, which
573
+ // also collapses the app-state echo of our own SettingPushName write.
574
+ const applyOwnPushName = async (name) => {
575
+ if (getCurrentCredentials()?.meDisplayName === name) {
576
+ return;
577
+ }
578
+ await authClient.persistSuccessAttributes({ meDisplayName: name });
579
+ if (connectionManager?.isConnected()) {
580
+ await presenceCoordinator.send();
581
+ }
582
+ };
567
583
  const appStateMutations = new WaAppStateMutationCoordinator_1.WaAppStateMutationCoordinator({
568
584
  logger,
569
585
  messageStore: sessionStore.messages,
@@ -574,7 +590,12 @@ function buildWaClientDependencies(input) {
574
590
  emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
575
591
  emitMutation: (event) => runtime.emitEvent('mutation', event),
576
592
  nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
577
- contactSink: runtime.persistContact
593
+ contactSink: runtime.persistContact,
594
+ pushNameSink: (name) => {
595
+ void applyOwnPushName(name).catch((error) => logger.debug('apply own pushName from app-state sync failed', {
596
+ message: (0, primitives_1.toError)(error).message
597
+ }));
598
+ }
578
599
  });
579
600
  const profileCoordinator = (0, WaProfileCoordinator_1.createProfileCoordinator)({
580
601
  queryWithContext: runtime.queryWithContext,
@@ -582,6 +603,7 @@ function buildWaClientDependencies(input) {
582
603
  mexSocket: { query: runtime.query },
583
604
  queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
584
605
  mutations: appStateMutations,
606
+ applyOwnPushName,
585
607
  logger
586
608
  });
587
609
  const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
@@ -979,7 +1001,7 @@ function buildWaClientDependencies(input) {
979
1001
  abPropsCoordinator,
980
1002
  markOnlineOnConnect: options.markOnlineOnConnect ?? false
981
1003
  }),
982
- mobilePrimary: options.mobileTransport !== undefined,
1004
+ mobilePrimary: isMobilePrimary,
983
1005
  appStateSync
984
1006
  });
985
1007
  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.
@@ -391,7 +391,7 @@ class WaIncomingNodeCoordinator {
391
391
  try {
392
392
  const routingInfo = (0, helpers_1.decodeNodeContentBase64OrBytes)(routingInfoNode.content, `ib.${constants_1.WA_NODE_TAGS.EDGE_ROUTING}.${constants_1.WA_NODE_TAGS.ROUTING_INFO}`);
393
393
  await this.runtime.persistRoutingInfo(routingInfo);
394
- this.logger.info('updated routing info from info bulletin', {
394
+ this.logger.debug('updated routing info from info bulletin', {
395
395
  byteLength: routingInfo.byteLength
396
396
  });
397
397
  }
@@ -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 = {}) {
@@ -657,7 +657,7 @@ class WaMessageDispatchCoordinator {
657
657
  const serverAddressingMode = result.ack.addressingMode;
658
658
  const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
659
659
  const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
660
- const hasAddressingError = ackError === 421;
660
+ const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
661
661
  if (!retryContext.retried &&
662
662
  (hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
663
663
  this.deps.logger.warn('group direct publish acknowledged with mismatch metadata', {
@@ -832,7 +832,7 @@ class WaMessageDispatchCoordinator {
832
832
  const serverAddressingMode = result.ack.addressingMode;
833
833
  const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
834
834
  const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
835
- const hasAddressingError = ackError === 421;
835
+ const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
836
836
  if (!retryContext.retried &&
837
837
  (hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
838
838
  this.deps.logger.warn('group message publish acknowledged with mismatch metadata', {
@@ -1194,7 +1194,7 @@ class WaMessageDispatchCoordinator {
1194
1194
  const meUserJid = (0, jid_1.toUserJid)(this.requireCurrentMeJid('sendMessage'));
1195
1195
  const timestampBytes = new Uint8Array(8);
1196
1196
  const dv = new DataView(timestampBytes.buffer, timestampBytes.byteOffset, timestampBytes.byteLength);
1197
- if (this.mobileMessageIdFormat) {
1197
+ if (this.mobileMessageIdFormat()) {
1198
1198
  dv.setBigUint64(0, BigInt(Date.now()), false);
1199
1199
  const digest = (0, primitives_1.md5Bytes)([
1200
1200
  timestampBytes,
@@ -1216,7 +1216,7 @@ class WaMessageDispatchCoordinator {
1216
1216
  this.deps.logger.warn('failed to generate message id, falling back to random', {
1217
1217
  message: (0, primitives_2.toError)(error).message
1218
1218
  });
1219
- if (this.mobileMessageIdFormat) {
1219
+ if (this.mobileMessageIdFormat()) {
1220
1220
  const bytes = await (0, _crypto_1.randomBytesAsync)(16);
1221
1221
  bytes[0] = 0xac;
1222
1222
  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);
@@ -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,12 @@ 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>;
147
152
  readonly logger: Logger;
148
153
  }
149
154
  /** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
@@ -195,7 +195,7 @@ function buildTextStatusMutationInput(input) {
195
195
  }
196
196
  /** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
197
197
  function createProfileCoordinator(options) {
198
- const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, logger } = options;
198
+ const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, logger } = options;
199
199
  return {
200
200
  getProfilePicture: async (jid, type, existingId) => {
201
201
  const node = (0, profile_1.buildGetProfilePictureIq)(jid, type, existingId);
@@ -245,6 +245,9 @@ function createProfileCoordinator(options) {
245
245
  (0, query_1.assertIqResult)(result, 'profile.setStatus');
246
246
  },
247
247
  setPushName: async (name) => {
248
+ // Local apply first: the app-state echo of this same write then
249
+ // collapses into a no-op via applyOwnPushName's idempotency guard.
250
+ await applyOwnPushName(name);
248
251
  await mutations.set({ schema: 'SettingPushName', name });
249
252
  },
250
253
  getProfiles: async (jids) => {