zapo-js 1.3.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 +25 -0
- package/dist/auth/WaAuthClient.js +63 -7
- package/dist/auth/credentials-flow.d.ts +8 -0
- package/dist/auth/credentials-flow.js +35 -2
- package/dist/auth/pairing/WaShortcakeFlow.d.ts +93 -0
- package/dist/auth/pairing/WaShortcakeFlow.js +210 -0
- package/dist/auth/pairing/companion-host.d.ts +105 -0
- package/dist/auth/pairing/companion-host.js +148 -0
- package/dist/auth/pairing/pairing-code-crypto.d.ts +1 -0
- package/dist/auth/pairing/pairing-code-crypto.js +3 -2
- package/dist/auth/pairing/shortcake-crypto.d.ts +59 -0
- package/dist/auth/pairing/shortcake-crypto.js +106 -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 +27 -7
- 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/coordinators/WaRetryCoordinator.js +1 -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 +90 -7
- package/dist/esm/auth/WaAuthClient.js +64 -8
- package/dist/esm/auth/credentials-flow.js +36 -3
- package/dist/esm/auth/pairing/WaShortcakeFlow.js +206 -0
- package/dist/esm/auth/pairing/companion-host.js +140 -0
- package/dist/esm/auth/pairing/pairing-code-crypto.js +2 -2
- package/dist/esm/auth/pairing/shortcake-crypto.js +99 -0
- package/dist/esm/client/WaClient.js +46 -12
- package/dist/esm/client/WaClientFactory.js +28 -8
- 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/coordinators/WaRetryCoordinator.js +1 -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 +17 -1
- package/dist/esm/protocol/notification.js +3 -1
- package/dist/esm/retry/replay.js +7 -15
- 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/node/builders/shortcake.js +50 -0
- package/dist/esm/transport/noise/WaClientPayload.js +17 -8
- package/dist/esm/transport/wa-version-fetcher.js +152 -0
- package/dist/esm/util/index.js +1 -0
- package/dist/index.d.ts +11 -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 +16 -0
- package/dist/protocol/nodes.js +17 -1
- package/dist/protocol/notification.d.ts +2 -0
- package/dist/protocol/notification.js +3 -1
- package/dist/retry/replay.d.ts +1 -0
- package/dist/retry/replay.js +6 -14
- 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/node/builders/shortcake.d.ts +19 -0
- package/dist/transport/node/builders/shortcake.js +57 -0
- package/dist/transport/noise/WaClientPayload.d.ts +2 -0
- package/dist/transport/noise/WaClientPayload.js +17 -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
package/dist/client/WaClient.js
CHANGED
|
@@ -15,7 +15,7 @@ const jid_1 = require("../protocol/jid");
|
|
|
15
15
|
const stream_1 = require("../protocol/stream");
|
|
16
16
|
const device_1 = require("../transport/node/builders/device");
|
|
17
17
|
const query_1 = require("../transport/node/query");
|
|
18
|
-
const
|
|
18
|
+
const wa_version_fetcher_1 = require("../transport/wa-version-fetcher");
|
|
19
19
|
const primitives_1 = require("../util/primitives");
|
|
20
20
|
const SYNC_RELATED_PROTOCOL_TYPES = new Set([
|
|
21
21
|
_proto_1.proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_REQUEST,
|
|
@@ -73,13 +73,14 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
73
73
|
this.deps = dependencies;
|
|
74
74
|
this.appStateSync = dependencies.appStateSync;
|
|
75
75
|
this.mediaTransfer = dependencies.mediaTransfer;
|
|
76
|
-
this.
|
|
76
|
+
this.pluginInstallInput = {
|
|
77
77
|
options: this.options,
|
|
78
78
|
logger: this.logger,
|
|
79
79
|
stores: this.stores,
|
|
80
80
|
deps: this.deps,
|
|
81
81
|
queryWithContext: this.queryWithContext.bind(this)
|
|
82
|
-
}
|
|
82
|
+
};
|
|
83
|
+
this.installPlugins();
|
|
83
84
|
this.bindNodeTransportEvents();
|
|
84
85
|
this.on('connection', (event) => {
|
|
85
86
|
if (event.status !== 'close')
|
|
@@ -88,22 +89,43 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
88
89
|
return;
|
|
89
90
|
if (event.reason !== stream_1.WA_DISCONNECT_REASONS.FAILURE_CLIENT_TOO_OLD)
|
|
90
91
|
return;
|
|
91
|
-
this.logger.warn('wa rejected the connect with client_too_old: the zapo default WA
|
|
92
|
-
'Auto-recovering by fetching the current version from
|
|
92
|
+
this.logger.warn('wa rejected the connect with client_too_old: the zapo default WA version is outdated. ' +
|
|
93
|
+
'Auto-recovering by fetching the current version from the public source – ' +
|
|
93
94
|
'please upgrade zapo so the shipped default catches up.');
|
|
94
95
|
void this.runClientTooOldRecover();
|
|
95
96
|
});
|
|
97
|
+
this.on('connection', (event) => {
|
|
98
|
+
if (event.status !== 'open')
|
|
99
|
+
return;
|
|
100
|
+
void this.deps.mobileCoordinator.reconcileCompanions().catch((error) => {
|
|
101
|
+
this.logger.debug('companion reconcile on connect failed', {
|
|
102
|
+
message: (0, primitives_1.toError)(error).message
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
installPlugins() {
|
|
108
|
+
this.disposePlugins = (0, install_1.installWaClientPlugins)(this, this.pluginInstallInput, this.options.plugins ?? []);
|
|
96
109
|
}
|
|
97
110
|
async runClientTooOldRecover() {
|
|
98
111
|
try {
|
|
99
112
|
if (this.connectPromise) {
|
|
100
113
|
await this.connectPromise.catch(() => undefined);
|
|
101
114
|
}
|
|
102
|
-
const
|
|
115
|
+
const mobile = this.deps.isMobilePrimary();
|
|
116
|
+
const latest = mobile
|
|
117
|
+
? await (0, wa_version_fetcher_1.fetchLatestWaMobileVersion)()
|
|
118
|
+
: await (0, wa_version_fetcher_1.fetchLatestWaWebVersion)();
|
|
103
119
|
this.logger.info('client_too_old auto-recover: reconnecting', {
|
|
120
|
+
transport: mobile ? 'mobile' : 'web',
|
|
104
121
|
version: latest.version
|
|
105
122
|
});
|
|
106
|
-
|
|
123
|
+
if (mobile) {
|
|
124
|
+
this.deps.authClient.setNextConnectMobileAppVersion(latest.version);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.deps.authClient.setNextConnectVersion(latest.version);
|
|
128
|
+
}
|
|
107
129
|
await this.connect();
|
|
108
130
|
}
|
|
109
131
|
catch (error) {
|
|
@@ -309,6 +331,9 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
309
331
|
this.logger.trace('wa client connect already in-flight');
|
|
310
332
|
return this.connectPromise;
|
|
311
333
|
}
|
|
334
|
+
if (!this.disposePlugins) {
|
|
335
|
+
this.installPlugins();
|
|
336
|
+
}
|
|
312
337
|
this.writeBehind.restart();
|
|
313
338
|
this.acceptingIncomingEvents = true;
|
|
314
339
|
this.connectPromise = this.deps.connectionManager
|
|
@@ -332,11 +357,12 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
332
357
|
}
|
|
333
358
|
/**
|
|
334
359
|
* Closes the transport gracefully: pauses incoming events, flushes the
|
|
335
|
-
* write-behind persistence queue, and emits a
|
|
336
|
-
* with reason `client_disconnected`. Does not
|
|
337
|
-
* call {@link connect} again to resume the same
|
|
338
|
-
*
|
|
339
|
-
*
|
|
360
|
+
* write-behind persistence queue, disposes installed plugins, and emits a
|
|
361
|
+
* `connection` close event with reason `client_disconnected`. Does not
|
|
362
|
+
* clear stored credentials - call {@link connect} again to resume the same
|
|
363
|
+
* session, which reinstalls the plugins. There is no built-in
|
|
364
|
+
* auto-reconnect; subscribe to `connection: { status: 'close' }` and decide
|
|
365
|
+
* your own backoff.
|
|
340
366
|
*/
|
|
341
367
|
async disconnect() {
|
|
342
368
|
await this.pauseIncomingEventsAndWaitDrain();
|
|
@@ -453,6 +479,14 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
453
479
|
get email() {
|
|
454
480
|
return this.deps.emailCoordinator;
|
|
455
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Mobile coordinator: host and manage companion devices linked to this
|
|
484
|
+
* mobile-primary account (link via QR or pairing code, list, revoke).
|
|
485
|
+
* Requires a registered primary session. See {@link WaMobileCoordinator}.
|
|
486
|
+
*/
|
|
487
|
+
get mobile() {
|
|
488
|
+
return this.deps.mobileCoordinator;
|
|
489
|
+
}
|
|
456
490
|
/**
|
|
457
491
|
* Unpairs this companion device by removing it server-side. Requires an
|
|
458
492
|
* authenticated session; throws when no `meJid` is present.
|
|
@@ -14,6 +14,7 @@ import { WaIncomingNodeCoordinator } from './coordinators/WaIncomingNodeCoordina
|
|
|
14
14
|
import { type WaLowLevelCoordinator } from './coordinators/WaLowLevelCoordinator';
|
|
15
15
|
import { WaMessageCoordinator } from './coordinators/WaMessageCoordinator';
|
|
16
16
|
import { WaMessageDispatchCoordinator } from './coordinators/WaMessageDispatchCoordinator';
|
|
17
|
+
import { WaMobileCoordinator } from './coordinators/WaMobileCoordinator';
|
|
17
18
|
import { type WaNewsletterCoordinator } from './coordinators/WaNewsletterCoordinator';
|
|
18
19
|
import { WaPassiveTasksCoordinator } from './coordinators/WaPassiveTasksCoordinator';
|
|
19
20
|
import { type WaPresenceCoordinator } from './coordinators/WaPresenceCoordinator';
|
|
@@ -119,6 +120,8 @@ export interface WaClientDependencies {
|
|
|
119
120
|
readonly trustedContactToken: WaTrustedContactTokenCoordinator;
|
|
120
121
|
readonly abPropsCoordinator: WaAbPropsCoordinator;
|
|
121
122
|
readonly peerDataOperation: PeerDataOperationRequester;
|
|
123
|
+
readonly mobileCoordinator: WaMobileCoordinator;
|
|
124
|
+
readonly isMobilePrimary: () => boolean;
|
|
122
125
|
}
|
|
123
126
|
export declare function resolveWaClientBase(options: WaClientOptions, logger: Logger): WaClientBase;
|
|
124
127
|
export declare function buildWaClientDependencies(input: {
|
|
@@ -17,6 +17,7 @@ const WaIncomingNodeCoordinator_1 = require("./coordinators/WaIncomingNodeCoordi
|
|
|
17
17
|
const WaLowLevelCoordinator_1 = require("./coordinators/WaLowLevelCoordinator");
|
|
18
18
|
const WaMessageCoordinator_1 = require("./coordinators/WaMessageCoordinator");
|
|
19
19
|
const WaMessageDispatchCoordinator_1 = require("./coordinators/WaMessageDispatchCoordinator");
|
|
20
|
+
const WaMobileCoordinator_1 = require("./coordinators/WaMobileCoordinator");
|
|
20
21
|
const WaNewsletterCoordinator_1 = require("./coordinators/WaNewsletterCoordinator");
|
|
21
22
|
const WaOfflineResumeCoordinator_1 = require("./coordinators/WaOfflineResumeCoordinator");
|
|
22
23
|
const WaPassiveTasksCoordinator_1 = require("./coordinators/WaPassiveTasksCoordinator");
|
|
@@ -65,7 +66,6 @@ const WaNodeTransport_1 = require("../transport/node/WaNodeTransport");
|
|
|
65
66
|
const proxy_1 = require("../transport/proxy");
|
|
66
67
|
const clock_1 = require("../util/clock");
|
|
67
68
|
const primitives_1 = require("../util/primitives");
|
|
68
|
-
const runtime_1 = require("../util/runtime");
|
|
69
69
|
function assertProxyTransport(value, path) {
|
|
70
70
|
if (value === undefined) {
|
|
71
71
|
return;
|
|
@@ -89,7 +89,7 @@ function validateProxyOptions(options) {
|
|
|
89
89
|
}
|
|
90
90
|
function resolveWaClientBase(options, logger) {
|
|
91
91
|
validateProxyOptions(options);
|
|
92
|
-
const
|
|
92
|
+
const device = (0, constants_1.resolveWaDeviceIdentity)(options);
|
|
93
93
|
const sessionId = options.sessionId.trim();
|
|
94
94
|
if (sessionId.length === 0) {
|
|
95
95
|
throw new Error('sessionId must be a non-empty string');
|
|
@@ -98,9 +98,9 @@ function resolveWaClientBase(options, logger) {
|
|
|
98
98
|
const normalizedOptions = Object.freeze({
|
|
99
99
|
...options,
|
|
100
100
|
sessionId,
|
|
101
|
-
deviceBrowser,
|
|
102
|
-
deviceOsDisplayName:
|
|
103
|
-
devicePlatform:
|
|
101
|
+
deviceBrowser: device.browser,
|
|
102
|
+
deviceOsDisplayName: device.osDisplayName,
|
|
103
|
+
devicePlatform: device.platform,
|
|
104
104
|
urls: options.urls ?? options.chatSocketUrls ?? constants_1.WA_DEFAULTS.CHAT_SOCKET_URLS,
|
|
105
105
|
iqTimeoutMs: options.iqTimeoutMs ?? constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS,
|
|
106
106
|
nodeQueryTimeoutMs: options.nodeQueryTimeoutMs ?? constants_1.WA_DEFAULTS.NODE_QUERY_TIMEOUT_MS,
|
|
@@ -352,7 +352,9 @@ function buildWaClientDependencies(input) {
|
|
|
352
352
|
runtime.emitEvent('auth_paired', { credentials });
|
|
353
353
|
scheduleReconnectAfterPairing();
|
|
354
354
|
},
|
|
355
|
-
onError: (error) => runtime.handleError(error)
|
|
355
|
+
onError: (error) => runtime.handleError(error),
|
|
356
|
+
onPasskeyRequired: (hasSigner) => runtime.emitEvent('auth_passkey_required', { hasSigner }),
|
|
357
|
+
signPasskeyAssertion: options.signPasskeyAssertion
|
|
356
358
|
}
|
|
357
359
|
});
|
|
358
360
|
const getCurrentCredentials = authClient.getCurrentCredentials.bind(authClient);
|
|
@@ -453,6 +455,7 @@ function buildWaClientDependencies(input) {
|
|
|
453
455
|
});
|
|
454
456
|
messageDispatch = new WaMessageDispatchCoordinator_1.WaMessageDispatchCoordinator({
|
|
455
457
|
logger,
|
|
458
|
+
emitMessageSend: (event) => runtime.emitEvent('message_send', event),
|
|
456
459
|
messageClient,
|
|
457
460
|
retryTracker,
|
|
458
461
|
sessionResolver,
|
|
@@ -589,6 +592,7 @@ function buildWaClientDependencies(input) {
|
|
|
589
592
|
serverClock,
|
|
590
593
|
emitSnapshotMutations: options.chatEvents?.emitSnapshotMutations === true,
|
|
591
594
|
emitMutation: (event) => runtime.emitEvent('mutation', event),
|
|
595
|
+
emitMutationSend: (event) => runtime.emitEvent('mutation_send', event),
|
|
592
596
|
nctSaltSink: (salt) => trustedContactToken.handleNctSaltSync(salt),
|
|
593
597
|
contactSink: runtime.persistContact,
|
|
594
598
|
pushNameSink: (name) => {
|
|
@@ -1015,6 +1019,20 @@ function buildWaClientDependencies(input) {
|
|
|
1015
1019
|
isConnected: () => connectionManager?.isConnected() ?? false,
|
|
1016
1020
|
defaultIqTimeoutMs: options.iqTimeoutMs
|
|
1017
1021
|
});
|
|
1022
|
+
const mobileCoordinator = new WaMobileCoordinator_1.WaMobileCoordinator({
|
|
1023
|
+
logger,
|
|
1024
|
+
authClient,
|
|
1025
|
+
messageDispatch,
|
|
1026
|
+
chatCoordinator: appStateMutations,
|
|
1027
|
+
deviceSync: signalDeviceSync,
|
|
1028
|
+
appStateStore: sessionStore.appState,
|
|
1029
|
+
queryWithContext: runtime.queryWithContext,
|
|
1030
|
+
emitEvent: runtime.emitEvent,
|
|
1031
|
+
registerIncomingHandler: (registration) => incomingNode.registerIncomingHandler(registration),
|
|
1032
|
+
isMobilePrimary,
|
|
1033
|
+
persistence: options.companionHost?.persistence,
|
|
1034
|
+
includePem: options.companionHost?.includePem
|
|
1035
|
+
});
|
|
1018
1036
|
return {
|
|
1019
1037
|
nodeTransport,
|
|
1020
1038
|
nodeOrchestrator,
|
|
@@ -1055,6 +1073,8 @@ function buildWaClientDependencies(input) {
|
|
|
1055
1073
|
connectionManager,
|
|
1056
1074
|
trustedContactToken,
|
|
1057
1075
|
abPropsCoordinator,
|
|
1058
|
-
peerDataOperation
|
|
1076
|
+
peerDataOperation,
|
|
1077
|
+
mobileCoordinator,
|
|
1078
|
+
isMobilePrimary
|
|
1059
1079
|
};
|
|
1060
1080
|
}
|
|
@@ -33,6 +33,7 @@ interface WaAppStateMutationCoordinatorOptions {
|
|
|
33
33
|
readonly serverClock: ServerClock;
|
|
34
34
|
readonly archiveRangeLimit?: number;
|
|
35
35
|
readonly emitMutation?: (event: WaAppStateMutationEvent) => void;
|
|
36
|
+
readonly emitMutationSend?: (event: WaAppStateMutationEvent) => void;
|
|
36
37
|
readonly emitSnapshotMutations?: boolean;
|
|
37
38
|
readonly nctSaltSink?: (salt: Uint8Array | null) => Promise<void>;
|
|
38
39
|
/**
|
|
@@ -79,6 +80,7 @@ export declare class WaAppStateMutationCoordinator {
|
|
|
79
80
|
private readonly serverClock;
|
|
80
81
|
private readonly archiveRangeLimit;
|
|
81
82
|
private readonly emitMutation?;
|
|
83
|
+
private readonly emitMutationSend?;
|
|
82
84
|
private readonly emitSnapshotMutations;
|
|
83
85
|
private readonly nctSaltSink?;
|
|
84
86
|
private readonly contactSink?;
|
|
@@ -160,6 +162,12 @@ export declare class WaAppStateMutationCoordinator {
|
|
|
160
162
|
*/
|
|
161
163
|
flushMutations(): Promise<void>;
|
|
162
164
|
private enqueueAndFlush;
|
|
165
|
+
/**
|
|
166
|
+
* Surfaces an action this client is sending via the outbound `mutation_send`
|
|
167
|
+
* event, so consumers can observe their own change at action time. Parse
|
|
168
|
+
* failures are swallowed (best-effort).
|
|
169
|
+
*/
|
|
170
|
+
private emitLocalMutation;
|
|
163
171
|
private enqueueMutation;
|
|
164
172
|
private takePendingMutationsBatch;
|
|
165
173
|
private flushPendingMutationsLoop;
|
|
@@ -14,6 +14,8 @@ const WA_APP_STATE_MUTATION_FLUSH_SUCCESS_STATES = new Set([
|
|
|
14
14
|
constants_1.WA_APP_STATE_COLLECTION_STATES.SUCCESS_HAS_MORE
|
|
15
15
|
]);
|
|
16
16
|
const WA_APP_STATE_ARCHIVE_RANGE_DEFAULT_LIMIT = 256;
|
|
17
|
+
/** Placeholder MACs for locally-emitted mutation events (the parser ignores them). */
|
|
18
|
+
const WA_EMPTY_MUTATION_MAC = new Uint8Array(0);
|
|
17
19
|
function buildMutationIndexFromSchema(schema, indexArgs) {
|
|
18
20
|
const parts = new Array(schema.indexParts.length);
|
|
19
21
|
const args = indexArgs;
|
|
@@ -160,7 +162,6 @@ function buildRemoveMutationFromSchema(input) {
|
|
|
160
162
|
collection: input.schema.collection,
|
|
161
163
|
operation: 'remove',
|
|
162
164
|
index: buildMutationIndexFromSchema(input.schema, input.indexArgs),
|
|
163
|
-
previousValue: { timestamp: input.timestamp },
|
|
164
165
|
version: input.schema.version,
|
|
165
166
|
timestamp: input.timestamp
|
|
166
167
|
};
|
|
@@ -180,6 +181,7 @@ class WaAppStateMutationCoordinator {
|
|
|
180
181
|
this.serverClock = options.serverClock;
|
|
181
182
|
this.archiveRangeLimit = (0, coercion_1.resolvePositive)(options.archiveRangeLimit, WA_APP_STATE_ARCHIVE_RANGE_DEFAULT_LIMIT, 'WaAppStateMutationCoordinatorOptions.archiveRangeLimit');
|
|
182
183
|
this.emitMutation = options.emitMutation;
|
|
184
|
+
this.emitMutationSend = options.emitMutationSend;
|
|
183
185
|
this.emitSnapshotMutations = options.emitSnapshotMutations === true;
|
|
184
186
|
this.nctSaltSink = options.nctSaltSink;
|
|
185
187
|
this.contactSink = options.contactSink;
|
|
@@ -617,9 +619,43 @@ class WaAppStateMutationCoordinator {
|
|
|
617
619
|
async enqueueAndFlush(mutations) {
|
|
618
620
|
for (const mutation of mutations) {
|
|
619
621
|
this.enqueueMutation(mutation);
|
|
622
|
+
this.emitLocalMutation(mutation);
|
|
620
623
|
}
|
|
621
624
|
await this.flushMutations();
|
|
622
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Surfaces an action this client is sending via the outbound `mutation_send`
|
|
628
|
+
* event, so consumers can observe their own change at action time. Parse
|
|
629
|
+
* failures are swallowed (best-effort).
|
|
630
|
+
*/
|
|
631
|
+
emitLocalMutation(input) {
|
|
632
|
+
if (!this.emitMutationSend)
|
|
633
|
+
return;
|
|
634
|
+
const value = input.operation === 'set' ? input.value : null;
|
|
635
|
+
try {
|
|
636
|
+
const event = (0, appstate_mutation_1.parseAppStateMutationEvent)({
|
|
637
|
+
collection: input.collection,
|
|
638
|
+
operation: input.operation,
|
|
639
|
+
source: 'local',
|
|
640
|
+
index: input.index,
|
|
641
|
+
value,
|
|
642
|
+
version: input.version,
|
|
643
|
+
indexMac: WA_EMPTY_MUTATION_MAC,
|
|
644
|
+
valueMac: WA_EMPTY_MUTATION_MAC,
|
|
645
|
+
keyId: WA_EMPTY_MUTATION_MAC,
|
|
646
|
+
timestamp: input.timestamp
|
|
647
|
+
});
|
|
648
|
+
if (event)
|
|
649
|
+
this.emitMutationSend(event);
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
this.logger.debug('failed to emit local app-state mutation event', {
|
|
653
|
+
collection: input.collection,
|
|
654
|
+
index: input.index,
|
|
655
|
+
message: (0, primitives_1.toError)(error).message
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
623
659
|
enqueueMutation(mutation) {
|
|
624
660
|
const key = `${mutation.collection}\u0001${mutation.index}`;
|
|
625
661
|
if (this.pendingMutations.has(key)) {
|
|
@@ -5,6 +5,7 @@ const group_1 = require("../events/group");
|
|
|
5
5
|
const defaults_1 = require("../../protocol/defaults");
|
|
6
6
|
const group_2 = require("../../protocol/group");
|
|
7
7
|
const jid_1 = require("../../protocol/jid");
|
|
8
|
+
const message_1 = require("../../protocol/message");
|
|
8
9
|
const nodes_1 = require("../../protocol/nodes");
|
|
9
10
|
const notification_1 = require("../../protocol/notification");
|
|
10
11
|
const account_sync_1 = require("../../transport/node/builders/account-sync");
|
|
@@ -90,7 +91,11 @@ function parseGroupCommons(target) {
|
|
|
90
91
|
? (0, helpers_1.findNodeChild)(membershipApprovalNode, nodes_1.WA_NODE_TAGS.GROUP_JOIN)
|
|
91
92
|
: undefined;
|
|
92
93
|
const addressingModeRaw = attrs.addressing_mode;
|
|
93
|
-
const addressingMode = addressingModeRaw ===
|
|
94
|
+
const addressingMode = addressingModeRaw === message_1.WA_ADDRESSING_MODES.LID
|
|
95
|
+
? message_1.WA_ADDRESSING_MODES.LID
|
|
96
|
+
: addressingModeRaw === message_1.WA_ADDRESSING_MODES.PN
|
|
97
|
+
? message_1.WA_ADDRESSING_MODES.PN
|
|
98
|
+
: undefined;
|
|
94
99
|
const rawJid = attrs.id ?? attrs.jid ?? '';
|
|
95
100
|
const jid = rawJid && !rawJid.includes('@') ? `${rawJid}@${defaults_1.WA_DEFAULTS.GROUP_SERVER}` : rawJid;
|
|
96
101
|
return {
|
|
@@ -3,7 +3,7 @@ import type { WaAuthCredentials } from '../../auth/types';
|
|
|
3
3
|
import type { DeviceFanoutResolver } from '../messaging/fanout';
|
|
4
4
|
import type { GroupMetadataCache } from '../messaging/group-metadata';
|
|
5
5
|
import type { AppStateSyncKeyProtocol } from '../messaging/key-protocol';
|
|
6
|
-
import type { WaGroupEvent, WaSendMessageOptions, WaSignalMessagePublishInput } from '../types';
|
|
6
|
+
import type { WaGroupEvent, WaOutgoingMessageEvent, WaSendMessageOptions, WaSignalMessagePublishInput } from '../types';
|
|
7
7
|
import type { Logger } from '../../infra/log/types';
|
|
8
8
|
import { type WaSendContextInfo } from '../../message/context-info';
|
|
9
9
|
import { type IcdcMeta } from '../../message/crypto/icdc';
|
|
@@ -26,6 +26,8 @@ import type { BinaryNode } from '../../transport/types';
|
|
|
26
26
|
import type { ServerClock } from '../../util/clock';
|
|
27
27
|
interface WaMessageDispatchCoordinatorOptions {
|
|
28
28
|
readonly logger: Logger;
|
|
29
|
+
/** Emits the outbound decrypted message (`message_send` event); best-effort. */
|
|
30
|
+
readonly emitMessageSend?: (event: WaOutgoingMessageEvent) => void;
|
|
29
31
|
readonly messageClient: WaMessageClient;
|
|
30
32
|
readonly retryTracker: OutboundRetryTracker;
|
|
31
33
|
readonly sessionResolver: SignalSessionResolver;
|
|
@@ -203,6 +203,16 @@ class WaMessageDispatchCoordinator {
|
|
|
203
203
|
}
|
|
204
204
|
: withViewOnce;
|
|
205
205
|
const upload = built.upload;
|
|
206
|
+
if (this.deps.emitMessageSend) {
|
|
207
|
+
try {
|
|
208
|
+
this.deps.emitMessageSend({ to: recipientJid, id: sendOptions.id, message });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
this.deps.logger.debug('message_send emit failed', {
|
|
212
|
+
message: (0, primitives_2.toError)(error).message
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
206
216
|
const messageWithOverride = options.messageSecret
|
|
207
217
|
? {
|
|
208
218
|
...message,
|
|
@@ -894,17 +904,17 @@ class WaMessageDispatchCoordinator {
|
|
|
894
904
|
resolveGroupAddressingMode(participantUserJids, groupJid) {
|
|
895
905
|
for (let index = 0; index < participantUserJids.length; index += 1) {
|
|
896
906
|
if ((0, jid_1.isLidJid)(participantUserJids[index])) {
|
|
897
|
-
return
|
|
907
|
+
return constants_1.WA_ADDRESSING_MODES.LID;
|
|
898
908
|
}
|
|
899
909
|
}
|
|
900
910
|
this.deps.logger.trace('group addressing mode resolved to pn (default)', {
|
|
901
911
|
groupJid,
|
|
902
912
|
participants: participantUserJids.length
|
|
903
913
|
});
|
|
904
|
-
return
|
|
914
|
+
return constants_1.WA_ADDRESSING_MODES.PN;
|
|
905
915
|
}
|
|
906
916
|
resolveSenderForAddressingMode(addressingMode, meJid) {
|
|
907
|
-
if (addressingMode ===
|
|
917
|
+
if (addressingMode === constants_1.WA_ADDRESSING_MODES.LID) {
|
|
908
918
|
const meLid = this.deps.getCurrentCredentials()?.meLid;
|
|
909
919
|
if (meLid && meLid.includes('@')) {
|
|
910
920
|
try {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { type PhoneNumberToLidMapping } from '../messaging/companion-host';
|
|
2
|
+
import type { CompanionHostPersistence, CompanionRecord } from '../persistence/companion-host';
|
|
3
|
+
import type { WaClientPluginContext } from '../plugins/types';
|
|
4
|
+
import type { WaClientEventMap } from '../types';
|
|
5
|
+
import type { WaClientDependencies } from '../WaClientFactory';
|
|
6
|
+
import type { Logger } from '../../infra/log/types';
|
|
7
|
+
type WaMobileEmit = <K extends keyof WaClientEventMap>(event: K, ...args: Parameters<WaClientEventMap[K]>) => void;
|
|
8
|
+
/** Result of a successful {@link WaMobileCoordinator.linkCompanion}. */
|
|
9
|
+
export interface LinkCompanionResult {
|
|
10
|
+
readonly deviceJid: string;
|
|
11
|
+
readonly keyIndex: number;
|
|
12
|
+
}
|
|
13
|
+
/** Dependencies for {@link WaMobileCoordinator}, injected by the client factory. */
|
|
14
|
+
export interface WaMobileCoordinatorDeps {
|
|
15
|
+
readonly logger: Logger;
|
|
16
|
+
readonly authClient: WaClientDependencies['authClient'];
|
|
17
|
+
readonly messageDispatch: WaClientDependencies['messageDispatch'];
|
|
18
|
+
readonly chatCoordinator: WaClientDependencies['chatCoordinator'];
|
|
19
|
+
readonly deviceSync: WaClientDependencies['signalDeviceSync'];
|
|
20
|
+
readonly appStateStore: WaClientPluginContext['stores']['appState'];
|
|
21
|
+
readonly queryWithContext: WaClientPluginContext['queryWithContext'];
|
|
22
|
+
readonly emitEvent: WaMobileEmit;
|
|
23
|
+
readonly registerIncomingHandler: WaClientPluginContext['registerIncomingHandler'];
|
|
24
|
+
readonly isMobilePrimary: () => boolean;
|
|
25
|
+
readonly persistence?: CompanionHostPersistence;
|
|
26
|
+
readonly includePem?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hosts companion devices from a mobile-primary session: signs a companion's
|
|
30
|
+
* device identity, uploads `pair-device`, tracks the linked set, and can revoke
|
|
31
|
+
* a device. Exposed at `client.mobile`; requires a registered primary session.
|
|
32
|
+
* Supports the QR ({@link linkCompanion}) and pairing-code
|
|
33
|
+
* ({@link linkCompanionByCode}) flows.
|
|
34
|
+
*/
|
|
35
|
+
export declare class WaMobileCoordinator {
|
|
36
|
+
private readonly authClient;
|
|
37
|
+
private readonly messageDispatch;
|
|
38
|
+
private readonly chatCoordinator;
|
|
39
|
+
private readonly deviceSync;
|
|
40
|
+
private readonly appStateStore;
|
|
41
|
+
private readonly queryWithContext;
|
|
42
|
+
private readonly emitEvent;
|
|
43
|
+
private readonly isMobilePrimary;
|
|
44
|
+
private readonly logger;
|
|
45
|
+
private readonly persistence?;
|
|
46
|
+
private readonly includePem;
|
|
47
|
+
private epoch;
|
|
48
|
+
private pendingCompanionHello;
|
|
49
|
+
private pendingFinish;
|
|
50
|
+
private accountKeyIndexes;
|
|
51
|
+
private pushNameSeeded;
|
|
52
|
+
constructor(deps: WaMobileCoordinatorDeps);
|
|
53
|
+
/**
|
|
54
|
+
* Links the companion from its pairing QR: signs the device identity, uploads
|
|
55
|
+
* `pair-device`, and returns the server-assigned device jid.
|
|
56
|
+
*
|
|
57
|
+
* @throws when there is no primary session or the server rejects the upload
|
|
58
|
+
* (e.g. the linked-device cap is reached).
|
|
59
|
+
*/
|
|
60
|
+
linkCompanion(qr: string): Promise<LinkCompanionResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Links a companion via its 8-character pairing code (the "link with phone
|
|
63
|
+
* number" flow). The companion must have requested a code for this account
|
|
64
|
+
* first (its `companion_hello` is recorded); this drives `primary_hello`,
|
|
65
|
+
* awaits `companion_finish`, and completes the same `pair-device` upload as
|
|
66
|
+
* the QR path.
|
|
67
|
+
*
|
|
68
|
+
* @throws when there is no primary session, no companion is pending, the code
|
|
69
|
+
* is wrong, or the handshake times out.
|
|
70
|
+
*/
|
|
71
|
+
linkCompanionByCode(pairingCode: string): Promise<LinkCompanionResult>;
|
|
72
|
+
/** Re-signs and republishes the key-index list for the current device set. */
|
|
73
|
+
publishKeyIndexList(): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Unlinks a companion this primary linked: sends the `remove-companion-device`
|
|
76
|
+
* IQ, drops it from the tracked set, and republishes the key-index list for
|
|
77
|
+
* the reduced device set.
|
|
78
|
+
*
|
|
79
|
+
* @throws when there is no registered primary session, the companion is not
|
|
80
|
+
* tracked, or the server rejects the removal.
|
|
81
|
+
*/
|
|
82
|
+
revokeCompanion(companionDeviceJid: string, reason?: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Unlinks EVERY companion from the account in a single `remove-companion-device`
|
|
85
|
+
* `all="true"` stanza (the phone's "log out all companion devices"), clears the
|
|
86
|
+
* tracked set, and republishes the key-index list for the primary alone.
|
|
87
|
+
* `excludeHostedCompanion` spares companions this account itself hosts.
|
|
88
|
+
*
|
|
89
|
+
* @throws when there is no registered primary session or the server rejects
|
|
90
|
+
* the removal.
|
|
91
|
+
*/
|
|
92
|
+
revokeAllCompanions(reason?: string, options?: {
|
|
93
|
+
readonly excludeHostedCompanion?: boolean;
|
|
94
|
+
}): Promise<void>;
|
|
95
|
+
/** Returns the companions this primary has linked in the current epoch. */
|
|
96
|
+
listCompanions(): Promise<readonly CompanionRecord[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Reconciles the tracked companion set against the account's live device list
|
|
99
|
+
* on the server (`usync`). Drops companions the server no longer lists - one
|
|
100
|
+
* the user unlinked, or one that self-removed while the primary was offline -
|
|
101
|
+
* then persists and emits `companion_host_revoked` for each. Runs on connect
|
|
102
|
+
* and on `account_sync`; safe to call manually. A no-op (no server query, no
|
|
103
|
+
* epoch created) when no companions are tracked. Returns the removed jids.
|
|
104
|
+
*/
|
|
105
|
+
reconcileCompanions(): Promise<readonly string[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Prunes tracked companions to those whose device slot is still in
|
|
108
|
+
* `serverDeviceIndexes`, persisting + emitting `companion_host_revoked` for
|
|
109
|
+
* each drop. Shared by the connect-time (`usync`) and `account_sync` paths.
|
|
110
|
+
*/
|
|
111
|
+
private pruneCompanionsToDevices;
|
|
112
|
+
/**
|
|
113
|
+
* Reconciles from an `account_sync` notification's `<devices>` payload - the
|
|
114
|
+
* fresh, authoritative device set - so a companion the user just unlinked is
|
|
115
|
+
* pruned immediately (no `usync` round-trip, which would read a stale cache).
|
|
116
|
+
*/
|
|
117
|
+
private reconcileFromAccountSync;
|
|
118
|
+
/**
|
|
119
|
+
* Shares the primary's active app-state sync key with a linked companion via
|
|
120
|
+
* an `APP_STATE_SYNC_KEY_SHARE` peer message so it can decrypt app-state.
|
|
121
|
+
* Establishes the outbound Signal session on demand, so call it once the
|
|
122
|
+
* companion is online (prekeys uploaded), not at link time.
|
|
123
|
+
*
|
|
124
|
+
* @throws when there is no primary session or no active sync key.
|
|
125
|
+
*/
|
|
126
|
+
shareAppStateSyncKeys(companionDeviceJid: string): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Pushes the `INITIAL_BOOTSTRAP` history sync to a linked companion as a
|
|
129
|
+
* `HISTORY_SYNC_NOTIFICATION` peer message. Flips the companion's
|
|
130
|
+
* `initialChatHistory` bootstrap flag; without it the companion self-removes
|
|
131
|
+
* with `HistorySyncTimeout`. Requires the companion online with prekeys.
|
|
132
|
+
*
|
|
133
|
+
* @throws when there is no registered primary session.
|
|
134
|
+
*/
|
|
135
|
+
sendHistorySyncBootstrap(companionDeviceJid: string, options?: {
|
|
136
|
+
readonly phoneNumberToLidMappings?: readonly PhoneNumberToLidMapping[];
|
|
137
|
+
}): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* `ClientPairingProps` for the `<client-props>` pair-device element. A
|
|
140
|
+
* LID-native primary declares the chat DB LID-migrated + pure-LID session so
|
|
141
|
+
* the companion runs `setIsLidMigrated` at pair time and does not self-remove
|
|
142
|
+
* on its LID-addressed blocklist.
|
|
143
|
+
*/
|
|
144
|
+
private buildClientPairingProps;
|
|
145
|
+
/**
|
|
146
|
+
* Seeds the primary's own `setting_pushName` into the `critical_block`
|
|
147
|
+
* app-state collection, once per session. The companion's critical bootstrap
|
|
148
|
+
* completes only when it finds an applied `SettingPushName` action; without
|
|
149
|
+
* one it self-removes with `syncd_timeout`. Falls back to the account phone
|
|
150
|
+
* number when no push name is set. Idempotent and best-effort.
|
|
151
|
+
*/
|
|
152
|
+
private ensureAccountPushNameAppState;
|
|
153
|
+
private completeLink;
|
|
154
|
+
/**
|
|
155
|
+
* Best-effort background provisioning after a companion links: pushes the
|
|
156
|
+
* history-sync bootstrap and shares the app-state sync key. Fire-and-forget
|
|
157
|
+
* and retried, because the companion needs a moment to upload prekeys before
|
|
158
|
+
* the outbound Signal session these peer messages ride on can be established.
|
|
159
|
+
*/
|
|
160
|
+
private provisionLinkedCompanion;
|
|
161
|
+
private provisionLinkedCompanionWithRetry;
|
|
162
|
+
private handleNotification;
|
|
163
|
+
/**
|
|
164
|
+
* Tracks the account's live device key-indexes from an `account_sync`
|
|
165
|
+
* notification so a new companion gets a non-colliding key index and the
|
|
166
|
+
* published key-index list keeps every existing device valid.
|
|
167
|
+
*/
|
|
168
|
+
private updateAccountKeyIndexes;
|
|
169
|
+
private forgetAccountKeyIndex;
|
|
170
|
+
/**
|
|
171
|
+
* The key indexes currently valid for this account - index 0 (the primary),
|
|
172
|
+
* the account's own live devices, and every tracked companion. Sorted
|
|
173
|
+
* ascending. Deliberately excludes `epoch.currentKeyIndex`, which is an
|
|
174
|
+
* allocation high-water mark, not proof a device still exists: after revoking
|
|
175
|
+
* the last-linked companion its index must drop out of the published list.
|
|
176
|
+
*/
|
|
177
|
+
private validKeyIndexes;
|
|
178
|
+
/**
|
|
179
|
+
* Allocates the next companion key index, advancing strictly past both the
|
|
180
|
+
* epoch's monotonic high-water mark and every currently-valid index so a
|
|
181
|
+
* revoked index is never reused (reuse breaks previously linked devices).
|
|
182
|
+
*/
|
|
183
|
+
private nextKeyIndex;
|
|
184
|
+
private storeCompanionHello;
|
|
185
|
+
private resolveCompanionFinish;
|
|
186
|
+
private nodeBytes;
|
|
187
|
+
private requirePrimaryIdentityKeyPair;
|
|
188
|
+
private ensureEpoch;
|
|
189
|
+
private generateRawId;
|
|
190
|
+
private persist;
|
|
191
|
+
}
|
|
192
|
+
export {};
|