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.
- package/README.md +5 -1
- 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/auth/pairing/WaPairingFlow.js +2 -0
- package/dist/client/WaClient.js +26 -20
- 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/WaIncomingNodeCoordinator.js +1 -1
- 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 -1
- package/dist/client/coordinators/WaRetryCoordinator.js +83 -30
- package/dist/client/messaging/ignore-key.js +4 -2
- package/dist/client/types.d.ts +13 -10
- package/dist/esm/appstate/sync/WaAppStateSyncClient.js +36 -15
- package/dist/esm/auth/credentials-flow.js +3 -1
- package/dist/esm/auth/pairing/WaPairingFlow.js +2 -0
- package/dist/esm/client/WaClient.js +26 -20
- 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/WaIncomingNodeCoordinator.js +1 -1
- 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 +84 -31
- package/dist/esm/client/messaging/ignore-key.js +4 -2
- package/dist/esm/message/WaMessageClient.js +3 -0
- package/dist/esm/message/primitives/incoming.js +20 -5
- 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/WaComms.js +4 -0
- package/dist/esm/transport/keepalive/WaKeepAlive.js +4 -0
- 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 +20 -5
- 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/WaComms.js +4 -0
- package/dist/transport/keepalive/WaKeepAlive.js +4 -0
- 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
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
550
|
+
state: resolvedState,
|
|
530
551
|
version: payload.version,
|
|
531
552
|
appliedMutations: appliedMutations.length
|
|
532
553
|
});
|
|
533
|
-
return this.createCollectionOutcome(collection,
|
|
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');
|
package/dist/client/WaClient.js
CHANGED
|
@@ -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 (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
234
|
-
// mutation per key INCLUDING snapshot sources, so
|
|
235
|
-
// bootstrap
|
|
236
|
-
|
|
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.
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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) => {
|