zapo-js 1.1.1 → 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.
- package/dist/appstate/sync/WaAppStateSyncClient.d.ts +11 -1
- package/dist/appstate/sync/WaAppStateSyncClient.js +36 -15
- package/dist/auth/credentials-flow.js +3 -1
- package/dist/client/WaClientFactory.js +28 -6
- package/dist/client/connection/WaConnectionManager.js +3 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +6 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +5 -5
- package/dist/client/coordinators/WaPassiveTasksCoordinator.d.ts +6 -1
- package/dist/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +11 -6
- package/dist/client/coordinators/WaProfileCoordinator.js +4 -1
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +18 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +87 -14
- package/dist/esm/appstate/sync/WaAppStateSyncClient.js +36 -15
- package/dist/esm/auth/credentials-flow.js +3 -1
- package/dist/esm/client/WaClientFactory.js +28 -6
- package/dist/esm/client/connection/WaConnectionManager.js +3 -0
- package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +29 -5
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +6 -6
- package/dist/esm/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
- package/dist/esm/client/coordinators/WaProfileCoordinator.js +4 -1
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +88 -15
- package/dist/esm/message/WaMessageClient.js +3 -0
- package/dist/esm/message/primitives/incoming.js +14 -3
- package/dist/esm/protocol/constants.js +1 -1
- package/dist/esm/protocol/message.js +22 -0
- package/dist/esm/retry/replay.js +36 -2
- package/dist/esm/signal/session/SignalRatchet.js +2 -2
- package/dist/esm/transport/node/WaNodeOrchestrator.js +2 -2
- package/dist/esm/transport/node/builders/global.js +3 -0
- package/dist/message/WaMessageClient.js +3 -0
- package/dist/message/primitives/incoming.d.ts +7 -1
- package/dist/message/primitives/incoming.js +14 -3
- package/dist/message/types.d.ts +5 -0
- package/dist/protocol/constants.d.ts +1 -1
- package/dist/protocol/constants.js +3 -2
- package/dist/protocol/message.d.ts +22 -0
- package/dist/protocol/message.js +23 -1
- package/dist/retry/replay.d.ts +12 -0
- package/dist/retry/replay.js +36 -2
- package/dist/signal/session/SignalRatchet.js +2 -2
- package/dist/transport/node/WaNodeOrchestrator.d.ts +6 -1
- package/dist/transport/node/WaNodeOrchestrator.js +2 -2
- package/dist/transport/node/builders/global.d.ts +1 -0
- package/dist/transport/node/builders/global.js +3 -0
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WaRetryCoordinator = void 0;
|
|
4
|
+
const keys_1 = require("../../crypto/core/keys");
|
|
5
|
+
const BoundedTaskQueue_1 = require("../../infra/perf/BoundedTaskQueue");
|
|
4
6
|
const incoming_1 = require("../../message/primitives/incoming");
|
|
5
7
|
const _proto_1 = require("../../proto");
|
|
6
8
|
const constants_1 = require("../../protocol/constants");
|
|
@@ -17,6 +19,10 @@ const collections_1 = require("../../util/collections");
|
|
|
17
19
|
const primitives_1 = require("../../util/primitives");
|
|
18
20
|
const RETRY_CLEANUP_INTERVAL_MS = 30000;
|
|
19
21
|
const RETRY_SESSION_BASE_KEY_CACHE_MAX_ENTRIES = 8192;
|
|
22
|
+
// Decrypt-failure handling runs off the inbound pipeline (keys-section builds
|
|
23
|
+
// serialize on the prekey lock and hit the store); excess under flood is dropped.
|
|
24
|
+
const DECRYPT_FAILURE_QUEUE_MAX_SIZE = 1024;
|
|
25
|
+
const DECRYPT_FAILURE_QUEUE_MAX_CONCURRENCY = 8;
|
|
20
26
|
const PLACEHOLDER_RESEND_RETRY_THRESHOLD = 3;
|
|
21
27
|
const PLACEHOLDER_RESEND_BATCH_SIZE = 32;
|
|
22
28
|
const PLACEHOLDER_RESEND_DEBOUNCE_MS = 200;
|
|
@@ -58,22 +64,46 @@ class WaRetryCoordinator {
|
|
|
58
64
|
signalProtocol: options.signalProtocol,
|
|
59
65
|
sessionResolver: options.sessionResolver,
|
|
60
66
|
getCurrentCredentials: options.getCurrentCredentials,
|
|
61
|
-
resolveUserIcdc: options.resolveUserIcdc
|
|
67
|
+
resolveUserIcdc: options.resolveUserIcdc,
|
|
68
|
+
resolvePrivacyTokenNode: options.resolvePrivacyTokenNode
|
|
62
69
|
});
|
|
63
70
|
this.retryProcessingByMessageId = new Map();
|
|
64
71
|
this.retrySessionBaseKeys = new Map();
|
|
72
|
+
this.decryptFailureQueue = new BoundedTaskQueue_1.BoundedTaskQueue(DECRYPT_FAILURE_QUEUE_MAX_SIZE, DECRYPT_FAILURE_QUEUE_MAX_CONCURRENCY);
|
|
65
73
|
}
|
|
66
|
-
|
|
74
|
+
onDecryptFailure(context, error) {
|
|
75
|
+
// Deferred to the bounded queue: building the receipt inline would stall
|
|
76
|
+
// the inbound node pipeline.
|
|
77
|
+
void this.decryptFailureQueue
|
|
78
|
+
.enqueue(() => this.handleDecryptFailure(context, error))
|
|
79
|
+
.catch((queueError) => {
|
|
80
|
+
if (queueError instanceof BoundedTaskQueue_1.BoundedTaskQueueFullError) {
|
|
81
|
+
this.deps.logger.warn('decrypt-failure retry dropped: queue saturated', {
|
|
82
|
+
id: context.stanzaId,
|
|
83
|
+
from: context.from,
|
|
84
|
+
participant: context.participant
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.deps.logger.warn('failed to schedule decrypt-failure retry', {
|
|
89
|
+
id: context.stanzaId,
|
|
90
|
+
from: context.from,
|
|
91
|
+
participant: context.participant,
|
|
92
|
+
message: (0, primitives_1.toError)(queueError).message
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
return Promise.resolve(true);
|
|
96
|
+
}
|
|
97
|
+
async handleDecryptFailure(context, error) {
|
|
67
98
|
try {
|
|
68
99
|
const prepared = await this.prepareDecryptFailureRetry(context, error);
|
|
69
|
-
if (!prepared) {
|
|
70
|
-
|
|
100
|
+
if (prepared && !prepared.delegatedToPlaceholderResend) {
|
|
101
|
+
await this.sendDecryptFailureRetryReceipt(context, prepared);
|
|
71
102
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await this.
|
|
76
|
-
return true;
|
|
103
|
+
// Ack the failed stanza even on the give-up path: the retry receipt
|
|
104
|
+
// asks for a resend but does not consume the message, so without the
|
|
105
|
+
// ack it is redelivered on every offline resume.
|
|
106
|
+
await this.sendDecryptFailureAck(context);
|
|
77
107
|
}
|
|
78
108
|
catch (sendError) {
|
|
79
109
|
this.deps.logger.warn('failed to send retry receipt for decrypt failure', {
|
|
@@ -82,7 +112,6 @@ class WaRetryCoordinator {
|
|
|
82
112
|
participant: context.participant,
|
|
83
113
|
message: (0, primitives_1.toError)(sendError).message
|
|
84
114
|
});
|
|
85
|
-
return false;
|
|
86
115
|
}
|
|
87
116
|
}
|
|
88
117
|
async handleIncomingRetryReceipt(receiptNode) {
|
|
@@ -141,6 +170,17 @@ class WaRetryCoordinator {
|
|
|
141
170
|
const requester = context.participant ?? context.from;
|
|
142
171
|
const expiresAtMs = nowMs + this.retryTtlMs;
|
|
143
172
|
const retryCount = await this.deps.retryStore.incrementInboundCounter(context.stanzaId, requester, nowMs, expiresAtMs);
|
|
173
|
+
if (retryCount > constants_2.MAX_RETRY_ATTEMPTS) {
|
|
174
|
+
// Give up past the ceiling: each attempt rebuilds an expensive keys
|
|
175
|
+
// section, so an uncapped retry on redelivered backlog hammers the store.
|
|
176
|
+
this.deps.logger.debug('retry receipt skipped: inbound retry limit exceeded', {
|
|
177
|
+
id: context.stanzaId,
|
|
178
|
+
from: context.from,
|
|
179
|
+
participant: context.participant,
|
|
180
|
+
retryCount
|
|
181
|
+
});
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
144
184
|
const delegatedToPlaceholderResend = retryCount >= PLACEHOLDER_RESEND_RETRY_THRESHOLD &&
|
|
145
185
|
this.enqueuePlaceholderResend(context);
|
|
146
186
|
if (delegatedToPlaceholderResend) {
|
|
@@ -194,6 +234,35 @@ class WaRetryCoordinator {
|
|
|
194
234
|
withKeys: prepared.retryKeys !== undefined
|
|
195
235
|
});
|
|
196
236
|
}
|
|
237
|
+
async sendDecryptFailureAck(context) {
|
|
238
|
+
if (!context.stanzaId || !context.from) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await this.deps.sendNode((0, global_1.buildAckNode)({
|
|
243
|
+
kind: 'message',
|
|
244
|
+
node: context.messageNode,
|
|
245
|
+
id: context.stanzaId,
|
|
246
|
+
to: context.from,
|
|
247
|
+
participant: context.participant,
|
|
248
|
+
from: this.deps.getCurrentCredentials()?.meJid ?? undefined,
|
|
249
|
+
error: constants_1.WA_NACK_REASONS.UNHANDLED_ERROR
|
|
250
|
+
}));
|
|
251
|
+
this.deps.logger.trace('acked undecryptable stanza', {
|
|
252
|
+
id: context.stanzaId,
|
|
253
|
+
from: context.from,
|
|
254
|
+
participant: context.participant
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
this.deps.logger.warn('failed to ack undecryptable stanza', {
|
|
259
|
+
id: context.stanzaId,
|
|
260
|
+
from: context.from,
|
|
261
|
+
participant: context.participant,
|
|
262
|
+
message: (0, primitives_1.toError)(error).message
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
197
266
|
async handleParsedRetryRequest(receiptNode, request) {
|
|
198
267
|
if (request.type === constants_1.WA_MESSAGE_TYPES.RECEIPT_TYPE_ENC_REKEY_RETRY) {
|
|
199
268
|
this.deps.logger.debug('received enc_rekey_retry request (voip path deferred)', {
|
|
@@ -367,14 +436,14 @@ class WaRetryCoordinator {
|
|
|
367
436
|
await this.deps.preKeyStore.markKeyAsUploaded(preKey.keyId);
|
|
368
437
|
const signedIdentity = this.deps.getCurrentCredentials()?.signedIdentity;
|
|
369
438
|
return {
|
|
370
|
-
identity,
|
|
439
|
+
identity: (0, keys_1.toRawPubKey)(identity),
|
|
371
440
|
key: {
|
|
372
441
|
id: preKey.keyId,
|
|
373
|
-
publicKey: preKey.keyPair.pubKey
|
|
442
|
+
publicKey: (0, keys_1.toRawPubKey)(preKey.keyPair.pubKey)
|
|
374
443
|
},
|
|
375
444
|
skey: {
|
|
376
445
|
id: signedPreKey.keyId,
|
|
377
|
-
publicKey: signedPreKey.keyPair.pubKey,
|
|
446
|
+
publicKey: (0, keys_1.toRawPubKey)(signedPreKey.keyPair.pubKey),
|
|
378
447
|
signature: signedPreKey.signature
|
|
379
448
|
},
|
|
380
449
|
deviceIdentity: signedIdentity
|
|
@@ -702,6 +771,9 @@ class WaRetryCoordinator {
|
|
|
702
771
|
if (!this.deps.peerDataOperation || !this.deps.emitIncomingMessage) {
|
|
703
772
|
return false;
|
|
704
773
|
}
|
|
774
|
+
if (this.deps.isMobilePrimary?.()) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
705
777
|
const subtype = context.messageNode.attrs.subtype;
|
|
706
778
|
if (typeof subtype === 'string' && PLACEHOLDER_RESEND_SKIP_SUBTYPES.has(subtype)) {
|
|
707
779
|
return false;
|
|
@@ -759,6 +831,7 @@ class WaRetryCoordinator {
|
|
|
759
831
|
}
|
|
760
832
|
}))
|
|
761
833
|
});
|
|
834
|
+
const meJid = this.deps.getCurrentCredentials()?.meJid;
|
|
762
835
|
for (const result of results) {
|
|
763
836
|
const bytes = result.placeholderMessageResendResponse?.webMessageInfoBytes;
|
|
764
837
|
if (!bytes) {
|
|
@@ -766,7 +839,7 @@ class WaRetryCoordinator {
|
|
|
766
839
|
}
|
|
767
840
|
try {
|
|
768
841
|
const recovered = _proto_1.proto.WebMessageInfo.decode(bytes);
|
|
769
|
-
emitIncomingMessage((0, incoming_1.buildRecoveredIncomingEvent)(recovered));
|
|
842
|
+
emitIncomingMessage((0, incoming_1.buildRecoveredIncomingEvent)(recovered, meJid));
|
|
770
843
|
}
|
|
771
844
|
catch (error) {
|
|
772
845
|
this.deps.logger.warn('placeholder resend: failed to decode WebMessageInfo', {
|
|
@@ -35,20 +35,33 @@ export class WaAppStateSyncClient {
|
|
|
35
35
|
this.sendKeyShare = options.sendKeyShare;
|
|
36
36
|
this.triggerSync = options.triggerSync;
|
|
37
37
|
this.crypto = new WaAppStateCrypto(undefined, options.skipMacVerification === true);
|
|
38
|
-
this.mobilePrimary = options.mobilePrimary ?? false;
|
|
38
|
+
this.mobilePrimary = options.mobilePrimary ?? (() => false);
|
|
39
39
|
this.syncContext = null;
|
|
40
40
|
this.syncPromise = null;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Returns the active app-state sync key, generating and persisting a new
|
|
44
44
|
* one when the store is empty (used during initial setup).
|
|
45
|
+
*
|
|
46
|
+
* The key id mirrors the primary device layout: 2 big-endian bytes of
|
|
47
|
+
* device id followed by a 4 big-endian byte epoch. `keyEpoch` and
|
|
48
|
+
* `pickActiveSyncKey` read that structure, so a shorter id would make the
|
|
49
|
+
* generated key invisible to active-key selection.
|
|
45
50
|
*/
|
|
46
51
|
async ensureInitialSyncKey() {
|
|
47
52
|
const existing = await this.store.getActiveSyncKey();
|
|
48
53
|
if (existing) {
|
|
49
54
|
return existing;
|
|
50
55
|
}
|
|
51
|
-
const
|
|
56
|
+
const deviceId = this.resolveDeviceIndex() ?? 0;
|
|
57
|
+
const epoch = await randomIntAsync(1, 65537);
|
|
58
|
+
const keyIdBytes = new Uint8Array(6);
|
|
59
|
+
keyIdBytes[0] = (deviceId >>> 8) & 0xff;
|
|
60
|
+
keyIdBytes[1] = deviceId & 0xff;
|
|
61
|
+
keyIdBytes[2] = (epoch >>> 24) & 0xff;
|
|
62
|
+
keyIdBytes[3] = (epoch >>> 16) & 0xff;
|
|
63
|
+
keyIdBytes[4] = (epoch >>> 8) & 0xff;
|
|
64
|
+
keyIdBytes[5] = epoch & 0xff;
|
|
52
65
|
const keyData = await randomBytesAsync(32);
|
|
53
66
|
const rawId = await randomIntAsync(0, 4294967295);
|
|
54
67
|
const key = {
|
|
@@ -61,6 +74,7 @@ export class WaAppStateSyncClient {
|
|
|
61
74
|
this.crypto.clearCache();
|
|
62
75
|
this.logger.info('app-state initial sync key generated (mobile primary)', {
|
|
63
76
|
keyId: bytesToHex(keyIdBytes),
|
|
77
|
+
epoch,
|
|
64
78
|
rawId
|
|
65
79
|
});
|
|
66
80
|
return key;
|
|
@@ -386,17 +400,18 @@ export class WaAppStateSyncClient {
|
|
|
386
400
|
async buildCollectionSyncRequest(collection, pendingByCollection, activeSyncKey) {
|
|
387
401
|
const collectionState = await this.getCollectionState(collection);
|
|
388
402
|
const hasPersistedState = collectionState.initialized;
|
|
403
|
+
const requestSnapshot = !this.mobilePrimary() && !hasPersistedState;
|
|
389
404
|
const attrs = {
|
|
390
405
|
name: collection,
|
|
391
406
|
version: String(hasPersistedState ? collectionState.version : APP_STATE_DEFAULT_COLLECTION_VERSION),
|
|
392
|
-
return_snapshot:
|
|
407
|
+
return_snapshot: requestSnapshot ? 'true' : 'false'
|
|
393
408
|
};
|
|
394
409
|
const children = [];
|
|
395
410
|
const pendingMutations = pendingByCollection.get(collection) ?? [];
|
|
396
411
|
let outgoingContext;
|
|
397
412
|
let skippedUpload = false;
|
|
398
413
|
if (pendingMutations.length > 0) {
|
|
399
|
-
if (!hasPersistedState) {
|
|
414
|
+
if (!hasPersistedState && !this.mobilePrimary()) {
|
|
400
415
|
skippedUpload = true;
|
|
401
416
|
this.logger.debug('app-state skipped outgoing patch upload until snapshot bootstrap', {
|
|
402
417
|
collection,
|
|
@@ -435,7 +450,7 @@ export class WaAppStateSyncClient {
|
|
|
435
450
|
content: [
|
|
436
451
|
{
|
|
437
452
|
tag: WA_NODE_TAGS.SYNC,
|
|
438
|
-
attrs: this.mobilePrimary ? { data_namespace: '3' } : {},
|
|
453
|
+
attrs: this.mobilePrimary() ? { data_namespace: '3' } : {},
|
|
439
454
|
content: collectionNodes
|
|
440
455
|
}
|
|
441
456
|
]
|
|
@@ -470,20 +485,19 @@ export class WaAppStateSyncClient {
|
|
|
470
485
|
return this.createCollectionOutcome(collection, payload.state, payload.version);
|
|
471
486
|
}
|
|
472
487
|
const pendingMutationsCount = pendingByCollection.get(collection)?.length ?? 0;
|
|
473
|
-
|
|
474
|
-
payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE
|
|
488
|
+
const isConflict = payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT ||
|
|
489
|
+
payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE;
|
|
490
|
+
if (isConflict) {
|
|
475
491
|
shouldRefetch =
|
|
476
492
|
payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT_HAS_MORE ||
|
|
477
493
|
pendingMutationsCount > 0;
|
|
478
|
-
return this.createCollectionOutcome(collection, payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT
|
|
479
|
-
? pendingMutationsCount > 0
|
|
480
|
-
? WA_APP_STATE_COLLECTION_STATES.CONFLICT
|
|
481
|
-
: WA_APP_STATE_COLLECTION_STATES.SUCCESS
|
|
482
|
-
: payload.state, payload.version, shouldRefetch);
|
|
483
494
|
}
|
|
484
495
|
try {
|
|
485
496
|
let appliedMutations = [];
|
|
486
|
-
if (payload.snapshotReference) {
|
|
497
|
+
if (payload.snapshotReference && this.mobilePrimary()) {
|
|
498
|
+
collectionLogger.debug('app-state ignoring server snapshot on primary device');
|
|
499
|
+
}
|
|
500
|
+
else if (payload.snapshotReference) {
|
|
487
501
|
const downloader = options.downloadExternalBlob;
|
|
488
502
|
if (!downloader) {
|
|
489
503
|
throw new Error(`snapshot for ${payload.collection} requires external blob downloader`);
|
|
@@ -522,12 +536,19 @@ export class WaAppStateSyncClient {
|
|
|
522
536
|
payload.state === WA_APP_STATE_COLLECTION_STATES.SUCCESS_HAS_MORE ||
|
|
523
537
|
(payload.state === WA_APP_STATE_COLLECTION_STATES.SUCCESS &&
|
|
524
538
|
skippedUploadCollections.has(collection));
|
|
539
|
+
const resolvedState = !isConflict
|
|
540
|
+
? payload.state
|
|
541
|
+
: payload.state === WA_APP_STATE_COLLECTION_STATES.CONFLICT
|
|
542
|
+
? pendingMutationsCount > 0
|
|
543
|
+
? WA_APP_STATE_COLLECTION_STATES.CONFLICT
|
|
544
|
+
: WA_APP_STATE_COLLECTION_STATES.SUCCESS
|
|
545
|
+
: payload.state;
|
|
525
546
|
collectionLogger.debug('app-state collection processed', {
|
|
526
|
-
state:
|
|
547
|
+
state: resolvedState,
|
|
527
548
|
version: payload.version,
|
|
528
549
|
appliedMutations: appliedMutations.length
|
|
529
550
|
});
|
|
530
|
-
return this.createCollectionOutcome(collection,
|
|
551
|
+
return this.createCollectionOutcome(collection, resolvedState, payload.version, shouldRefetch, collectionStateChanged, appliedMutations);
|
|
531
552
|
}
|
|
532
553
|
catch (error) {
|
|
533
554
|
if (error instanceof WaAppStateMissingKeyError) {
|
|
@@ -30,6 +30,8 @@ export async function loadOrCreateCredentials(args) {
|
|
|
30
30
|
}
|
|
31
31
|
await restoreSignalStore(args.signalStore, args.preKeyStore, existing);
|
|
32
32
|
args.logger.trace('auth credentials restored into signal store');
|
|
33
|
+
// A mobile primary has no self-signed device-identity and no key-index-list:
|
|
34
|
+
// both are companion-only (set at pairing). Do not re-add them here.
|
|
33
35
|
return existing;
|
|
34
36
|
}
|
|
35
37
|
export async function persistCredentials(args, credentials) {
|
|
@@ -129,7 +131,7 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
129
131
|
noise: {
|
|
130
132
|
clientStaticKeyPair: credentials.noiseKeyPair,
|
|
131
133
|
isRegistered: registered,
|
|
132
|
-
serverStaticKey: credentials.serverStaticKey,
|
|
134
|
+
serverStaticKey: registered ? credentials.serverStaticKey : undefined,
|
|
133
135
|
routingInfo: credentials.routingInfo,
|
|
134
136
|
trustedRootCa: clientOptions.noiseTrustedRootCa,
|
|
135
137
|
verifyCertificateChain: clientOptions.disableNoiseCertificateChainVerification
|
|
@@ -210,7 +210,7 @@ export function buildWaClientDependencies(input) {
|
|
|
210
210
|
logger,
|
|
211
211
|
defaultTimeoutMs: options.nodeQueryTimeoutMs,
|
|
212
212
|
hostDomain: WA_DEFAULTS.HOST_DOMAIN,
|
|
213
|
-
mobileIqIdFormat:
|
|
213
|
+
mobileIqIdFormat: () => isMobilePrimary()
|
|
214
214
|
});
|
|
215
215
|
const keepAlive = new WaKeepAlive({
|
|
216
216
|
logger,
|
|
@@ -352,6 +352,7 @@ export function buildWaClientDependencies(input) {
|
|
|
352
352
|
}
|
|
353
353
|
});
|
|
354
354
|
const getCurrentCredentials = authClient.getCurrentCredentials.bind(authClient);
|
|
355
|
+
const isMobilePrimary = () => options.mobileTransport !== undefined || Boolean(getCurrentCredentials()?.deviceInfo);
|
|
355
356
|
const groupCoordinator = createGroupCoordinator({
|
|
356
357
|
queryWithContext: runtime.queryWithContext,
|
|
357
358
|
mexSocket: { query: runtime.query }
|
|
@@ -477,7 +478,7 @@ export function buildWaClientDependencies(input) {
|
|
|
477
478
|
additionalAttributes: sendOptions.additionalAttributes
|
|
478
479
|
}),
|
|
479
480
|
getIcdcHashLength: () => abPropsCoordinator.getConfigValue('md_icdc_hash_length'),
|
|
480
|
-
mobileMessageIdFormat:
|
|
481
|
+
mobileMessageIdFormat: isMobilePrimary,
|
|
481
482
|
serverClock
|
|
482
483
|
});
|
|
483
484
|
const presenceCoordinator = createPresenceCoordinator({
|
|
@@ -517,12 +518,15 @@ export function buildWaClientDependencies(input) {
|
|
|
517
518
|
sendNode: runtime.sendNode,
|
|
518
519
|
getCurrentCredentials,
|
|
519
520
|
resolveUserIcdc: (userJid) => messageDispatch.resolveUserIcdc(userJid),
|
|
521
|
+
resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
|
|
522
|
+
// Placeholder resend asks the primary phone (a peer) for the plaintext.
|
|
520
523
|
peerDataOperation,
|
|
521
524
|
emitIncomingMessage: (event) => {
|
|
522
525
|
void runtime
|
|
523
526
|
.handleIncomingMessageEvent(event)
|
|
524
527
|
.catch((err) => runtime.handleError(toError(err)));
|
|
525
|
-
}
|
|
528
|
+
},
|
|
529
|
+
isMobilePrimary
|
|
526
530
|
});
|
|
527
531
|
const botCoordinator = createBotCoordinator({
|
|
528
532
|
logger,
|
|
@@ -546,7 +550,7 @@ export function buildWaClientDependencies(input) {
|
|
|
546
550
|
await messageDispatch.requestAppStateSyncKeys(keyIds);
|
|
547
551
|
},
|
|
548
552
|
skipMacVerification: options.dangerous?.disableAppStateMacVerification,
|
|
549
|
-
mobilePrimary:
|
|
553
|
+
mobilePrimary: isMobilePrimary,
|
|
550
554
|
isOwnAccountDevice: (deviceJid) => {
|
|
551
555
|
const credentials = getCurrentCredentials();
|
|
552
556
|
if (!credentials)
|
|
@@ -560,6 +564,18 @@ export function buildWaClientDependencies(input) {
|
|
|
560
564
|
await runtime.syncAppState();
|
|
561
565
|
}
|
|
562
566
|
});
|
|
567
|
+
// Persists a pushName change and re-broadcasts presence carrying it (how the
|
|
568
|
+
// name reaches peers on primary connections). No-op when unchanged, which
|
|
569
|
+
// also collapses the app-state echo of our own SettingPushName write.
|
|
570
|
+
const applyOwnPushName = async (name) => {
|
|
571
|
+
if (getCurrentCredentials()?.meDisplayName === name) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
await authClient.persistSuccessAttributes({ meDisplayName: name });
|
|
575
|
+
if (connectionManager?.isConnected()) {
|
|
576
|
+
await presenceCoordinator.send();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
563
579
|
const appStateMutations = new WaAppStateMutationCoordinator({
|
|
564
580
|
logger,
|
|
565
581
|
messageStore: sessionStore.messages,
|
|
@@ -570,7 +586,12 @@ export function buildWaClientDependencies(input) {
|
|
|
570
586
|
emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
|
|
571
587
|
emitMutation: (event) => runtime.emitEvent('mutation', event),
|
|
572
588
|
nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
|
|
573
|
-
contactSink: runtime.persistContact
|
|
589
|
+
contactSink: runtime.persistContact,
|
|
590
|
+
pushNameSink: (name) => {
|
|
591
|
+
void applyOwnPushName(name).catch((error) => logger.debug('apply own pushName from app-state sync failed', {
|
|
592
|
+
message: toError(error).message
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
574
595
|
});
|
|
575
596
|
const profileCoordinator = createProfileCoordinator({
|
|
576
597
|
queryWithContext: runtime.queryWithContext,
|
|
@@ -578,6 +599,7 @@ export function buildWaClientDependencies(input) {
|
|
|
578
599
|
mexSocket: { query: runtime.query },
|
|
579
600
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
580
601
|
mutations: appStateMutations,
|
|
602
|
+
applyOwnPushName,
|
|
581
603
|
logger
|
|
582
604
|
});
|
|
583
605
|
const statusCoordinator = createStatusCoordinator({
|
|
@@ -975,7 +997,7 @@ export function buildWaClientDependencies(input) {
|
|
|
975
997
|
abPropsCoordinator,
|
|
976
998
|
markOnlineOnConnect: options.markOnlineOnConnect ?? false
|
|
977
999
|
}),
|
|
978
|
-
mobilePrimary:
|
|
1000
|
+
mobilePrimary: isMobilePrimary,
|
|
979
1001
|
appStateSync
|
|
980
1002
|
});
|
|
981
1003
|
const lowLevelCoordinator = createLowLevelCoordinator({
|
|
@@ -246,6 +246,9 @@ export class WaConnectionManager {
|
|
|
246
246
|
if (!serverStaticKey) {
|
|
247
247
|
this.logger.trace('no server static key available to persist');
|
|
248
248
|
}
|
|
249
|
+
else if (!credentials.meJid) {
|
|
250
|
+
this.logger.trace('skipping server static key persist while unregistered');
|
|
251
|
+
}
|
|
249
252
|
else {
|
|
250
253
|
await this.authClient.persistServerStaticKey(serverStaticKey);
|
|
251
254
|
this.assertLifecycleCurrent(lifecycleGeneration, 'start comms');
|
|
@@ -180,6 +180,7 @@ export class WaAppStateMutationCoordinator {
|
|
|
180
180
|
this.emitSnapshotMutations = options.emitSnapshotMutations === true;
|
|
181
181
|
this.nctSaltSink = options.nctSaltSink;
|
|
182
182
|
this.contactSink = options.contactSink;
|
|
183
|
+
this.pushNameSink = options.pushNameSink;
|
|
183
184
|
this.pendingMutations = new Map();
|
|
184
185
|
this.flushPromise = null;
|
|
185
186
|
}
|
|
@@ -227,11 +228,10 @@ export class WaAppStateMutationCoordinator {
|
|
|
227
228
|
emitEventsFromSyncResult(syncResult) {
|
|
228
229
|
for (const collectionResult of syncResult.collections) {
|
|
229
230
|
const mutations = collectionResult.mutations ?? [];
|
|
230
|
-
// Persistence sinks (contact store,
|
|
231
|
-
// mutation per key INCLUDING snapshot sources, so
|
|
232
|
-
// bootstrap
|
|
233
|
-
|
|
234
|
-
if (this.contactSink) {
|
|
231
|
+
// Persistence sinks (contact store, own pushName): run on the
|
|
232
|
+
// last-wins mutation per key INCLUDING snapshot sources, so
|
|
233
|
+
// pair-time bootstrap lands even when snapshot events are suppressed.
|
|
234
|
+
if (this.contactSink || this.pushNameSink) {
|
|
235
235
|
const sinkLastIndex = new Map();
|
|
236
236
|
for (let i = 0; i < mutations.length; i += 1) {
|
|
237
237
|
const m = mutations[i];
|
|
@@ -252,6 +252,16 @@ export class WaAppStateMutationCoordinator {
|
|
|
252
252
|
message: toError(error).message
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
|
+
try {
|
|
256
|
+
this.handlePushNameMutation(m);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
this.logger.debug('pushName sink failed', {
|
|
260
|
+
collection: m.collection,
|
|
261
|
+
index: m.index,
|
|
262
|
+
message: toError(error).message
|
|
263
|
+
});
|
|
264
|
+
}
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
const lastMutationIndexByKey = new Map();
|
|
@@ -356,6 +366,20 @@ export class WaAppStateMutationCoordinator {
|
|
|
356
366
|
lastUpdatedMs
|
|
357
367
|
});
|
|
358
368
|
}
|
|
369
|
+
handlePushNameMutation(mutation) {
|
|
370
|
+
if (!this.pushNameSink)
|
|
371
|
+
return;
|
|
372
|
+
// A `set` under the literal index ["setting_pushName"]; cheap reject
|
|
373
|
+
// before reading the value.
|
|
374
|
+
if (mutation.operation !== 'set')
|
|
375
|
+
return;
|
|
376
|
+
if (!mutation.index.includes('setting_pushName'))
|
|
377
|
+
return;
|
|
378
|
+
const name = mutation.value?.pushNameSetting?.name;
|
|
379
|
+
if (typeof name !== 'string')
|
|
380
|
+
return;
|
|
381
|
+
this.pushNameSink(name);
|
|
382
|
+
}
|
|
359
383
|
/**
|
|
360
384
|
* Mutes or unmutes a chat. `muteEndTimestampMs` is required when
|
|
361
385
|
* `muted` is `true` and must be a non-negative safe-integer epoch.
|
|
@@ -11,7 +11,7 @@ import { wrapDeviceSentMessage } from '../../message/encode/device-sent.js';
|
|
|
11
11
|
import { writeRandomPadMax16 } from '../../message/encode/padding.js';
|
|
12
12
|
import { buildBotInvokeProtoCopy, extractInvokedBotJid, genBotMsgSecret } from '../../message/kinds/bot.js';
|
|
13
13
|
import { proto } from '../../proto.js';
|
|
14
|
-
import { WA_DEFAULTS } from '../../protocol/constants.js';
|
|
14
|
+
import { WA_DEFAULTS, WA_NACK_REASONS } from '../../protocol/constants.js';
|
|
15
15
|
import { isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parseSignalAddressFromJid, signalAddressKey, toUserJid } from '../../protocol/jid.js';
|
|
16
16
|
import { encodeBinaryNode } from '../../transport/binary/index.js';
|
|
17
17
|
import { buildButtonAddonNode, buildDirectMessageFanoutNode, buildGroupSenderKeyMessageNode, buildMetaNode } from '../../transport/node/builders/message.js';
|
|
@@ -23,7 +23,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
23
23
|
this.privacyTokenDedup = new PromiseDedup();
|
|
24
24
|
this.distributionDedup = new PromiseDedup();
|
|
25
25
|
this.deps = options;
|
|
26
|
-
this.mobileMessageIdFormat = options.mobileMessageIdFormat ?? false;
|
|
26
|
+
this.mobileMessageIdFormat = options.mobileMessageIdFormat ?? (() => false);
|
|
27
27
|
this.serverClock = options.serverClock;
|
|
28
28
|
}
|
|
29
29
|
async publishMessageNode(node, options = {}) {
|
|
@@ -654,7 +654,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
654
654
|
const serverAddressingMode = result.ack.addressingMode;
|
|
655
655
|
const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
|
|
656
656
|
const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
|
|
657
|
-
const hasAddressingError = ackError ===
|
|
657
|
+
const hasAddressingError = ackError === WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
|
|
658
658
|
if (!retryContext.retried &&
|
|
659
659
|
(hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
|
|
660
660
|
this.deps.logger.warn('group direct publish acknowledged with mismatch metadata', {
|
|
@@ -829,7 +829,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
829
829
|
const serverAddressingMode = result.ack.addressingMode;
|
|
830
830
|
const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
|
|
831
831
|
const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
|
|
832
|
-
const hasAddressingError = ackError ===
|
|
832
|
+
const hasAddressingError = ackError === WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
|
|
833
833
|
if (!retryContext.retried &&
|
|
834
834
|
(hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
|
|
835
835
|
this.deps.logger.warn('group message publish acknowledged with mismatch metadata', {
|
|
@@ -1191,7 +1191,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
1191
1191
|
const meUserJid = toUserJid(this.requireCurrentMeJid('sendMessage'));
|
|
1192
1192
|
const timestampBytes = new Uint8Array(8);
|
|
1193
1193
|
const dv = new DataView(timestampBytes.buffer, timestampBytes.byteOffset, timestampBytes.byteLength);
|
|
1194
|
-
if (this.mobileMessageIdFormat) {
|
|
1194
|
+
if (this.mobileMessageIdFormat()) {
|
|
1195
1195
|
dv.setBigUint64(0, BigInt(Date.now()), false);
|
|
1196
1196
|
const digest = md5Bytes([
|
|
1197
1197
|
timestampBytes,
|
|
@@ -1213,7 +1213,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
1213
1213
|
this.deps.logger.warn('failed to generate message id, falling back to random', {
|
|
1214
1214
|
message: toError(error).message
|
|
1215
1215
|
});
|
|
1216
|
-
if (this.mobileMessageIdFormat) {
|
|
1216
|
+
if (this.mobileMessageIdFormat()) {
|
|
1217
1217
|
const bytes = await randomBytesAsync(16);
|
|
1218
1218
|
bytes[0] = 0xac;
|
|
1219
1219
|
return bytesToHex(bytes).toUpperCase();
|
|
@@ -16,7 +16,7 @@ export class WaPassiveTasksCoordinator {
|
|
|
16
16
|
this.signedPreKeyServerErrorBackoffMs =
|
|
17
17
|
options.signedPreKeyServerErrorBackoffMs ?? SIGNAL_SIGNED_PREKEY_SERVER_ERROR_BACKOFF_MS;
|
|
18
18
|
this.runtime = options.runtime;
|
|
19
|
-
this.mobilePrimary = options.mobilePrimary ?? false;
|
|
19
|
+
this.mobilePrimary = options.mobilePrimary ?? (() => false);
|
|
20
20
|
this.appStateSync = options.appStateSync;
|
|
21
21
|
this.passiveTasksPromise = null;
|
|
22
22
|
}
|
|
@@ -59,7 +59,7 @@ export class WaPassiveTasksCoordinator {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
this.runtime.syncAbProps();
|
|
62
|
-
if (this.mobilePrimary && this.appStateSync) {
|
|
62
|
+
if (this.mobilePrimary() && this.appStateSync) {
|
|
63
63
|
await this.appStateSync.ensureInitialSyncKey().catch((error) => {
|
|
64
64
|
this.logger.warn('app-state initial key generation failed', {
|
|
65
65
|
message: toError(error).message
|
|
@@ -146,7 +146,7 @@ export class WaPassiveTasksCoordinator {
|
|
|
146
146
|
const response = await this.runtime.queryWithContext('prekeys.upload', uploadNode, WA_DEFAULTS.IQ_TIMEOUT_MS, {
|
|
147
147
|
count: preKeys.length,
|
|
148
148
|
lastPreKeyId
|
|
149
|
-
}, this.mobilePrimary ? { useSystemId: true } : undefined);
|
|
149
|
+
}, this.mobilePrimary() ? { useSystemId: true } : undefined);
|
|
150
150
|
if (response.attrs.type === WA_IQ_TYPES.RESULT) {
|
|
151
151
|
// Mark uploaded key first so the serverHasPreKeys flag never commits ahead of local key progress.
|
|
152
152
|
await this.preKeyStore.markKeyAsUploaded(lastPreKeyId);
|
|
@@ -192,7 +192,7 @@ function buildTextStatusMutationInput(input) {
|
|
|
192
192
|
}
|
|
193
193
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|
|
194
194
|
export function createProfileCoordinator(options) {
|
|
195
|
-
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, logger } = options;
|
|
195
|
+
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, logger } = options;
|
|
196
196
|
return {
|
|
197
197
|
getProfilePicture: async (jid, type, existingId) => {
|
|
198
198
|
const node = buildGetProfilePictureIq(jid, type, existingId);
|
|
@@ -242,6 +242,9 @@ export function createProfileCoordinator(options) {
|
|
|
242
242
|
assertIqResult(result, 'profile.setStatus');
|
|
243
243
|
},
|
|
244
244
|
setPushName: async (name) => {
|
|
245
|
+
// Local apply first: the app-state echo of this same write then
|
|
246
|
+
// collapses into a no-op via applyOwnPushName's idempotency guard.
|
|
247
|
+
await applyOwnPushName(name);
|
|
245
248
|
await mutations.set({ schema: 'SettingPushName', name });
|
|
246
249
|
},
|
|
247
250
|
getProfiles: async (jids) => {
|