zapo-js 1.1.2 → 1.2.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/client/WaClient.js +1 -7
- package/dist/client/WaClientFactory.js +7 -4
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +16 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +43 -6
- package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
- package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +7 -0
- package/dist/client/coordinators/WaProfileCoordinator.js +10 -4
- package/dist/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/client/persistence/mailbox.d.ts +6 -0
- package/dist/client/persistence/mailbox.js +2 -2
- package/dist/client/types.d.ts +49 -0
- package/dist/esm/client/WaClient.js +1 -7
- package/dist/esm/client/WaClientFactory.js +8 -5
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +44 -7
- package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/esm/client/coordinators/WaProfileCoordinator.js +10 -4
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/esm/client/persistence/mailbox.js +2 -2
- package/dist/esm/message/primitives/incoming.js +99 -42
- package/dist/esm/protocol/jid.js +42 -0
- package/dist/esm/retry/reason.js +2 -2
- package/dist/esm/transport/node/WaNodeOrchestrator.js +6 -2
- package/dist/esm/transport/node/builders/message.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/index.d.ts +1 -1
- package/dist/message/primitives/incoming.d.ts +3 -1
- package/dist/message/primitives/incoming.js +98 -41
- package/dist/protocol/jid.d.ts +12 -0
- package/dist/protocol/jid.js +44 -0
- package/dist/retry/reason.js +2 -2
- package/dist/transport/node/WaNodeOrchestrator.js +6 -2
- package/dist/transport/node/builders/message.d.ts +1 -0
- package/dist/transport/node/builders/message.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
package/dist/client/WaClient.js
CHANGED
|
@@ -12,7 +12,6 @@ const _proto_1 = require("../proto");
|
|
|
12
12
|
const constants_1 = require("../protocol/constants");
|
|
13
13
|
const jid_1 = require("../protocol/jid");
|
|
14
14
|
const stream_1 = require("../protocol/stream");
|
|
15
|
-
const noop_store_1 = require("../store/noop.store");
|
|
16
15
|
const device_1 = require("../transport/node/builders/device");
|
|
17
16
|
const query_1 = require("../transport/node/query");
|
|
18
17
|
const wa_web_version_fetcher_1 = require("../transport/wa-web-version-fetcher");
|
|
@@ -91,12 +90,6 @@ class WaClient extends node_events_1.EventEmitter {
|
|
|
91
90
|
threadStore: this.stores.threads,
|
|
92
91
|
contactStore: this.stores.contacts
|
|
93
92
|
}, this.logger, this.options.writeBehind);
|
|
94
|
-
if (this.options.addons?.autoDecrypt !== false &&
|
|
95
|
-
this.stores.messageSecret === noop_store_1.NOOP_MESSAGE_SECRET_STORE) {
|
|
96
|
-
this.logger.warn('addons.autoDecrypt is on (default) but messageSecret cache is noop – ' +
|
|
97
|
-
'addon decryption will only work if secrets are in the message store. ' +
|
|
98
|
-
'Set addons.autoDecrypt: false to silence this warning.');
|
|
99
|
-
}
|
|
100
93
|
const dependencies = (0, WaClientFactory_1.buildWaClientDependencies)({
|
|
101
94
|
base,
|
|
102
95
|
runtime: {
|
|
@@ -212,6 +205,7 @@ class WaClient extends node_events_1.EventEmitter {
|
|
|
212
205
|
logger: this.logger,
|
|
213
206
|
writeBehind: this.writeBehind,
|
|
214
207
|
messageSecretStore: this.stores.messageSecret,
|
|
208
|
+
persistAllSecrets: this.options.addons?.persistAllSecrets === true,
|
|
215
209
|
event
|
|
216
210
|
});
|
|
217
211
|
if (this.options.addons?.autoDecrypt !== false && event.message) {
|
|
@@ -468,6 +468,7 @@ function buildWaClientDependencies(input) {
|
|
|
468
468
|
deviceListStore: sessionStore.deviceList,
|
|
469
469
|
signalDeviceSync,
|
|
470
470
|
messageSecretStore: sessionStore.messageSecret,
|
|
471
|
+
persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
|
|
471
472
|
getCurrentCredentials,
|
|
472
473
|
resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
|
|
473
474
|
onDirectMessageSent: (recipientJid) => {
|
|
@@ -487,7 +488,8 @@ function buildWaClientDependencies(input) {
|
|
|
487
488
|
});
|
|
488
489
|
const presenceCoordinator = (0, WaPresenceCoordinator_1.createPresenceCoordinator)({
|
|
489
490
|
sendNode: (node) => nodeOrchestrator.sendNode(node, false),
|
|
490
|
-
getCurrentCredentials
|
|
491
|
+
getCurrentCredentials,
|
|
492
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
|
|
491
493
|
});
|
|
492
494
|
const peerDataOperation = (0, peer_data_operation_1.createPeerDataOperationRequester)({
|
|
493
495
|
logger,
|
|
@@ -559,9 +561,7 @@ function buildWaClientDependencies(input) {
|
|
|
559
561
|
const credentials = getCurrentCredentials();
|
|
560
562
|
if (!credentials)
|
|
561
563
|
return false;
|
|
562
|
-
|
|
563
|
-
return ((!!credentials.meJid && (0, jid_1.toUserJid)(credentials.meJid) === candidateUser) ||
|
|
564
|
-
(!!credentials.meLid && (0, jid_1.toUserJid)(credentials.meLid) === candidateUser));
|
|
564
|
+
return (0, jid_1.isOwnAccountJid)(deviceJid, credentials.meJid, credentials.meLid);
|
|
565
565
|
},
|
|
566
566
|
sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
|
|
567
567
|
triggerSync: async () => {
|
|
@@ -604,6 +604,7 @@ function buildWaClientDependencies(input) {
|
|
|
604
604
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
605
605
|
mutations: appStateMutations,
|
|
606
606
|
applyOwnPushName,
|
|
607
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
|
|
607
608
|
logger
|
|
608
609
|
});
|
|
609
610
|
const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
|
|
@@ -684,6 +685,7 @@ function buildWaClientDependencies(input) {
|
|
|
684
685
|
logger,
|
|
685
686
|
sendNode: runtime.sendNode,
|
|
686
687
|
getMeJid: () => getCurrentCredentials()?.meJid,
|
|
688
|
+
getMeLid: () => getCurrentCredentials()?.meLid,
|
|
687
689
|
signalProtocol,
|
|
688
690
|
senderKeyManager,
|
|
689
691
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
@@ -693,6 +695,7 @@ function buildWaClientDependencies(input) {
|
|
|
693
695
|
.catch((err) => runtime.handleError((0, primitives_1.toError)(err)));
|
|
694
696
|
},
|
|
695
697
|
emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
|
|
698
|
+
emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
|
|
696
699
|
emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
|
|
697
700
|
};
|
|
698
701
|
const handleClientDirtyBits = (dirtyBits) => (0, dirty_1.handleDirtyBits)({
|
|
@@ -45,6 +45,12 @@ interface WaMessageDispatchCoordinatorOptions {
|
|
|
45
45
|
readonly deviceListStore: WaDeviceListStore;
|
|
46
46
|
readonly signalDeviceSync: SignalDeviceSyncApi;
|
|
47
47
|
readonly messageSecretStore: WaMessageSecretStore;
|
|
48
|
+
/**
|
|
49
|
+
* When `true`, persist the message secret for every outgoing message, not
|
|
50
|
+
* only the poll / event / bot prompts that `needsSecretPersistence` flags.
|
|
51
|
+
* Wired from the client-level `addons.persistAllSecrets` option.
|
|
52
|
+
*/
|
|
53
|
+
readonly persistAllMessageSecrets?: boolean;
|
|
48
54
|
readonly getCurrentCredentials: () => WaAuthCredentials | null;
|
|
49
55
|
readonly resolvePrivacyTokenNode: (recipientJid: string) => Promise<BinaryNode | null>;
|
|
50
56
|
readonly onDirectMessageSent: (recipientJid: string) => void;
|
|
@@ -79,6 +85,16 @@ export declare class WaMessageDispatchCoordinator {
|
|
|
79
85
|
* no LID is known/resolvable. Inputs already in LID form pass through.
|
|
80
86
|
*/
|
|
81
87
|
private resolveDirectRecipientLid;
|
|
88
|
+
/**
|
|
89
|
+
* Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
|
|
90
|
+
* `undefined` to drop the attribute. Only set when the envelope is
|
|
91
|
+
* LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
|
|
92
|
+
* JID when they passed a PN (zapo switched it to LID for sending), else the
|
|
93
|
+
* device-list snapshot counterpart for a recipient passed in LID form.
|
|
94
|
+
* Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
|
|
95
|
+
* LID destination.
|
|
96
|
+
*/
|
|
97
|
+
private resolvePeerRecipientPn;
|
|
82
98
|
syncSignalSession(jid: string, reasonIdentity?: boolean): Promise<void>;
|
|
83
99
|
sendReceipt(input: WaSendReceiptInput): Promise<void>;
|
|
84
100
|
publishProtocolMessageToDevice(deviceJid: string, protocolMessage: Proto.Message.IProtocolMessage, 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';
|
|
@@ -214,7 +217,7 @@ class WaMessageDispatchCoordinator {
|
|
|
214
217
|
if (rawSecret &&
|
|
215
218
|
rawSecret.length > 0 &&
|
|
216
219
|
sendOptions.id &&
|
|
217
|
-
(0, content_1.needsSecretPersistence)(messageWithSecret)) {
|
|
220
|
+
(this.deps.persistAllMessageSecrets || (0, content_1.needsSecretPersistence)(messageWithSecret))) {
|
|
218
221
|
const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
|
|
219
222
|
void this.deps.messageSecretStore
|
|
220
223
|
.set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
|
|
@@ -275,11 +278,14 @@ class WaMessageDispatchCoordinator {
|
|
|
275
278
|
const directRecipientJid = isGroup
|
|
276
279
|
? recipientJid
|
|
277
280
|
: await this.resolveDirectRecipientLid((0, jid_1.toUserJid)(recipientJid));
|
|
281
|
+
const peerRecipientPn = isGroup
|
|
282
|
+
? undefined
|
|
283
|
+
: await this.resolvePeerRecipientPn((0, jid_1.toUserJid)(recipientJid), directRecipientJid);
|
|
278
284
|
const publishResult = isGroup
|
|
279
285
|
? this.shouldUseGroupDirectPath(messageWithIcdc)
|
|
280
286
|
? await this.publishGroupDirectMessage(recipientJid, envelope)
|
|
281
287
|
: await this.publishGroupSenderKeyMessage(recipientJid, envelope)
|
|
282
|
-
: await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope);
|
|
288
|
+
: await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope, peerRecipientPn);
|
|
283
289
|
return upload ? { ...publishResult, upload } : publishResult;
|
|
284
290
|
}
|
|
285
291
|
/**
|
|
@@ -314,6 +320,35 @@ class WaMessageDispatchCoordinator {
|
|
|
314
320
|
}
|
|
315
321
|
return pnUserJid;
|
|
316
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
|
|
325
|
+
* `undefined` to drop the attribute. Only set when the envelope is
|
|
326
|
+
* LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
|
|
327
|
+
* JID when they passed a PN (zapo switched it to LID for sending), else the
|
|
328
|
+
* device-list snapshot counterpart for a recipient passed in LID form.
|
|
329
|
+
* Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
|
|
330
|
+
* LID destination.
|
|
331
|
+
*/
|
|
332
|
+
async resolvePeerRecipientPn(recipientUserJid, directRecipientJid) {
|
|
333
|
+
if (!(0, jid_1.isLidJid)(directRecipientJid))
|
|
334
|
+
return undefined;
|
|
335
|
+
if ((0, jid_1.isUserJid)(recipientUserJid))
|
|
336
|
+
return recipientUserJid;
|
|
337
|
+
try {
|
|
338
|
+
const snapshot = await this.deps.deviceListStore.findByAnyUserJid(directRecipientJid);
|
|
339
|
+
if (snapshot?.userJid && (0, jid_1.isUserJid)(snapshot.userJid))
|
|
340
|
+
return snapshot.userJid;
|
|
341
|
+
if (snapshot?.altUserJid && (0, jid_1.isUserJid)(snapshot.altUserJid))
|
|
342
|
+
return snapshot.altUserJid;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
this.deps.logger.trace('peer_recipient_pn store lookup failed', {
|
|
346
|
+
lid: directRecipientJid,
|
|
347
|
+
message: (0, primitives_2.toError)(error).message
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
317
352
|
async syncSignalSession(jid, reasonIdentity = false) {
|
|
318
353
|
const address = (0, jid_1.parseSignalAddressFromJid)(jid);
|
|
319
354
|
if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
|
|
@@ -325,7 +360,8 @@ class WaMessageDispatchCoordinator {
|
|
|
325
360
|
await this.deps.messageClient.sendReceipt(input);
|
|
326
361
|
}
|
|
327
362
|
async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
|
|
328
|
-
const
|
|
363
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
364
|
+
const meJid = credentials?.meJid;
|
|
329
365
|
const meParsed = meJid ? (0, jid_1.parseJidFull)(meJid) : undefined;
|
|
330
366
|
const meUserJid = meParsed?.userJid;
|
|
331
367
|
let senderIcdc = null;
|
|
@@ -993,7 +1029,7 @@ class WaMessageDispatchCoordinator {
|
|
|
993
1029
|
distributionParticipants
|
|
994
1030
|
};
|
|
995
1031
|
}
|
|
996
|
-
async publishDirectSignalMessageWithFanout(recipientJid, envelope) {
|
|
1032
|
+
async publishDirectSignalMessageWithFanout(recipientJid, envelope, peerRecipientPn) {
|
|
997
1033
|
const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
|
|
998
1034
|
const meJid = this.requireCurrentMeJid('sendMessage');
|
|
999
1035
|
const meLid = this.deps.getCurrentCredentials()?.meLid;
|
|
@@ -1156,6 +1192,7 @@ class WaMessageDispatchCoordinator {
|
|
|
1156
1192
|
customNodes: customNodes.length > 0 ? customNodes : undefined,
|
|
1157
1193
|
mediatype,
|
|
1158
1194
|
decryptFail: envelope.decryptFail,
|
|
1195
|
+
peerRecipientPn,
|
|
1159
1196
|
additionalAttributes: sendOptions.additionalAttributes
|
|
1160
1197
|
});
|
|
1161
1198
|
const replayPayload = {
|
|
@@ -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
|
}
|
|
@@ -149,6 +149,13 @@ interface WaProfileCoordinatorOptions {
|
|
|
149
149
|
* idempotent (a no-op when the name already matches).
|
|
150
150
|
*/
|
|
151
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>;
|
|
152
159
|
readonly logger: Logger;
|
|
153
160
|
}
|
|
154
161
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|
|
@@ -195,10 +195,11 @@ 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, applyOwnPushName, logger } = options;
|
|
198
|
+
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, resolvePrivacyTokenNode, logger } = options;
|
|
199
199
|
return {
|
|
200
200
|
getProfilePicture: async (jid, type, existingId) => {
|
|
201
|
-
const
|
|
201
|
+
const privacyTokenNode = (await resolvePrivacyTokenNode(jid)) ?? undefined;
|
|
202
|
+
const node = (0, profile_1.buildGetProfilePictureIq)(jid, type, existingId, privacyTokenNode);
|
|
202
203
|
const result = await queryWithContext('profile.getPicture', node, undefined, {
|
|
203
204
|
jid,
|
|
204
205
|
type: type ?? 'preview'
|
|
@@ -225,10 +226,11 @@ function createProfileCoordinator(options) {
|
|
|
225
226
|
getStatus: async (jid) => {
|
|
226
227
|
const sid = await generateSid();
|
|
227
228
|
const queryNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
|
|
229
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
228
230
|
const usyncNode = (0, usync_1.buildUsyncIq)({
|
|
229
231
|
sid,
|
|
230
232
|
queryProtocolNodes: [queryNodes[1]],
|
|
231
|
-
users: [{ jid }]
|
|
233
|
+
users: [{ jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) }]
|
|
232
234
|
});
|
|
233
235
|
const result = await queryWithContext('profile.getStatus', usyncNode, undefined, {
|
|
234
236
|
jid
|
|
@@ -256,10 +258,14 @@ function createProfileCoordinator(options) {
|
|
|
256
258
|
}
|
|
257
259
|
const sid = await generateSid();
|
|
258
260
|
const queryProtocolNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
|
|
261
|
+
const users = await Promise.all(jids.map(async (jid) => {
|
|
262
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
263
|
+
return { jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) };
|
|
264
|
+
}));
|
|
259
265
|
const usyncNode = (0, usync_1.buildUsyncIq)({
|
|
260
266
|
sid,
|
|
261
267
|
queryProtocolNodes,
|
|
262
|
-
users
|
|
268
|
+
users
|
|
263
269
|
});
|
|
264
270
|
const result = await queryWithContext('profile.getProfiles', usyncNode, undefined, {
|
|
265
271
|
count: jids.length
|
|
@@ -502,7 +502,7 @@ class WaRetryCoordinator {
|
|
|
502
502
|
id: request.keyBundle.key.id,
|
|
503
503
|
publicKey: request.keyBundle.key.publicKey
|
|
504
504
|
}
|
|
505
|
-
});
|
|
505
|
+
}, { reuseExisting: true });
|
|
506
506
|
return this.applySessionBaseKeyPolicy(request, requesterJid, requesterAddress, requesterNormalizedDeviceJid);
|
|
507
507
|
}
|
|
508
508
|
const sessionStillExists = currentSession !== null && !regIdMismatch;
|
|
@@ -41,6 +41,15 @@ export declare class WaTrustedContactTokenCoordinator {
|
|
|
41
41
|
readonly getConfigOverrides?: () => Partial<WaTrustedContactTokenConfig>;
|
|
42
42
|
});
|
|
43
43
|
private resolveConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the receiver-mode `<tctoken>` node to echo back on outbound
|
|
46
|
+
* queries against a contact's privacy-gated data (presence subscribe,
|
|
47
|
+
* profile-picture get, about/status usync). Returns the token only when a
|
|
48
|
+
* non-expired receiver token exists for `jid`; unlike
|
|
49
|
+
* {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
|
|
50
|
+
* (the gated query flows attach the trusted-contact token only).
|
|
51
|
+
*/
|
|
52
|
+
resolveReceiverTokenNode(jid: string): Promise<BinaryNode | null>;
|
|
44
53
|
resolveTokenForMessage(recipientJid: string): Promise<BinaryNode | null>;
|
|
45
54
|
handleIncomingToken(fromJid: string, tokens: readonly ParsedPrivacyToken[]): Promise<void>;
|
|
46
55
|
maybeIssueSenderToken(recipientJid: string): Promise<void>;
|
|
@@ -42,14 +42,30 @@ class WaTrustedContactTokenCoordinator {
|
|
|
42
42
|
maxDurationS
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the receiver-mode `<tctoken>` node to echo back on outbound
|
|
47
|
+
* queries against a contact's privacy-gated data (presence subscribe,
|
|
48
|
+
* profile-picture get, about/status usync). Returns the token only when a
|
|
49
|
+
* non-expired receiver token exists for `jid`; unlike
|
|
50
|
+
* {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
|
|
51
|
+
* (the gated query flows attach the trusted-contact token only).
|
|
52
|
+
*/
|
|
53
|
+
async resolveReceiverTokenNode(jid) {
|
|
54
|
+
const record = await this.store.getByJid(jid);
|
|
55
|
+
if (!record?.tcToken || record.tcTokenTimestamp === undefined) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
48
58
|
const config = this.resolveConfig();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const nowS = this.serverClock.nowSeconds();
|
|
60
|
+
if ((0, tc_token_1.isTokenExpired)(record.tcTokenTimestamp, nowS, config.durationS, config.numBuckets)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return (0, privacy_token_2.buildTcTokenMessageNode)(record.tcToken);
|
|
64
|
+
}
|
|
65
|
+
async resolveTokenForMessage(recipientJid) {
|
|
66
|
+
const tcTokenNode = await this.resolveReceiverTokenNode(recipientJid);
|
|
67
|
+
if (tcTokenNode) {
|
|
68
|
+
return tcTokenNode;
|
|
53
69
|
}
|
|
54
70
|
const nctSalt = await this.getNctSalt();
|
|
55
71
|
if (!nctSalt) {
|
|
@@ -6,6 +6,12 @@ interface WaPersistIncomingMailboxOptions {
|
|
|
6
6
|
readonly logger: Logger;
|
|
7
7
|
readonly writeBehind: WriteBehindPersistence;
|
|
8
8
|
readonly messageSecretStore: WaMessageSecretStore;
|
|
9
|
+
/**
|
|
10
|
+
* When `true`, persist the secret of every incoming message, not only the
|
|
11
|
+
* poll / event / bot prompts that `needsSecretPersistence` flags. Wired
|
|
12
|
+
* from the client-level `addons.persistAllSecrets` option.
|
|
13
|
+
*/
|
|
14
|
+
readonly persistAllSecrets?: boolean;
|
|
9
15
|
readonly event: WaIncomingMessageEvent;
|
|
10
16
|
}
|
|
11
17
|
export declare function persistIncomingMailboxEntities(options: WaPersistIncomingMailboxOptions): void;
|
|
@@ -56,7 +56,7 @@ function persistContacts(writeBehind, event, nowMs) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function persistIncomingMailboxEntities(options) {
|
|
59
|
-
const { logger, writeBehind, messageSecretStore, event } = options;
|
|
59
|
+
const { logger, writeBehind, messageSecretStore, persistAllSecrets, event } = options;
|
|
60
60
|
const stanzaId = event.key.id;
|
|
61
61
|
const chatJid = event.key.remoteJid;
|
|
62
62
|
if (!stanzaId || !chatJid) {
|
|
@@ -81,7 +81,7 @@ function persistIncomingMailboxEntities(options) {
|
|
|
81
81
|
if (rawSecret &&
|
|
82
82
|
rawSecret.length > 0 &&
|
|
83
83
|
event.message &&
|
|
84
|
-
(0, content_1.needsSecretPersistence)(event.message)) {
|
|
84
|
+
(persistAllSecrets || (0, content_1.needsSecretPersistence)(event.message))) {
|
|
85
85
|
const rawSender = event.key.participant ?? event.rawNode.attrs.participant ?? event.key.remoteJid;
|
|
86
86
|
const senderJid = rawSender ? (0, jid_1.toUserJid)(rawSender) : '';
|
|
87
87
|
void messageSecretStore
|
package/dist/client/types.d.ts
CHANGED
|
@@ -241,6 +241,26 @@ export interface WaAddonOptions {
|
|
|
241
241
|
* poll votes, event responses, comments).
|
|
242
242
|
*/
|
|
243
243
|
readonly autoDecrypt?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Persist the 32-byte message secret of every sent and received message,
|
|
246
|
+
* not just the poll / event / bot-prompt messages the library knows will
|
|
247
|
+
* get a follow-up. Off by default.
|
|
248
|
+
*
|
|
249
|
+
* Encrypted addons whose parent can be any message type - reactions,
|
|
250
|
+
* comments, and `secretEncryptedMessage` edits - need the parent's secret
|
|
251
|
+
* to decrypt. Without this, those parents stay decryptable after a restart
|
|
252
|
+
* only when the full `messages` archive is persistent. Enable this to keep
|
|
253
|
+
* them decryptable while storing only the secret, not the message body
|
|
254
|
+
* (e.g. with `messages: 'none'`).
|
|
255
|
+
*
|
|
256
|
+
* Has no effect when the `messageSecret` cache is `'none'`: every write
|
|
257
|
+
* lands in the noop store and is silently discarded. With the default
|
|
258
|
+
* `'memory'` provider it works for the lifetime of the process but is lost
|
|
259
|
+
* on restart and bounded by the cache's LRU and `messageSecretMs` TTL;
|
|
260
|
+
* point `messageSecret` at a persistent backend to keep the secrets across
|
|
261
|
+
* restarts.
|
|
262
|
+
*/
|
|
263
|
+
readonly persistAllSecrets?: boolean;
|
|
244
264
|
}
|
|
245
265
|
export interface WaPrivacyTokenOptions {
|
|
246
266
|
readonly tcTokenDurationS?: number;
|
|
@@ -721,6 +741,27 @@ export interface WaIncomingUnhandledStanzaEvent extends WaIncomingBaseEvent {
|
|
|
721
741
|
/** Short reason describing why the dispatcher did not match a typed handler. */
|
|
722
742
|
readonly reason: string;
|
|
723
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Why an incoming message arrived as a content-less placeholder. `view_once`:
|
|
746
|
+
* a view-once already consumed elsewhere. `hosted`: a hosted/bot message that
|
|
747
|
+
* could not be fanned out. `other`: an `unavailable` marker the lib does not
|
|
748
|
+
* categorize yet.
|
|
749
|
+
*/
|
|
750
|
+
export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'other';
|
|
751
|
+
export interface WaIncomingUnavailableMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJid' | 'stanzaId'> {
|
|
752
|
+
/** Which flavour of content the server signalled as unavailable. */
|
|
753
|
+
readonly kind: WaUnavailableMessageKind;
|
|
754
|
+
/**
|
|
755
|
+
* The message key (chat, stanza id, author, addressing metadata) – same
|
|
756
|
+
* shape the `message` event carries, so it can be stored or correlated. There
|
|
757
|
+
* is no decrypted `message`: the payload is unavailable and cannot be fetched.
|
|
758
|
+
*/
|
|
759
|
+
readonly key: WaIncomingMessageKey;
|
|
760
|
+
/** Stanza `t` attr (seconds since epoch). */
|
|
761
|
+
readonly timestampSeconds?: number;
|
|
762
|
+
/** Sender's display name from the stanza's `notify` attr. */
|
|
763
|
+
readonly pushName?: string;
|
|
764
|
+
}
|
|
724
765
|
export interface WaIncomingErrorStanzaEvent extends WaIncomingBaseEvent {
|
|
725
766
|
readonly code?: number;
|
|
726
767
|
readonly text?: string;
|
|
@@ -1057,6 +1098,14 @@ export interface WaClientEventMap {
|
|
|
1057
1098
|
* typed protocol payload directly.
|
|
1058
1099
|
*/
|
|
1059
1100
|
readonly message_protocol: (event: WaIncomingProtocolMessageEvent) => void;
|
|
1101
|
+
/**
|
|
1102
|
+
* A message the server delivered as a content-less placeholder: the payload
|
|
1103
|
+
* is unavailable and cannot be recovered (a view-once already consumed, or a
|
|
1104
|
+
* hosted/bot message that could not be fanned out). The lib acks it and emits
|
|
1105
|
+
* this instead of a `message` event for the same stanza. Branch on
|
|
1106
|
+
* `event.kind`.
|
|
1107
|
+
*/
|
|
1108
|
+
readonly message_unavailable: (event: WaIncomingUnavailableMessageEvent) => void;
|
|
1060
1109
|
/**
|
|
1061
1110
|
* Inbound `<receipt>` for an outgoing message – delivery, read, played,
|
|
1062
1111
|
* server, etc. Use this to track message ACK progression.
|
|
@@ -9,7 +9,6 @@ import { proto } from '../proto.js';
|
|
|
9
9
|
import { WA_DEFAULTS, WA_MESSAGE_TYPES } from '../protocol/constants.js';
|
|
10
10
|
import { normalizeDeviceJid } from '../protocol/jid.js';
|
|
11
11
|
import { WA_DISCONNECT_REASONS, WA_LOGOUT_REASONS } from '../protocol/stream.js';
|
|
12
|
-
import { NOOP_MESSAGE_SECRET_STORE } from '../store/noop.store.js';
|
|
13
12
|
import { buildRemoveCompanionDeviceIq } from '../transport/node/builders/device.js';
|
|
14
13
|
import { assertIqResult, queryWithContext as queryNodeWithContext } from '../transport/node/query.js';
|
|
15
14
|
import { fetchLatestWaWebVersion } from '../transport/wa-web-version-fetcher.js';
|
|
@@ -88,12 +87,6 @@ export class WaClient extends EventEmitter {
|
|
|
88
87
|
threadStore: this.stores.threads,
|
|
89
88
|
contactStore: this.stores.contacts
|
|
90
89
|
}, this.logger, this.options.writeBehind);
|
|
91
|
-
if (this.options.addons?.autoDecrypt !== false &&
|
|
92
|
-
this.stores.messageSecret === NOOP_MESSAGE_SECRET_STORE) {
|
|
93
|
-
this.logger.warn('addons.autoDecrypt is on (default) but messageSecret cache is noop – ' +
|
|
94
|
-
'addon decryption will only work if secrets are in the message store. ' +
|
|
95
|
-
'Set addons.autoDecrypt: false to silence this warning.');
|
|
96
|
-
}
|
|
97
90
|
const dependencies = buildWaClientDependencies({
|
|
98
91
|
base,
|
|
99
92
|
runtime: {
|
|
@@ -209,6 +202,7 @@ export class WaClient extends EventEmitter {
|
|
|
209
202
|
logger: this.logger,
|
|
210
203
|
writeBehind: this.writeBehind,
|
|
211
204
|
messageSecretStore: this.stores.messageSecret,
|
|
205
|
+
persistAllSecrets: this.options.addons?.persistAllSecrets === true,
|
|
212
206
|
event
|
|
213
207
|
});
|
|
214
208
|
if (this.options.addons?.autoDecrypt !== false && event.message) {
|
|
@@ -39,7 +39,7 @@ import { handleIncomingMessageAck } from '../message/primitives/incoming.js';
|
|
|
39
39
|
import { createPeerDataOperationRequester } from '../message/primitives/peer-data-operation.js';
|
|
40
40
|
import { WaMessageClient } from '../message/WaMessageClient.js';
|
|
41
41
|
import { getWaCompanionPlatformId, 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
|
-
import { isNewsletterJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
|
|
42
|
+
import { isNewsletterJid, isOwnAccountJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
|
|
43
43
|
import { WA_PRESENCE_TYPES } from '../protocol/presence.js';
|
|
44
44
|
import { createOutboundRetryTracker } from '../retry/tracker.js';
|
|
45
45
|
import { SignalDeviceSyncApi } from '../signal/api/SignalDeviceSyncApi.js';
|
|
@@ -464,6 +464,7 @@ export function buildWaClientDependencies(input) {
|
|
|
464
464
|
deviceListStore: sessionStore.deviceList,
|
|
465
465
|
signalDeviceSync,
|
|
466
466
|
messageSecretStore: sessionStore.messageSecret,
|
|
467
|
+
persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
|
|
467
468
|
getCurrentCredentials,
|
|
468
469
|
resolvePrivacyTokenNode: (recipientJid) => trustedContactToken.resolveTokenForMessage(recipientJid),
|
|
469
470
|
onDirectMessageSent: (recipientJid) => {
|
|
@@ -483,7 +484,8 @@ export function buildWaClientDependencies(input) {
|
|
|
483
484
|
});
|
|
484
485
|
const presenceCoordinator = createPresenceCoordinator({
|
|
485
486
|
sendNode: (node) => nodeOrchestrator.sendNode(node, false),
|
|
486
|
-
getCurrentCredentials
|
|
487
|
+
getCurrentCredentials,
|
|
488
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
|
|
487
489
|
});
|
|
488
490
|
const peerDataOperation = createPeerDataOperationRequester({
|
|
489
491
|
logger,
|
|
@@ -555,9 +557,7 @@ export function buildWaClientDependencies(input) {
|
|
|
555
557
|
const credentials = getCurrentCredentials();
|
|
556
558
|
if (!credentials)
|
|
557
559
|
return false;
|
|
558
|
-
|
|
559
|
-
return ((!!credentials.meJid && toUserJid(credentials.meJid) === candidateUser) ||
|
|
560
|
-
(!!credentials.meLid && toUserJid(credentials.meLid) === candidateUser));
|
|
560
|
+
return isOwnAccountJid(deviceJid, credentials.meJid, credentials.meLid);
|
|
561
561
|
},
|
|
562
562
|
sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
|
|
563
563
|
triggerSync: async () => {
|
|
@@ -600,6 +600,7 @@ export function buildWaClientDependencies(input) {
|
|
|
600
600
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
601
601
|
mutations: appStateMutations,
|
|
602
602
|
applyOwnPushName,
|
|
603
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
|
|
603
604
|
logger
|
|
604
605
|
});
|
|
605
606
|
const statusCoordinator = createStatusCoordinator({
|
|
@@ -680,6 +681,7 @@ export function buildWaClientDependencies(input) {
|
|
|
680
681
|
logger,
|
|
681
682
|
sendNode: runtime.sendNode,
|
|
682
683
|
getMeJid: () => getCurrentCredentials()?.meJid,
|
|
684
|
+
getMeLid: () => getCurrentCredentials()?.meLid,
|
|
683
685
|
signalProtocol,
|
|
684
686
|
senderKeyManager,
|
|
685
687
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
@@ -689,6 +691,7 @@ export function buildWaClientDependencies(input) {
|
|
|
689
691
|
.catch((err) => runtime.handleError(toError(err)));
|
|
690
692
|
},
|
|
691
693
|
emitNewsletterMessageUpdate: (event) => runtime.emitEvent('newsletter_message_update', event),
|
|
694
|
+
emitUnavailableMessage: (event) => runtime.emitEvent('message_unavailable', event),
|
|
692
695
|
emitUnhandledStanza: (event) => runtime.emitEvent('debug_unhandled_stanza', event)
|
|
693
696
|
};
|
|
694
697
|
const handleClientDirtyBits = (dirtyBits) => handleDirtyBits({
|