zapo-js 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +33 -10
- 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 +12 -8
- package/dist/client/coordinators/WaPassiveTasksCoordinator.d.ts +6 -1
- package/dist/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
- package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
- package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +18 -6
- package/dist/client/coordinators/WaProfileCoordinator.js +13 -4
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +18 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +88 -15
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/esm/appstate/sync/WaAppStateSyncClient.js +36 -15
- package/dist/esm/auth/credentials-flow.js +3 -1
- package/dist/esm/client/WaClientFactory.js +34 -11
- 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 +14 -10
- package/dist/esm/client/coordinators/WaPassiveTasksCoordinator.js +3 -3
- package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/esm/client/coordinators/WaProfileCoordinator.js +13 -4
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +89 -16
- package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/esm/message/WaMessageClient.js +3 -0
- package/dist/esm/message/primitives/incoming.js +77 -45
- package/dist/esm/protocol/constants.js +1 -1
- package/dist/esm/protocol/jid.js +42 -0
- package/dist/esm/protocol/message.js +22 -0
- package/dist/esm/retry/reason.js +2 -2
- package/dist/esm/retry/replay.js +36 -2
- package/dist/esm/signal/session/SignalRatchet.js +2 -2
- package/dist/esm/transport/node/WaNodeOrchestrator.js +8 -4
- package/dist/esm/transport/node/builders/global.js +3 -0
- package/dist/esm/transport/node/builders/presence.js +2 -1
- package/dist/esm/transport/node/builders/profile.js +3 -2
- package/dist/esm/transport/node/builders/retry.js +2 -4
- package/dist/message/WaMessageClient.js +3 -0
- package/dist/message/primitives/incoming.d.ts +8 -1
- package/dist/message/primitives/incoming.js +76 -44
- 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/jid.d.ts +12 -0
- package/dist/protocol/jid.js +44 -0
- package/dist/protocol/message.d.ts +22 -0
- package/dist/protocol/message.js +23 -1
- package/dist/retry/reason.js +2 -2
- 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 +8 -4
- package/dist/transport/node/builders/global.d.ts +1 -0
- package/dist/transport/node/builders/global.js +3 -0
- package/dist/transport/node/builders/presence.d.ts +6 -0
- package/dist/transport/node/builders/presence.js +2 -1
- package/dist/transport/node/builders/profile.d.ts +1 -1
- package/dist/transport/node/builders/profile.js +3 -2
- package/dist/transport/node/builders/retry.js +2 -4
- package/package.json +2 -2
|
@@ -25,7 +25,12 @@ interface WaAppStateSyncClientOptions {
|
|
|
25
25
|
readonly defaultTimeoutMs?: number;
|
|
26
26
|
readonly onMissingKeys?: (event: WaAppStateMissingKeysEvent) => Promise<void>;
|
|
27
27
|
readonly skipMacVerification?: boolean;
|
|
28
|
-
|
|
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
|
|
@@ -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,12 +482,13 @@ 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)({
|
|
488
489
|
sendNode: (node) => nodeOrchestrator.sendNode(node, false),
|
|
489
|
-
getCurrentCredentials
|
|
490
|
+
getCurrentCredentials,
|
|
491
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
|
|
490
492
|
});
|
|
491
493
|
const peerDataOperation = (0, peer_data_operation_1.createPeerDataOperationRequester)({
|
|
492
494
|
logger,
|
|
@@ -521,12 +523,15 @@ function buildWaClientDependencies(input) {
|
|
|
521
523
|
sendNode: runtime.sendNode,
|
|
522
524
|
getCurrentCredentials,
|
|
523
525
|
resolveUserIcdc: (userJid) => messageDispatch.resolveUserIcdc(userJid),
|
|
526
|
+
resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
|
|
527
|
+
// Placeholder resend asks the primary phone (a peer) for the plaintext.
|
|
524
528
|
peerDataOperation,
|
|
525
529
|
emitIncomingMessage: (event) => {
|
|
526
530
|
void runtime
|
|
527
531
|
.handleIncomingMessageEvent(event)
|
|
528
532
|
.catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
|
|
529
|
-
}
|
|
533
|
+
},
|
|
534
|
+
isMobilePrimary
|
|
530
535
|
});
|
|
531
536
|
const botCoordinator = (0, WaBotCoordinator_1.createBotCoordinator)({
|
|
532
537
|
logger,
|
|
@@ -550,20 +555,30 @@ function buildWaClientDependencies(input) {
|
|
|
550
555
|
await messageDispatch.requestAppStateSyncKeys(keyIds);
|
|
551
556
|
},
|
|
552
557
|
skipMacVerification: options.dangerous?.disableAppStateMacVerification,
|
|
553
|
-
mobilePrimary:
|
|
558
|
+
mobilePrimary: isMobilePrimary,
|
|
554
559
|
isOwnAccountDevice: (deviceJid) => {
|
|
555
560
|
const credentials = getCurrentCredentials();
|
|
556
561
|
if (!credentials)
|
|
557
562
|
return false;
|
|
558
|
-
|
|
559
|
-
return ((!!credentials.meJid && (0, jid_1.toUserJid)(credentials.meJid) === candidateUser) ||
|
|
560
|
-
(!!credentials.meLid && (0, jid_1.toUserJid)(credentials.meLid) === candidateUser));
|
|
563
|
+
return (0, jid_1.isOwnAccountJid)(deviceJid, credentials.meJid, credentials.meLid);
|
|
561
564
|
},
|
|
562
565
|
sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
|
|
563
566
|
triggerSync: async () => {
|
|
564
567
|
await runtime.syncAppState();
|
|
565
568
|
}
|
|
566
569
|
});
|
|
570
|
+
// Persists a pushName change and re-broadcasts presence carrying it (how the
|
|
571
|
+
// name reaches peers on primary connections). No-op when unchanged, which
|
|
572
|
+
// also collapses the app-state echo of our own SettingPushName write.
|
|
573
|
+
const applyOwnPushName = async (name) => {
|
|
574
|
+
if (getCurrentCredentials()?.meDisplayName === name) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
await authClient.persistSuccessAttributes({ meDisplayName: name });
|
|
578
|
+
if (connectionManager?.isConnected()) {
|
|
579
|
+
await presenceCoordinator.send();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
567
582
|
const appStateMutations = new WaAppStateMutationCoordinator_1.WaAppStateMutationCoordinator({
|
|
568
583
|
logger,
|
|
569
584
|
messageStore: sessionStore.messages,
|
|
@@ -574,7 +589,12 @@ function buildWaClientDependencies(input) {
|
|
|
574
589
|
emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
|
|
575
590
|
emitMutation: (event) => runtime.emitEvent('mutation', event),
|
|
576
591
|
nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
|
|
577
|
-
contactSink: runtime.persistContact
|
|
592
|
+
contactSink: runtime.persistContact,
|
|
593
|
+
pushNameSink: (name) => {
|
|
594
|
+
void applyOwnPushName(name).catch((error) => logger.debug('apply own pushName from app-state sync failed', {
|
|
595
|
+
message: (0, primitives_1.toError)(error).message
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
578
598
|
});
|
|
579
599
|
const profileCoordinator = (0, WaProfileCoordinator_1.createProfileCoordinator)({
|
|
580
600
|
queryWithContext: runtime.queryWithContext,
|
|
@@ -582,6 +602,8 @@ function buildWaClientDependencies(input) {
|
|
|
582
602
|
mexSocket: { query: runtime.query },
|
|
583
603
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
584
604
|
mutations: appStateMutations,
|
|
605
|
+
applyOwnPushName,
|
|
606
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
|
|
585
607
|
logger
|
|
586
608
|
});
|
|
587
609
|
const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
|
|
@@ -662,6 +684,7 @@ function buildWaClientDependencies(input) {
|
|
|
662
684
|
logger,
|
|
663
685
|
sendNode: runtime.sendNode,
|
|
664
686
|
getMeJid: () => getCurrentCredentials()?.meJid,
|
|
687
|
+
getMeLid: () => getCurrentCredentials()?.meLid,
|
|
665
688
|
signalProtocol,
|
|
666
689
|
senderKeyManager,
|
|
667
690
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
@@ -979,7 +1002,7 @@ function buildWaClientDependencies(input) {
|
|
|
979
1002
|
abPropsCoordinator,
|
|
980
1003
|
markOnlineOnConnect: options.markOnlineOnConnect ?? false
|
|
981
1004
|
}),
|
|
982
|
-
mobilePrimary:
|
|
1005
|
+
mobilePrimary: isMobilePrimary,
|
|
983
1006
|
appStateSync
|
|
984
1007
|
});
|
|
985
1008
|
const lowLevelCoordinator = (0, WaLowLevelCoordinator_1.createLowLevelCoordinator)({
|
|
@@ -249,6 +249,9 @@ class WaConnectionManager {
|
|
|
249
249
|
if (!serverStaticKey) {
|
|
250
250
|
this.logger.trace('no server static key available to persist');
|
|
251
251
|
}
|
|
252
|
+
else if (!credentials.meJid) {
|
|
253
|
+
this.logger.trace('skipping server static key persist while unregistered');
|
|
254
|
+
}
|
|
252
255
|
else {
|
|
253
256
|
await this.authClient.persistServerStaticKey(serverStaticKey);
|
|
254
257
|
this.assertLifecycleCurrent(lifecycleGeneration, 'start comms');
|
|
@@ -42,6 +42,12 @@ interface WaAppStateMutationCoordinatorOptions {
|
|
|
42
42
|
* bootstrapped at pair-time regardless of `emitSnapshotMutations`.
|
|
43
43
|
*/
|
|
44
44
|
readonly contactSink?: (record: WaStoredContactRecord) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Sink for applied `SettingPushName` mutations (the account's own display
|
|
47
|
+
* name). Invoked on every winning mutation including snapshot ones, so the
|
|
48
|
+
* local display name is bootstrapped at pair-time.
|
|
49
|
+
*/
|
|
50
|
+
readonly pushNameSink?: (name: string) => void;
|
|
45
51
|
}
|
|
46
52
|
export interface WaSetStatusPrivacyInput {
|
|
47
53
|
readonly mode: StatusDistributionModeKey | StatusDistributionMode;
|
|
@@ -76,6 +82,7 @@ export declare class WaAppStateMutationCoordinator {
|
|
|
76
82
|
private readonly emitSnapshotMutations;
|
|
77
83
|
private readonly nctSaltSink?;
|
|
78
84
|
private readonly contactSink?;
|
|
85
|
+
private readonly pushNameSink?;
|
|
79
86
|
private readonly pendingMutations;
|
|
80
87
|
private flushPromise;
|
|
81
88
|
constructor(options: WaAppStateMutationCoordinatorOptions);
|
|
@@ -96,6 +103,7 @@ export declare class WaAppStateMutationCoordinator {
|
|
|
96
103
|
emitEventsFromSyncResult(syncResult: WaAppStateSyncResult): void;
|
|
97
104
|
private handleNctSaltMutation;
|
|
98
105
|
private handleContactMutation;
|
|
106
|
+
private handlePushNameMutation;
|
|
99
107
|
/**
|
|
100
108
|
* Mutes or unmutes a chat. `muteEndTimestampMs` is required when
|
|
101
109
|
* `muted` is `true` and must be a non-negative safe-integer epoch.
|
|
@@ -183,6 +183,7 @@ class WaAppStateMutationCoordinator {
|
|
|
183
183
|
this.emitSnapshotMutations = options.emitSnapshotMutations === true;
|
|
184
184
|
this.nctSaltSink = options.nctSaltSink;
|
|
185
185
|
this.contactSink = options.contactSink;
|
|
186
|
+
this.pushNameSink = options.pushNameSink;
|
|
186
187
|
this.pendingMutations = new Map();
|
|
187
188
|
this.flushPromise = null;
|
|
188
189
|
}
|
|
@@ -230,11 +231,10 @@ class WaAppStateMutationCoordinator {
|
|
|
230
231
|
emitEventsFromSyncResult(syncResult) {
|
|
231
232
|
for (const collectionResult of syncResult.collections) {
|
|
232
233
|
const mutations = collectionResult.mutations ?? [];
|
|
233
|
-
// Persistence sinks (contact store,
|
|
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.
|
|
@@ -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 = {}) {
|
|
@@ -68,17 +68,20 @@ class WaMessageDispatchCoordinator {
|
|
|
68
68
|
}
|
|
69
69
|
async publishSignalMessage(input, options = {}) {
|
|
70
70
|
this.requireCurrentMeJid('publishSignalMessage');
|
|
71
|
-
const
|
|
71
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
72
|
+
const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(input.to, credentials?.meJid, credentials?.meLid);
|
|
73
|
+
const address = (0, jid_1.parseSignalAddressFromJid)(sessionJid);
|
|
72
74
|
if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
|
|
73
75
|
throw new Error('publishSignalMessage currently supports only direct chats; use sender-key flow for groups');
|
|
74
76
|
}
|
|
75
77
|
this.deps.logger.trace('wa client publish signal message', {
|
|
76
78
|
to: input.to,
|
|
79
|
+
sessionJid,
|
|
77
80
|
type: input.type
|
|
78
81
|
});
|
|
79
82
|
const [paddedPlaintext] = await Promise.all([
|
|
80
83
|
(0, padding_1.writeRandomPadMax16)(input.plaintext),
|
|
81
|
-
this.deps.sessionResolver.ensureSession(address,
|
|
84
|
+
this.deps.sessionResolver.ensureSession(address, sessionJid, input.expectedIdentity)
|
|
82
85
|
]);
|
|
83
86
|
const encrypted = await this.deps.signalProtocol.encryptMessage(address, paddedPlaintext, input.expectedIdentity);
|
|
84
87
|
const messageType = input.type ?? 'text';
|
|
@@ -325,7 +328,8 @@ class WaMessageDispatchCoordinator {
|
|
|
325
328
|
await this.deps.messageClient.sendReceipt(input);
|
|
326
329
|
}
|
|
327
330
|
async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
|
|
328
|
-
const
|
|
331
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
332
|
+
const meJid = credentials?.meJid;
|
|
329
333
|
const meParsed = meJid ? (0, jid_1.parseJidFull)(meJid) : undefined;
|
|
330
334
|
const meUserJid = meParsed?.userJid;
|
|
331
335
|
let senderIcdc = null;
|
|
@@ -657,7 +661,7 @@ class WaMessageDispatchCoordinator {
|
|
|
657
661
|
const serverAddressingMode = result.ack.addressingMode;
|
|
658
662
|
const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
|
|
659
663
|
const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
|
|
660
|
-
const hasAddressingError = ackError ===
|
|
664
|
+
const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
|
|
661
665
|
if (!retryContext.retried &&
|
|
662
666
|
(hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
|
|
663
667
|
this.deps.logger.warn('group direct publish acknowledged with mismatch metadata', {
|
|
@@ -832,7 +836,7 @@ class WaMessageDispatchCoordinator {
|
|
|
832
836
|
const serverAddressingMode = result.ack.addressingMode;
|
|
833
837
|
const hasPhashMismatch = !!serverPhash && serverPhash !== localPhash;
|
|
834
838
|
const hasAddressingMismatch = !!serverAddressingMode && serverAddressingMode !== addressingMode;
|
|
835
|
-
const hasAddressingError = ackError ===
|
|
839
|
+
const hasAddressingError = ackError === constants_1.WA_NACK_REASONS.STALE_GROUP_ADDRESSING_MODE;
|
|
836
840
|
if (!retryContext.retried &&
|
|
837
841
|
(hasPhashMismatch || hasAddressingMismatch || hasAddressingError)) {
|
|
838
842
|
this.deps.logger.warn('group message publish acknowledged with mismatch metadata', {
|
|
@@ -1194,7 +1198,7 @@ class WaMessageDispatchCoordinator {
|
|
|
1194
1198
|
const meUserJid = (0, jid_1.toUserJid)(this.requireCurrentMeJid('sendMessage'));
|
|
1195
1199
|
const timestampBytes = new Uint8Array(8);
|
|
1196
1200
|
const dv = new DataView(timestampBytes.buffer, timestampBytes.byteOffset, timestampBytes.byteLength);
|
|
1197
|
-
if (this.mobileMessageIdFormat) {
|
|
1201
|
+
if (this.mobileMessageIdFormat()) {
|
|
1198
1202
|
dv.setBigUint64(0, BigInt(Date.now()), false);
|
|
1199
1203
|
const digest = (0, primitives_1.md5Bytes)([
|
|
1200
1204
|
timestampBytes,
|
|
@@ -1216,7 +1220,7 @@ class WaMessageDispatchCoordinator {
|
|
|
1216
1220
|
this.deps.logger.warn('failed to generate message id, falling back to random', {
|
|
1217
1221
|
message: (0, primitives_2.toError)(error).message
|
|
1218
1222
|
});
|
|
1219
|
-
if (this.mobileMessageIdFormat) {
|
|
1223
|
+
if (this.mobileMessageIdFormat()) {
|
|
1220
1224
|
const bytes = await (0, _crypto_1.randomBytesAsync)(16);
|
|
1221
1225
|
bytes[0] = 0xac;
|
|
1222
1226
|
return (0, bytes_1.bytesToHex)(bytes).toUpperCase();
|
|
@@ -40,7 +40,12 @@ export declare class WaPassiveTasksCoordinator {
|
|
|
40
40
|
readonly signedPreKeyRotationIntervalMs?: number;
|
|
41
41
|
readonly signedPreKeyServerErrorBackoffMs?: number;
|
|
42
42
|
readonly runtime: WaPassiveTasksRuntime;
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Resolved when passive tasks run (post-connect, after credentials
|
|
45
|
+
* load) so a registered mobile-primary session reconnecting without an
|
|
46
|
+
* explicit `mobileTransport` option still bootstraps the primary path.
|
|
47
|
+
*/
|
|
48
|
+
readonly mobilePrimary?: () => boolean;
|
|
44
49
|
readonly appStateSync?: WaAppStateSyncClient;
|
|
45
50
|
});
|
|
46
51
|
startPassiveTasksAfterConnect(): void;
|
|
@@ -19,7 +19,7 @@ class WaPassiveTasksCoordinator {
|
|
|
19
19
|
this.signedPreKeyServerErrorBackoffMs =
|
|
20
20
|
options.signedPreKeyServerErrorBackoffMs ?? constants_2.SIGNAL_SIGNED_PREKEY_SERVER_ERROR_BACKOFF_MS;
|
|
21
21
|
this.runtime = options.runtime;
|
|
22
|
-
this.mobilePrimary = options.mobilePrimary ?? false;
|
|
22
|
+
this.mobilePrimary = options.mobilePrimary ?? (() => false);
|
|
23
23
|
this.appStateSync = options.appStateSync;
|
|
24
24
|
this.passiveTasksPromise = null;
|
|
25
25
|
}
|
|
@@ -62,7 +62,7 @@ class WaPassiveTasksCoordinator {
|
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
this.runtime.syncAbProps();
|
|
65
|
-
if (this.mobilePrimary && this.appStateSync) {
|
|
65
|
+
if (this.mobilePrimary() && this.appStateSync) {
|
|
66
66
|
await this.appStateSync.ensureInitialSyncKey().catch((error) => {
|
|
67
67
|
this.logger.warn('app-state initial key generation failed', {
|
|
68
68
|
message: (0, primitives_1.toError)(error).message
|
|
@@ -149,7 +149,7 @@ class WaPassiveTasksCoordinator {
|
|
|
149
149
|
const response = await this.runtime.queryWithContext('prekeys.upload', uploadNode, constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS, {
|
|
150
150
|
count: preKeys.length,
|
|
151
151
|
lastPreKeyId
|
|
152
|
-
}, this.mobilePrimary ? { useSystemId: true } : undefined);
|
|
152
|
+
}, this.mobilePrimary() ? { useSystemId: true } : undefined);
|
|
153
153
|
if (response.attrs.type === constants_1.WA_IQ_TYPES.RESULT) {
|
|
154
154
|
// Mark uploaded key first so the serverHasPreKeys flag never commits ahead of local key progress.
|
|
155
155
|
await this.preKeyStore.markKeyAsUploaded(lastPreKeyId);
|
|
@@ -21,6 +21,12 @@ export interface WaPresenceCoordinator {
|
|
|
21
21
|
interface WaPresenceCoordinatorOptions {
|
|
22
22
|
readonly sendNode: (node: BinaryNode) => Promise<void>;
|
|
23
23
|
readonly getCurrentCredentials: () => WaAuthCredentials | null;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the receiver-mode `<tctoken>` node for a contact, echoed back
|
|
26
|
+
* on a user presence subscription to unlock the target's presence
|
|
27
|
+
* visibility. Returns `null` when no valid token is held.
|
|
28
|
+
*/
|
|
29
|
+
readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
|
|
24
30
|
}
|
|
25
31
|
/** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
|
|
26
32
|
export declare function createPresenceCoordinator(options: WaPresenceCoordinatorOptions): WaPresenceCoordinator;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createPresenceCoordinator = createPresenceCoordinator;
|
|
4
|
+
const jid_1 = require("../../protocol/jid");
|
|
4
5
|
const chatstate_1 = require("../../transport/node/builders/chatstate");
|
|
5
6
|
const presence_1 = require("../../transport/node/builders/presence");
|
|
6
7
|
/** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
|
|
7
8
|
function createPresenceCoordinator(options) {
|
|
8
|
-
const { sendNode, getCurrentCredentials } = options;
|
|
9
|
+
const { sendNode, getCurrentCredentials, resolvePrivacyTokenNode } = options;
|
|
9
10
|
return {
|
|
10
11
|
send: async (type) => {
|
|
11
12
|
const credentials = getCurrentCredentials();
|
|
@@ -15,7 +16,12 @@ function createPresenceCoordinator(options) {
|
|
|
15
16
|
await sendNode((0, chatstate_1.buildChatstateNode)({ jid, ...opts }));
|
|
16
17
|
},
|
|
17
18
|
subscribe: async (jid, opts) => {
|
|
18
|
-
|
|
19
|
+
const privacyTokenNode = (0, jid_1.isGroupJid)(jid) ? null : await resolvePrivacyTokenNode(jid);
|
|
20
|
+
await sendNode((0, presence_1.buildPresenceSubscribeNode)({
|
|
21
|
+
jid,
|
|
22
|
+
...opts,
|
|
23
|
+
...(privacyTokenNode ? { privacyTokenNode } : {})
|
|
24
|
+
}));
|
|
19
25
|
}
|
|
20
26
|
};
|
|
21
27
|
}
|
|
@@ -79,12 +79,11 @@ export interface WaProfileCoordinator {
|
|
|
79
79
|
readonly setStatus: (text: string) => Promise<void>;
|
|
80
80
|
/**
|
|
81
81
|
* Sets the account's pushName - the display name broadcast to other
|
|
82
|
-
* users in chats and group participant lists.
|
|
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,19 @@ interface WaProfileCoordinatorOptions {
|
|
|
144
143
|
* mutations.
|
|
145
144
|
*/
|
|
146
145
|
readonly mutations: WaAppStateMutationCoordinator;
|
|
146
|
+
/**
|
|
147
|
+
* Applies a pushName change locally: persists the display name and
|
|
148
|
+
* re-broadcasts an available presence carrying it. Expected to be
|
|
149
|
+
* idempotent (a no-op when the name already matches).
|
|
150
|
+
*/
|
|
151
|
+
readonly applyOwnPushName: (name: string) => Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Resolves the receiver-mode `<tctoken>` node for a contact, echoed back on
|
|
154
|
+
* privacy-gated profile queries (picture get, about/status usync) to prove
|
|
155
|
+
* this account is a trusted contact. Returns `null` when no valid token is
|
|
156
|
+
* held for the JID.
|
|
157
|
+
*/
|
|
158
|
+
readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
|
|
147
159
|
readonly logger: Logger;
|
|
148
160
|
}
|
|
149
161
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|