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
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WaMobileCoordinator = void 0;
|
|
4
|
+
const companion_host_1 = require("../../auth/pairing/companion-host");
|
|
5
|
+
const companion_host_2 = require("../messaging/companion-host");
|
|
6
|
+
const _crypto_1 = require("../../crypto");
|
|
7
|
+
const jid_1 = require("../../protocol/jid");
|
|
8
|
+
const nodes_1 = require("../../protocol/nodes");
|
|
9
|
+
const _transport_1 = require("../../transport");
|
|
10
|
+
const mobile_1 = require("../../transport/node/builders/mobile");
|
|
11
|
+
const async_1 = require("../../util/async");
|
|
12
|
+
const primitives_1 = require("../../util/primitives");
|
|
13
|
+
const PAIR_DEVICE_TIMEOUT_MS = 32000;
|
|
14
|
+
const KEY_INDEX_TIMEOUT_MS = 32000;
|
|
15
|
+
const REMOVE_DEVICE_TIMEOUT_MS = 32000;
|
|
16
|
+
const PRIMARY_HELLO_TIMEOUT_MS = 120000;
|
|
17
|
+
const LINK_CODE_FINISH_TIMEOUT_MS = 120000;
|
|
18
|
+
const PEM_TTL_SECONDS = 5 * 24 * 60 * 60;
|
|
19
|
+
const COMPANION_PROVISION_ATTEMPTS = 8;
|
|
20
|
+
const COMPANION_PROVISION_RETRY_MS = 1500;
|
|
21
|
+
/**
|
|
22
|
+
* Hosts companion devices from a mobile-primary session: signs a companion's
|
|
23
|
+
* device identity, uploads `pair-device`, tracks the linked set, and can revoke
|
|
24
|
+
* a device. Exposed at `client.mobile`; requires a registered primary session.
|
|
25
|
+
* Supports the QR ({@link linkCompanion}) and pairing-code
|
|
26
|
+
* ({@link linkCompanionByCode}) flows.
|
|
27
|
+
*/
|
|
28
|
+
class WaMobileCoordinator {
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.epoch = null;
|
|
31
|
+
this.pendingCompanionHello = null;
|
|
32
|
+
this.pendingFinish = null;
|
|
33
|
+
this.accountKeyIndexes = new Set([0]);
|
|
34
|
+
this.pushNameSeeded = false;
|
|
35
|
+
this.authClient = deps.authClient;
|
|
36
|
+
this.messageDispatch = deps.messageDispatch;
|
|
37
|
+
this.chatCoordinator = deps.chatCoordinator;
|
|
38
|
+
this.deviceSync = deps.deviceSync;
|
|
39
|
+
this.appStateStore = deps.appStateStore;
|
|
40
|
+
this.queryWithContext = deps.queryWithContext;
|
|
41
|
+
this.emitEvent = deps.emitEvent;
|
|
42
|
+
this.isMobilePrimary = deps.isMobilePrimary;
|
|
43
|
+
this.logger = deps.logger.child({ scope: 'mobile' });
|
|
44
|
+
this.persistence = deps.persistence;
|
|
45
|
+
this.includePem = deps.includePem ?? false;
|
|
46
|
+
deps.registerIncomingHandler({
|
|
47
|
+
tag: nodes_1.WA_NODE_TAGS.NOTIFICATION,
|
|
48
|
+
prepend: true,
|
|
49
|
+
handler: (node) => this.handleNotification(node)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Links the companion from its pairing QR: signs the device identity, uploads
|
|
54
|
+
* `pair-device`, and returns the server-assigned device jid.
|
|
55
|
+
*
|
|
56
|
+
* @throws when there is no primary session or the server rejects the upload
|
|
57
|
+
* (e.g. the linked-device cap is reached).
|
|
58
|
+
*/
|
|
59
|
+
async linkCompanion(qr) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = (0, companion_host_1.parseCompanionQr)(qr);
|
|
62
|
+
return await this.completeLink({
|
|
63
|
+
ref: parsed.ref,
|
|
64
|
+
companionIdentityPublicKey: parsed.identityPublicKey,
|
|
65
|
+
companionNoisePublicKey: parsed.noisePublicKey,
|
|
66
|
+
advSecretKey: parsed.advSecretKey,
|
|
67
|
+
platform: parsed.platform
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const normalized = (0, primitives_1.toError)(error);
|
|
72
|
+
this.logger.warn('companion link failed', { message: normalized.message });
|
|
73
|
+
this.emitEvent('companion_host_error', normalized);
|
|
74
|
+
throw normalized;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Links a companion via its 8-character pairing code (the "link with phone
|
|
79
|
+
* number" flow). The companion must have requested a code for this account
|
|
80
|
+
* first (its `companion_hello` is recorded); this drives `primary_hello`,
|
|
81
|
+
* awaits `companion_finish`, and completes the same `pair-device` upload as
|
|
82
|
+
* the QR path.
|
|
83
|
+
*
|
|
84
|
+
* @throws when there is no primary session, no companion is pending, the code
|
|
85
|
+
* is wrong, or the handshake times out.
|
|
86
|
+
*/
|
|
87
|
+
async linkCompanionByCode(pairingCode) {
|
|
88
|
+
try {
|
|
89
|
+
const accountIdentityKeyPair = this.requirePrimaryIdentityKeyPair();
|
|
90
|
+
const hello = this.pendingCompanionHello;
|
|
91
|
+
if (!hello) {
|
|
92
|
+
throw new Error('no pending companion; a companion must request a pairing code for this account first');
|
|
93
|
+
}
|
|
94
|
+
const prepared = await (0, companion_host_1.preparePrimaryHello)({
|
|
95
|
+
pairingCode: pairingCode.replace(/[^0-9A-Za-z]/g, '').toUpperCase(),
|
|
96
|
+
wrappedCompanionEphemeralPub: hello.wrappedCompanionEphemeralPub
|
|
97
|
+
});
|
|
98
|
+
let finishTimeout;
|
|
99
|
+
const finishPromise = new Promise((resolve, reject) => {
|
|
100
|
+
this.pendingFinish = { ref: hello.ref, resolve };
|
|
101
|
+
finishTimeout = setTimeout(() => {
|
|
102
|
+
if (this.pendingFinish?.ref === hello.ref) {
|
|
103
|
+
this.pendingFinish = null;
|
|
104
|
+
reject(new Error('link-code companion_finish timed out'));
|
|
105
|
+
}
|
|
106
|
+
}, LINK_CODE_FINISH_TIMEOUT_MS);
|
|
107
|
+
});
|
|
108
|
+
finishPromise.catch(() => undefined);
|
|
109
|
+
let finish;
|
|
110
|
+
try {
|
|
111
|
+
const helloResult = await this.queryWithContext('companion-host.primary-hello', (0, mobile_1.buildPrimaryHelloIq)({
|
|
112
|
+
ref: hello.ref,
|
|
113
|
+
wrappedPrimaryEphemeralPub: prepared.wrappedPrimaryEphemeralPub,
|
|
114
|
+
primaryIdentityPub: (0, _crypto_1.toRawPubKey)(accountIdentityKeyPair.pubKey)
|
|
115
|
+
}), PRIMARY_HELLO_TIMEOUT_MS);
|
|
116
|
+
(0, _transport_1.assertIqResult)(helloResult, 'companion-host.primary-hello');
|
|
117
|
+
finish = await finishPromise;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearTimeout(finishTimeout);
|
|
121
|
+
if (this.pendingFinish?.ref === hello.ref) {
|
|
122
|
+
this.pendingFinish = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const advSecretKey = await (0, companion_host_1.completePrimaryHandshake)({
|
|
126
|
+
sharedEphemeral: prepared.sharedEphemeral,
|
|
127
|
+
wrappedKeyBundle: finish.wrappedKeyBundle,
|
|
128
|
+
companionIdentityPub: finish.companionIdentityPub,
|
|
129
|
+
primaryIdentityKeyPair: accountIdentityKeyPair
|
|
130
|
+
});
|
|
131
|
+
this.pendingCompanionHello = null;
|
|
132
|
+
return await this.completeLink({
|
|
133
|
+
ref: hello.ref,
|
|
134
|
+
companionIdentityPublicKey: (0, _crypto_1.toRawPubKey)(finish.companionIdentityPub),
|
|
135
|
+
companionNoisePublicKey: hello.companionServerAuthKeyPub,
|
|
136
|
+
advSecretKey,
|
|
137
|
+
platform: 'link-code'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const normalized = (0, primitives_1.toError)(error);
|
|
142
|
+
this.logger.warn('companion link-by-code failed', { message: normalized.message });
|
|
143
|
+
this.emitEvent('companion_host_error', normalized);
|
|
144
|
+
throw normalized;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Re-signs and republishes the key-index list for the current device set. */
|
|
148
|
+
async publishKeyIndexList() {
|
|
149
|
+
const accountIdentityKeyPair = this.requirePrimaryIdentityKeyPair();
|
|
150
|
+
const epoch = await this.ensureEpoch();
|
|
151
|
+
const timestampSeconds = Math.floor(Date.now() / 1000);
|
|
152
|
+
const validIndexes = this.validKeyIndexes(epoch);
|
|
153
|
+
const currentIndex = validIndexes[validIndexes.length - 1];
|
|
154
|
+
const keyIndexListBytes = await (0, companion_host_1.buildSignedKeyIndexList)({
|
|
155
|
+
accountIdentityKeyPair,
|
|
156
|
+
rawId: epoch.rawId,
|
|
157
|
+
currentIndex,
|
|
158
|
+
timestampSeconds,
|
|
159
|
+
validIndexes
|
|
160
|
+
});
|
|
161
|
+
const iq = (0, mobile_1.buildKeyIndexListPublishIq)({ keyIndexListBytes, timestampSeconds });
|
|
162
|
+
const result = await this.queryWithContext('companion-host.key-index-list', iq, KEY_INDEX_TIMEOUT_MS);
|
|
163
|
+
(0, _transport_1.assertIqResult)(result, 'companion-host.key-index-list');
|
|
164
|
+
this.logger.debug('key-index list published', { currentIndex, validIndexes });
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Unlinks a companion this primary linked: sends the `remove-companion-device`
|
|
168
|
+
* IQ, drops it from the tracked set, and republishes the key-index list for
|
|
169
|
+
* the reduced device set.
|
|
170
|
+
*
|
|
171
|
+
* @throws when there is no registered primary session, the companion is not
|
|
172
|
+
* tracked, or the server rejects the removal.
|
|
173
|
+
*/
|
|
174
|
+
async revokeCompanion(companionDeviceJid, reason = 'user_initiated') {
|
|
175
|
+
this.requirePrimaryIdentityKeyPair();
|
|
176
|
+
const epoch = await this.ensureEpoch();
|
|
177
|
+
if (!epoch.companions.some((companion) => companion.deviceJid === companionDeviceJid)) {
|
|
178
|
+
throw new Error(`companion ${companionDeviceJid} is not tracked by this primary`);
|
|
179
|
+
}
|
|
180
|
+
const result = await this.queryWithContext('companion-host.remove-device', (0, mobile_1.buildRemoveCompanionDeviceIq)({ deviceJid: companionDeviceJid, reason }), REMOVE_DEVICE_TIMEOUT_MS);
|
|
181
|
+
(0, _transport_1.assertIqResult)(result, 'companion-host.remove-device');
|
|
182
|
+
this.epoch = {
|
|
183
|
+
rawId: epoch.rawId,
|
|
184
|
+
currentKeyIndex: epoch.currentKeyIndex,
|
|
185
|
+
companions: epoch.companions.filter((companion) => companion.deviceJid !== companionDeviceJid)
|
|
186
|
+
};
|
|
187
|
+
this.forgetAccountKeyIndex((0, jid_1.parseSignalAddressFromJid)(companionDeviceJid).device);
|
|
188
|
+
await this.persist(this.epoch);
|
|
189
|
+
await this.publishKeyIndexList().catch((error) => {
|
|
190
|
+
this.logger.warn('key-index list republish failed after companion revoke', {
|
|
191
|
+
deviceJid: companionDeviceJid,
|
|
192
|
+
message: (0, primitives_1.toError)(error).message
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
this.logger.info('companion revoked', { deviceJid: companionDeviceJid, reason });
|
|
196
|
+
this.emitEvent('companion_host_revoked', { deviceJid: companionDeviceJid });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Unlinks EVERY companion from the account in a single `remove-companion-device`
|
|
200
|
+
* `all="true"` stanza (the phone's "log out all companion devices"), clears the
|
|
201
|
+
* tracked set, and republishes the key-index list for the primary alone.
|
|
202
|
+
* `excludeHostedCompanion` spares companions this account itself hosts.
|
|
203
|
+
*
|
|
204
|
+
* @throws when there is no registered primary session or the server rejects
|
|
205
|
+
* the removal.
|
|
206
|
+
*/
|
|
207
|
+
async revokeAllCompanions(reason = 'user_initiated', options = {}) {
|
|
208
|
+
this.requirePrimaryIdentityKeyPair();
|
|
209
|
+
const epoch = await this.ensureEpoch();
|
|
210
|
+
const result = await this.queryWithContext('companion-host.remove-all-devices', (0, mobile_1.buildRemoveAllCompanionDevicesIq)({
|
|
211
|
+
reason,
|
|
212
|
+
excludeHostedCompanion: options.excludeHostedCompanion
|
|
213
|
+
}), REMOVE_DEVICE_TIMEOUT_MS);
|
|
214
|
+
(0, _transport_1.assertIqResult)(result, 'companion-host.remove-all-devices');
|
|
215
|
+
if (options.excludeHostedCompanion) {
|
|
216
|
+
this.logger.info('revoked non-hosted companions; hosted set kept', {
|
|
217
|
+
kept: epoch.companions.length,
|
|
218
|
+
reason
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const removed = epoch.companions.map((companion) => companion.deviceJid);
|
|
223
|
+
this.epoch = { rawId: epoch.rawId, currentKeyIndex: epoch.currentKeyIndex, companions: [] };
|
|
224
|
+
this.accountKeyIndexes = new Set([0]);
|
|
225
|
+
await this.persist(this.epoch);
|
|
226
|
+
await this.publishKeyIndexList().catch((error) => {
|
|
227
|
+
this.logger.warn('key-index list republish failed after revoke-all', {
|
|
228
|
+
message: (0, primitives_1.toError)(error).message
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
this.logger.info('all companions revoked', { count: removed.length, reason });
|
|
232
|
+
for (const deviceJid of removed) {
|
|
233
|
+
this.emitEvent('companion_host_revoked', { deviceJid });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Returns the companions this primary has linked in the current epoch. */
|
|
237
|
+
async listCompanions() {
|
|
238
|
+
const epoch = await this.ensureEpoch();
|
|
239
|
+
return epoch.companions;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Reconciles the tracked companion set against the account's live device list
|
|
243
|
+
* on the server (`usync`). Drops companions the server no longer lists - one
|
|
244
|
+
* the user unlinked, or one that self-removed while the primary was offline -
|
|
245
|
+
* then persists and emits `companion_host_revoked` for each. Runs on connect
|
|
246
|
+
* and on `account_sync`; safe to call manually. A no-op (no server query, no
|
|
247
|
+
* epoch created) when no companions are tracked. Returns the removed jids.
|
|
248
|
+
*/
|
|
249
|
+
async reconcileCompanions() {
|
|
250
|
+
if (!this.isMobilePrimary()) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const meJid = this.authClient.getCurrentCredentials()?.meJid;
|
|
254
|
+
if (!meJid) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const epoch = this.epoch ?? (this.persistence ? await this.persistence.load() : null);
|
|
258
|
+
if (!epoch || epoch.companions.length === 0) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
this.epoch = epoch;
|
|
262
|
+
const [synced] = await this.deviceSync.syncDeviceList([meJid]);
|
|
263
|
+
const serverJids = synced?.deviceJids;
|
|
264
|
+
if (!serverJids || serverJids.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
return this.pruneCompanionsToDevices(epoch, new Set(serverJids.map((jid) => (0, jid_1.parseSignalAddressFromJid)(jid).device)));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Prunes tracked companions to those whose device slot is still in
|
|
271
|
+
* `serverDeviceIndexes`, persisting + emitting `companion_host_revoked` for
|
|
272
|
+
* each drop. Shared by the connect-time (`usync`) and `account_sync` paths.
|
|
273
|
+
*/
|
|
274
|
+
async pruneCompanionsToDevices(epoch, serverDeviceIndexes) {
|
|
275
|
+
const kept = epoch.companions.filter((companion) => serverDeviceIndexes.has((0, jid_1.parseSignalAddressFromJid)(companion.deviceJid).device));
|
|
276
|
+
if (kept.length === epoch.companions.length) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const removed = epoch.companions
|
|
280
|
+
.filter((companion) => !serverDeviceIndexes.has((0, jid_1.parseSignalAddressFromJid)(companion.deviceJid).device))
|
|
281
|
+
.map((companion) => companion.deviceJid);
|
|
282
|
+
this.epoch = {
|
|
283
|
+
rawId: epoch.rawId,
|
|
284
|
+
currentKeyIndex: epoch.currentKeyIndex,
|
|
285
|
+
companions: kept
|
|
286
|
+
};
|
|
287
|
+
await this.persist(this.epoch);
|
|
288
|
+
this.logger.info('reconciled companions with server device list', {
|
|
289
|
+
kept: kept.length,
|
|
290
|
+
removed: removed.length
|
|
291
|
+
});
|
|
292
|
+
for (const deviceJid of removed) {
|
|
293
|
+
this.emitEvent('companion_host_revoked', { deviceJid });
|
|
294
|
+
}
|
|
295
|
+
return removed;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Reconciles from an `account_sync` notification's `<devices>` payload - the
|
|
299
|
+
* fresh, authoritative device set - so a companion the user just unlinked is
|
|
300
|
+
* pruned immediately (no `usync` round-trip, which would read a stale cache).
|
|
301
|
+
*/
|
|
302
|
+
async reconcileFromAccountSync(node) {
|
|
303
|
+
if (!this.isMobilePrimary()) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const devices = (0, _transport_1.findNodeChild)(node, nodes_1.WA_NODE_TAGS.DEVICES);
|
|
307
|
+
if (!devices) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const epoch = this.epoch ?? (this.persistence ? await this.persistence.load() : null);
|
|
311
|
+
if (!epoch || epoch.companions.length === 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
this.epoch = epoch;
|
|
315
|
+
const serverDeviceIndexes = new Set([0]);
|
|
316
|
+
for (const child of (0, _transport_1.getNodeChildren)(devices)) {
|
|
317
|
+
if (child.tag !== nodes_1.WA_NODE_TAGS.DEVICE) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const jid = child.attrs.jid;
|
|
321
|
+
if (jid) {
|
|
322
|
+
serverDeviceIndexes.add((0, jid_1.parseSignalAddressFromJid)(jid).device);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
await this.pruneCompanionsToDevices(epoch, serverDeviceIndexes);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Shares the primary's active app-state sync key with a linked companion via
|
|
329
|
+
* an `APP_STATE_SYNC_KEY_SHARE` peer message so it can decrypt app-state.
|
|
330
|
+
* Establishes the outbound Signal session on demand, so call it once the
|
|
331
|
+
* companion is online (prekeys uploaded), not at link time.
|
|
332
|
+
*
|
|
333
|
+
* @throws when there is no primary session or no active sync key.
|
|
334
|
+
*/
|
|
335
|
+
async shareAppStateSyncKeys(companionDeviceJid) {
|
|
336
|
+
this.requirePrimaryIdentityKeyPair();
|
|
337
|
+
const activeKey = await this.appStateStore.getActiveSyncKey();
|
|
338
|
+
if (!activeKey) {
|
|
339
|
+
throw new Error('no active app-state sync key to share; the primary session is not initialized');
|
|
340
|
+
}
|
|
341
|
+
await this.messageDispatch.sendAppStateSyncKeyShare(companionDeviceJid, [activeKey]);
|
|
342
|
+
this.logger.info('shared app-state sync key with companion', {
|
|
343
|
+
deviceJid: companionDeviceJid
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Pushes the `INITIAL_BOOTSTRAP` history sync to a linked companion as a
|
|
348
|
+
* `HISTORY_SYNC_NOTIFICATION` peer message. Flips the companion's
|
|
349
|
+
* `initialChatHistory` bootstrap flag; without it the companion self-removes
|
|
350
|
+
* with `HistorySyncTimeout`. Requires the companion online with prekeys.
|
|
351
|
+
*
|
|
352
|
+
* @throws when there is no registered primary session.
|
|
353
|
+
*/
|
|
354
|
+
async sendHistorySyncBootstrap(companionDeviceJid, options = {}) {
|
|
355
|
+
this.requirePrimaryIdentityKeyPair();
|
|
356
|
+
const { message, payloadBytes } = await (0, companion_host_2.buildHistorySyncBootstrapMessage)({
|
|
357
|
+
phoneNumberToLidMappings: options.phoneNumberToLidMappings
|
|
358
|
+
});
|
|
359
|
+
await this.messageDispatch.publishProtocolMessageToDevice(companionDeviceJid, message);
|
|
360
|
+
this.logger.info('companion-host history sync bootstrap sent', {
|
|
361
|
+
deviceJid: companionDeviceJid,
|
|
362
|
+
mappings: options.phoneNumberToLidMappings?.length ?? 0,
|
|
363
|
+
payloadBytes
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* `ClientPairingProps` for the `<client-props>` pair-device element. A
|
|
368
|
+
* LID-native primary declares the chat DB LID-migrated + pure-LID session so
|
|
369
|
+
* the companion runs `setIsLidMigrated` at pair time and does not self-remove
|
|
370
|
+
* on its LID-addressed blocklist.
|
|
371
|
+
*/
|
|
372
|
+
buildClientPairingProps() {
|
|
373
|
+
const meLid = this.authClient.getCurrentCredentials()?.meLid;
|
|
374
|
+
const lidNative = typeof meLid === 'string' && meLid.length > 0;
|
|
375
|
+
return {
|
|
376
|
+
isChatDbLidMigrated: lidNative,
|
|
377
|
+
isSyncdPureLidSession: lidNative,
|
|
378
|
+
isSyncdSnapshotRecoveryEnabled: false
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Seeds the primary's own `setting_pushName` into the `critical_block`
|
|
383
|
+
* app-state collection, once per session. The companion's critical bootstrap
|
|
384
|
+
* completes only when it finds an applied `SettingPushName` action; without
|
|
385
|
+
* one it self-removes with `syncd_timeout`. Falls back to the account phone
|
|
386
|
+
* number when no push name is set. Idempotent and best-effort.
|
|
387
|
+
*/
|
|
388
|
+
async ensureAccountPushNameAppState() {
|
|
389
|
+
if (this.pushNameSeeded) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const credentials = this.authClient.getCurrentCredentials();
|
|
393
|
+
const meJidUser = credentials?.meJid?.split('@')[0]?.split(':')[0];
|
|
394
|
+
const name = credentials?.pushName?.trim() || meJidUser;
|
|
395
|
+
if (!name) {
|
|
396
|
+
throw new Error('cannot seed setting_pushName: primary has no pushName or meJid');
|
|
397
|
+
}
|
|
398
|
+
await this.chatCoordinator.set({ schema: 'SettingPushName', name });
|
|
399
|
+
this.pushNameSeeded = true;
|
|
400
|
+
this.logger.info('seeded primary setting_pushName into critical_block app-state', {
|
|
401
|
+
name
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async completeLink(input) {
|
|
405
|
+
const accountIdentityKeyPair = this.requirePrimaryIdentityKeyPair();
|
|
406
|
+
await this.ensureAccountPushNameAppState().catch((error) => {
|
|
407
|
+
this.logger.warn('failed to seed primary setting_pushName app-state', {
|
|
408
|
+
message: (0, primitives_1.toError)(error).message
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
const epoch = await this.ensureEpoch();
|
|
412
|
+
const keyIndex = this.nextKeyIndex(epoch);
|
|
413
|
+
const validIndexes = [...this.validKeyIndexes(epoch), keyIndex].sort((a, b) => a - b);
|
|
414
|
+
const timestampSeconds = Math.floor(Date.now() / 1000);
|
|
415
|
+
const record = {
|
|
416
|
+
deviceJid: '',
|
|
417
|
+
keyIndex,
|
|
418
|
+
companionIdentityPublicKey: input.companionIdentityPublicKey,
|
|
419
|
+
addedAtSeconds: timestampSeconds
|
|
420
|
+
};
|
|
421
|
+
const signed = await (0, companion_host_1.buildSignedCompanionIdentity)({
|
|
422
|
+
accountIdentityKeyPair,
|
|
423
|
+
companionIdentityPublicKey: input.companionIdentityPublicKey,
|
|
424
|
+
advSecretKey: input.advSecretKey,
|
|
425
|
+
rawId: epoch.rawId,
|
|
426
|
+
keyIndex,
|
|
427
|
+
timestampSeconds,
|
|
428
|
+
validIndexes
|
|
429
|
+
});
|
|
430
|
+
const iq = (0, mobile_1.buildPairDeviceIq)({
|
|
431
|
+
ref: input.ref,
|
|
432
|
+
companionNoisePublicKey: input.companionNoisePublicKey,
|
|
433
|
+
deviceIdentityBytes: signed.deviceIdentityBytes,
|
|
434
|
+
keyIndexListBytes: signed.keyIndexListBytes,
|
|
435
|
+
keyIndexListTimestampSeconds: timestampSeconds,
|
|
436
|
+
clientProps: this.buildClientPairingProps(),
|
|
437
|
+
pem: this.includePem
|
|
438
|
+
? {
|
|
439
|
+
aesGcmKeyBytes: (0, companion_host_1.derivePemKey)(input.advSecretKey, input.companionNoisePublicKey),
|
|
440
|
+
ttlSeconds: PEM_TTL_SECONDS
|
|
441
|
+
}
|
|
442
|
+
: undefined
|
|
443
|
+
});
|
|
444
|
+
this.logger.info('linking companion', { platform: input.platform, keyIndex });
|
|
445
|
+
const result = await this.queryWithContext('companion-host.pair-device', iq, PAIR_DEVICE_TIMEOUT_MS);
|
|
446
|
+
(0, _transport_1.assertIqResult)(result, 'companion-host.pair-device');
|
|
447
|
+
const wrapper = (0, _transport_1.getFirstNodeChild)(result);
|
|
448
|
+
const deviceNode = (0, _transport_1.findNodeChild)(result, nodes_1.WA_NODE_TAGS.DEVICE) ??
|
|
449
|
+
(wrapper ? (0, _transport_1.findNodeChild)(wrapper, nodes_1.WA_NODE_TAGS.DEVICE) : undefined);
|
|
450
|
+
const deviceJid = deviceNode?.attrs.jid;
|
|
451
|
+
if (!deviceJid) {
|
|
452
|
+
throw new Error('pair-device result missing <device jid>');
|
|
453
|
+
}
|
|
454
|
+
this.epoch = {
|
|
455
|
+
rawId: epoch.rawId,
|
|
456
|
+
currentKeyIndex: keyIndex,
|
|
457
|
+
companions: [...epoch.companions, { ...record, deviceJid }]
|
|
458
|
+
};
|
|
459
|
+
await this.persist(this.epoch);
|
|
460
|
+
const linked = { deviceJid, keyIndex };
|
|
461
|
+
this.logger.info('companion linked', { deviceJid, keyIndex });
|
|
462
|
+
this.emitEvent('companion_host_linked', linked);
|
|
463
|
+
this.provisionLinkedCompanion(deviceJid);
|
|
464
|
+
return linked;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Best-effort background provisioning after a companion links: pushes the
|
|
468
|
+
* history-sync bootstrap and shares the app-state sync key. Fire-and-forget
|
|
469
|
+
* and retried, because the companion needs a moment to upload prekeys before
|
|
470
|
+
* the outbound Signal session these peer messages ride on can be established.
|
|
471
|
+
*/
|
|
472
|
+
provisionLinkedCompanion(deviceJid) {
|
|
473
|
+
void this.provisionLinkedCompanionWithRetry(deviceJid).catch((error) => {
|
|
474
|
+
const normalized = (0, primitives_1.toError)(error);
|
|
475
|
+
this.logger.warn('companion provisioning failed', {
|
|
476
|
+
deviceJid,
|
|
477
|
+
message: normalized.message
|
|
478
|
+
});
|
|
479
|
+
this.emitEvent('companion_host_error', normalized);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async provisionLinkedCompanionWithRetry(deviceJid) {
|
|
483
|
+
let lastError;
|
|
484
|
+
for (let attempt = 1; attempt <= COMPANION_PROVISION_ATTEMPTS; attempt += 1) {
|
|
485
|
+
if (attempt > 1) {
|
|
486
|
+
await (0, async_1.delay)(COMPANION_PROVISION_RETRY_MS);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await this.sendHistorySyncBootstrap(deviceJid);
|
|
490
|
+
await this.shareAppStateSyncKeys(deviceJid).catch((error) => {
|
|
491
|
+
this.logger.debug('companion app-state key share (best-effort) failed', {
|
|
492
|
+
deviceJid,
|
|
493
|
+
message: (0, primitives_1.toError)(error).message
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
this.logger.info('companion provisioned', { deviceJid, attempts: attempt });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
lastError = error;
|
|
501
|
+
this.logger.debug('companion provisioning attempt failed', {
|
|
502
|
+
deviceJid,
|
|
503
|
+
attempt,
|
|
504
|
+
message: (0, primitives_1.toError)(error).message
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
throw (0, primitives_1.toError)(lastError);
|
|
509
|
+
}
|
|
510
|
+
handleNotification(node) {
|
|
511
|
+
if (node.attrs.type === 'account_sync') {
|
|
512
|
+
this.updateAccountKeyIndexes(node);
|
|
513
|
+
void this.reconcileFromAccountSync(node).catch((error) => {
|
|
514
|
+
this.logger.debug('companion reconcile on account_sync failed', {
|
|
515
|
+
message: (0, primitives_1.toError)(error).message
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
return Promise.resolve(false);
|
|
519
|
+
}
|
|
520
|
+
const linkCode = (0, _transport_1.findNodeChild)(node, nodes_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
|
|
521
|
+
if (linkCode) {
|
|
522
|
+
const stage = linkCode.attrs.stage;
|
|
523
|
+
if (stage === 'companion_hello') {
|
|
524
|
+
this.storeCompanionHello(linkCode);
|
|
525
|
+
}
|
|
526
|
+
else if (stage === 'companion_finish') {
|
|
527
|
+
this.resolveCompanionFinish(linkCode);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return Promise.resolve(false);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Tracks the account's live device key-indexes from an `account_sync`
|
|
534
|
+
* notification so a new companion gets a non-colliding key index and the
|
|
535
|
+
* published key-index list keeps every existing device valid.
|
|
536
|
+
*/
|
|
537
|
+
updateAccountKeyIndexes(node) {
|
|
538
|
+
const devices = (0, _transport_1.findNodeChild)(node, nodes_1.WA_NODE_TAGS.DEVICES);
|
|
539
|
+
if (!devices) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const indexes = new Set([0]);
|
|
543
|
+
for (const child of (0, _transport_1.getNodeChildren)(devices)) {
|
|
544
|
+
if (child.tag !== nodes_1.WA_NODE_TAGS.DEVICE) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const rawIndex = child.attrs['key-index'];
|
|
548
|
+
const parsed = rawIndex ? Number.parseInt(rawIndex, 10) : Number.NaN;
|
|
549
|
+
if (Number.isInteger(parsed) && parsed >= 0) {
|
|
550
|
+
indexes.add(parsed);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
this.accountKeyIndexes = indexes;
|
|
554
|
+
this.logger.debug('account device key-indexes updated', { indexes: [...indexes] });
|
|
555
|
+
}
|
|
556
|
+
forgetAccountKeyIndex(index) {
|
|
557
|
+
if (!this.accountKeyIndexes.has(index)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const next = new Set(this.accountKeyIndexes);
|
|
561
|
+
next.delete(index);
|
|
562
|
+
this.accountKeyIndexes = next;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* The key indexes currently valid for this account - index 0 (the primary),
|
|
566
|
+
* the account's own live devices, and every tracked companion. Sorted
|
|
567
|
+
* ascending. Deliberately excludes `epoch.currentKeyIndex`, which is an
|
|
568
|
+
* allocation high-water mark, not proof a device still exists: after revoking
|
|
569
|
+
* the last-linked companion its index must drop out of the published list.
|
|
570
|
+
*/
|
|
571
|
+
validKeyIndexes(epoch) {
|
|
572
|
+
const indexes = new Set([0, ...this.accountKeyIndexes]);
|
|
573
|
+
for (const companion of epoch.companions) {
|
|
574
|
+
indexes.add(companion.keyIndex);
|
|
575
|
+
}
|
|
576
|
+
return [...indexes].sort((a, b) => a - b);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Allocates the next companion key index, advancing strictly past both the
|
|
580
|
+
* epoch's monotonic high-water mark and every currently-valid index so a
|
|
581
|
+
* revoked index is never reused (reuse breaks previously linked devices).
|
|
582
|
+
*/
|
|
583
|
+
nextKeyIndex(epoch) {
|
|
584
|
+
return Math.max(epoch.currentKeyIndex, ...this.validKeyIndexes(epoch)) + 1;
|
|
585
|
+
}
|
|
586
|
+
storeCompanionHello(linkCode) {
|
|
587
|
+
const ref = (0, _transport_1.getNodeTextContent)((0, _transport_1.findNodeChild)(linkCode, nodes_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF));
|
|
588
|
+
const wrapped = this.nodeBytes(linkCode, nodes_1.WA_NODE_TAGS.LINK_CODE_PAIRING_WRAPPED_COMPANION_EPHEMERAL_PUB);
|
|
589
|
+
const authKey = this.nodeBytes(linkCode, nodes_1.WA_NODE_TAGS.COMPANION_SERVER_AUTH_KEY_PUB);
|
|
590
|
+
if (!ref || !wrapped || !authKey) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.pendingCompanionHello = {
|
|
594
|
+
ref,
|
|
595
|
+
wrappedCompanionEphemeralPub: wrapped,
|
|
596
|
+
companionServerAuthKeyPub: authKey
|
|
597
|
+
};
|
|
598
|
+
this.logger.debug('recorded link-code companion_hello', { ref });
|
|
599
|
+
}
|
|
600
|
+
resolveCompanionFinish(linkCode) {
|
|
601
|
+
const ref = (0, _transport_1.getNodeTextContent)((0, _transport_1.findNodeChild)(linkCode, nodes_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF));
|
|
602
|
+
const wrappedKeyBundle = this.nodeBytes(linkCode, nodes_1.WA_NODE_TAGS.LINK_CODE_PAIRING_WRAPPED_KEY_BUNDLE);
|
|
603
|
+
const companionIdentityPub = this.nodeBytes(linkCode, nodes_1.WA_NODE_TAGS.COMPANION_IDENTITY_PUBLIC);
|
|
604
|
+
if (ref && wrappedKeyBundle && companionIdentityPub && this.pendingFinish?.ref === ref) {
|
|
605
|
+
this.pendingFinish.resolve({ companionIdentityPub, wrappedKeyBundle });
|
|
606
|
+
this.pendingFinish = null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
nodeBytes(parent, tag) {
|
|
610
|
+
const content = (0, _transport_1.findNodeChild)(parent, tag)?.content;
|
|
611
|
+
return content instanceof Uint8Array ? content : null;
|
|
612
|
+
}
|
|
613
|
+
requirePrimaryIdentityKeyPair() {
|
|
614
|
+
if (!this.isMobilePrimary()) {
|
|
615
|
+
throw new Error('client.mobile requires a mobile-primary session (connect via mobileTransport / a registered phone identity)');
|
|
616
|
+
}
|
|
617
|
+
const credentials = this.authClient.getCurrentCredentials();
|
|
618
|
+
if (!credentials?.meJid) {
|
|
619
|
+
throw new Error('companion-host requires a registered primary session (no meJid)');
|
|
620
|
+
}
|
|
621
|
+
return credentials.registrationInfo.identityKeyPair;
|
|
622
|
+
}
|
|
623
|
+
async ensureEpoch() {
|
|
624
|
+
if (this.epoch) {
|
|
625
|
+
return this.epoch;
|
|
626
|
+
}
|
|
627
|
+
const loaded = this.persistence ? await this.persistence.load() : null;
|
|
628
|
+
if (loaded) {
|
|
629
|
+
this.epoch = loaded;
|
|
630
|
+
return loaded;
|
|
631
|
+
}
|
|
632
|
+
this.epoch = { rawId: await this.generateRawId(), currentKeyIndex: 0, companions: [] };
|
|
633
|
+
await this.persist(this.epoch);
|
|
634
|
+
return this.epoch;
|
|
635
|
+
}
|
|
636
|
+
async generateRawId() {
|
|
637
|
+
return (0, _crypto_1.randomIntAsync)(1, 2147483647);
|
|
638
|
+
}
|
|
639
|
+
async persist(state) {
|
|
640
|
+
if (this.persistence) {
|
|
641
|
+
await this.persistence.save(state);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
exports.WaMobileCoordinator = WaMobileCoordinator;
|
|
@@ -64,6 +64,7 @@ class WaRetryCoordinator {
|
|
|
64
64
|
signalProtocol: options.signalProtocol,
|
|
65
65
|
sessionResolver: options.sessionResolver,
|
|
66
66
|
getCurrentCredentials: options.getCurrentCredentials,
|
|
67
|
+
isMobilePrimary: options.isMobilePrimary,
|
|
67
68
|
resolveUserIcdc: options.resolveUserIcdc,
|
|
68
69
|
resolvePrivacyTokenNode: options.resolvePrivacyTokenNode
|
|
69
70
|
});
|