zapo-js 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appstate/types.d.ts +1 -2
- package/dist/auth/WaAuthClient.d.ts +10 -0
- package/dist/auth/WaAuthClient.js +17 -8
- package/dist/auth/credentials-flow.d.ts +8 -0
- package/dist/auth/credentials-flow.js +35 -2
- package/dist/auth/pairing/companion-host.d.ts +105 -0
- package/dist/auth/pairing/companion-host.js +148 -0
- package/dist/auth/types.d.ts +20 -9
- package/dist/client/WaClient.d.ts +15 -5
- package/dist/client/WaClient.js +46 -12
- package/dist/client/WaClientFactory.d.ts +3 -0
- package/dist/client/WaClientFactory.js +24 -6
- package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +3 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +13 -3
- package/dist/client/coordinators/WaMobileCoordinator.d.ts +192 -0
- package/dist/client/coordinators/WaMobileCoordinator.js +645 -0
- package/dist/client/messaging/companion-host.d.ts +43 -0
- package/dist/client/messaging/companion-host.js +61 -0
- package/dist/client/messaging/key-protocol.js +25 -1
- package/dist/client/persistence/companion-host.d.ts +36 -0
- package/dist/client/persistence/companion-host.js +57 -0
- package/dist/client/plugins/install.d.ts +2 -1
- package/dist/client/plugins/install.js +44 -11
- package/dist/client/types.d.ts +71 -7
- package/dist/esm/auth/WaAuthClient.js +18 -9
- package/dist/esm/auth/credentials-flow.js +36 -3
- package/dist/esm/auth/pairing/companion-host.js +140 -0
- package/dist/esm/client/WaClient.js +46 -12
- package/dist/esm/client/WaClientFactory.js +25 -7
- package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/esm/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -4
- package/dist/esm/client/coordinators/WaMobileCoordinator.js +641 -0
- package/dist/esm/client/messaging/companion-host.js +56 -0
- package/dist/esm/client/messaging/key-protocol.js +25 -1
- package/dist/esm/client/persistence/companion-host.js +54 -0
- package/dist/esm/client/plugins/install.js +44 -11
- package/dist/esm/index.js +5 -2
- package/dist/esm/message/WaMessageClient.js +3 -2
- package/dist/esm/message/context-info.js +4 -0
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/device-identity.js +18 -0
- package/dist/esm/protocol/group.js +6 -0
- package/dist/esm/protocol/message.js +12 -0
- package/dist/esm/protocol/nodes.js +15 -1
- package/dist/esm/retry/replay.js +3 -3
- package/dist/esm/signal/attestation/WaAdvSignature.js +67 -2
- package/dist/esm/signal/attestation/constants.js +2 -0
- package/dist/esm/signal/index.js +1 -0
- package/dist/esm/transport/index.js +1 -1
- package/dist/esm/transport/node/builders/group.js +4 -3
- package/dist/esm/transport/node/builders/mobile.js +113 -0
- package/dist/esm/transport/noise/WaClientPayload.js +16 -7
- package/dist/esm/transport/wa-version-fetcher.js +152 -0
- package/dist/esm/util/index.js +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -4
- package/dist/message/WaMessageClient.js +2 -1
- package/dist/message/context-info.d.ts +2 -0
- package/dist/message/context-info.js +5 -0
- package/dist/protocol/constants.d.ts +6 -4
- package/dist/protocol/constants.js +8 -2
- package/dist/protocol/device-identity.d.ts +25 -0
- package/dist/protocol/device-identity.js +21 -0
- package/dist/protocol/group.d.ts +6 -0
- package/dist/protocol/group.js +7 -1
- package/dist/protocol/message.d.ts +14 -0
- package/dist/protocol/message.js +13 -1
- package/dist/protocol/nodes.d.ts +14 -0
- package/dist/protocol/nodes.js +15 -1
- package/dist/retry/replay.js +2 -2
- package/dist/signal/attestation/WaAdvSignature.d.ts +50 -1
- package/dist/signal/attestation/WaAdvSignature.js +71 -1
- package/dist/signal/attestation/constants.d.ts +2 -0
- package/dist/signal/attestation/constants.js +3 -1
- package/dist/signal/index.d.ts +1 -0
- package/dist/signal/index.js +13 -1
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js +4 -3
- package/dist/transport/node/builders/group.js +4 -3
- package/dist/transport/node/builders/mobile.d.ts +68 -0
- package/dist/transport/node/builders/mobile.js +120 -0
- package/dist/transport/noise/WaClientPayload.js +16 -7
- package/dist/transport/wa-version-fetcher.d.ts +92 -0
- package/dist/transport/wa-version-fetcher.js +156 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
- package/dist/esm/transport/wa-web-version-fetcher.js +0 -91
- package/dist/transport/wa-web-version-fetcher.d.ts +0 -44
- package/dist/transport/wa-web-version-fetcher.js +0 -94
package/dist/appstate/types.d.ts
CHANGED
|
@@ -28,14 +28,13 @@ export type WaAppStateMutationInput = {
|
|
|
28
28
|
readonly collection: AppStateCollectionName;
|
|
29
29
|
readonly operation: 'remove';
|
|
30
30
|
readonly index: string;
|
|
31
|
-
readonly previousValue: Proto.ISyncActionValue;
|
|
32
31
|
readonly version: number;
|
|
33
32
|
readonly timestamp: number;
|
|
34
33
|
};
|
|
35
34
|
export interface WaAppStateMutation {
|
|
36
35
|
readonly collection: AppStateCollectionName;
|
|
37
36
|
readonly operation: 'set' | 'remove';
|
|
38
|
-
readonly source: 'snapshot' | 'patch';
|
|
37
|
+
readonly source: 'snapshot' | 'patch' | 'local';
|
|
39
38
|
readonly index: string;
|
|
40
39
|
readonly value: Proto.ISyncActionValue | null;
|
|
41
40
|
readonly version: number;
|
|
@@ -51,6 +51,7 @@ export declare class WaAuthClient {
|
|
|
51
51
|
private readonly shortcakeFlow;
|
|
52
52
|
private credentials;
|
|
53
53
|
private versionOverride;
|
|
54
|
+
private mobileAppVersionOverride;
|
|
54
55
|
constructor(options: WaAuthClientOptions, deps: WaAuthClientDeps);
|
|
55
56
|
/**
|
|
56
57
|
* Returns a snapshot of auth readiness flags (connection, registration,
|
|
@@ -85,6 +86,15 @@ export declare class WaAuthClient {
|
|
|
85
86
|
* options.
|
|
86
87
|
*/
|
|
87
88
|
setNextConnectVersion(version: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* One-shot override for the mobile `deviceInfo.appVersion` on the next
|
|
91
|
+
* {@link buildCommsConfig} call, then clears. The mobile transport reads
|
|
92
|
+
* its version from `deviceInfo.appVersion` (the login payload), not the
|
|
93
|
+
* `version` string, so {@link setNextConnectVersion} has no effect on a
|
|
94
|
+
* mobile session. Used by the `recoverFromClientTooOld` auto-retry to
|
|
95
|
+
* inject a fresh Android app version without mutating the user's options.
|
|
96
|
+
*/
|
|
97
|
+
setNextConnectMobileAppVersion(appVersion: string): void;
|
|
88
98
|
/** Clears the in-memory QR and pairing sessions without touching storage. */
|
|
89
99
|
clearTransientState(): Promise<void>;
|
|
90
100
|
/**
|
|
@@ -10,7 +10,6 @@ const global_1 = require("../transport/node/builders/global");
|
|
|
10
10
|
const WaClientPayload_1 = require("../transport/noise/WaClientPayload");
|
|
11
11
|
const bytes_1 = require("../util/bytes");
|
|
12
12
|
const primitives_1 = require("../util/primitives");
|
|
13
|
-
const runtime_1 = require("../util/runtime");
|
|
14
13
|
/**
|
|
15
14
|
* Owns the auth/pairing lifecycle and persistence of {@link WaAuthCredentials}.
|
|
16
15
|
* Exposed as `client.auth` on a {@link WaClient}.
|
|
@@ -23,12 +22,8 @@ const runtime_1 = require("../util/runtime");
|
|
|
23
22
|
class WaAuthClient {
|
|
24
23
|
constructor(options, deps) {
|
|
25
24
|
this.versionOverride = null;
|
|
26
|
-
|
|
27
|
-
const device =
|
|
28
|
-
browser: deviceBrowser,
|
|
29
|
-
osDisplayName: options.deviceOsDisplayName ?? (0, runtime_1.getRuntimeOsDisplayName)(),
|
|
30
|
-
platform: options.devicePlatform ?? (0, constants_1.getWaCompanionPlatformId)(deviceBrowser)
|
|
31
|
-
});
|
|
25
|
+
this.mobileAppVersionOverride = null;
|
|
26
|
+
const device = (0, constants_1.resolveWaDeviceIdentity)(options);
|
|
32
27
|
this.options = Object.freeze({
|
|
33
28
|
...options,
|
|
34
29
|
deviceBrowser: device.browser,
|
|
@@ -79,7 +74,7 @@ class WaAuthClient {
|
|
|
79
74
|
getCredentials: () => this.credentials,
|
|
80
75
|
updateCredentials: this.updateCredentials.bind(this)
|
|
81
76
|
},
|
|
82
|
-
deviceType: (0, WaClientPayload_1.resolveDevicePropsPlatformType)(
|
|
77
|
+
deviceType: (0, WaClientPayload_1.resolveDevicePropsPlatformType)(device.browser)
|
|
83
78
|
})
|
|
84
79
|
: null;
|
|
85
80
|
}
|
|
@@ -127,12 +122,15 @@ class WaAuthClient {
|
|
|
127
122
|
this.logger.trace('auth client building comms config');
|
|
128
123
|
const override = this.versionOverride;
|
|
129
124
|
this.versionOverride = null;
|
|
125
|
+
const mobileAppVersionOverride = this.mobileAppVersionOverride;
|
|
126
|
+
this.mobileAppVersionOverride = null;
|
|
130
127
|
return (0, credentials_flow_1.buildCommsConfig)(this.logger, this.requireCredentials(), socketOptions, {
|
|
131
128
|
deviceBrowser: this.options.deviceBrowser,
|
|
132
129
|
deviceOsDisplayName: this.options.deviceOsDisplayName,
|
|
133
130
|
requireFullSync: this.options.requireFullSync,
|
|
134
131
|
version: override ?? this.options.version,
|
|
135
132
|
mobileTransport: this.options.mobileTransport,
|
|
133
|
+
mobileAppVersionOverride: mobileAppVersionOverride ?? undefined,
|
|
136
134
|
noiseTrustedRootCa: overrides.noiseTrustedRootCa,
|
|
137
135
|
disableNoiseCertificateChainVerification: overrides.disableNoiseCertificateChainVerification ??
|
|
138
136
|
this.options.dangerous?.disableNoiseCertificateChainVerification
|
|
@@ -148,6 +146,17 @@ class WaAuthClient {
|
|
|
148
146
|
setNextConnectVersion(version) {
|
|
149
147
|
this.versionOverride = version;
|
|
150
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* One-shot override for the mobile `deviceInfo.appVersion` on the next
|
|
151
|
+
* {@link buildCommsConfig} call, then clears. The mobile transport reads
|
|
152
|
+
* its version from `deviceInfo.appVersion` (the login payload), not the
|
|
153
|
+
* `version` string, so {@link setNextConnectVersion} has no effect on a
|
|
154
|
+
* mobile session. Used by the `recoverFromClientTooOld` auto-retry to
|
|
155
|
+
* inject a fresh Android app version without mutating the user's options.
|
|
156
|
+
*/
|
|
157
|
+
setNextConnectMobileAppVersion(appVersion) {
|
|
158
|
+
this.mobileAppVersionOverride = appVersion;
|
|
159
|
+
}
|
|
151
160
|
/** Clears the in-memory QR and pairing sessions without touching storage. */
|
|
152
161
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
153
162
|
async clearTransientState() {
|
|
@@ -17,5 +17,13 @@ export declare function persistCredentials(args: WaAuthCredentialsFlowArgs, cred
|
|
|
17
17
|
export declare function buildCommsConfig(logger: Logger, credentials: WaAuthCredentials, socketOptions: WaAuthSocketOptions, clientOptions: Pick<WaAuthClientOptions, 'deviceBrowser' | 'deviceOsDisplayName' | 'requireFullSync' | 'version' | 'mobileTransport'> & {
|
|
18
18
|
readonly noiseTrustedRootCa?: WaNoiseRootCa;
|
|
19
19
|
readonly disableNoiseCertificateChainVerification?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* One-shot override for the mobile `deviceInfo.appVersion` on this
|
|
22
|
+
* connect, applied on top of the resolved mobile transport. Used by
|
|
23
|
+
* the `recoverFromClientTooOld` auto-retry to inject a fresh Android
|
|
24
|
+
* app version fetched from the public listing. Ignored for web
|
|
25
|
+
* sessions.
|
|
26
|
+
*/
|
|
27
|
+
readonly mobileAppVersionOverride?: string;
|
|
20
28
|
}): Promise<WaCommsConfig>;
|
|
21
29
|
export {};
|
|
@@ -66,6 +66,26 @@ async function resolveVersion(version) {
|
|
|
66
66
|
}
|
|
67
67
|
return resolved;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Validates the user-supplied `version` string against the transport it will
|
|
71
|
+
* feed. A mobile session sends the version as the four-part Android app
|
|
72
|
+
* version (`2.YY.WW.RR`) in the login payload, so it must have exactly four
|
|
73
|
+
* numeric parts. A web session advertises up to five parts (`2.3000.x[.y.z]`),
|
|
74
|
+
* so three to five numeric parts are accepted.
|
|
75
|
+
*/
|
|
76
|
+
function assertValidVersion(version, mobile) {
|
|
77
|
+
const parts = version.split('.');
|
|
78
|
+
const allNumeric = parts.every((part) => (0, primitives_1.parseOptionalInt)(part) !== undefined);
|
|
79
|
+
if (mobile) {
|
|
80
|
+
if (parts.length !== 4 || !allNumeric) {
|
|
81
|
+
throw new Error(`mobile session requires a 4-part numeric version (e.g. 2.26.27.70), got: ${version}`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (parts.length < 3 || parts.length > 5 || !allNumeric) {
|
|
86
|
+
throw new Error(`web session requires a 3- to 5-part numeric version (e.g. 2.3000.1040229458), got: ${version}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
69
89
|
async function buildCommsConfig(logger, credentials, socketOptions, clientOptions) {
|
|
70
90
|
const meJid = credentials.meJid;
|
|
71
91
|
const registered = meJid !== null && meJid !== undefined;
|
|
@@ -86,6 +106,12 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
|
|
|
86
106
|
? 'credentials'
|
|
87
107
|
: 'none'
|
|
88
108
|
});
|
|
109
|
+
const resolvedVersion = effectiveMobileTransport && clientOptions.mobileAppVersionOverride !== undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: await resolveVersion(clientOptions.version);
|
|
112
|
+
if (resolvedVersion !== undefined) {
|
|
113
|
+
assertValidVersion(resolvedVersion, Boolean(effectiveMobileTransport));
|
|
114
|
+
}
|
|
89
115
|
if (effectiveMobileTransport) {
|
|
90
116
|
if (wsProxy) {
|
|
91
117
|
throw new Error('mobileTransport does not support socketOptions.proxy.ws – remove the proxy option or open an issue to add TCP proxy support');
|
|
@@ -93,11 +119,18 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
|
|
|
93
119
|
if (!loginIdentity) {
|
|
94
120
|
throw new Error('mobileTransport requires registered credentials (meJid) – run the mobile bridge flow first');
|
|
95
121
|
}
|
|
122
|
+
if (clientOptions.mobileAppVersionOverride !== undefined) {
|
|
123
|
+
assertValidVersion(clientOptions.mobileAppVersionOverride, true);
|
|
124
|
+
}
|
|
125
|
+
const appVersionOverride = clientOptions.mobileAppVersionOverride ?? resolvedVersion;
|
|
126
|
+
const deviceInfo = appVersionOverride
|
|
127
|
+
? { ...effectiveMobileTransport.deviceInfo, appVersion: appVersionOverride }
|
|
128
|
+
: effectiveMobileTransport.deviceInfo;
|
|
96
129
|
const loginPayload = (0, WaMobileClientPayload_1.buildMobileLoginPayload)({
|
|
97
130
|
username: loginIdentity.username,
|
|
98
131
|
device: loginIdentity.device,
|
|
99
132
|
passive: effectiveMobileTransport.passive ?? false,
|
|
100
|
-
deviceInfo
|
|
133
|
+
deviceInfo,
|
|
101
134
|
pushName: effectiveMobileTransport.pushName,
|
|
102
135
|
yearClass: effectiveMobileTransport.yearClass,
|
|
103
136
|
memClass: effectiveMobileTransport.memClass
|
|
@@ -122,7 +155,7 @@ async function buildCommsConfig(logger, credentials, socketOptions, clientOption
|
|
|
122
155
|
}
|
|
123
156
|
};
|
|
124
157
|
}
|
|
125
|
-
const versionBase =
|
|
158
|
+
const versionBase = resolvedVersion;
|
|
126
159
|
return {
|
|
127
160
|
url: socketOptions.url,
|
|
128
161
|
urls: socketOptions.urls,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type SignalKeyPair } from '../../crypto';
|
|
2
|
+
/**
|
|
3
|
+
* `@sensitive` `advSecretKey` is companion secret material; consumers must not
|
|
4
|
+
* persist it without encryption at rest.
|
|
5
|
+
*
|
|
6
|
+
* Fields decoded from a companion pairing QR string. The companion (the device
|
|
7
|
+
* being linked) generates these; the primary consumes them to sign the device
|
|
8
|
+
* identity. `advSecretKey` is the companion's secret, not the primary's.
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedCompanionQr {
|
|
11
|
+
readonly ref: string;
|
|
12
|
+
readonly noisePublicKey: Uint8Array;
|
|
13
|
+
readonly identityPublicKey: Uint8Array;
|
|
14
|
+
readonly advSecretKey: Uint8Array;
|
|
15
|
+
readonly platform: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses a companion pairing QR of the form
|
|
19
|
+
* `ref,noisePubB64,identityPubB64,advSecretB64,platform`. The `ref` may itself
|
|
20
|
+
* contain commas, so the four trailing fields are taken from the end.
|
|
21
|
+
*
|
|
22
|
+
* @throws when fewer than 5 comma-separated fields are present.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseCompanionQr(qr: string): ParsedCompanionQr;
|
|
25
|
+
/** `@sensitive` Carries the primary's `accountIdentityKeyPair` private key. */
|
|
26
|
+
export interface BuildSignedKeyIndexListInput {
|
|
27
|
+
readonly accountIdentityKeyPair: SignalKeyPair;
|
|
28
|
+
readonly rawId: number;
|
|
29
|
+
readonly currentIndex: number;
|
|
30
|
+
readonly timestampSeconds: number;
|
|
31
|
+
readonly validIndexes: readonly number[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds the account-signed `ADVSignedKeyIndexList` bytes for the current device
|
|
35
|
+
* set. The zero-valued `accountType` enum is omitted to match the phone's byte
|
|
36
|
+
* layout.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildSignedKeyIndexList(input: BuildSignedKeyIndexListInput): Promise<Uint8Array>;
|
|
39
|
+
/**
|
|
40
|
+
* `@sensitive` Carries the primary's `accountIdentityKeyPair` private key and the
|
|
41
|
+
* companion's `advSecretKey`; do not persist unencrypted.
|
|
42
|
+
*/
|
|
43
|
+
export interface BuildSignedCompanionIdentityInput {
|
|
44
|
+
readonly accountIdentityKeyPair: SignalKeyPair;
|
|
45
|
+
readonly companionIdentityPublicKey: Uint8Array;
|
|
46
|
+
readonly advSecretKey: Uint8Array;
|
|
47
|
+
readonly rawId: number;
|
|
48
|
+
readonly keyIndex: number;
|
|
49
|
+
readonly timestampSeconds: number;
|
|
50
|
+
readonly validIndexes: readonly number[];
|
|
51
|
+
}
|
|
52
|
+
export interface SignedCompanionIdentity {
|
|
53
|
+
/** `ADVSignedDeviceIdentityHMAC` bytes for the `<device-identity>` node. */
|
|
54
|
+
readonly deviceIdentityBytes: Uint8Array;
|
|
55
|
+
/** `ADVSignedKeyIndexList` bytes for the `<key-index-list>` node. */
|
|
56
|
+
readonly keyIndexListBytes: Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds the signed device identity and key-index list for a linking companion.
|
|
60
|
+
* The byte layout mirrors WhatsApp's phone (and the in-repo fake-server
|
|
61
|
+
* reference): the zero-valued `accountType`/`deviceType` enum fields are omitted
|
|
62
|
+
* so the signed `details` match what the companion verifies.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildSignedCompanionIdentity(input: BuildSignedCompanionIdentityInput): Promise<SignedCompanionIdentity>;
|
|
65
|
+
/**
|
|
66
|
+
* Derives the `<pem>` AES-GCM key the phone ships in the pair-device upload:
|
|
67
|
+
* `HKDF-SHA256(ikm = advSecret, salt = companionNoisePublicKey, info)`. Not yet
|
|
68
|
+
* verified against a live server from this implementation; see
|
|
69
|
+
* `CompanionHostOptions.includePem`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function derivePemKey(advSecretKey: Uint8Array, companionNoisePublicKey: Uint8Array): Uint8Array;
|
|
72
|
+
/**
|
|
73
|
+
* `@sensitive` Holds the `primaryEphemeralKeyPair` private key and the derived
|
|
74
|
+
* `sharedEphemeral`; keep in memory only, never persist unencrypted.
|
|
75
|
+
*/
|
|
76
|
+
export interface LinkCodePrimaryHello {
|
|
77
|
+
readonly primaryEphemeralKeyPair: SignalKeyPair;
|
|
78
|
+
/** `salt(32) || counter(16) || AES-CTR(primaryEphemeralPub)` for `primary_hello`. */
|
|
79
|
+
readonly wrappedPrimaryEphemeralPub: Uint8Array;
|
|
80
|
+
/** ECDH(primaryEphemeralPriv, companionEphemeralPub); stashed for the finish step. */
|
|
81
|
+
readonly sharedEphemeral: Uint8Array;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Link-code handshake, primary step 1: unwrap the companion ephemeral with the
|
|
85
|
+
* pairing code, generate the primary ephemeral, compute the shared ECDH secret,
|
|
86
|
+
* and wrap the primary ephemeral for the `primary_hello` stanza. Mirror of the
|
|
87
|
+
* companion's `createCompanionHello` + primary-ephemeral unwrap.
|
|
88
|
+
*/
|
|
89
|
+
export declare function preparePrimaryHello(args: {
|
|
90
|
+
readonly pairingCode: string;
|
|
91
|
+
readonly wrappedCompanionEphemeralPub: Uint8Array;
|
|
92
|
+
}): Promise<LinkCodePrimaryHello>;
|
|
93
|
+
/**
|
|
94
|
+
* Link-code handshake, primary step 2: decrypt the `companion_finish` key bundle
|
|
95
|
+
* and derive the shared `advSecret` (byte-identical to what the companion
|
|
96
|
+
* derives). Validates that the bundle binds both identities.
|
|
97
|
+
*
|
|
98
|
+
* @throws when the bundle's embedded identities do not match.
|
|
99
|
+
*/
|
|
100
|
+
export declare function completePrimaryHandshake(args: {
|
|
101
|
+
readonly sharedEphemeral: Uint8Array;
|
|
102
|
+
readonly wrappedKeyBundle: Uint8Array;
|
|
103
|
+
readonly companionIdentityPub: Uint8Array;
|
|
104
|
+
readonly primaryIdentityKeyPair: SignalKeyPair;
|
|
105
|
+
}): Promise<Uint8Array>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCompanionQr = parseCompanionQr;
|
|
4
|
+
exports.buildSignedKeyIndexList = buildSignedKeyIndexList;
|
|
5
|
+
exports.buildSignedCompanionIdentity = buildSignedCompanionIdentity;
|
|
6
|
+
exports.derivePemKey = derivePemKey;
|
|
7
|
+
exports.preparePrimaryHello = preparePrimaryHello;
|
|
8
|
+
exports.completePrimaryHandshake = completePrimaryHandshake;
|
|
9
|
+
const _crypto_1 = require("../../crypto");
|
|
10
|
+
const _proto_1 = require("../../proto");
|
|
11
|
+
const _protocol_1 = require("../../protocol");
|
|
12
|
+
const _signal_1 = require("../../signal");
|
|
13
|
+
const bytes_1 = require("../../util/bytes");
|
|
14
|
+
const PEM_HKDF_INFO = bytes_1.TEXT_ENCODER.encode('Canonical Ent Companion Nonce Encrypt');
|
|
15
|
+
const PBKDF2_ITERATIONS = 131072;
|
|
16
|
+
const PAIRING_AES_KEY_BYTES = 32;
|
|
17
|
+
/**
|
|
18
|
+
* Parses a companion pairing QR of the form
|
|
19
|
+
* `ref,noisePubB64,identityPubB64,advSecretB64,platform`. The `ref` may itself
|
|
20
|
+
* contain commas, so the four trailing fields are taken from the end.
|
|
21
|
+
*
|
|
22
|
+
* @throws when fewer than 5 comma-separated fields are present.
|
|
23
|
+
*/
|
|
24
|
+
function parseCompanionQr(qr) {
|
|
25
|
+
const parts = qr.split(',');
|
|
26
|
+
if (parts.length < 5) {
|
|
27
|
+
throw new Error(`companion qr must have at least 5 comma-separated parts, got ${parts.length}`);
|
|
28
|
+
}
|
|
29
|
+
const platform = parts[parts.length - 1];
|
|
30
|
+
const advSecretB64 = parts[parts.length - 2];
|
|
31
|
+
const identityPubB64 = parts[parts.length - 3];
|
|
32
|
+
const noisePubB64 = parts[parts.length - 4];
|
|
33
|
+
const ref = parts.slice(0, parts.length - 4).join(',');
|
|
34
|
+
return {
|
|
35
|
+
ref,
|
|
36
|
+
noisePublicKey: (0, bytes_1.decodeBase64Url)(noisePubB64, 'companionQr.noisePublicKey'),
|
|
37
|
+
identityPublicKey: (0, bytes_1.decodeBase64Url)(identityPubB64, 'companionQr.identityPublicKey'),
|
|
38
|
+
advSecretKey: (0, bytes_1.decodeBase64Url)(advSecretB64, 'companionQr.advSecretKey'),
|
|
39
|
+
platform
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds the account-signed `ADVSignedKeyIndexList` bytes for the current device
|
|
44
|
+
* set. The zero-valued `accountType` enum is omitted to match the phone's byte
|
|
45
|
+
* layout.
|
|
46
|
+
*/
|
|
47
|
+
async function buildSignedKeyIndexList(input) {
|
|
48
|
+
const details = _proto_1.proto.ADVKeyIndexList.encode({
|
|
49
|
+
rawId: input.rawId,
|
|
50
|
+
timestamp: input.timestampSeconds,
|
|
51
|
+
currentIndex: input.currentIndex,
|
|
52
|
+
validIndexes: [...input.validIndexes]
|
|
53
|
+
}).finish();
|
|
54
|
+
const accountSignature = await (0, _signal_1.signKeyIndexList)(details, input.accountIdentityKeyPair);
|
|
55
|
+
return _proto_1.proto.ADVSignedKeyIndexList.encode({ details, accountSignature }).finish();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds the signed device identity and key-index list for a linking companion.
|
|
59
|
+
* The byte layout mirrors WhatsApp's phone (and the in-repo fake-server
|
|
60
|
+
* reference): the zero-valued `accountType`/`deviceType` enum fields are omitted
|
|
61
|
+
* so the signed `details` match what the companion verifies.
|
|
62
|
+
*/
|
|
63
|
+
async function buildSignedCompanionIdentity(input) {
|
|
64
|
+
const accountSignatureKey = (0, _crypto_1.toRawPubKey)(input.accountIdentityKeyPair.pubKey);
|
|
65
|
+
const details = _proto_1.proto.ADVDeviceIdentity.encode({
|
|
66
|
+
rawId: input.rawId,
|
|
67
|
+
timestamp: input.timestampSeconds,
|
|
68
|
+
keyIndex: input.keyIndex
|
|
69
|
+
}).finish();
|
|
70
|
+
const accountSignature = await (0, _signal_1.generateDeviceIdentityAccountSignature)(details, input.companionIdentityPublicKey, input.accountIdentityKeyPair);
|
|
71
|
+
const signedIdentity = _proto_1.proto.ADVSignedDeviceIdentity.encode({
|
|
72
|
+
details,
|
|
73
|
+
accountSignatureKey,
|
|
74
|
+
accountSignature
|
|
75
|
+
}).finish();
|
|
76
|
+
const hmac = (0, _signal_1.computeAdvIdentityHmac)(input.advSecretKey, signedIdentity);
|
|
77
|
+
const deviceIdentityBytes = _proto_1.proto.ADVSignedDeviceIdentityHMAC.encode({
|
|
78
|
+
details: signedIdentity,
|
|
79
|
+
hmac
|
|
80
|
+
}).finish();
|
|
81
|
+
const keyIndexListBytes = await buildSignedKeyIndexList({
|
|
82
|
+
accountIdentityKeyPair: input.accountIdentityKeyPair,
|
|
83
|
+
rawId: input.rawId,
|
|
84
|
+
currentIndex: input.keyIndex,
|
|
85
|
+
timestampSeconds: input.timestampSeconds,
|
|
86
|
+
validIndexes: input.validIndexes
|
|
87
|
+
});
|
|
88
|
+
return { deviceIdentityBytes, keyIndexListBytes };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Derives the `<pem>` AES-GCM key the phone ships in the pair-device upload:
|
|
92
|
+
* `HKDF-SHA256(ikm = advSecret, salt = companionNoisePublicKey, info)`. Not yet
|
|
93
|
+
* verified against a live server from this implementation; see
|
|
94
|
+
* `CompanionHostOptions.includePem`.
|
|
95
|
+
*/
|
|
96
|
+
function derivePemKey(advSecretKey, companionNoisePublicKey) {
|
|
97
|
+
return (0, _crypto_1.hkdf)(advSecretKey, companionNoisePublicKey, PEM_HKDF_INFO, 32);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Link-code handshake, primary step 1: unwrap the companion ephemeral with the
|
|
101
|
+
* pairing code, generate the primary ephemeral, compute the shared ECDH secret,
|
|
102
|
+
* and wrap the primary ephemeral for the `primary_hello` stanza. Mirror of the
|
|
103
|
+
* companion's `createCompanionHello` + primary-ephemeral unwrap.
|
|
104
|
+
*/
|
|
105
|
+
async function preparePrimaryHello(args) {
|
|
106
|
+
if (args.wrappedCompanionEphemeralPub.length < 48) {
|
|
107
|
+
throw new Error(`wrapped companion ephemeral pub too short: ${args.wrappedCompanionEphemeralPub.length} bytes (need >= 48)`);
|
|
108
|
+
}
|
|
109
|
+
const codeBytes = bytes_1.TEXT_ENCODER.encode(args.pairingCode);
|
|
110
|
+
const companionCipherKey = await (0, _crypto_1.pbkdf2Sha256)(codeBytes, args.wrappedCompanionEphemeralPub.subarray(0, 32), PBKDF2_ITERATIONS, PAIRING_AES_KEY_BYTES);
|
|
111
|
+
const companionEphemeralPub = (0, _crypto_1.aesCtrDecrypt)(companionCipherKey, args.wrappedCompanionEphemeralPub.subarray(32, 48), args.wrappedCompanionEphemeralPub.subarray(48));
|
|
112
|
+
const [primaryEphemeralKeyPair, salt, counter] = await Promise.all([
|
|
113
|
+
_crypto_1.X25519.generateKeyPair(),
|
|
114
|
+
(0, _crypto_1.randomBytesAsync)(32),
|
|
115
|
+
(0, _crypto_1.randomBytesAsync)(16)
|
|
116
|
+
]);
|
|
117
|
+
const primaryCipherKey = await (0, _crypto_1.pbkdf2Sha256)(codeBytes, salt, PBKDF2_ITERATIONS, PAIRING_AES_KEY_BYTES);
|
|
118
|
+
const encrypted = (0, _crypto_1.aesCtrEncrypt)(primaryCipherKey, counter, primaryEphemeralKeyPair.pubKey);
|
|
119
|
+
const sharedEphemeral = await _crypto_1.X25519.scalarMult(primaryEphemeralKeyPair.privKey, companionEphemeralPub);
|
|
120
|
+
return {
|
|
121
|
+
primaryEphemeralKeyPair,
|
|
122
|
+
wrappedPrimaryEphemeralPub: (0, bytes_1.concatBytes)([salt, counter, encrypted]),
|
|
123
|
+
sharedEphemeral
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Link-code handshake, primary step 2: decrypt the `companion_finish` key bundle
|
|
128
|
+
* and derive the shared `advSecret` (byte-identical to what the companion
|
|
129
|
+
* derives). Validates that the bundle binds both identities.
|
|
130
|
+
*
|
|
131
|
+
* @throws when the bundle's embedded identities do not match.
|
|
132
|
+
*/
|
|
133
|
+
async function completePrimaryHandshake(args) {
|
|
134
|
+
const bundleEncryptionKey = (0, _crypto_1.hkdf)(args.sharedEphemeral, args.wrappedKeyBundle.subarray(0, 32), _protocol_1.WA_PAIRING_KDF_INFO.LINK_CODE_BUNDLE, 32);
|
|
135
|
+
const plaintextBundle = (0, _crypto_1.aesGcmDecrypt)(bundleEncryptionKey, args.wrappedKeyBundle.subarray(32, 44), args.wrappedKeyBundle.subarray(44));
|
|
136
|
+
const bundleCompanionIdentity = plaintextBundle.subarray(0, 32);
|
|
137
|
+
const bundlePrimaryIdentity = plaintextBundle.subarray(32, 64);
|
|
138
|
+
const bundleSecret = plaintextBundle.subarray(64);
|
|
139
|
+
const rawCompanionIdentity = (0, _crypto_1.toRawPubKey)(args.companionIdentityPub);
|
|
140
|
+
if (!(0, bytes_1.uint8Equal)(bundlePrimaryIdentity, (0, _crypto_1.toRawPubKey)(args.primaryIdentityKeyPair.pubKey))) {
|
|
141
|
+
throw new Error('link-code bundle primary identity mismatch');
|
|
142
|
+
}
|
|
143
|
+
if (!(0, bytes_1.uint8Equal)(bundleCompanionIdentity, rawCompanionIdentity)) {
|
|
144
|
+
throw new Error('link-code bundle companion identity mismatch');
|
|
145
|
+
}
|
|
146
|
+
const sharedIdentity = await _crypto_1.X25519.scalarMult(args.primaryIdentityKeyPair.privKey, rawCompanionIdentity);
|
|
147
|
+
return (0, _crypto_1.hkdf)((0, bytes_1.concatBytes)([args.sharedEphemeral, sharedIdentity, bundleSecret]), null, _protocol_1.WA_PAIRING_KDF_INFO.ADV_SECRET, 32);
|
|
148
|
+
}
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -39,10 +39,11 @@ export type WaAuthSocketOptions = Pick<WaCommsConfig, 'url' | 'urls' | 'protocol
|
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* letting callers refresh the version (e.g. via `fetchLatestWaWebVersion`
|
|
45
|
-
* without rebuilding the client.
|
|
42
|
+
* Version supplied to {@link WaAuthClientOptions.version}. Either a literal
|
|
43
|
+
* dotted-numeric string or an async resolver invoked once per connect,
|
|
44
|
+
* letting callers refresh the version (e.g. via `fetchLatestWaWebVersion` or
|
|
45
|
+
* `fetchLatestWaMobileVersion`) without rebuilding the client. See
|
|
46
|
+
* {@link WaAuthClientOptions.version} for the per-transport part-count rules.
|
|
46
47
|
*/
|
|
47
48
|
export type WaVersionResolver = string | (() => string | Promise<string>);
|
|
48
49
|
export interface WaAuthClientOptions {
|
|
@@ -70,11 +71,21 @@ export interface WaAuthClientOptions {
|
|
|
70
71
|
*/
|
|
71
72
|
readonly requireFullSync?: boolean;
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* Version the client advertises. Either a dotted-numeric literal or a
|
|
75
|
+
* (sync/async) resolver invoked once per connect – use the resolver form
|
|
76
|
+
* together with {@link fetchLatestWaWebVersion} /
|
|
77
|
+
* {@link fetchLatestWaMobileVersion} to refresh the version when the
|
|
78
|
+
* hardcoded default starts hitting `failure_client_too_old`.
|
|
79
|
+
*
|
|
80
|
+
* The accepted shape depends on the transport resolved for the connect:
|
|
81
|
+
* - **Web** sessions take a 3- to 5-part version (`2.3000.x[.y.z]`); all
|
|
82
|
+
* supplied parts are advertised in the noise payload.
|
|
83
|
+
* - **Mobile** sessions take exactly a 4-part Android app version
|
|
84
|
+
* (`2.26.27.70`); it overrides `mobileTransport.deviceInfo.appVersion`
|
|
85
|
+
* in the login payload.
|
|
86
|
+
*
|
|
87
|
+
* An invalid part count for the resolved transport throws on connect.
|
|
88
|
+
* Defaults to a tested production version per transport.
|
|
78
89
|
*/
|
|
79
90
|
readonly version?: WaVersionResolver;
|
|
80
91
|
/**
|
|
@@ -8,6 +8,7 @@ import type { WaEmailCoordinator } from './coordinators/WaEmailCoordinator';
|
|
|
8
8
|
import type { WaGroupCoordinator } from './coordinators/WaGroupCoordinator';
|
|
9
9
|
import type { WaLowLevelCoordinator } from './coordinators/WaLowLevelCoordinator';
|
|
10
10
|
import type { WaMessageCoordinator } from './coordinators/WaMessageCoordinator';
|
|
11
|
+
import type { WaMobileCoordinator } from './coordinators/WaMobileCoordinator';
|
|
11
12
|
import type { WaNewsletterCoordinator } from './coordinators/WaNewsletterCoordinator';
|
|
12
13
|
import type { WaPresenceCoordinator } from './coordinators/WaPresenceCoordinator';
|
|
13
14
|
import type { WaPrivacyCoordinator } from './coordinators/WaPrivacyCoordinator';
|
|
@@ -31,11 +32,13 @@ declare class WaClientImpl extends EventEmitter {
|
|
|
31
32
|
private activeIncomingHandlers;
|
|
32
33
|
private readonly incomingHandlersDrainedWaiters;
|
|
33
34
|
private disposePlugins;
|
|
35
|
+
private readonly pluginInstallInput;
|
|
34
36
|
/**
|
|
35
37
|
* @param options Client configuration (store, transport, addons, history...).
|
|
36
38
|
* @param logger Optional structured logger. Defaults to a `ConsoleLogger('info')`.
|
|
37
39
|
*/
|
|
38
40
|
constructor(options: WaClientOptions, logger?: Logger);
|
|
41
|
+
private installPlugins;
|
|
39
42
|
private runClientTooOldRecover;
|
|
40
43
|
/** Strongly-typed `EventEmitter#on` over {@link WaClientEventMap}. */
|
|
41
44
|
on<K extends keyof WaClientEventMap>(event: K, listener: WaClientEventMap[K]): this;
|
|
@@ -100,11 +103,12 @@ declare class WaClientImpl extends EventEmitter {
|
|
|
100
103
|
connect(): Promise<void>;
|
|
101
104
|
/**
|
|
102
105
|
* Closes the transport gracefully: pauses incoming events, flushes the
|
|
103
|
-
* write-behind persistence queue, and emits a
|
|
104
|
-
* with reason `client_disconnected`. Does not
|
|
105
|
-
* call {@link connect} again to resume the same
|
|
106
|
-
*
|
|
107
|
-
*
|
|
106
|
+
* write-behind persistence queue, disposes installed plugins, and emits a
|
|
107
|
+
* `connection` close event with reason `client_disconnected`. Does not
|
|
108
|
+
* clear stored credentials - call {@link connect} again to resume the same
|
|
109
|
+
* session, which reinstalls the plugins. There is no built-in
|
|
110
|
+
* auto-reconnect; subscribe to `connection: { status: 'close' }` and decide
|
|
111
|
+
* your own backoff.
|
|
108
112
|
*/
|
|
109
113
|
disconnect(): Promise<void>;
|
|
110
114
|
/**
|
|
@@ -166,6 +170,12 @@ declare class WaClientImpl extends EventEmitter {
|
|
|
166
170
|
* via `options.mobileTransport`. See {@link WaEmailCoordinator}.
|
|
167
171
|
*/
|
|
168
172
|
get email(): WaEmailCoordinator;
|
|
173
|
+
/**
|
|
174
|
+
* Mobile coordinator: host and manage companion devices linked to this
|
|
175
|
+
* mobile-primary account (link via QR or pairing code, list, revoke).
|
|
176
|
+
* Requires a registered primary session. See {@link WaMobileCoordinator}.
|
|
177
|
+
*/
|
|
178
|
+
get mobile(): WaMobileCoordinator;
|
|
169
179
|
/**
|
|
170
180
|
* Unpairs this companion device by removing it server-side. Requires an
|
|
171
181
|
* authenticated session; throws when no `meJid` is present.
|
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.
|