zapo-js 1.10.0 → 1.11.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/auth/WaAuthClient.d.ts +5 -0
- package/dist/auth/WaAuthClient.js +5 -0
- package/dist/auth/pairing/WaPairingFlow.d.ts +11 -0
- package/dist/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/client/WaClient.js +2 -0
- package/dist/client/WaClientFactory.js +6 -5
- package/dist/client/connection/WaConnectionManager.d.ts +14 -2
- package/dist/client/connection/WaConnectionManager.js +55 -22
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +16 -4
- package/dist/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +2 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +4 -1
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +5 -1
- package/dist/client/coordinators/WaStreamControlCoordinator.d.ts +2 -0
- package/dist/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/client/messaging/messages.js +24 -8
- package/dist/client/persistence/history-sync.d.ts +5 -1
- package/dist/client/persistence/history-sync.js +172 -38
- package/dist/client/types.d.ts +65 -10
- package/dist/esm/auth/WaAuthClient.js +5 -0
- package/dist/esm/auth/pairing/WaPairingFlow.js +97 -40
- package/dist/esm/client/WaClient.js +2 -0
- package/dist/esm/client/WaClientFactory.js +6 -5
- package/dist/esm/client/connection/WaConnectionManager.js +55 -22
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +4 -8
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +4 -1
- package/dist/esm/client/coordinators/WaStreamControlCoordinator.js +6 -1
- package/dist/esm/client/messaging/messages.js +25 -9
- package/dist/esm/client/persistence/history-sync.js +172 -38
- package/dist/esm/infra/log/types.js +2 -1
- package/dist/esm/message/context-info.js +2 -0
- package/dist/esm/message/encode/content.js +16 -0
- package/dist/esm/message/primitives/incoming.js +12 -5
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/message.js +2 -1
- package/dist/esm/signal/api/SignalDeviceSyncApi.js +64 -32
- package/dist/esm/transport/WaComms.js +94 -16
- package/dist/esm/transport/WaWebSocket.js +79 -32
- package/dist/esm/transport/node/builders/message.js +1 -1
- package/dist/esm/transport/noise/WaNoiseSession.js +6 -5
- package/dist/index.d.ts +3 -3
- package/dist/infra/log/types.d.ts +1 -0
- package/dist/infra/log/types.js +2 -1
- package/dist/message/context-info.js +2 -0
- package/dist/message/encode/content.d.ts +3 -2
- package/dist/message/encode/content.js +17 -0
- package/dist/message/primitives/incoming.js +12 -5
- package/dist/message/types.d.ts +21 -12
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/message.d.ts +1 -0
- package/dist/protocol/message.js +2 -1
- package/dist/signal/api/SignalDeviceSyncApi.d.ts +27 -5
- package/dist/signal/api/SignalDeviceSyncApi.js +64 -32
- package/dist/transport/WaComms.d.ts +9 -1
- package/dist/transport/WaComms.js +94 -16
- package/dist/transport/WaWebSocket.d.ts +8 -2
- package/dist/transport/WaWebSocket.js +79 -32
- package/dist/transport/node/builders/message.js +1 -1
- package/dist/transport/noise/WaNoiseSession.js +6 -5
- package/package.json +1 -1
|
@@ -146,6 +146,11 @@ export declare class WaAuthClient {
|
|
|
146
146
|
* call this. The user then enters the returned code in WhatsApp on their
|
|
147
147
|
* phone under *Linked Devices → Link with phone number instead*.
|
|
148
148
|
*
|
|
149
|
+
* The server binds the code to the connection that requested it. If the
|
|
150
|
+
* socket drops and reconnects first, the client registers the same code on
|
|
151
|
+
* the new connection; if that fails, `auth_pairing_required` fires with
|
|
152
|
+
* `forceManual: true` and a new code must be requested.
|
|
153
|
+
*
|
|
149
154
|
* @example
|
|
150
155
|
* ```ts
|
|
151
156
|
* // Start the handshake; do NOT await – connect() resolves only after pairing
|
|
@@ -307,6 +307,11 @@ class WaAuthClient {
|
|
|
307
307
|
* call this. The user then enters the returned code in WhatsApp on their
|
|
308
308
|
* phone under *Linked Devices → Link with phone number instead*.
|
|
309
309
|
*
|
|
310
|
+
* The server binds the code to the connection that requested it. If the
|
|
311
|
+
* socket drops and reconnects first, the client registers the same code on
|
|
312
|
+
* the new connection; if that fails, `auth_pairing_required` fires with
|
|
313
|
+
* `forceManual: true` and a new code must be requested.
|
|
314
|
+
*
|
|
310
315
|
* @example
|
|
311
316
|
* ```ts
|
|
312
317
|
* // Start the handshake; do NOT await – connect() resolves only after pairing
|
|
@@ -31,6 +31,7 @@ interface WaPairingFlowOptions {
|
|
|
31
31
|
export declare class WaPairingFlow {
|
|
32
32
|
private readonly opts;
|
|
33
33
|
private pairingSession;
|
|
34
|
+
private rearmGeneration;
|
|
34
35
|
private pairSuccessInFlight;
|
|
35
36
|
constructor(options: WaPairingFlowOptions);
|
|
36
37
|
hasPairingSession(): boolean;
|
|
@@ -48,6 +49,16 @@ export declare class WaPairingFlow {
|
|
|
48
49
|
/** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
|
|
49
50
|
private sendPairSuccessValidationError;
|
|
50
51
|
private handlePrimaryHello;
|
|
52
|
+
private sendCompanionHello;
|
|
53
|
+
/**
|
|
54
|
+
* The server delivers `primary_hello` only to the connection that sent the
|
|
55
|
+
* `companion_hello`, so a code requested on a dropped connection is dead.
|
|
56
|
+
* Registers the same code again, without a push, like WA Web's code refresh.
|
|
57
|
+
* The ADV secret is not rotated: the QR just emitted for this connection carries it.
|
|
58
|
+
* Only the latest pair-device may settle the session, since an older attempt can
|
|
59
|
+
* outlive its dropped connection until the IQ times out.
|
|
60
|
+
*/
|
|
61
|
+
private rearmPairingCode;
|
|
51
62
|
private rotateAdvSecret;
|
|
52
63
|
private requireCredentials;
|
|
53
64
|
}
|
|
@@ -19,6 +19,7 @@ const PAIR_SUCCESS_DEVICE_IDENTITY_MIN_BYTES = 1;
|
|
|
19
19
|
const PAIR_SUCCESS_DEVICE_IDENTITY_MAX_BYTES = 500;
|
|
20
20
|
class WaPairingFlow {
|
|
21
21
|
constructor(options) {
|
|
22
|
+
this.rearmGeneration = 0;
|
|
22
23
|
this.pairSuccessInFlight = false;
|
|
23
24
|
this.opts = options;
|
|
24
25
|
this.pairingSession = null;
|
|
@@ -38,49 +39,17 @@ class WaPairingFlow {
|
|
|
38
39
|
const normalizedCustomCode = customCode !== undefined ? (0, pairing_code_crypto_1.normalizeCustomPairingCode)(customCode) : undefined;
|
|
39
40
|
const credentials = this.requireCredentials();
|
|
40
41
|
const phoneJid = (0, jid_1.parsePhoneJid)(phoneNumber);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
(0, pairing_code_crypto_1.createCompanionHello)({ customCode: normalizedCustomCode }),
|
|
42
|
+
const [session] = await Promise.all([
|
|
43
|
+
this.sendCompanionHello(phoneJid, shouldShowPushNotification, normalizedCustomCode),
|
|
44
44
|
this.rotateAdvSecret(credentials)
|
|
45
45
|
]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
phoneJid,
|
|
49
|
-
shouldShowPushNotification,
|
|
50
|
-
wrappedCompanionEphemeralPub: companionHello.wrappedCompanionEphemeralPub,
|
|
51
|
-
companionServerAuthKeyPub: refreshedCredentials.noiseKeyPair.pubKey,
|
|
52
|
-
companionPlatformId: this.opts.device.platform,
|
|
53
|
-
companionPlatformDisplay: `${browserDisplayName} (${this.opts.device.osDisplayName})`
|
|
54
|
-
}), constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS);
|
|
55
|
-
this.opts.logger.debug('pairing code request response received', {
|
|
56
|
-
responseTag: response.tag,
|
|
57
|
-
responseType: response.attrs.type
|
|
58
|
-
});
|
|
59
|
-
(0, query_1.assertIqResult)(response, 'companion hello');
|
|
60
|
-
const linkCodeNode = (0, helpers_1.findNodeChild)(response, constants_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
|
|
61
|
-
if (!linkCodeNode) {
|
|
62
|
-
throw new Error('companion hello response missing link_code_companion_reg');
|
|
63
|
-
}
|
|
64
|
-
const refNode = (0, helpers_1.findNodeChild)(linkCodeNode, constants_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF);
|
|
65
|
-
if (!refNode) {
|
|
66
|
-
throw new Error('companion hello response missing link_code_pairing_ref');
|
|
67
|
-
}
|
|
68
|
-
const ref = (0, helpers_1.decodeNodeContentUtf8OrBytes)(refNode.content, 'link_code_pairing_ref');
|
|
69
|
-
this.pairingSession = {
|
|
70
|
-
pairingCode: companionHello.pairingCode,
|
|
71
|
-
phoneJid,
|
|
72
|
-
ref,
|
|
73
|
-
createdAtSeconds,
|
|
74
|
-
companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
|
|
75
|
-
attempts: 0,
|
|
76
|
-
finished: false
|
|
77
|
-
};
|
|
78
|
-
this.opts.callbacks.emitPairingCode(companionHello.pairingCode);
|
|
46
|
+
this.pairingSession = session;
|
|
47
|
+
this.opts.callbacks.emitPairingCode(session.pairingCode);
|
|
79
48
|
this.opts.logger.debug('pairing code emitted', {
|
|
80
49
|
phoneJid,
|
|
81
|
-
createdAtSeconds:
|
|
50
|
+
createdAtSeconds: session.createdAtSeconds
|
|
82
51
|
});
|
|
83
|
-
return
|
|
52
|
+
return session.pairingCode;
|
|
84
53
|
}
|
|
85
54
|
async fetchPairingCountryCodeIso() {
|
|
86
55
|
this.opts.logger.trace('fetching pairing country code ISO');
|
|
@@ -170,6 +139,15 @@ class WaPairingFlow {
|
|
|
170
139
|
await this.opts.socket.sendNode((0, global_1.buildIqResultNode)(iqNode));
|
|
171
140
|
this.opts.qrFlow.setRefs(refs);
|
|
172
141
|
this.opts.logger.debug('pair-device refs updated', { refsCount: refs.length });
|
|
142
|
+
// The server sends pair-device once per connection: a pending code belongs to an older one.
|
|
143
|
+
if (this.pairingSession) {
|
|
144
|
+
const generation = ++this.rearmGeneration;
|
|
145
|
+
void this.rearmPairingCode(this.pairingSession, generation).catch((error) => {
|
|
146
|
+
this.opts.logger.warn('pairing code re-registration failed unexpectedly', {
|
|
147
|
+
message: (0, primitives_1.toError)(error).message
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
173
151
|
}
|
|
174
152
|
async handlePairSuccess(iqNode, pairSuccessNode) {
|
|
175
153
|
if (this.pairSuccessInFlight) {
|
|
@@ -231,7 +209,13 @@ class WaPairingFlow {
|
|
|
231
209
|
await this.sendPairSuccessValidationError(iqNode);
|
|
232
210
|
throw new Error('pair-success account signature validation failed');
|
|
233
211
|
}
|
|
234
|
-
const { signedIdentity, keyIndex, responseIdentityBytes } = built;
|
|
212
|
+
const { signedIdentity, keyIndex, responseIdentityBytes, advDeviceIdentity } = built;
|
|
213
|
+
this.opts.logger.debug('pair-success adv identity', {
|
|
214
|
+
envelopeAccountType: wrappedIdentity.accountType ?? null,
|
|
215
|
+
accountType: advDeviceIdentity.accountType ?? null,
|
|
216
|
+
deviceType: advDeviceIdentity.deviceType ?? null,
|
|
217
|
+
keyIndex: advDeviceIdentity.keyIndex ?? null
|
|
218
|
+
});
|
|
235
219
|
const nextCredentials = {
|
|
236
220
|
...credentials,
|
|
237
221
|
signedIdentity,
|
|
@@ -301,7 +285,8 @@ class WaPairingFlow {
|
|
|
301
285
|
return {
|
|
302
286
|
signedIdentity,
|
|
303
287
|
keyIndex: advDeviceIdentity.keyIndex ?? 0,
|
|
304
|
-
responseIdentityBytes
|
|
288
|
+
responseIdentityBytes,
|
|
289
|
+
advDeviceIdentity
|
|
305
290
|
};
|
|
306
291
|
}
|
|
307
292
|
/** Mirrors the `not-authorized` IQ error WhatsApp Web returns on a failed pair-success. */
|
|
@@ -377,6 +362,78 @@ class WaPairingFlow {
|
|
|
377
362
|
pairingSession.finished = true;
|
|
378
363
|
this.opts.logger.debug('primary_hello completed with companion_finish success');
|
|
379
364
|
}
|
|
365
|
+
async sendCompanionHello(phoneJid, shouldShowPushNotification, customCode) {
|
|
366
|
+
const credentials = this.requireCredentials();
|
|
367
|
+
const createdAtSeconds = Math.floor(Date.now() / 1000);
|
|
368
|
+
const companionHello = await (0, pairing_code_crypto_1.createCompanionHello)({ customCode });
|
|
369
|
+
const browserDisplayName = (0, browser_1.getWaBrowserDisplayName)(this.opts.device.browser);
|
|
370
|
+
const response = await this.opts.socket.query((0, pairing_1.buildCompanionHelloRequestNode)({
|
|
371
|
+
phoneJid,
|
|
372
|
+
shouldShowPushNotification,
|
|
373
|
+
wrappedCompanionEphemeralPub: companionHello.wrappedCompanionEphemeralPub,
|
|
374
|
+
companionServerAuthKeyPub: credentials.noiseKeyPair.pubKey,
|
|
375
|
+
companionPlatformId: this.opts.device.platform,
|
|
376
|
+
companionPlatformDisplay: `${browserDisplayName} (${this.opts.device.osDisplayName})`
|
|
377
|
+
}), constants_1.WA_DEFAULTS.IQ_TIMEOUT_MS);
|
|
378
|
+
this.opts.logger.debug('pairing code request response received', {
|
|
379
|
+
responseTag: response.tag,
|
|
380
|
+
responseType: response.attrs.type
|
|
381
|
+
});
|
|
382
|
+
(0, query_1.assertIqResult)(response, 'companion hello');
|
|
383
|
+
const linkCodeNode = (0, helpers_1.findNodeChild)(response, constants_1.WA_NODE_TAGS.LINK_CODE_COMPANION_REG);
|
|
384
|
+
if (!linkCodeNode) {
|
|
385
|
+
throw new Error('companion hello response missing link_code_companion_reg');
|
|
386
|
+
}
|
|
387
|
+
const refNode = (0, helpers_1.findNodeChild)(linkCodeNode, constants_1.WA_NODE_TAGS.LINK_CODE_PAIRING_REF);
|
|
388
|
+
if (!refNode) {
|
|
389
|
+
throw new Error('companion hello response missing link_code_pairing_ref');
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
pairingCode: companionHello.pairingCode,
|
|
393
|
+
phoneJid,
|
|
394
|
+
ref: (0, helpers_1.decodeNodeContentUtf8OrBytes)(refNode.content, 'link_code_pairing_ref'),
|
|
395
|
+
createdAtSeconds,
|
|
396
|
+
companionEphemeralKeyPair: companionHello.companionEphemeralKeyPair,
|
|
397
|
+
attempts: 0,
|
|
398
|
+
finished: false
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The server delivers `primary_hello` only to the connection that sent the
|
|
403
|
+
* `companion_hello`, so a code requested on a dropped connection is dead.
|
|
404
|
+
* Registers the same code again, without a push, like WA Web's code refresh.
|
|
405
|
+
* The ADV secret is not rotated: the QR just emitted for this connection carries it.
|
|
406
|
+
* Only the latest pair-device may settle the session, since an older attempt can
|
|
407
|
+
* outlive its dropped connection until the IQ times out.
|
|
408
|
+
*/
|
|
409
|
+
async rearmPairingCode(previous, generation) {
|
|
410
|
+
const isCurrent = () => this.pairingSession === previous && this.rearmGeneration === generation;
|
|
411
|
+
try {
|
|
412
|
+
const session = await this.sendCompanionHello(previous.phoneJid, false, previous.pairingCode);
|
|
413
|
+
if (!isCurrent()) {
|
|
414
|
+
this.opts.logger.debug('pairing code re-registration superseded');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.pairingSession = session;
|
|
418
|
+
this.opts.logger.info('pairing code re-registered on the new connection', {
|
|
419
|
+
phoneJid: session.phoneJid
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const message = (0, primitives_1.toError)(error).message;
|
|
424
|
+
if (!isCurrent()) {
|
|
425
|
+
this.opts.logger.debug('superseded pairing code re-registration failed', {
|
|
426
|
+
message
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
this.pairingSession = null;
|
|
431
|
+
this.opts.logger.warn('failed to re-register pairing code on the new connection', {
|
|
432
|
+
message
|
|
433
|
+
});
|
|
434
|
+
this.opts.callbacks.emitPairingRefresh(true);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
380
437
|
async rotateAdvSecret(credentials) {
|
|
381
438
|
const nextCredentials = {
|
|
382
439
|
...credentials,
|
package/dist/client/WaClient.js
CHANGED
|
@@ -275,6 +275,8 @@ class WaClientImpl extends node_events_1.EventEmitter {
|
|
|
275
275
|
mediaTransfer: this.mediaTransfer,
|
|
276
276
|
chatMetadataStore: this.stores.chatMetadata,
|
|
277
277
|
writeBehind: this.writeBehind,
|
|
278
|
+
chunkId: event.key.id,
|
|
279
|
+
sink: this.options.history?.sink,
|
|
278
280
|
emitEvent: this.emit.bind(this),
|
|
279
281
|
onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
|
|
280
282
|
onNctSalt: (salt) => this.deps.trustedContactToken.hydrateNctSaltFromHistorySync(salt),
|
|
@@ -211,7 +211,7 @@ function buildWaClientDependencies(input) {
|
|
|
211
211
|
let connectionManager = null;
|
|
212
212
|
let passiveTasks = null;
|
|
213
213
|
let mediaConnCacheFallback = null;
|
|
214
|
-
let
|
|
214
|
+
let awaitServerRestartAfterPairing = () => undefined;
|
|
215
215
|
const serverClock = (0, clock_1.createServerClock)(() => connectionManager?.getClockSkewMs() ?? null);
|
|
216
216
|
const nodeTransport = new WaNodeTransport_1.WaNodeTransport(logger);
|
|
217
217
|
const nodeOrchestrator = new WaNodeOrchestrator_1.WaNodeOrchestrator({
|
|
@@ -338,7 +338,7 @@ function buildWaClientDependencies(input) {
|
|
|
338
338
|
deviceOsDisplayName: options.deviceOsDisplayName,
|
|
339
339
|
deviceOsVersion: options.deviceOsVersion,
|
|
340
340
|
devicePlatform: options.devicePlatform,
|
|
341
|
-
requireFullSync: options.requireFullSync,
|
|
341
|
+
requireFullSync: options.history?.requireFullSync ?? options.requireFullSync,
|
|
342
342
|
version: options.version,
|
|
343
343
|
dangerous: options.dangerous,
|
|
344
344
|
mobileTransport: options.mobileTransport
|
|
@@ -358,7 +358,7 @@ function buildWaClientDependencies(input) {
|
|
|
358
358
|
onPairingRefresh: (forceManual) => runtime.emitEvent('auth_pairing_required', { forceManual }),
|
|
359
359
|
onPaired: (credentials) => {
|
|
360
360
|
runtime.emitEvent('auth_paired', { credentials });
|
|
361
|
-
|
|
361
|
+
awaitServerRestartAfterPairing();
|
|
362
362
|
},
|
|
363
363
|
onError: (error) => runtime.handleError(error),
|
|
364
364
|
onPasskeyRequired: (hasSigner) => runtime.emitEvent('auth_passkey_required', { hasSigner }),
|
|
@@ -671,7 +671,7 @@ function buildWaClientDependencies(input) {
|
|
|
671
671
|
if (mediaConnCacheFallback !== null) {
|
|
672
672
|
connectionManager.setMediaConnCache(mediaConnCacheFallback);
|
|
673
673
|
}
|
|
674
|
-
|
|
674
|
+
awaitServerRestartAfterPairing = () => connectionManager?.awaitServerRestartAfterPairing();
|
|
675
675
|
const disconnectWithClientSideEffects = async (reason, isLogout, code) => {
|
|
676
676
|
abPropsCoordinator.reset();
|
|
677
677
|
offlineResume.reset();
|
|
@@ -712,7 +712,8 @@ function buildWaClientDependencies(input) {
|
|
|
712
712
|
},
|
|
713
713
|
disconnect: disconnectWithClientSideEffects,
|
|
714
714
|
clearStoredCredentials: clearStoredCredentialsWithClientSideEffects,
|
|
715
|
-
connect: connectWithClientSideEffects
|
|
715
|
+
connect: connectWithClientSideEffects,
|
|
716
|
+
handlePairingRestart: () => connectionManager?.handleServerRestartRequest() ?? false
|
|
716
717
|
});
|
|
717
718
|
const incomingMessageAckOptions = {
|
|
718
719
|
logger,
|
|
@@ -33,14 +33,24 @@ export declare class WaConnectionManager {
|
|
|
33
33
|
private mediaConnCache;
|
|
34
34
|
private connectPromise;
|
|
35
35
|
private pairingReconnectPromise;
|
|
36
|
-
private
|
|
36
|
+
private pairRestartTimer;
|
|
37
37
|
private frameHandler;
|
|
38
38
|
private pendingComms;
|
|
39
39
|
private lifecycleGeneration;
|
|
40
40
|
private lifecycleQueue;
|
|
41
41
|
constructor(options: WaConnectionManagerOptions);
|
|
42
42
|
connect(frameHandler: (frame: Uint8Array) => Promise<void>): Promise<void>;
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Starts the post-pairing restart window: the server ends the unregistered connection
|
|
45
|
+
* itself (`stream:error` 515, as WA Web waits for), so the socket stays open until that
|
|
46
|
+
* signal, a server close, or the fallback timer, whichever comes first.
|
|
47
|
+
*/
|
|
48
|
+
awaitServerRestartAfterPairing(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Claims a `stream:error` 515 for the post-pairing restart. Returns `true` while the
|
|
51
|
+
* window is open or its restart is running, so the generic force-login path must skip it.
|
|
52
|
+
*/
|
|
53
|
+
handleServerRestartRequest(): boolean;
|
|
44
54
|
disconnect(): Promise<void>;
|
|
45
55
|
clearStoredCredentials(): Promise<void>;
|
|
46
56
|
isConnected(): boolean;
|
|
@@ -51,6 +61,8 @@ export declare class WaConnectionManager {
|
|
|
51
61
|
updateClockSkewFromSuccess(serverUnixSeconds: number): void;
|
|
52
62
|
setClockSkewMs(clockSkewMs: number, source: string): void;
|
|
53
63
|
private connectInternal;
|
|
64
|
+
private startPairRestart;
|
|
65
|
+
private clearPairRestartTimer;
|
|
54
66
|
private reconnectAsRegisteredAfterPairing;
|
|
55
67
|
private reconnectAsRegisteredAfterPairingInternal;
|
|
56
68
|
private disconnectInternal;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WaConnectionManager = void 0;
|
|
4
|
+
const constants_1 = require("../../protocol/constants");
|
|
4
5
|
const WaComms_1 = require("../../transport/WaComms");
|
|
5
6
|
const primitives_1 = require("../../util/primitives");
|
|
6
7
|
class WaConnectionManager {
|
|
@@ -19,7 +20,7 @@ class WaConnectionManager {
|
|
|
19
20
|
this.mediaConnCache = null;
|
|
20
21
|
this.connectPromise = null;
|
|
21
22
|
this.pairingReconnectPromise = null;
|
|
22
|
-
this.
|
|
23
|
+
this.pairRestartTimer = null;
|
|
23
24
|
this.frameHandler = null;
|
|
24
25
|
this.pendingComms = null;
|
|
25
26
|
this.lifecycleGeneration = 0;
|
|
@@ -43,26 +44,37 @@ class WaConnectionManager {
|
|
|
43
44
|
this.connectPromise = promise;
|
|
44
45
|
return this.connectPromise;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Starts the post-pairing restart window: the server ends the unregistered connection
|
|
49
|
+
* itself (`stream:error` 515, as WA Web waits for), so the socket stays open until that
|
|
50
|
+
* signal, a server close, or the fallback timer, whichever comes first.
|
|
51
|
+
*/
|
|
52
|
+
awaitServerRestartAfterPairing() {
|
|
53
|
+
const comms = this.comms;
|
|
54
|
+
if (!comms) {
|
|
55
|
+
this.logger.trace('pairing restart wait skipped: no active comms');
|
|
56
|
+
return;
|
|
50
57
|
}
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
this.clearPairRestartTimer();
|
|
59
|
+
this.logger.debug('wa client waiting for server restart after pairing', {
|
|
60
|
+
fallbackMs: constants_1.WA_DEFAULTS.PAIRING_RESTART_WAIT_MS
|
|
61
|
+
});
|
|
62
|
+
this.pairRestartTimer = setTimeout(() => this.startPairRestart('fallback_timeout'), constants_1.WA_DEFAULTS.PAIRING_RESTART_WAIT_MS);
|
|
63
|
+
comms.holdForServerRestart(() => this.startPairRestart('socket_closed'));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Claims a `stream:error` 515 for the post-pairing restart. Returns `true` while the
|
|
67
|
+
* window is open or its restart is running, so the generic force-login path must skip it.
|
|
68
|
+
*/
|
|
69
|
+
handleServerRestartRequest() {
|
|
70
|
+
if (this.pairRestartTimer) {
|
|
71
|
+
this.startPairRestart('stream_error_515');
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return this.pairingReconnectPromise !== null;
|
|
64
75
|
}
|
|
65
76
|
async disconnect() {
|
|
77
|
+
this.clearPairRestartTimer();
|
|
66
78
|
const lifecycleGeneration = this.nextLifecycleGeneration();
|
|
67
79
|
this.invalidatePendingComms('failed to stop pending comms during disconnect');
|
|
68
80
|
await this.runLifecycleOperation(async () => {
|
|
@@ -140,6 +152,30 @@ class WaConnectionManager {
|
|
|
140
152
|
this.assertLifecycleCurrent(lifecycleGeneration, 'connect');
|
|
141
153
|
this.logger.info('wa client connected');
|
|
142
154
|
}
|
|
155
|
+
startPairRestart(trigger) {
|
|
156
|
+
if (!this.pairRestartTimer) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.clearPairRestartTimer();
|
|
160
|
+
this.logger.debug('wa client restarting after pairing', { trigger });
|
|
161
|
+
void this.reconnectAsRegisteredAfterPairing().catch((error) => {
|
|
162
|
+
if (this.isLifecycleSupersededError(error)) {
|
|
163
|
+
this.logger.trace('pairing reconnect canceled by newer lifecycle');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.logger.warn('failed to reconnect after pairing', {
|
|
167
|
+
meJid: this.authClient.getCurrentCredentials()?.meJid,
|
|
168
|
+
message: (0, primitives_1.toError)(error).message
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
clearPairRestartTimer() {
|
|
173
|
+
if (!this.pairRestartTimer) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
clearTimeout(this.pairRestartTimer);
|
|
177
|
+
this.pairRestartTimer = null;
|
|
178
|
+
}
|
|
143
179
|
async reconnectAsRegisteredAfterPairing() {
|
|
144
180
|
if (this.pairingReconnectPromise) {
|
|
145
181
|
this.logger.trace('pairing reconnect already in-flight');
|
|
@@ -188,10 +224,7 @@ class WaConnectionManager {
|
|
|
188
224
|
return;
|
|
189
225
|
}
|
|
190
226
|
this.logger.debug('wa client disconnect start');
|
|
191
|
-
|
|
192
|
-
clearTimeout(this.pairingReconnectTimeout);
|
|
193
|
-
this.pairingReconnectTimeout = null;
|
|
194
|
-
}
|
|
227
|
+
this.clearPairRestartTimer();
|
|
195
228
|
this.keepAlive.stop();
|
|
196
229
|
this.nodeOrchestrator.clearPending(new Error('client disconnected'));
|
|
197
230
|
this.clockSkewMs = null;
|
|
@@ -110,6 +110,13 @@ export interface WaRequestHistorySyncInput {
|
|
|
110
110
|
*/
|
|
111
111
|
readonly count?: number;
|
|
112
112
|
}
|
|
113
|
+
/** Options for {@link WaMessageCoordinator.syncSignalSession}. */
|
|
114
|
+
export interface WaSyncSignalSessionOptions {
|
|
115
|
+
/** Drops the session, keeping the identity, and fetches a fresh one. */
|
|
116
|
+
readonly force?: boolean;
|
|
117
|
+
/** The identity changed: drops the session and fetches with `reason="identity"`. */
|
|
118
|
+
readonly reasonIdentity?: boolean;
|
|
119
|
+
}
|
|
113
120
|
export interface WaReachoutTimelock {
|
|
114
121
|
readonly isActive: boolean;
|
|
115
122
|
readonly enforcementType: string | null;
|
|
@@ -239,11 +246,16 @@ export declare class WaMessageCoordinator {
|
|
|
239
246
|
*/
|
|
240
247
|
getNewChatMessageCapping(type?: WaMessageCappingType): Promise<WaMessageCappingInfo>;
|
|
241
248
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
249
|
+
* Syncs the Signal session of the device `jid` resolves to (a user JID is
|
|
250
|
+
* device 0). With no options it only ensures a session exists. `force`
|
|
251
|
+
* rebuilds it, keeping the identity. `reasonIdentity` rebuilds it as an
|
|
252
|
+
* identity change and reissues the trusted-contact token; it wins over
|
|
253
|
+
* `force`. Throws for group JIDs and when the key-bundle fetch fails, in
|
|
254
|
+
* which case the old session is already gone.
|
|
245
255
|
*/
|
|
246
|
-
syncSignalSession(jid: string,
|
|
256
|
+
syncSignalSession(jid: string, options?: WaSyncSignalSessionOptions): Promise<void>;
|
|
257
|
+
/** @deprecated Pass `{ reasonIdentity: true }` instead. */
|
|
258
|
+
syncSignalSession(jid: string, reasonIdentity: boolean): Promise<void>;
|
|
247
259
|
/**
|
|
248
260
|
* Sends a message (any {@link WaSendMessageContent} kind – text, media,
|
|
249
261
|
* poll, reaction, edit, revoke, etc.) to `to` and returns the publish
|
|
@@ -292,14 +292,10 @@ class WaMessageCoordinator {
|
|
|
292
292
|
});
|
|
293
293
|
return parseMessageCappingMexResponse(data);
|
|
294
294
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
*/
|
|
300
|
-
async syncSignalSession(jid, reasonIdentity = false) {
|
|
301
|
-
await this.messageDispatch.syncSignalSession(jid, reasonIdentity);
|
|
302
|
-
if (reasonIdentity) {
|
|
295
|
+
async syncSignalSession(jid, options = {}) {
|
|
296
|
+
const normalized = typeof options === 'boolean' ? { reasonIdentity: options } : options;
|
|
297
|
+
await this.messageDispatch.syncSignalSession(jid, normalized);
|
|
298
|
+
if (normalized.reasonIdentity) {
|
|
303
299
|
this.trustedContactToken.reissueOnIdentityChange(jid).catch((err) => this.logger.warn('tc token reissue on identity change failed', {
|
|
304
300
|
jid,
|
|
305
301
|
message: (0, primitives_1.toError)(err).message
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WaAppStateSyncKey } from '../../appstate/types';
|
|
2
2
|
import type { WaAuthCredentials } from '../../auth/types';
|
|
3
|
+
import type { WaSyncSignalSessionOptions } from '../coordinators/WaMessageCoordinator';
|
|
3
4
|
import type { DeviceFanoutResolver } from '../messaging/fanout';
|
|
4
5
|
import type { GroupMetadataCache } from '../messaging/group-metadata';
|
|
5
6
|
import type { AppStateSyncKeyProtocol } from '../messaging/key-protocol';
|
|
@@ -156,7 +157,7 @@ export declare class WaMessageDispatchCoordinator {
|
|
|
156
157
|
* LID destination.
|
|
157
158
|
*/
|
|
158
159
|
private resolvePeerRecipientPn;
|
|
159
|
-
syncSignalSession(jid: string, reasonIdentity?:
|
|
160
|
+
syncSignalSession(jid: string, { force, reasonIdentity }?: WaSyncSignalSessionOptions): Promise<void>;
|
|
160
161
|
sendReceipt(input: WaSendReceiptInput): Promise<void>;
|
|
161
162
|
publishProtocolMessageToDevice(deviceJid: string, protocolMessage: Proto.Message.IProtocolMessage, options?: {
|
|
162
163
|
readonly id?: string;
|
|
@@ -483,11 +483,14 @@ class WaMessageDispatchCoordinator {
|
|
|
483
483
|
const pair = await this.deps.signalDeviceSync.resolveUserJidPair(directRecipientJid);
|
|
484
484
|
return pair.pnJid ?? undefined;
|
|
485
485
|
}
|
|
486
|
-
async syncSignalSession(jid, reasonIdentity = false) {
|
|
486
|
+
async syncSignalSession(jid, { force = false, reasonIdentity = false } = {}) {
|
|
487
487
|
const address = (0, jid_1.parseSignalAddressFromJid)(jid);
|
|
488
488
|
if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
|
|
489
489
|
throw new Error('syncSignalSession supports only direct chats');
|
|
490
490
|
}
|
|
491
|
+
if (force || reasonIdentity) {
|
|
492
|
+
await this.deps.sessionStore.deleteSession(address);
|
|
493
|
+
}
|
|
491
494
|
await this.deps.sessionResolver.ensureSession(address, jid, undefined, reasonIdentity);
|
|
492
495
|
}
|
|
493
496
|
async sendReceipt(input) {
|
|
@@ -158,7 +158,11 @@ export interface WaProfileCoordinator {
|
|
|
158
158
|
* @throws when `pin` is not exactly 4 digits, before any round-trip.
|
|
159
159
|
*/
|
|
160
160
|
readonly setUsernameKey: (pin: string) => Promise<boolean>;
|
|
161
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Resolves LIDs for a list of phone numbers (handles normalization). Check
|
|
163
|
+
* `status`: `'out'` means no WhatsApp account, `'unanswered'` or `'error'` mean
|
|
164
|
+
* the server gave no verdict (see `error` for the reason).
|
|
165
|
+
*/
|
|
162
166
|
readonly getLidsByPhoneNumbers: (phoneNumbers: readonly string[]) => Promise<readonly SignalLidSyncResult[]>;
|
|
163
167
|
}
|
|
164
168
|
interface WaProfileCoordinatorOptions {
|
|
@@ -10,6 +10,8 @@ interface WaStreamControlCoordinatorOptions {
|
|
|
10
10
|
readonly disconnect: (reason: WaDisconnectReason, isLogout: boolean, code: WaConnectionCode | null) => Promise<void>;
|
|
11
11
|
readonly clearStoredCredentials: () => Promise<void>;
|
|
12
12
|
readonly connect: (reason: WaConnectionOpenReason) => Promise<void>;
|
|
13
|
+
/** Returns `true` when a pairing restart already owns the force-login `stream:error`. */
|
|
14
|
+
readonly handlePairingRestart: () => boolean;
|
|
13
15
|
}
|
|
14
16
|
export interface WaStreamControlHandler {
|
|
15
17
|
readonly handleStreamControlResult: (result: WaStreamControlNodeResult) => Promise<void>;
|
|
@@ -4,7 +4,7 @@ exports.createStreamControlHandler = createStreamControlHandler;
|
|
|
4
4
|
const constants_1 = require("../../protocol/constants");
|
|
5
5
|
const primitives_1 = require("../../util/primitives");
|
|
6
6
|
function createStreamControlHandler(options) {
|
|
7
|
-
const { logger, getComms, clearPendingQueries, clearMediaConnCache, disconnect, clearStoredCredentials, connect } = options;
|
|
7
|
+
const { logger, getComms, clearPendingQueries, clearMediaConnCache, disconnect, clearStoredCredentials, connect, handlePairingRestart } = options;
|
|
8
8
|
let lifecyclePromise = null;
|
|
9
9
|
const runStreamControlLifecycle = (reason, action) => {
|
|
10
10
|
if (lifecyclePromise) {
|
|
@@ -88,6 +88,11 @@ function createStreamControlHandler(options) {
|
|
|
88
88
|
logger.info('received xmlstreamend stanza');
|
|
89
89
|
return;
|
|
90
90
|
case 'stream_error_code':
|
|
91
|
+
if (result.code === constants_1.WA_STREAM_SIGNALING.FORCE_LOGIN_CODE &&
|
|
92
|
+
handlePairingRestart()) {
|
|
93
|
+
logger.info('stream:error force login claimed by the pairing restart');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
91
96
|
logger.warn('received stream:error with code', { code: result.code });
|
|
92
97
|
if (result.code >= 500 && result.code < 600) {
|
|
93
98
|
if (result.code === constants_1.WA_STREAM_SIGNALING.FORCE_LOGIN_CODE) {
|
|
@@ -228,6 +228,27 @@ async function buildMediaMessageContent(options, content, ctx) {
|
|
|
228
228
|
if (typeof content === 'string') {
|
|
229
229
|
return { message: { conversation: content } };
|
|
230
230
|
}
|
|
231
|
+
if ((0, content_1.isSendTypedMessage)(content)) {
|
|
232
|
+
return withMessageContextInfo(await buildTypedMessageContent(options, content, ctx), content.messageContextInfo);
|
|
233
|
+
}
|
|
234
|
+
if (!content || typeof content !== 'object') {
|
|
235
|
+
throw new Error('invalid message content');
|
|
236
|
+
}
|
|
237
|
+
return { message: content };
|
|
238
|
+
}
|
|
239
|
+
/** Adds the caller's `messageContextInfo` under the built one: fields the builder set win. */
|
|
240
|
+
function withMessageContextInfo(built, messageContextInfo) {
|
|
241
|
+
if (!messageContextInfo)
|
|
242
|
+
return built;
|
|
243
|
+
return {
|
|
244
|
+
...built,
|
|
245
|
+
message: {
|
|
246
|
+
...built.message,
|
|
247
|
+
messageContextInfo: { ...messageContextInfo, ...built.message.messageContextInfo }
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function buildTypedMessageContent(options, content, ctx) {
|
|
231
252
|
if ((0, content_1.isSendTextMessage)(content)) {
|
|
232
253
|
if (options.linkPreviewResolver) {
|
|
233
254
|
try {
|
|
@@ -267,13 +288,7 @@ async function buildMediaMessageContent(options, content, ctx) {
|
|
|
267
288
|
if ((0, content_1.isSendEventResponseMessage)(content)) {
|
|
268
289
|
return { message: await buildEventResponseMessage(content, options.serverClock, ctx) };
|
|
269
290
|
}
|
|
270
|
-
|
|
271
|
-
return buildMediaMessage(options, content);
|
|
272
|
-
}
|
|
273
|
-
if (!content || typeof content !== 'object') {
|
|
274
|
-
throw new Error('invalid message content');
|
|
275
|
-
}
|
|
276
|
-
return { message: content };
|
|
291
|
+
return buildMediaMessage(options, content);
|
|
277
292
|
}
|
|
278
293
|
async function getMediaConn(options, forceRefresh = false) {
|
|
279
294
|
const cached = options.getMediaConnCache();
|
|
@@ -394,7 +409,8 @@ async function buildMediaMessage(options, content) {
|
|
|
394
409
|
if (key !== 'type' &&
|
|
395
410
|
key !== 'media' &&
|
|
396
411
|
key !== 'fileLength' &&
|
|
397
|
-
key !== 'mimetype'
|
|
412
|
+
key !== 'mimetype' &&
|
|
413
|
+
key !== 'messageContextInfo') {
|
|
398
414
|
result[key] = c[key];
|
|
399
415
|
}
|
|
400
416
|
}
|