zapo-js 1.4.0 → 1.5.0
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/types.d.ts +1 -2
- package/dist/auth/WaAuthClient.d.ts +10 -0
- package/dist/auth/WaAuthClient.js +17 -8
- package/dist/auth/credentials-flow.d.ts +8 -0
- package/dist/auth/credentials-flow.js +35 -2
- package/dist/auth/pairing/companion-host.d.ts +105 -0
- package/dist/auth/pairing/companion-host.js +148 -0
- package/dist/auth/types.d.ts +20 -9
- package/dist/client/WaClient.d.ts +15 -5
- package/dist/client/WaClient.js +46 -12
- package/dist/client/WaClientFactory.d.ts +3 -0
- package/dist/client/WaClientFactory.js +24 -6
- package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +3 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +13 -3
- package/dist/client/coordinators/WaMobileCoordinator.d.ts +192 -0
- package/dist/client/coordinators/WaMobileCoordinator.js +645 -0
- package/dist/client/messaging/companion-host.d.ts +43 -0
- package/dist/client/messaging/companion-host.js +61 -0
- package/dist/client/messaging/key-protocol.js +25 -1
- package/dist/client/persistence/companion-host.d.ts +36 -0
- package/dist/client/persistence/companion-host.js +57 -0
- package/dist/client/plugins/install.d.ts +2 -1
- package/dist/client/plugins/install.js +44 -11
- package/dist/client/types.d.ts +71 -7
- package/dist/esm/auth/WaAuthClient.js +18 -9
- package/dist/esm/auth/credentials-flow.js +36 -3
- package/dist/esm/auth/pairing/companion-host.js +140 -0
- package/dist/esm/client/WaClient.js +46 -12
- package/dist/esm/client/WaClientFactory.js +25 -7
- package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/esm/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -4
- package/dist/esm/client/coordinators/WaMobileCoordinator.js +641 -0
- package/dist/esm/client/messaging/companion-host.js +56 -0
- package/dist/esm/client/messaging/key-protocol.js +25 -1
- package/dist/esm/client/persistence/companion-host.js +54 -0
- package/dist/esm/client/plugins/install.js +44 -11
- package/dist/esm/index.js +5 -2
- package/dist/esm/message/WaMessageClient.js +3 -2
- package/dist/esm/message/context-info.js +4 -0
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/device-identity.js +18 -0
- package/dist/esm/protocol/group.js +6 -0
- package/dist/esm/protocol/message.js +12 -0
- package/dist/esm/protocol/nodes.js +15 -1
- package/dist/esm/retry/replay.js +3 -3
- package/dist/esm/signal/attestation/WaAdvSignature.js +67 -2
- package/dist/esm/signal/attestation/constants.js +2 -0
- package/dist/esm/signal/index.js +1 -0
- package/dist/esm/transport/index.js +1 -1
- package/dist/esm/transport/node/builders/group.js +4 -3
- package/dist/esm/transport/node/builders/mobile.js +113 -0
- package/dist/esm/transport/noise/WaClientPayload.js +16 -7
- package/dist/esm/transport/wa-version-fetcher.js +152 -0
- package/dist/esm/util/index.js +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -4
- package/dist/message/WaMessageClient.js +2 -1
- package/dist/message/context-info.d.ts +2 -0
- package/dist/message/context-info.js +5 -0
- package/dist/protocol/constants.d.ts +6 -4
- package/dist/protocol/constants.js +8 -2
- package/dist/protocol/device-identity.d.ts +25 -0
- package/dist/protocol/device-identity.js +21 -0
- package/dist/protocol/group.d.ts +6 -0
- package/dist/protocol/group.js +7 -1
- package/dist/protocol/message.d.ts +14 -0
- package/dist/protocol/message.js +13 -1
- package/dist/protocol/nodes.d.ts +14 -0
- package/dist/protocol/nodes.js +15 -1
- package/dist/retry/replay.js +2 -2
- package/dist/signal/attestation/WaAdvSignature.d.ts +50 -1
- package/dist/signal/attestation/WaAdvSignature.js +71 -1
- package/dist/signal/attestation/constants.d.ts +2 -0
- package/dist/signal/attestation/constants.js +3 -1
- package/dist/signal/index.d.ts +1 -0
- package/dist/signal/index.js +13 -1
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js +4 -3
- package/dist/transport/node/builders/group.js +4 -3
- package/dist/transport/node/builders/mobile.d.ts +68 -0
- package/dist/transport/node/builders/mobile.js +120 -0
- package/dist/transport/noise/WaClientPayload.js +16 -7
- package/dist/transport/wa-version-fetcher.d.ts +92 -0
- package/dist/transport/wa-version-fetcher.js +156 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
- package/dist/esm/transport/wa-web-version-fetcher.js +0 -91
- package/dist/transport/wa-web-version-fetcher.d.ts +0 -44
- package/dist/transport/wa-web-version-fetcher.js +0 -94
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { aesCtrDecrypt, aesCtrEncrypt, aesGcmDecrypt, hkdf, pbkdf2Sha256, randomBytesAsync, toRawPubKey, X25519 } from '../../crypto/index.js';
|
|
2
|
+
import { proto } from '../../proto.js';
|
|
3
|
+
import { WA_PAIRING_KDF_INFO } from '../../protocol/index.js';
|
|
4
|
+
import { computeAdvIdentityHmac, generateDeviceIdentityAccountSignature, signKeyIndexList } from '../../signal/index.js';
|
|
5
|
+
import { concatBytes, decodeBase64Url, TEXT_ENCODER, uint8Equal } from '../../util/bytes.js';
|
|
6
|
+
const PEM_HKDF_INFO = TEXT_ENCODER.encode('Canonical Ent Companion Nonce Encrypt');
|
|
7
|
+
const PBKDF2_ITERATIONS = 131072;
|
|
8
|
+
const PAIRING_AES_KEY_BYTES = 32;
|
|
9
|
+
/**
|
|
10
|
+
* Parses a companion pairing QR of the form
|
|
11
|
+
* `ref,noisePubB64,identityPubB64,advSecretB64,platform`. The `ref` may itself
|
|
12
|
+
* contain commas, so the four trailing fields are taken from the end.
|
|
13
|
+
*
|
|
14
|
+
* @throws when fewer than 5 comma-separated fields are present.
|
|
15
|
+
*/
|
|
16
|
+
export function parseCompanionQr(qr) {
|
|
17
|
+
const parts = qr.split(',');
|
|
18
|
+
if (parts.length < 5) {
|
|
19
|
+
throw new Error(`companion qr must have at least 5 comma-separated parts, got ${parts.length}`);
|
|
20
|
+
}
|
|
21
|
+
const platform = parts[parts.length - 1];
|
|
22
|
+
const advSecretB64 = parts[parts.length - 2];
|
|
23
|
+
const identityPubB64 = parts[parts.length - 3];
|
|
24
|
+
const noisePubB64 = parts[parts.length - 4];
|
|
25
|
+
const ref = parts.slice(0, parts.length - 4).join(',');
|
|
26
|
+
return {
|
|
27
|
+
ref,
|
|
28
|
+
noisePublicKey: decodeBase64Url(noisePubB64, 'companionQr.noisePublicKey'),
|
|
29
|
+
identityPublicKey: decodeBase64Url(identityPubB64, 'companionQr.identityPublicKey'),
|
|
30
|
+
advSecretKey: decodeBase64Url(advSecretB64, 'companionQr.advSecretKey'),
|
|
31
|
+
platform
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds the account-signed `ADVSignedKeyIndexList` bytes for the current device
|
|
36
|
+
* set. The zero-valued `accountType` enum is omitted to match the phone's byte
|
|
37
|
+
* layout.
|
|
38
|
+
*/
|
|
39
|
+
export async function buildSignedKeyIndexList(input) {
|
|
40
|
+
const details = proto.ADVKeyIndexList.encode({
|
|
41
|
+
rawId: input.rawId,
|
|
42
|
+
timestamp: input.timestampSeconds,
|
|
43
|
+
currentIndex: input.currentIndex,
|
|
44
|
+
validIndexes: [...input.validIndexes]
|
|
45
|
+
}).finish();
|
|
46
|
+
const accountSignature = await signKeyIndexList(details, input.accountIdentityKeyPair);
|
|
47
|
+
return proto.ADVSignedKeyIndexList.encode({ details, accountSignature }).finish();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds the signed device identity and key-index list for a linking companion.
|
|
51
|
+
* The byte layout mirrors WhatsApp's phone (and the in-repo fake-server
|
|
52
|
+
* reference): the zero-valued `accountType`/`deviceType` enum fields are omitted
|
|
53
|
+
* so the signed `details` match what the companion verifies.
|
|
54
|
+
*/
|
|
55
|
+
export async function buildSignedCompanionIdentity(input) {
|
|
56
|
+
const accountSignatureKey = toRawPubKey(input.accountIdentityKeyPair.pubKey);
|
|
57
|
+
const details = proto.ADVDeviceIdentity.encode({
|
|
58
|
+
rawId: input.rawId,
|
|
59
|
+
timestamp: input.timestampSeconds,
|
|
60
|
+
keyIndex: input.keyIndex
|
|
61
|
+
}).finish();
|
|
62
|
+
const accountSignature = await generateDeviceIdentityAccountSignature(details, input.companionIdentityPublicKey, input.accountIdentityKeyPair);
|
|
63
|
+
const signedIdentity = proto.ADVSignedDeviceIdentity.encode({
|
|
64
|
+
details,
|
|
65
|
+
accountSignatureKey,
|
|
66
|
+
accountSignature
|
|
67
|
+
}).finish();
|
|
68
|
+
const hmac = computeAdvIdentityHmac(input.advSecretKey, signedIdentity);
|
|
69
|
+
const deviceIdentityBytes = proto.ADVSignedDeviceIdentityHMAC.encode({
|
|
70
|
+
details: signedIdentity,
|
|
71
|
+
hmac
|
|
72
|
+
}).finish();
|
|
73
|
+
const keyIndexListBytes = await buildSignedKeyIndexList({
|
|
74
|
+
accountIdentityKeyPair: input.accountIdentityKeyPair,
|
|
75
|
+
rawId: input.rawId,
|
|
76
|
+
currentIndex: input.keyIndex,
|
|
77
|
+
timestampSeconds: input.timestampSeconds,
|
|
78
|
+
validIndexes: input.validIndexes
|
|
79
|
+
});
|
|
80
|
+
return { deviceIdentityBytes, keyIndexListBytes };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Derives the `<pem>` AES-GCM key the phone ships in the pair-device upload:
|
|
84
|
+
* `HKDF-SHA256(ikm = advSecret, salt = companionNoisePublicKey, info)`. Not yet
|
|
85
|
+
* verified against a live server from this implementation; see
|
|
86
|
+
* `CompanionHostOptions.includePem`.
|
|
87
|
+
*/
|
|
88
|
+
export function derivePemKey(advSecretKey, companionNoisePublicKey) {
|
|
89
|
+
return hkdf(advSecretKey, companionNoisePublicKey, PEM_HKDF_INFO, 32);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Link-code handshake, primary step 1: unwrap the companion ephemeral with the
|
|
93
|
+
* pairing code, generate the primary ephemeral, compute the shared ECDH secret,
|
|
94
|
+
* and wrap the primary ephemeral for the `primary_hello` stanza. Mirror of the
|
|
95
|
+
* companion's `createCompanionHello` + primary-ephemeral unwrap.
|
|
96
|
+
*/
|
|
97
|
+
export async function preparePrimaryHello(args) {
|
|
98
|
+
if (args.wrappedCompanionEphemeralPub.length < 48) {
|
|
99
|
+
throw new Error(`wrapped companion ephemeral pub too short: ${args.wrappedCompanionEphemeralPub.length} bytes (need >= 48)`);
|
|
100
|
+
}
|
|
101
|
+
const codeBytes = TEXT_ENCODER.encode(args.pairingCode);
|
|
102
|
+
const companionCipherKey = await pbkdf2Sha256(codeBytes, args.wrappedCompanionEphemeralPub.subarray(0, 32), PBKDF2_ITERATIONS, PAIRING_AES_KEY_BYTES);
|
|
103
|
+
const companionEphemeralPub = aesCtrDecrypt(companionCipherKey, args.wrappedCompanionEphemeralPub.subarray(32, 48), args.wrappedCompanionEphemeralPub.subarray(48));
|
|
104
|
+
const [primaryEphemeralKeyPair, salt, counter] = await Promise.all([
|
|
105
|
+
X25519.generateKeyPair(),
|
|
106
|
+
randomBytesAsync(32),
|
|
107
|
+
randomBytesAsync(16)
|
|
108
|
+
]);
|
|
109
|
+
const primaryCipherKey = await pbkdf2Sha256(codeBytes, salt, PBKDF2_ITERATIONS, PAIRING_AES_KEY_BYTES);
|
|
110
|
+
const encrypted = aesCtrEncrypt(primaryCipherKey, counter, primaryEphemeralKeyPair.pubKey);
|
|
111
|
+
const sharedEphemeral = await X25519.scalarMult(primaryEphemeralKeyPair.privKey, companionEphemeralPub);
|
|
112
|
+
return {
|
|
113
|
+
primaryEphemeralKeyPair,
|
|
114
|
+
wrappedPrimaryEphemeralPub: concatBytes([salt, counter, encrypted]),
|
|
115
|
+
sharedEphemeral
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Link-code handshake, primary step 2: decrypt the `companion_finish` key bundle
|
|
120
|
+
* and derive the shared `advSecret` (byte-identical to what the companion
|
|
121
|
+
* derives). Validates that the bundle binds both identities.
|
|
122
|
+
*
|
|
123
|
+
* @throws when the bundle's embedded identities do not match.
|
|
124
|
+
*/
|
|
125
|
+
export async function completePrimaryHandshake(args) {
|
|
126
|
+
const bundleEncryptionKey = hkdf(args.sharedEphemeral, args.wrappedKeyBundle.subarray(0, 32), WA_PAIRING_KDF_INFO.LINK_CODE_BUNDLE, 32);
|
|
127
|
+
const plaintextBundle = aesGcmDecrypt(bundleEncryptionKey, args.wrappedKeyBundle.subarray(32, 44), args.wrappedKeyBundle.subarray(44));
|
|
128
|
+
const bundleCompanionIdentity = plaintextBundle.subarray(0, 32);
|
|
129
|
+
const bundlePrimaryIdentity = plaintextBundle.subarray(32, 64);
|
|
130
|
+
const bundleSecret = plaintextBundle.subarray(64);
|
|
131
|
+
const rawCompanionIdentity = toRawPubKey(args.companionIdentityPub);
|
|
132
|
+
if (!uint8Equal(bundlePrimaryIdentity, toRawPubKey(args.primaryIdentityKeyPair.pubKey))) {
|
|
133
|
+
throw new Error('link-code bundle primary identity mismatch');
|
|
134
|
+
}
|
|
135
|
+
if (!uint8Equal(bundleCompanionIdentity, rawCompanionIdentity)) {
|
|
136
|
+
throw new Error('link-code bundle companion identity mismatch');
|
|
137
|
+
}
|
|
138
|
+
const sharedIdentity = await X25519.scalarMult(args.primaryIdentityKeyPair.privKey, rawCompanionIdentity);
|
|
139
|
+
return hkdf(concatBytes([args.sharedEphemeral, sharedIdentity, bundleSecret]), null, WA_PAIRING_KDF_INFO.ADV_SECRET, 32);
|
|
140
|
+
}
|
|
@@ -12,7 +12,7 @@ import { normalizeDeviceJid } from '../protocol/jid.js';
|
|
|
12
12
|
import { WA_DISCONNECT_REASONS, WA_LOGOUT_REASONS } from '../protocol/stream.js';
|
|
13
13
|
import { buildRemoveCompanionDeviceIq } from '../transport/node/builders/device.js';
|
|
14
14
|
import { assertIqResult, queryWithContext as queryNodeWithContext } from '../transport/node/query.js';
|
|
15
|
-
import { fetchLatestWaWebVersion } from '../transport/wa-
|
|
15
|
+
import { fetchLatestWaMobileVersion, fetchLatestWaWebVersion } from '../transport/wa-version-fetcher.js';
|
|
16
16
|
import { toError } from '../util/primitives.js';
|
|
17
17
|
const SYNC_RELATED_PROTOCOL_TYPES = new Set([
|
|
18
18
|
proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_REQUEST,
|
|
@@ -70,13 +70,14 @@ class WaClientImpl extends EventEmitter {
|
|
|
70
70
|
this.deps = dependencies;
|
|
71
71
|
this.appStateSync = dependencies.appStateSync;
|
|
72
72
|
this.mediaTransfer = dependencies.mediaTransfer;
|
|
73
|
-
this.
|
|
73
|
+
this.pluginInstallInput = {
|
|
74
74
|
options: this.options,
|
|
75
75
|
logger: this.logger,
|
|
76
76
|
stores: this.stores,
|
|
77
77
|
deps: this.deps,
|
|
78
78
|
queryWithContext: this.queryWithContext.bind(this)
|
|
79
|
-
}
|
|
79
|
+
};
|
|
80
|
+
this.installPlugins();
|
|
80
81
|
this.bindNodeTransportEvents();
|
|
81
82
|
this.on('connection', (event) => {
|
|
82
83
|
if (event.status !== 'close')
|
|
@@ -85,22 +86,43 @@ class WaClientImpl extends EventEmitter {
|
|
|
85
86
|
return;
|
|
86
87
|
if (event.reason !== WA_DISCONNECT_REASONS.FAILURE_CLIENT_TOO_OLD)
|
|
87
88
|
return;
|
|
88
|
-
this.logger.warn('wa rejected the connect with client_too_old: the zapo default WA
|
|
89
|
-
'Auto-recovering by fetching the current version from
|
|
89
|
+
this.logger.warn('wa rejected the connect with client_too_old: the zapo default WA version is outdated. ' +
|
|
90
|
+
'Auto-recovering by fetching the current version from the public source – ' +
|
|
90
91
|
'please upgrade zapo so the shipped default catches up.');
|
|
91
92
|
void this.runClientTooOldRecover();
|
|
92
93
|
});
|
|
94
|
+
this.on('connection', (event) => {
|
|
95
|
+
if (event.status !== 'open')
|
|
96
|
+
return;
|
|
97
|
+
void this.deps.mobileCoordinator.reconcileCompanions().catch((error) => {
|
|
98
|
+
this.logger.debug('companion reconcile on connect failed', {
|
|
99
|
+
message: toError(error).message
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
installPlugins() {
|
|
105
|
+
this.disposePlugins = installWaClientPlugins(this, this.pluginInstallInput, this.options.plugins ?? []);
|
|
93
106
|
}
|
|
94
107
|
async runClientTooOldRecover() {
|
|
95
108
|
try {
|
|
96
109
|
if (this.connectPromise) {
|
|
97
110
|
await this.connectPromise.catch(() => undefined);
|
|
98
111
|
}
|
|
99
|
-
const
|
|
112
|
+
const mobile = this.deps.isMobilePrimary();
|
|
113
|
+
const latest = mobile
|
|
114
|
+
? await fetchLatestWaMobileVersion()
|
|
115
|
+
: await fetchLatestWaWebVersion();
|
|
100
116
|
this.logger.info('client_too_old auto-recover: reconnecting', {
|
|
117
|
+
transport: mobile ? 'mobile' : 'web',
|
|
101
118
|
version: latest.version
|
|
102
119
|
});
|
|
103
|
-
|
|
120
|
+
if (mobile) {
|
|
121
|
+
this.deps.authClient.setNextConnectMobileAppVersion(latest.version);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.deps.authClient.setNextConnectVersion(latest.version);
|
|
125
|
+
}
|
|
104
126
|
await this.connect();
|
|
105
127
|
}
|
|
106
128
|
catch (error) {
|
|
@@ -306,6 +328,9 @@ class WaClientImpl extends EventEmitter {
|
|
|
306
328
|
this.logger.trace('wa client connect already in-flight');
|
|
307
329
|
return this.connectPromise;
|
|
308
330
|
}
|
|
331
|
+
if (!this.disposePlugins) {
|
|
332
|
+
this.installPlugins();
|
|
333
|
+
}
|
|
309
334
|
this.writeBehind.restart();
|
|
310
335
|
this.acceptingIncomingEvents = true;
|
|
311
336
|
this.connectPromise = this.deps.connectionManager
|
|
@@ -329,11 +354,12 @@ class WaClientImpl extends EventEmitter {
|
|
|
329
354
|
}
|
|
330
355
|
/**
|
|
331
356
|
* Closes the transport gracefully: pauses incoming events, flushes the
|
|
332
|
-
* write-behind persistence queue, and emits a
|
|
333
|
-
* with reason `client_disconnected`. Does not
|
|
334
|
-
* call {@link connect} again to resume the same
|
|
335
|
-
*
|
|
336
|
-
*
|
|
357
|
+
* write-behind persistence queue, disposes installed plugins, and emits a
|
|
358
|
+
* `connection` close event with reason `client_disconnected`. Does not
|
|
359
|
+
* clear stored credentials - call {@link connect} again to resume the same
|
|
360
|
+
* session, which reinstalls the plugins. There is no built-in
|
|
361
|
+
* auto-reconnect; subscribe to `connection: { status: 'close' }` and decide
|
|
362
|
+
* your own backoff.
|
|
337
363
|
*/
|
|
338
364
|
async disconnect() {
|
|
339
365
|
await this.pauseIncomingEventsAndWaitDrain();
|
|
@@ -450,6 +476,14 @@ class WaClientImpl extends EventEmitter {
|
|
|
450
476
|
get email() {
|
|
451
477
|
return this.deps.emailCoordinator;
|
|
452
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Mobile coordinator: host and manage companion devices linked to this
|
|
481
|
+
* mobile-primary account (link via QR or pairing code, list, revoke).
|
|
482
|
+
* Requires a registered primary session. See {@link WaMobileCoordinator}.
|
|
483
|
+
*/
|
|
484
|
+
get mobile() {
|
|
485
|
+
return this.deps.mobileCoordinator;
|
|
486
|
+
}
|
|
453
487
|
/**
|
|
454
488
|
* Unpairs this companion device by removing it server-side. Requires an
|
|
455
489
|
* authenticated session; throws when no `meJid` is present.
|
|
@@ -13,6 +13,7 @@ import { WaIncomingNodeCoordinator } from './coordinators/WaIncomingNodeCoordina
|
|
|
13
13
|
import { createLowLevelCoordinator } from './coordinators/WaLowLevelCoordinator.js';
|
|
14
14
|
import { WaMessageCoordinator } from './coordinators/WaMessageCoordinator.js';
|
|
15
15
|
import { WaMessageDispatchCoordinator } from './coordinators/WaMessageDispatchCoordinator.js';
|
|
16
|
+
import { WaMobileCoordinator } from './coordinators/WaMobileCoordinator.js';
|
|
16
17
|
import { createNewsletterCoordinator } from './coordinators/WaNewsletterCoordinator.js';
|
|
17
18
|
import { WaOfflineResumeCoordinator } from './coordinators/WaOfflineResumeCoordinator.js';
|
|
18
19
|
import { WaPassiveTasksCoordinator } from './coordinators/WaPassiveTasksCoordinator.js';
|
|
@@ -38,7 +39,7 @@ import { processNewsletterLiveUpdates } from '../message/kinds/newsletter.js';
|
|
|
38
39
|
import { handleIncomingMessageAck } from '../message/primitives/incoming.js';
|
|
39
40
|
import { createPeerDataOperationRequester } from '../message/primitives/peer-data-operation.js';
|
|
40
41
|
import { WaMessageClient } from '../message/WaMessageClient.js';
|
|
41
|
-
import {
|
|
42
|
+
import { resolveWaDeviceIdentity, WA_DEFAULTS, WA_DISCONNECT_REASONS, WA_NEWSLETTER_NOTIFICATION_TAGS, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_PRIVACY_TOKEN_NOTIFICATION_TYPE } from '../protocol/constants.js';
|
|
42
43
|
import { isNewsletterJid, isOwnAccountJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
|
|
43
44
|
import { WA_PRESENCE_TYPES } from '../protocol/presence.js';
|
|
44
45
|
import { createOutboundRetryTracker } from '../retry/tracker.js';
|
|
@@ -61,7 +62,6 @@ import { WaNodeTransport } from '../transport/node/WaNodeTransport.js';
|
|
|
61
62
|
import { isProxyTransport, toProxyAgent } from '../transport/proxy.js';
|
|
62
63
|
import { createServerClock } from '../util/clock.js';
|
|
63
64
|
import { toError } from '../util/primitives.js';
|
|
64
|
-
import { getRuntimeOsDisplayName } from '../util/runtime.js';
|
|
65
65
|
function assertProxyTransport(value, path) {
|
|
66
66
|
if (value === undefined) {
|
|
67
67
|
return;
|
|
@@ -85,7 +85,7 @@ function validateProxyOptions(options) {
|
|
|
85
85
|
}
|
|
86
86
|
export function resolveWaClientBase(options, logger) {
|
|
87
87
|
validateProxyOptions(options);
|
|
88
|
-
const
|
|
88
|
+
const device = resolveWaDeviceIdentity(options);
|
|
89
89
|
const sessionId = options.sessionId.trim();
|
|
90
90
|
if (sessionId.length === 0) {
|
|
91
91
|
throw new Error('sessionId must be a non-empty string');
|
|
@@ -94,9 +94,9 @@ export function resolveWaClientBase(options, logger) {
|
|
|
94
94
|
const normalizedOptions = Object.freeze({
|
|
95
95
|
...options,
|
|
96
96
|
sessionId,
|
|
97
|
-
deviceBrowser,
|
|
98
|
-
deviceOsDisplayName:
|
|
99
|
-
devicePlatform:
|
|
97
|
+
deviceBrowser: device.browser,
|
|
98
|
+
deviceOsDisplayName: device.osDisplayName,
|
|
99
|
+
devicePlatform: device.platform,
|
|
100
100
|
urls: options.urls ?? options.chatSocketUrls ?? WA_DEFAULTS.CHAT_SOCKET_URLS,
|
|
101
101
|
iqTimeoutMs: options.iqTimeoutMs ?? WA_DEFAULTS.IQ_TIMEOUT_MS,
|
|
102
102
|
nodeQueryTimeoutMs: options.nodeQueryTimeoutMs ?? WA_DEFAULTS.NODE_QUERY_TIMEOUT_MS,
|
|
@@ -451,6 +451,7 @@ export function buildWaClientDependencies(input) {
|
|
|
451
451
|
});
|
|
452
452
|
messageDispatch = new WaMessageDispatchCoordinator({
|
|
453
453
|
logger,
|
|
454
|
+
emitMessageSend: (event) => runtime.emitEvent('message_send', event),
|
|
454
455
|
messageClient,
|
|
455
456
|
retryTracker,
|
|
456
457
|
sessionResolver,
|
|
@@ -587,6 +588,7 @@ export function buildWaClientDependencies(input) {
|
|
|
587
588
|
serverClock,
|
|
588
589
|
emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
|
|
589
590
|
emitMutation: (event) => runtime.emitEvent('mutation', event),
|
|
591
|
+
emitMutationSend: (event) => runtime.emitEvent('mutation_send', event),
|
|
590
592
|
nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
|
|
591
593
|
contactSink: runtime.persistContact,
|
|
592
594
|
pushNameSink: (name) => {
|
|
@@ -1013,6 +1015,20 @@ export function buildWaClientDependencies(input) {
|
|
|
1013
1015
|
isConnected: () => connectionManager?.isConnected() ?? false,
|
|
1014
1016
|
defaultIqTimeoutMs: options.iqTimeoutMs
|
|
1015
1017
|
});
|
|
1018
|
+
const mobileCoordinator = new WaMobileCoordinator({
|
|
1019
|
+
logger,
|
|
1020
|
+
authClient,
|
|
1021
|
+
messageDispatch,
|
|
1022
|
+
chatCoordinator: appStateMutations,
|
|
1023
|
+
deviceSync: signalDeviceSync,
|
|
1024
|
+
appStateStore: sessionStore.appState,
|
|
1025
|
+
queryWithContext: runtime.queryWithContext,
|
|
1026
|
+
emitEvent: runtime.emitEvent,
|
|
1027
|
+
registerIncomingHandler: (registration) => incomingNode.registerIncomingHandler(registration),
|
|
1028
|
+
isMobilePrimary,
|
|
1029
|
+
persistence: options.companionHost?.persistence,
|
|
1030
|
+
includePem: options.companionHost?.includePem
|
|
1031
|
+
});
|
|
1016
1032
|
return {
|
|
1017
1033
|
nodeTransport,
|
|
1018
1034
|
nodeOrchestrator,
|
|
@@ -1053,6 +1069,8 @@ export function buildWaClientDependencies(input) {
|
|
|
1053
1069
|
connectionManager,
|
|
1054
1070
|
trustedContactToken,
|
|
1055
1071
|
abPropsCoordinator,
|
|
1056
|
-
peerDataOperation
|
|
1072
|
+
peerDataOperation,
|
|
1073
|
+
mobileCoordinator,
|
|
1074
|
+
isMobilePrimary
|
|
1057
1075
|
};
|
|
1058
1076
|
}
|
|
@@ -11,6 +11,8 @@ const WA_APP_STATE_MUTATION_FLUSH_SUCCESS_STATES = new Set([
|
|
|
11
11
|
WA_APP_STATE_COLLECTION_STATES.SUCCESS_HAS_MORE
|
|
12
12
|
]);
|
|
13
13
|
const WA_APP_STATE_ARCHIVE_RANGE_DEFAULT_LIMIT = 256;
|
|
14
|
+
/** Placeholder MACs for locally-emitted mutation events (the parser ignores them). */
|
|
15
|
+
const WA_EMPTY_MUTATION_MAC = new Uint8Array(0);
|
|
14
16
|
function buildMutationIndexFromSchema(schema, indexArgs) {
|
|
15
17
|
const parts = new Array(schema.indexParts.length);
|
|
16
18
|
const args = indexArgs;
|
|
@@ -157,7 +159,6 @@ function buildRemoveMutationFromSchema(input) {
|
|
|
157
159
|
collection: input.schema.collection,
|
|
158
160
|
operation: 'remove',
|
|
159
161
|
index: buildMutationIndexFromSchema(input.schema, input.indexArgs),
|
|
160
|
-
previousValue: { timestamp: input.timestamp },
|
|
161
162
|
version: input.schema.version,
|
|
162
163
|
timestamp: input.timestamp
|
|
163
164
|
};
|
|
@@ -177,6 +178,7 @@ export class WaAppStateMutationCoordinator {
|
|
|
177
178
|
this.serverClock = options.serverClock;
|
|
178
179
|
this.archiveRangeLimit = resolvePositive(options.archiveRangeLimit, WA_APP_STATE_ARCHIVE_RANGE_DEFAULT_LIMIT, 'WaAppStateMutationCoordinatorOptions.archiveRangeLimit');
|
|
179
180
|
this.emitMutation = options.emitMutation;
|
|
181
|
+
this.emitMutationSend = options.emitMutationSend;
|
|
180
182
|
this.emitSnapshotMutations = options.emitSnapshotMutations === true;
|
|
181
183
|
this.nctSaltSink = options.nctSaltSink;
|
|
182
184
|
this.contactSink = options.contactSink;
|
|
@@ -614,9 +616,43 @@ export class WaAppStateMutationCoordinator {
|
|
|
614
616
|
async enqueueAndFlush(mutations) {
|
|
615
617
|
for (const mutation of mutations) {
|
|
616
618
|
this.enqueueMutation(mutation);
|
|
619
|
+
this.emitLocalMutation(mutation);
|
|
617
620
|
}
|
|
618
621
|
await this.flushMutations();
|
|
619
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Surfaces an action this client is sending via the outbound `mutation_send`
|
|
625
|
+
* event, so consumers can observe their own change at action time. Parse
|
|
626
|
+
* failures are swallowed (best-effort).
|
|
627
|
+
*/
|
|
628
|
+
emitLocalMutation(input) {
|
|
629
|
+
if (!this.emitMutationSend)
|
|
630
|
+
return;
|
|
631
|
+
const value = input.operation === 'set' ? input.value : null;
|
|
632
|
+
try {
|
|
633
|
+
const event = parseAppStateMutationEvent({
|
|
634
|
+
collection: input.collection,
|
|
635
|
+
operation: input.operation,
|
|
636
|
+
source: 'local',
|
|
637
|
+
index: input.index,
|
|
638
|
+
value,
|
|
639
|
+
version: input.version,
|
|
640
|
+
indexMac: WA_EMPTY_MUTATION_MAC,
|
|
641
|
+
valueMac: WA_EMPTY_MUTATION_MAC,
|
|
642
|
+
keyId: WA_EMPTY_MUTATION_MAC,
|
|
643
|
+
timestamp: input.timestamp
|
|
644
|
+
});
|
|
645
|
+
if (event)
|
|
646
|
+
this.emitMutationSend(event);
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
this.logger.debug('failed to emit local app-state mutation event', {
|
|
650
|
+
collection: input.collection,
|
|
651
|
+
index: input.index,
|
|
652
|
+
message: toError(error).message
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
620
656
|
enqueueMutation(mutation) {
|
|
621
657
|
const key = `${mutation.collection}\u0001${mutation.index}`;
|
|
622
658
|
if (this.pendingMutations.has(key)) {
|
|
@@ -2,6 +2,7 @@ import { parseParticipants as parseGroupEventParticipants } from '../events/grou
|
|
|
2
2
|
import { WA_DEFAULTS } from '../../protocol/defaults.js';
|
|
3
3
|
import { WA_GROUP_PARTICIPANT_TYPES } from '../../protocol/group.js';
|
|
4
4
|
import { parseJidFull } from '../../protocol/jid.js';
|
|
5
|
+
import { WA_ADDRESSING_MODES } from '../../protocol/message.js';
|
|
5
6
|
import { WA_IQ_TYPES, WA_NODE_TAGS, WA_XMLNS } from '../../protocol/nodes.js';
|
|
6
7
|
import { WA_GROUP_NOTIFICATION_TAGS } from '../../protocol/notification.js';
|
|
7
8
|
import { buildListParticipatingGroupsIq } from '../../transport/node/builders/account-sync.js';
|
|
@@ -87,7 +88,11 @@ function parseGroupCommons(target) {
|
|
|
87
88
|
? findNodeChild(membershipApprovalNode, WA_NODE_TAGS.GROUP_JOIN)
|
|
88
89
|
: undefined;
|
|
89
90
|
const addressingModeRaw = attrs.addressing_mode;
|
|
90
|
-
const addressingMode = addressingModeRaw ===
|
|
91
|
+
const addressingMode = addressingModeRaw === WA_ADDRESSING_MODES.LID
|
|
92
|
+
? WA_ADDRESSING_MODES.LID
|
|
93
|
+
: addressingModeRaw === WA_ADDRESSING_MODES.PN
|
|
94
|
+
? WA_ADDRESSING_MODES.PN
|
|
95
|
+
: undefined;
|
|
91
96
|
const rawJid = attrs.id ?? attrs.jid ?? '';
|
|
92
97
|
const jid = rawJid && !rawJid.includes('@') ? `${rawJid}@${WA_DEFAULTS.GROUP_SERVER}` : rawJid;
|
|
93
98
|
return {
|
|
@@ -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, WA_NACK_REASONS } from '../../protocol/constants.js';
|
|
14
|
+
import { WA_ADDRESSING_MODES, WA_DEFAULTS, WA_NACK_REASONS } from '../../protocol/constants.js';
|
|
15
15
|
import { canonicalizeOwnAccountJid, isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, isUserJid, 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';
|
|
@@ -200,6 +200,16 @@ export class WaMessageDispatchCoordinator {
|
|
|
200
200
|
}
|
|
201
201
|
: withViewOnce;
|
|
202
202
|
const upload = built.upload;
|
|
203
|
+
if (this.deps.emitMessageSend) {
|
|
204
|
+
try {
|
|
205
|
+
this.deps.emitMessageSend({ to: recipientJid, id: sendOptions.id, message });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
this.deps.logger.debug('message_send emit failed', {
|
|
209
|
+
message: toError(error).message
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
203
213
|
const messageWithOverride = options.messageSecret
|
|
204
214
|
? {
|
|
205
215
|
...message,
|
|
@@ -891,17 +901,17 @@ export class WaMessageDispatchCoordinator {
|
|
|
891
901
|
resolveGroupAddressingMode(participantUserJids, groupJid) {
|
|
892
902
|
for (let index = 0; index < participantUserJids.length; index += 1) {
|
|
893
903
|
if (isLidJid(participantUserJids[index])) {
|
|
894
|
-
return
|
|
904
|
+
return WA_ADDRESSING_MODES.LID;
|
|
895
905
|
}
|
|
896
906
|
}
|
|
897
907
|
this.deps.logger.trace('group addressing mode resolved to pn (default)', {
|
|
898
908
|
groupJid,
|
|
899
909
|
participants: participantUserJids.length
|
|
900
910
|
});
|
|
901
|
-
return
|
|
911
|
+
return WA_ADDRESSING_MODES.PN;
|
|
902
912
|
}
|
|
903
913
|
resolveSenderForAddressingMode(addressingMode, meJid) {
|
|
904
|
-
if (addressingMode ===
|
|
914
|
+
if (addressingMode === WA_ADDRESSING_MODES.LID) {
|
|
905
915
|
const meLid = this.deps.getCurrentCredentials()?.meLid;
|
|
906
916
|
if (meLid && meLid.includes('@')) {
|
|
907
917
|
try {
|