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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeHistorySync = encodeHistorySync;
|
|
4
|
+
exports.buildInlineHistorySyncNotification = buildInlineHistorySyncNotification;
|
|
5
|
+
exports.buildHistorySyncBootstrapMessage = buildHistorySyncBootstrapMessage;
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
const node_zlib_1 = require("node:zlib");
|
|
8
|
+
const _proto_1 = require("../../proto");
|
|
9
|
+
const deflateAsync = (0, node_util_1.promisify)(node_zlib_1.deflate);
|
|
10
|
+
/**
|
|
11
|
+
* Serializes a `HistorySync` and zlib-deflates it. The phone uses DEFLATE
|
|
12
|
+
* level 1; any zlib-framed level inflates identically, so it is only a
|
|
13
|
+
* size/speed tradeoff.
|
|
14
|
+
*/
|
|
15
|
+
async function encodeHistorySync(content) {
|
|
16
|
+
const serialized = _proto_1.proto.HistorySync.encode(content).finish();
|
|
17
|
+
const compressed = await deflateAsync(serialized, { level: 1 });
|
|
18
|
+
return new Uint8Array(compressed);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wraps a deflated `HistorySync` inline in a `HISTORY_SYNC_NOTIFICATION` protocol
|
|
22
|
+
* message. Only small syncs are inlined; larger ones ride an external
|
|
23
|
+
* `md-msg-hist` MMS blob referenced by the same notification.
|
|
24
|
+
*/
|
|
25
|
+
function buildInlineHistorySyncNotification(syncType, deflatedPayload, options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
type: _proto_1.proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
|
|
28
|
+
historySyncNotification: {
|
|
29
|
+
syncType,
|
|
30
|
+
chunkOrder: options.chunkOrder ?? 0,
|
|
31
|
+
progress: options.progress ?? 100,
|
|
32
|
+
initialHistBootstrapInlinePayload: deflatedPayload
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds the `INITIAL_BOOTSTRAP` `HISTORY_SYNC_NOTIFICATION` a mobile-primary
|
|
38
|
+
* pushes to a freshly-linked companion: a deflated `HistorySync` wrapped in a
|
|
39
|
+
* protocol message for delivery as an encrypted peer message. Note
|
|
40
|
+
* `HistorySync.syncType` and `HistorySyncNotification.syncType` are distinct
|
|
41
|
+
* enum types with shared values, so each field uses its own.
|
|
42
|
+
*/
|
|
43
|
+
async function buildHistorySyncBootstrapMessage(input = {}) {
|
|
44
|
+
const historySync = {
|
|
45
|
+
syncType: _proto_1.proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP,
|
|
46
|
+
chunkOrder: 0,
|
|
47
|
+
progress: 100,
|
|
48
|
+
conversations: input.conversations ? [...input.conversations] : [],
|
|
49
|
+
pushnames: input.pushnames ? [...input.pushnames] : [],
|
|
50
|
+
phoneNumberToLidMappings: (input.phoneNumberToLidMappings ?? []).map((mapping) => ({
|
|
51
|
+
pnJid: mapping.pnJid,
|
|
52
|
+
lidJid: mapping.lidJid
|
|
53
|
+
})),
|
|
54
|
+
...(input.companionMetaNonce !== undefined
|
|
55
|
+
? { companionMetaNonce: input.companionMetaNonce }
|
|
56
|
+
: {})
|
|
57
|
+
};
|
|
58
|
+
const payload = await encodeHistorySync(historySync);
|
|
59
|
+
const message = buildInlineHistorySyncNotification(_proto_1.proto.Message.HistorySyncType.INITIAL_BOOTSTRAP, payload);
|
|
60
|
+
return { message, payloadBytes: payload.byteLength };
|
|
61
|
+
}
|
|
@@ -4,6 +4,15 @@ exports.createAppStateSyncKeyProtocol = createAppStateSyncKeyProtocol;
|
|
|
4
4
|
const _proto_1 = require("../../proto");
|
|
5
5
|
const jid_1 = require("../../protocol/jid");
|
|
6
6
|
const bytes_1 = require("../../util/bytes");
|
|
7
|
+
/**
|
|
8
|
+
* Valid app-state sync key ids are exactly 6 bytes: a 2-byte device id followed
|
|
9
|
+
* by a 4-byte big-endian epoch (matching the phone primary's `crypto_info`
|
|
10
|
+
* (device_id, epoch) schema and WhatsApp Web's `handleAppStateSyncKeyShare`,
|
|
11
|
+
* which fatally rejects any share whose keyId byte length is not 6). A primary
|
|
12
|
+
* must never emit a malformed key, so legacy/short key ids are dropped from
|
|
13
|
+
* outgoing shares rather than poisoning the peer's syncd engine.
|
|
14
|
+
*/
|
|
15
|
+
const APP_STATE_SYNC_KEY_ID_LENGTH = 6;
|
|
7
16
|
function createAppStateSyncKeyProtocol(options) {
|
|
8
17
|
const { publishProtocolMessageToDevice, fanoutResolver, getCurrentCredentials, logger } = options;
|
|
9
18
|
const requireCurrentIdentity = (context) => {
|
|
@@ -67,7 +76,12 @@ function createAppStateSyncKeyProtocol(options) {
|
|
|
67
76
|
const seenKeyIds = new Set();
|
|
68
77
|
const keyShareEntries = [];
|
|
69
78
|
let sharedKeyCount = 0;
|
|
79
|
+
const droppedKeyIds = [];
|
|
70
80
|
for (const key of keys) {
|
|
81
|
+
if (key.keyId.byteLength !== APP_STATE_SYNC_KEY_ID_LENGTH) {
|
|
82
|
+
droppedKeyIds.push((0, bytes_1.bytesToHex)(key.keyId));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
71
85
|
const keyHex = (0, bytes_1.bytesToHex)(key.keyId);
|
|
72
86
|
if (seenKeyIds.has(keyHex)) {
|
|
73
87
|
continue;
|
|
@@ -84,7 +98,10 @@ function createAppStateSyncKeyProtocol(options) {
|
|
|
84
98
|
});
|
|
85
99
|
}
|
|
86
100
|
for (const keyId of missingKeyIds) {
|
|
87
|
-
if (keyId.byteLength
|
|
101
|
+
if (keyId.byteLength !== APP_STATE_SYNC_KEY_ID_LENGTH) {
|
|
102
|
+
if (keyId.byteLength > 0) {
|
|
103
|
+
droppedKeyIds.push((0, bytes_1.bytesToHex)(keyId));
|
|
104
|
+
}
|
|
88
105
|
continue;
|
|
89
106
|
}
|
|
90
107
|
const keyHex = (0, bytes_1.bytesToHex)(keyId);
|
|
@@ -96,6 +113,13 @@ function createAppStateSyncKeyProtocol(options) {
|
|
|
96
113
|
keyId: { keyId }
|
|
97
114
|
});
|
|
98
115
|
}
|
|
116
|
+
if (droppedKeyIds.length > 0) {
|
|
117
|
+
logger.warn('app-state refusing to share malformed sync keys: keyId byte length must be 6', {
|
|
118
|
+
droppedCount: droppedKeyIds.length,
|
|
119
|
+
totalExpected: keys.length + missingKeyIds.length,
|
|
120
|
+
sample: droppedKeyIds.slice(0, 5)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
99
123
|
const protocolMessage = {
|
|
100
124
|
type: _proto_1.proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
|
|
101
125
|
appStateSyncKeyShare: {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A companion device this primary has linked. */
|
|
2
|
+
export interface CompanionRecord {
|
|
3
|
+
readonly deviceJid: string;
|
|
4
|
+
readonly keyIndex: number;
|
|
5
|
+
readonly companionIdentityPublicKey: Uint8Array;
|
|
6
|
+
readonly addedAtSeconds: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The primary's ADV epoch state. `rawId` is the account's stable ADV identity
|
|
10
|
+
* id; `currentKeyIndex` is the last-issued companion key index (companions get
|
|
11
|
+
* `currentKeyIndex + 1`). This MUST persist across restarts, otherwise reissuing
|
|
12
|
+
* an already-used key index breaks previously linked devices.
|
|
13
|
+
*/
|
|
14
|
+
export interface CompanionHostEpochState {
|
|
15
|
+
readonly rawId: number;
|
|
16
|
+
readonly currentKeyIndex: number;
|
|
17
|
+
readonly companions: readonly CompanionRecord[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Persistence hook for {@link CompanionHostEpochState}. Supply one via plugin
|
|
21
|
+
* options to survive restarts; without it the epoch is in-memory only (fine for
|
|
22
|
+
* a single-session smoke test, unsafe for production relinking).
|
|
23
|
+
*/
|
|
24
|
+
export interface CompanionHostPersistence {
|
|
25
|
+
readonly load: () => Promise<CompanionHostEpochState | null> | CompanionHostEpochState | null;
|
|
26
|
+
readonly save: (state: CompanionHostEpochState) => Promise<void> | void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* File-backed {@link CompanionHostPersistence}: stores the ADV epoch as JSON at
|
|
30
|
+
* `filePath` (the companion identity keys are base64-encoded). Zero native
|
|
31
|
+
* dependencies - the state is tiny (the epoch header plus the linked-companion
|
|
32
|
+
* list). For a database-backed store, implement the two-method
|
|
33
|
+
* `CompanionHostPersistence` contract directly against your DB; there is no need
|
|
34
|
+
* for a dedicated core store domain.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createFileCompanionHostPersistence(filePath: string): CompanionHostPersistence;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFileCompanionHostPersistence = createFileCompanionHostPersistence;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const bytes_1 = require("../../util/bytes");
|
|
7
|
+
/**
|
|
8
|
+
* File-backed {@link CompanionHostPersistence}: stores the ADV epoch as JSON at
|
|
9
|
+
* `filePath` (the companion identity keys are base64-encoded). Zero native
|
|
10
|
+
* dependencies - the state is tiny (the epoch header plus the linked-companion
|
|
11
|
+
* list). For a database-backed store, implement the two-method
|
|
12
|
+
* `CompanionHostPersistence` contract directly against your DB; there is no need
|
|
13
|
+
* for a dedicated core store domain.
|
|
14
|
+
*/
|
|
15
|
+
function createFileCompanionHostPersistence(filePath) {
|
|
16
|
+
return {
|
|
17
|
+
async load() {
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await (0, promises_1.readFile)(filePath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
return {
|
|
30
|
+
rawId: parsed.rawId,
|
|
31
|
+
currentKeyIndex: parsed.currentKeyIndex,
|
|
32
|
+
companions: parsed.companions.map((companion) => ({
|
|
33
|
+
deviceJid: companion.deviceJid,
|
|
34
|
+
keyIndex: companion.keyIndex,
|
|
35
|
+
companionIdentityPublicKey: (0, bytes_1.base64ToBytes)(companion.companionIdentityPublicKey),
|
|
36
|
+
addedAtSeconds: companion.addedAtSeconds
|
|
37
|
+
}))
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async save(state) {
|
|
41
|
+
const serialized = {
|
|
42
|
+
rawId: state.rawId,
|
|
43
|
+
currentKeyIndex: state.currentKeyIndex,
|
|
44
|
+
companions: state.companions.map((companion) => ({
|
|
45
|
+
deviceJid: companion.deviceJid,
|
|
46
|
+
keyIndex: companion.keyIndex,
|
|
47
|
+
companionIdentityPublicKey: (0, bytes_1.bytesToBase64)(companion.companionIdentityPublicKey),
|
|
48
|
+
addedAtSeconds: companion.addedAtSeconds
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
52
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
53
|
+
await (0, promises_1.writeFile)(tempPath, JSON.stringify(serialized), 'utf8');
|
|
54
|
+
await (0, promises_1.rename)(tempPath, filePath);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -13,6 +13,7 @@ export interface WaClientPluginInstallInput {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Installs {@link WaClientOptions.plugins} on `client`. Returns a dispose
|
|
16
|
-
* function invoked by {@link WaClient.disconnect}.
|
|
16
|
+
* function invoked by {@link WaClient.disconnect}. Safe to call again after a
|
|
17
|
+
* dispose to reinstall the same plugins on reconnect.
|
|
17
18
|
*/
|
|
18
19
|
export declare function installWaClientPlugins(client: WaClient, input: WaClientPluginInstallInput, plugins: readonly WaClientPluginDefinition[]): () => Promise<void>;
|
|
@@ -3,11 +3,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.installWaClientPlugins = installWaClientPlugins;
|
|
4
4
|
const types_1 = require("../plugins/types");
|
|
5
5
|
const primitives_1 = require("../../util/primitives");
|
|
6
|
+
const PLUGIN_REGISTRY = Symbol('waClientPluginRegistry');
|
|
7
|
+
/**
|
|
8
|
+
* Per-client store for exposed-plugin state that must survive a
|
|
9
|
+
* disconnect/connect cycle. Each `exposeAs` property is defined on the client
|
|
10
|
+
* exactly once (a non-configurable getter reading `instances`); reinstalls
|
|
11
|
+
* only repopulate the slot, so plugins can be torn down on disconnect and
|
|
12
|
+
* rebuilt on the next connect without redefining reserved members.
|
|
13
|
+
*/
|
|
14
|
+
function getPluginRegistry(client) {
|
|
15
|
+
const holder = client;
|
|
16
|
+
const existing = holder[PLUGIN_REGISTRY];
|
|
17
|
+
if (existing) {
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
const registry = { instances: new Map(), exposedDefined: new Set() };
|
|
21
|
+
Object.defineProperty(client, PLUGIN_REGISTRY, {
|
|
22
|
+
value: registry,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
configurable: false,
|
|
25
|
+
writable: false
|
|
26
|
+
});
|
|
27
|
+
return registry;
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* Installs {@link WaClientOptions.plugins} on `client`. Returns a dispose
|
|
8
|
-
* function invoked by {@link WaClient.disconnect}.
|
|
31
|
+
* function invoked by {@link WaClient.disconnect}. Safe to call again after a
|
|
32
|
+
* dispose to reinstall the same plugins on reconnect.
|
|
9
33
|
*/
|
|
10
34
|
function installWaClientPlugins(client, input, plugins) {
|
|
35
|
+
const registry = getPluginRegistry(client);
|
|
11
36
|
const seenIds = new Set();
|
|
12
37
|
const seenExposeAs = new Set();
|
|
13
38
|
const disposeCallbacks = [];
|
|
@@ -40,24 +65,32 @@ function installWaClientPlugins(client, input, plugins) {
|
|
|
40
65
|
logger: input.logger.child({ plugin: plugin.id })
|
|
41
66
|
};
|
|
42
67
|
if ((0, types_1.isWaClientExposePluginDefinition)(plugin)) {
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
const exposeAs = plugin.exposeAs;
|
|
69
|
+
if (seenExposeAs.has(exposeAs)) {
|
|
70
|
+
throw new Error(`duplicate wa client plugin exposeAs: ${exposeAs}`);
|
|
45
71
|
}
|
|
46
|
-
seenExposeAs.add(
|
|
47
|
-
if (
|
|
48
|
-
|
|
72
|
+
seenExposeAs.add(exposeAs);
|
|
73
|
+
if (!registry.exposedDefined.has(exposeAs)) {
|
|
74
|
+
if (exposeAs in client) {
|
|
75
|
+
throw new Error(`wa client plugin exposeAs "${exposeAs}" collides with a reserved client member`);
|
|
76
|
+
}
|
|
77
|
+
registry.exposedDefined.add(exposeAs);
|
|
78
|
+
Object.defineProperty(client, exposeAs, {
|
|
79
|
+
get: () => registry.instances.get(exposeAs),
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: false
|
|
82
|
+
});
|
|
49
83
|
}
|
|
50
84
|
const instance = plugin.setup(pluginCtx);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
configurable: false
|
|
85
|
+
registry.instances.set(exposeAs, instance);
|
|
86
|
+
registerDispose(() => {
|
|
87
|
+
registry.instances.delete(exposeAs);
|
|
55
88
|
});
|
|
56
89
|
if (plugin.dispose) {
|
|
57
90
|
const dispose = plugin.dispose;
|
|
58
91
|
registerDispose(() => dispose(instance, pluginCtx));
|
|
59
92
|
}
|
|
60
|
-
pluginCtx.logger.debug('wa client plugin installed', { exposeAs
|
|
93
|
+
pluginCtx.logger.debug('wa client plugin installed', { exposeAs });
|
|
61
94
|
}
|
|
62
95
|
else {
|
|
63
96
|
plugin.setup(pluginCtx);
|
package/dist/client/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { WaShortcakeAssertionSigner } from '../auth/pairing/WaShortcakeFlow
|
|
|
4
4
|
import type { WaAuthClientOptions, WaAuthCredentials, WaAuthDangerousOptions, WaAuthSocketOptions } from '../auth/types';
|
|
5
5
|
import type { WaCallGroupParticipant, WaCallType } from './events/call';
|
|
6
6
|
import type { IncomingPresenceType, PresenceLastSeen } from './events/presence';
|
|
7
|
+
import type { CompanionHostPersistence } from './persistence/companion-host';
|
|
7
8
|
import type { WaClientPluginDefinition } from './plugins/types';
|
|
8
9
|
import type { WaMediaProcessor } from '../media/processor';
|
|
9
10
|
import type { WaLinkPreviewOptions } from '../message/addons/link-preview/types';
|
|
@@ -83,6 +84,16 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
83
84
|
* tests or pinning a specific edge.
|
|
84
85
|
*/
|
|
85
86
|
readonly chatSocketUrls?: readonly string[];
|
|
87
|
+
/**
|
|
88
|
+
* Companion-hosting config for a mobile-primary session (see
|
|
89
|
+
* {@link WaClient.mobile}). `persistence` survives the ADV epoch across
|
|
90
|
+
* restarts (without it, relinking breaks previously linked devices);
|
|
91
|
+
* `includePem` adds the experimental `<pem>` node to the pair-device upload.
|
|
92
|
+
*/
|
|
93
|
+
readonly companionHost?: {
|
|
94
|
+
readonly persistence?: CompanionHostPersistence;
|
|
95
|
+
readonly includePem?: boolean;
|
|
96
|
+
};
|
|
86
97
|
/** Default timeout (ms) for IQ queries. Defaults to `WA_DEFAULTS.IQ_TIMEOUT_MS` (60s). */
|
|
87
98
|
readonly iqTimeoutMs?: number;
|
|
88
99
|
/** Default timeout (ms) for raw node `query()` calls when none is passed. */
|
|
@@ -119,8 +130,11 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
119
130
|
* Automatically reconnect when the server rejects the noise handshake
|
|
120
131
|
* with HTTP 405 / `failure_client_too_old`. On every 405 the client
|
|
121
132
|
* logs a warning asking you to upgrade zapo, fetches the current
|
|
122
|
-
* version
|
|
123
|
-
*
|
|
133
|
+
* version, swaps it in for the next connect, and retries. Off by
|
|
134
|
+
* default. Web sessions fetch the WA Web version via
|
|
135
|
+
* `fetchLatestWaWebVersion()` (swapped in as the connect `version`);
|
|
136
|
+
* mobile sessions fetch the Android app version via
|
|
137
|
+
* `fetchLatestWaMobileVersion()` (swapped in as `deviceInfo.appVersion`).
|
|
124
138
|
*/
|
|
125
139
|
readonly recoverFromClientTooOld?: boolean;
|
|
126
140
|
/**
|
|
@@ -141,6 +155,7 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
141
155
|
* Chat-event emission tuning. `emitSnapshotMutations: true` re-emits
|
|
142
156
|
* `app_state_mutation` events for every mutation seen during a snapshot
|
|
143
157
|
* sync (off by default – those mutations represent historical state).
|
|
158
|
+
* This client's own outbound app-state actions surface on `mutation_send`.
|
|
144
159
|
*/
|
|
145
160
|
readonly chatEvents?: {
|
|
146
161
|
readonly emitSnapshotMutations?: boolean;
|
|
@@ -540,6 +555,22 @@ export interface WaIncomingMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJ
|
|
|
540
555
|
readonly pushName?: string;
|
|
541
556
|
readonly message?: Proto.IMessage;
|
|
542
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* An outbound message this client sent, surfaced with its decrypted
|
|
560
|
+
* {@link Proto.IMessage} – the symmetric counterpart to
|
|
561
|
+
* {@link WaIncomingMessageEvent}. Fires from {@link WaMessageCoordinator.send}
|
|
562
|
+
* as the stanza is built, so plugins/loggers can observe outgoing content
|
|
563
|
+
* (forwards, reactions, polls, media) that the encrypted `<message>` on the
|
|
564
|
+
* wire does not reveal.
|
|
565
|
+
*/
|
|
566
|
+
export interface WaOutgoingMessageEvent {
|
|
567
|
+
/** Destination jid (chat / group / status). */
|
|
568
|
+
readonly to: string;
|
|
569
|
+
/** Outgoing stanza id, when assigned. */
|
|
570
|
+
readonly id?: string;
|
|
571
|
+
/** The decrypted message proto being sent. */
|
|
572
|
+
readonly message: Proto.IMessage;
|
|
573
|
+
}
|
|
543
574
|
export interface WaIncomingProtocolMessageEvent extends WaIncomingMessageEvent {
|
|
544
575
|
readonly protocolMessage: Proto.Message.IProtocolMessage;
|
|
545
576
|
}
|
|
@@ -1001,7 +1032,7 @@ export interface WaHistorySyncChunkEvent {
|
|
|
1001
1032
|
/** Server-reported progress, 0–100. */
|
|
1002
1033
|
readonly progress?: number;
|
|
1003
1034
|
}
|
|
1004
|
-
export type WaAppStateMutationSource = 'snapshot' | 'patch';
|
|
1035
|
+
export type WaAppStateMutationSource = 'snapshot' | 'patch' | 'local';
|
|
1005
1036
|
type MutationEventBase = {
|
|
1006
1037
|
readonly source: WaAppStateMutationSource;
|
|
1007
1038
|
readonly collection: AppStateCollectionName;
|
|
@@ -1022,6 +1053,12 @@ export type WaAppStateMutationEvent = {
|
|
|
1022
1053
|
readonly operation: 'remove';
|
|
1023
1054
|
} & MutationEventBase & WaAppstateIndexArgs<K>);
|
|
1024
1055
|
}[WaAppstateActionKey];
|
|
1056
|
+
/**
|
|
1057
|
+
* Listener shared by the inbound `mutation` and outbound `mutation_send`
|
|
1058
|
+
* events. A single named alias so the (large) `WaAppStateMutationEvent` union
|
|
1059
|
+
* is referenced once by the event map rather than expanded per event.
|
|
1060
|
+
*/
|
|
1061
|
+
type WaAppStateMutationListener = (event: WaAppStateMutationEvent) => void;
|
|
1025
1062
|
export type WaConnectionEvent = {
|
|
1026
1063
|
readonly status: 'open';
|
|
1027
1064
|
readonly reason: WaConnectionOpenReason;
|
|
@@ -1103,6 +1140,13 @@ export interface WaClientEventMap {
|
|
|
1103
1140
|
* `options.quote` for threads).
|
|
1104
1141
|
*/
|
|
1105
1142
|
readonly message: (event: WaIncomingMessageEvent) => void;
|
|
1143
|
+
/**
|
|
1144
|
+
* An outbound message this client is sending, with its decrypted
|
|
1145
|
+
* {@link Proto.IMessage} – the symmetric counterpart to `message`. Lets
|
|
1146
|
+
* plugins/loggers observe outgoing content (forwards, reactions, polls,
|
|
1147
|
+
* media) the encrypted wire stanza hides.
|
|
1148
|
+
*/
|
|
1149
|
+
readonly message_send: (event: WaOutgoingMessageEvent) => void;
|
|
1106
1150
|
/**
|
|
1107
1151
|
* A decrypted addon (poll vote, reaction, edit, comment, ...) attached to
|
|
1108
1152
|
* a previous message. Fires unless `addons.autoDecrypt` is explicitly
|
|
@@ -1162,11 +1206,20 @@ export interface WaClientEventMap {
|
|
|
1162
1206
|
/** Profile/group/community picture change notification – the new picture must still be fetched explicitly. */
|
|
1163
1207
|
readonly picture: (event: WaPictureEvent) => void;
|
|
1164
1208
|
/**
|
|
1165
|
-
* A parsed app-state mutation
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1209
|
+
* A parsed app-state mutation arriving from a sync – chat mute/star/read/
|
|
1210
|
+
* pin/archive/contact/label/etc. changed on another device. Inbound only;
|
|
1211
|
+
* this client's own outbound actions surface on `mutation_send`. Use the
|
|
1212
|
+
* discriminator (`event.schema`) to branch on the mutation kind.
|
|
1168
1213
|
*/
|
|
1169
|
-
readonly mutation:
|
|
1214
|
+
readonly mutation: WaAppStateMutationListener;
|
|
1215
|
+
/**
|
|
1216
|
+
* An app-state action this client is sending (mute/star/read/pin/archive/
|
|
1217
|
+
* clear/delete/…) surfaced at action time – the outbound counterpart to
|
|
1218
|
+
* `mutation`, symmetric to how `message_send` pairs with `message`.
|
|
1219
|
+
* `event.source` is always `'local'`. Optimistic: emitted as the mutation
|
|
1220
|
+
* is enqueued, before the server flush confirms it.
|
|
1221
|
+
*/
|
|
1222
|
+
readonly mutation_send: WaAppStateMutationListener;
|
|
1170
1223
|
/**
|
|
1171
1224
|
* One chunk of history-sync data, fired both during the initial
|
|
1172
1225
|
* bootstrap that the primary device pushes after pairing and for any
|
|
@@ -1204,6 +1257,17 @@ export interface WaClientEventMap {
|
|
|
1204
1257
|
* `mobileTransport`.
|
|
1205
1258
|
*/
|
|
1206
1259
|
readonly mobile_account_takeover_notice: (event: WaAccountTakeoverNoticeEvent) => void;
|
|
1260
|
+
/** A companion device was linked from this mobile-primary account. */
|
|
1261
|
+
readonly companion_host_linked: (result: {
|
|
1262
|
+
readonly deviceJid: string;
|
|
1263
|
+
readonly keyIndex: number;
|
|
1264
|
+
}) => void;
|
|
1265
|
+
/** A hosted companion device was revoked / unlinked. */
|
|
1266
|
+
readonly companion_host_revoked: (payload: {
|
|
1267
|
+
readonly deviceJid: string;
|
|
1268
|
+
}) => void;
|
|
1269
|
+
/** A companion-host link or provisioning operation failed. */
|
|
1270
|
+
readonly companion_host_error: (error: Error) => void;
|
|
1207
1271
|
/** **debug** – the success node closing the noise handshake; emitted before `connection: { status: 'open' }`. */
|
|
1208
1272
|
readonly debug_connection_success: (event: {
|
|
1209
1273
|
readonly node: BinaryNode;
|
|
@@ -2,12 +2,11 @@ import { buildCommsConfig, loadOrCreateCredentials, persistCredentials } from '.
|
|
|
2
2
|
import { WaPairingFlow } from './pairing/WaPairingFlow.js';
|
|
3
3
|
import { WaQrFlow } from './pairing/WaQrFlow.js';
|
|
4
4
|
import { WaShortcakeFlow } from './pairing/WaShortcakeFlow.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveWaDeviceIdentity, WA_NOTIFICATION_TYPES } from '../protocol/constants.js';
|
|
6
6
|
import { buildAckNode } from '../transport/node/builders/global.js';
|
|
7
7
|
import { resolveDevicePropsPlatformType } from '../transport/noise/WaClientPayload.js';
|
|
8
8
|
import { uint8Equal } from '../util/bytes.js';
|
|
9
9
|
import { toError } from '../util/primitives.js';
|
|
10
|
-
import { getRuntimeOsDisplayName } from '../util/runtime.js';
|
|
11
10
|
/**
|
|
12
11
|
* Owns the auth/pairing lifecycle and persistence of {@link WaAuthCredentials}.
|
|
13
12
|
* Exposed as `client.auth` on a {@link WaClient}.
|
|
@@ -20,12 +19,8 @@ import { getRuntimeOsDisplayName } from '../util/runtime.js';
|
|
|
20
19
|
export class WaAuthClient {
|
|
21
20
|
constructor(options, deps) {
|
|
22
21
|
this.versionOverride = null;
|
|
23
|
-
|
|
24
|
-
const device =
|
|
25
|
-
browser: deviceBrowser,
|
|
26
|
-
osDisplayName: options.deviceOsDisplayName ?? getRuntimeOsDisplayName(),
|
|
27
|
-
platform: options.devicePlatform ?? getWaCompanionPlatformId(deviceBrowser)
|
|
28
|
-
});
|
|
22
|
+
this.mobileAppVersionOverride = null;
|
|
23
|
+
const device = resolveWaDeviceIdentity(options);
|
|
29
24
|
this.options = Object.freeze({
|
|
30
25
|
...options,
|
|
31
26
|
deviceBrowser: device.browser,
|
|
@@ -76,7 +71,7 @@ export class WaAuthClient {
|
|
|
76
71
|
getCredentials: () => this.credentials,
|
|
77
72
|
updateCredentials: this.updateCredentials.bind(this)
|
|
78
73
|
},
|
|
79
|
-
deviceType: resolveDevicePropsPlatformType(
|
|
74
|
+
deviceType: resolveDevicePropsPlatformType(device.browser)
|
|
80
75
|
})
|
|
81
76
|
: null;
|
|
82
77
|
}
|
|
@@ -124,12 +119,15 @@ export class WaAuthClient {
|
|
|
124
119
|
this.logger.trace('auth client building comms config');
|
|
125
120
|
const override = this.versionOverride;
|
|
126
121
|
this.versionOverride = null;
|
|
122
|
+
const mobileAppVersionOverride = this.mobileAppVersionOverride;
|
|
123
|
+
this.mobileAppVersionOverride = null;
|
|
127
124
|
return buildCommsConfig(this.logger, this.requireCredentials(), socketOptions, {
|
|
128
125
|
deviceBrowser: this.options.deviceBrowser,
|
|
129
126
|
deviceOsDisplayName: this.options.deviceOsDisplayName,
|
|
130
127
|
requireFullSync: this.options.requireFullSync,
|
|
131
128
|
version: override ?? this.options.version,
|
|
132
129
|
mobileTransport: this.options.mobileTransport,
|
|
130
|
+
mobileAppVersionOverride: mobileAppVersionOverride ?? undefined,
|
|
133
131
|
noiseTrustedRootCa: overrides.noiseTrustedRootCa,
|
|
134
132
|
disableNoiseCertificateChainVerification: overrides.disableNoiseCertificateChainVerification ??
|
|
135
133
|
this.options.dangerous?.disableNoiseCertificateChainVerification
|
|
@@ -145,6 +143,17 @@ export class WaAuthClient {
|
|
|
145
143
|
setNextConnectVersion(version) {
|
|
146
144
|
this.versionOverride = version;
|
|
147
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* One-shot override for the mobile `deviceInfo.appVersion` on the next
|
|
148
|
+
* {@link buildCommsConfig} call, then clears. The mobile transport reads
|
|
149
|
+
* its version from `deviceInfo.appVersion` (the login payload), not the
|
|
150
|
+
* `version` string, so {@link setNextConnectVersion} has no effect on a
|
|
151
|
+
* mobile session. Used by the `recoverFromClientTooOld` auto-retry to
|
|
152
|
+
* inject a fresh Android app version without mutating the user's options.
|
|
153
|
+
*/
|
|
154
|
+
setNextConnectMobileAppVersion(appVersion) {
|
|
155
|
+
this.mobileAppVersionOverride = appVersion;
|
|
156
|
+
}
|
|
148
157
|
/** Clears the in-memory QR and pairing sessions without touching storage. */
|
|
149
158
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
150
159
|
async clearTransientState() {
|
|
@@ -6,7 +6,7 @@ import { createAndStoreInitialKeys } from '../signal/index.js';
|
|
|
6
6
|
import { WaMobileTcpSocketCtor } from '../transport/node/WaMobileTcpSocket.js';
|
|
7
7
|
import { buildMobileLoginPayload } from '../transport/noise/WaMobileClientPayload.js';
|
|
8
8
|
import { toProxyAgent, toProxyDispatcher } from '../transport/proxy.js';
|
|
9
|
-
import { toError } from '../util/primitives.js';
|
|
9
|
+
import { parseOptionalInt, toError } from '../util/primitives.js';
|
|
10
10
|
export async function loadOrCreateCredentials(args) {
|
|
11
11
|
args.logger.trace('auth credentials loadOrCreate start');
|
|
12
12
|
const existing = await args.authStore.load();
|
|
@@ -61,6 +61,26 @@ async function resolveVersion(version) {
|
|
|
61
61
|
}
|
|
62
62
|
return resolved;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Validates the user-supplied `version` string against the transport it will
|
|
66
|
+
* feed. A mobile session sends the version as the four-part Android app
|
|
67
|
+
* version (`2.YY.WW.RR`) in the login payload, so it must have exactly four
|
|
68
|
+
* numeric parts. A web session advertises up to five parts (`2.3000.x[.y.z]`),
|
|
69
|
+
* so three to five numeric parts are accepted.
|
|
70
|
+
*/
|
|
71
|
+
function assertValidVersion(version, mobile) {
|
|
72
|
+
const parts = version.split('.');
|
|
73
|
+
const allNumeric = parts.every((part) => parseOptionalInt(part) !== undefined);
|
|
74
|
+
if (mobile) {
|
|
75
|
+
if (parts.length !== 4 || !allNumeric) {
|
|
76
|
+
throw new Error(`mobile session requires a 4-part numeric version (e.g. 2.26.27.70), got: ${version}`);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (parts.length < 3 || parts.length > 5 || !allNumeric) {
|
|
81
|
+
throw new Error(`web session requires a 3- to 5-part numeric version (e.g. 2.3000.1040229458), got: ${version}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
64
84
|
export async function buildCommsConfig(logger, credentials, socketOptions, clientOptions) {
|
|
65
85
|
const meJid = credentials.meJid;
|
|
66
86
|
const registered = meJid !== null && meJid !== undefined;
|
|
@@ -81,6 +101,12 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
81
101
|
? 'credentials'
|
|
82
102
|
: 'none'
|
|
83
103
|
});
|
|
104
|
+
const resolvedVersion = effectiveMobileTransport && clientOptions.mobileAppVersionOverride !== undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: await resolveVersion(clientOptions.version);
|
|
107
|
+
if (resolvedVersion !== undefined) {
|
|
108
|
+
assertValidVersion(resolvedVersion, Boolean(effectiveMobileTransport));
|
|
109
|
+
}
|
|
84
110
|
if (effectiveMobileTransport) {
|
|
85
111
|
if (wsProxy) {
|
|
86
112
|
throw new Error('mobileTransport does not support socketOptions.proxy.ws – remove the proxy option or open an issue to add TCP proxy support');
|
|
@@ -88,11 +114,18 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
88
114
|
if (!loginIdentity) {
|
|
89
115
|
throw new Error('mobileTransport requires registered credentials (meJid) – run the mobile bridge flow first');
|
|
90
116
|
}
|
|
117
|
+
if (clientOptions.mobileAppVersionOverride !== undefined) {
|
|
118
|
+
assertValidVersion(clientOptions.mobileAppVersionOverride, true);
|
|
119
|
+
}
|
|
120
|
+
const appVersionOverride = clientOptions.mobileAppVersionOverride ?? resolvedVersion;
|
|
121
|
+
const deviceInfo = appVersionOverride
|
|
122
|
+
? { ...effectiveMobileTransport.deviceInfo, appVersion: appVersionOverride }
|
|
123
|
+
: effectiveMobileTransport.deviceInfo;
|
|
91
124
|
const loginPayload = buildMobileLoginPayload({
|
|
92
125
|
username: loginIdentity.username,
|
|
93
126
|
device: loginIdentity.device,
|
|
94
127
|
passive: effectiveMobileTransport.passive ?? false,
|
|
95
|
-
deviceInfo
|
|
128
|
+
deviceInfo,
|
|
96
129
|
pushName: effectiveMobileTransport.pushName,
|
|
97
130
|
yearClass: effectiveMobileTransport.yearClass,
|
|
98
131
|
memClass: effectiveMobileTransport.memClass
|
|
@@ -117,7 +150,7 @@ export async function buildCommsConfig(logger, credentials, socketOptions, clien
|
|
|
117
150
|
}
|
|
118
151
|
};
|
|
119
152
|
}
|
|
120
|
-
const versionBase =
|
|
153
|
+
const versionBase = resolvedVersion;
|
|
121
154
|
return {
|
|
122
155
|
url: socketOptions.url,
|
|
123
156
|
urls: socketOptions.urls,
|