zapo-js 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appstate/types.d.ts +1 -2
- package/dist/auth/WaAuthClient.d.ts +25 -0
- package/dist/auth/WaAuthClient.js +63 -7
- package/dist/auth/credentials-flow.d.ts +8 -0
- package/dist/auth/credentials-flow.js +35 -2
- package/dist/auth/pairing/WaShortcakeFlow.d.ts +93 -0
- package/dist/auth/pairing/WaShortcakeFlow.js +210 -0
- package/dist/auth/pairing/companion-host.d.ts +105 -0
- package/dist/auth/pairing/companion-host.js +148 -0
- package/dist/auth/pairing/pairing-code-crypto.d.ts +1 -0
- package/dist/auth/pairing/pairing-code-crypto.js +3 -2
- package/dist/auth/pairing/shortcake-crypto.d.ts +59 -0
- package/dist/auth/pairing/shortcake-crypto.js +106 -0
- package/dist/auth/types.d.ts +20 -9
- package/dist/client/WaClient.d.ts +15 -5
- package/dist/client/WaClient.js +46 -12
- package/dist/client/WaClientFactory.d.ts +3 -0
- package/dist/client/WaClientFactory.js +27 -7
- package/dist/client/coordinators/WaAppStateMutationCoordinator.d.ts +8 -0
- package/dist/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +3 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +13 -3
- package/dist/client/coordinators/WaMobileCoordinator.d.ts +192 -0
- package/dist/client/coordinators/WaMobileCoordinator.js +645 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +1 -0
- package/dist/client/messaging/companion-host.d.ts +43 -0
- package/dist/client/messaging/companion-host.js +61 -0
- package/dist/client/messaging/key-protocol.js +25 -1
- package/dist/client/persistence/companion-host.d.ts +36 -0
- package/dist/client/persistence/companion-host.js +57 -0
- package/dist/client/plugins/install.d.ts +2 -1
- package/dist/client/plugins/install.js +44 -11
- package/dist/client/types.d.ts +90 -7
- package/dist/esm/auth/WaAuthClient.js +64 -8
- package/dist/esm/auth/credentials-flow.js +36 -3
- package/dist/esm/auth/pairing/WaShortcakeFlow.js +206 -0
- package/dist/esm/auth/pairing/companion-host.js +140 -0
- package/dist/esm/auth/pairing/pairing-code-crypto.js +2 -2
- package/dist/esm/auth/pairing/shortcake-crypto.js +99 -0
- package/dist/esm/client/WaClient.js +46 -12
- package/dist/esm/client/WaClientFactory.js +28 -8
- package/dist/esm/client/coordinators/WaAppStateMutationCoordinator.js +37 -1
- package/dist/esm/client/coordinators/WaGroupCoordinator.js +6 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -4
- package/dist/esm/client/coordinators/WaMobileCoordinator.js +641 -0
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -0
- package/dist/esm/client/messaging/companion-host.js +56 -0
- package/dist/esm/client/messaging/key-protocol.js +25 -1
- package/dist/esm/client/persistence/companion-host.js +54 -0
- package/dist/esm/client/plugins/install.js +44 -11
- package/dist/esm/index.js +5 -2
- package/dist/esm/message/WaMessageClient.js +3 -2
- package/dist/esm/message/context-info.js +4 -0
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/device-identity.js +18 -0
- package/dist/esm/protocol/group.js +6 -0
- package/dist/esm/protocol/message.js +12 -0
- package/dist/esm/protocol/nodes.js +17 -1
- package/dist/esm/protocol/notification.js +3 -1
- package/dist/esm/retry/replay.js +7 -15
- package/dist/esm/signal/attestation/WaAdvSignature.js +67 -2
- package/dist/esm/signal/attestation/constants.js +2 -0
- package/dist/esm/signal/index.js +1 -0
- package/dist/esm/transport/index.js +1 -1
- package/dist/esm/transport/node/builders/group.js +4 -3
- package/dist/esm/transport/node/builders/mobile.js +113 -0
- package/dist/esm/transport/node/builders/shortcake.js +50 -0
- package/dist/esm/transport/noise/WaClientPayload.js +17 -8
- package/dist/esm/transport/wa-version-fetcher.js +152 -0
- package/dist/esm/util/index.js +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +13 -4
- package/dist/message/WaMessageClient.js +2 -1
- package/dist/message/context-info.d.ts +2 -0
- package/dist/message/context-info.js +5 -0
- package/dist/protocol/constants.d.ts +6 -4
- package/dist/protocol/constants.js +8 -2
- package/dist/protocol/device-identity.d.ts +25 -0
- package/dist/protocol/device-identity.js +21 -0
- package/dist/protocol/group.d.ts +6 -0
- package/dist/protocol/group.js +7 -1
- package/dist/protocol/message.d.ts +14 -0
- package/dist/protocol/message.js +13 -1
- package/dist/protocol/nodes.d.ts +16 -0
- package/dist/protocol/nodes.js +17 -1
- package/dist/protocol/notification.d.ts +2 -0
- package/dist/protocol/notification.js +3 -1
- package/dist/retry/replay.d.ts +1 -0
- package/dist/retry/replay.js +6 -14
- package/dist/signal/attestation/WaAdvSignature.d.ts +50 -1
- package/dist/signal/attestation/WaAdvSignature.js +71 -1
- package/dist/signal/attestation/constants.d.ts +2 -0
- package/dist/signal/attestation/constants.js +3 -1
- package/dist/signal/index.d.ts +1 -0
- package/dist/signal/index.js +13 -1
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js +4 -3
- package/dist/transport/node/builders/group.js +4 -3
- package/dist/transport/node/builders/mobile.d.ts +68 -0
- package/dist/transport/node/builders/mobile.js +120 -0
- package/dist/transport/node/builders/shortcake.d.ts +19 -0
- package/dist/transport/node/builders/shortcake.js +57 -0
- package/dist/transport/noise/WaClientPayload.d.ts +2 -0
- package/dist/transport/noise/WaClientPayload.js +17 -7
- package/dist/transport/wa-version-fetcher.d.ts +92 -0
- package/dist/transport/wa-version-fetcher.js +156 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
- package/dist/esm/transport/wa-web-version-fetcher.js +0 -91
- package/dist/transport/wa-web-version-fetcher.d.ts +0 -44
- package/dist/transport/wa-web-version-fetcher.js +0 -94
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Proto } from '../../proto';
|
|
2
|
+
/** A PN→LID mapping carried inside a history sync (one per chat peer with a LID). */
|
|
3
|
+
export interface PhoneNumberToLidMapping {
|
|
4
|
+
readonly pnJid: string;
|
|
5
|
+
readonly lidJid: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HistorySyncBootstrapInput {
|
|
8
|
+
/**
|
|
9
|
+
* Chat-peer PN→LID pairs (the primary's own account excluded, only peers
|
|
10
|
+
* resolving to a LID). A fresh account with no chats yields an empty list.
|
|
11
|
+
*/
|
|
12
|
+
readonly phoneNumberToLidMappings?: readonly PhoneNumberToLidMapping[];
|
|
13
|
+
readonly conversations?: readonly Proto.IConversation[];
|
|
14
|
+
readonly pushnames?: readonly Proto.IPushname[];
|
|
15
|
+
/** The companion pairing session id, echoed so the companion can bind it. */
|
|
16
|
+
readonly companionMetaNonce?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Serializes a `HistorySync` and zlib-deflates it. The phone uses DEFLATE
|
|
20
|
+
* level 1; any zlib-framed level inflates identically, so it is only a
|
|
21
|
+
* size/speed tradeoff.
|
|
22
|
+
*/
|
|
23
|
+
export declare function encodeHistorySync(content: Proto.IHistorySync): Promise<Uint8Array>;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps a deflated `HistorySync` inline in a `HISTORY_SYNC_NOTIFICATION` protocol
|
|
26
|
+
* message. Only small syncs are inlined; larger ones ride an external
|
|
27
|
+
* `md-msg-hist` MMS blob referenced by the same notification.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildInlineHistorySyncNotification(syncType: Proto.Message.HistorySyncType, deflatedPayload: Uint8Array, options?: {
|
|
30
|
+
readonly chunkOrder?: number;
|
|
31
|
+
readonly progress?: number;
|
|
32
|
+
}): Proto.Message.IProtocolMessage;
|
|
33
|
+
/**
|
|
34
|
+
* Builds the `INITIAL_BOOTSTRAP` `HISTORY_SYNC_NOTIFICATION` a mobile-primary
|
|
35
|
+
* pushes to a freshly-linked companion: a deflated `HistorySync` wrapped in a
|
|
36
|
+
* protocol message for delivery as an encrypted peer message. Note
|
|
37
|
+
* `HistorySync.syncType` and `HistorySyncNotification.syncType` are distinct
|
|
38
|
+
* enum types with shared values, so each field uses its own.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildHistorySyncBootstrapMessage(input?: HistorySyncBootstrapInput): Promise<{
|
|
41
|
+
readonly message: Proto.Message.IProtocolMessage;
|
|
42
|
+
readonly payloadBytes: number;
|
|
43
|
+
}>;
|
|
@@ -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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AppStateCollectionName } from '../appstate/types';
|
|
2
2
|
import type { DataForKey, WaAppstateActionKey, WaAppstateIndexArgs } from '../appstate-spec';
|
|
3
|
+
import type { WaShortcakeAssertionSigner } from '../auth/pairing/WaShortcakeFlow';
|
|
3
4
|
import type { WaAuthClientOptions, WaAuthCredentials, WaAuthDangerousOptions, WaAuthSocketOptions } from '../auth/types';
|
|
4
5
|
import type { WaCallGroupParticipant, WaCallType } from './events/call';
|
|
5
6
|
import type { IncomingPresenceType, PresenceLastSeen } from './events/presence';
|
|
7
|
+
import type { CompanionHostPersistence } from './persistence/companion-host';
|
|
6
8
|
import type { WaClientPluginDefinition } from './plugins/types';
|
|
7
9
|
import type { WaMediaProcessor } from '../media/processor';
|
|
8
10
|
import type { WaLinkPreviewOptions } from '../message/addons/link-preview/types';
|
|
@@ -82,6 +84,16 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
82
84
|
* tests or pinning a specific edge.
|
|
83
85
|
*/
|
|
84
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
|
+
};
|
|
85
97
|
/** Default timeout (ms) for IQ queries. Defaults to `WA_DEFAULTS.IQ_TIMEOUT_MS` (60s). */
|
|
86
98
|
readonly iqTimeoutMs?: number;
|
|
87
99
|
/** Default timeout (ms) for raw node `query()` calls when none is passed. */
|
|
@@ -118,8 +130,11 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
118
130
|
* Automatically reconnect when the server rejects the noise handshake
|
|
119
131
|
* with HTTP 405 / `failure_client_too_old`. On every 405 the client
|
|
120
132
|
* logs a warning asking you to upgrade zapo, fetches the current
|
|
121
|
-
* version
|
|
122
|
-
*
|
|
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`).
|
|
123
138
|
*/
|
|
124
139
|
readonly recoverFromClientTooOld?: boolean;
|
|
125
140
|
/**
|
|
@@ -140,6 +155,7 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
140
155
|
* Chat-event emission tuning. `emitSnapshotMutations: true` re-emits
|
|
141
156
|
* `app_state_mutation` events for every mutation seen during a snapshot
|
|
142
157
|
* sync (off by default – those mutations represent historical state).
|
|
158
|
+
* This client's own outbound app-state actions surface on `mutation_send`.
|
|
143
159
|
*/
|
|
144
160
|
readonly chatEvents?: {
|
|
145
161
|
readonly emitSnapshotMutations?: boolean;
|
|
@@ -193,6 +209,14 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
193
209
|
* disables a security check the production code path enforces.
|
|
194
210
|
*/
|
|
195
211
|
readonly dangerous?: WaClientDangerousOptions;
|
|
212
|
+
/**
|
|
213
|
+
* External WebAuthn signer. Configuring it enables handling a server-forced
|
|
214
|
+
* passkey ("Shortcake") prologue – e.g. when the server demands a passkey
|
|
215
|
+
* right after a successful pairing-code `companion_finish`. Called with the
|
|
216
|
+
* server's request options; returns the assertion + credential id. The
|
|
217
|
+
* credential source (real/virtual authenticator) stays outside the library.
|
|
218
|
+
*/
|
|
219
|
+
readonly signPasskeyAssertion?: WaShortcakeAssertionSigner;
|
|
196
220
|
}
|
|
197
221
|
export interface WaClientDangerousOptions extends WaAuthDangerousOptions {
|
|
198
222
|
/**
|
|
@@ -531,6 +555,22 @@ export interface WaIncomingMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJ
|
|
|
531
555
|
readonly pushName?: string;
|
|
532
556
|
readonly message?: Proto.IMessage;
|
|
533
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
|
+
}
|
|
534
574
|
export interface WaIncomingProtocolMessageEvent extends WaIncomingMessageEvent {
|
|
535
575
|
readonly protocolMessage: Proto.Message.IProtocolMessage;
|
|
536
576
|
}
|
|
@@ -992,7 +1032,7 @@ export interface WaHistorySyncChunkEvent {
|
|
|
992
1032
|
/** Server-reported progress, 0–100. */
|
|
993
1033
|
readonly progress?: number;
|
|
994
1034
|
}
|
|
995
|
-
export type WaAppStateMutationSource = 'snapshot' | 'patch';
|
|
1035
|
+
export type WaAppStateMutationSource = 'snapshot' | 'patch' | 'local';
|
|
996
1036
|
type MutationEventBase = {
|
|
997
1037
|
readonly source: WaAppStateMutationSource;
|
|
998
1038
|
readonly collection: AppStateCollectionName;
|
|
@@ -1013,6 +1053,12 @@ export type WaAppStateMutationEvent = {
|
|
|
1013
1053
|
readonly operation: 'remove';
|
|
1014
1054
|
} & MutationEventBase & WaAppstateIndexArgs<K>);
|
|
1015
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;
|
|
1016
1062
|
export type WaConnectionEvent = {
|
|
1017
1063
|
readonly status: 'open';
|
|
1018
1064
|
readonly reason: WaConnectionOpenReason;
|
|
@@ -1071,6 +1117,16 @@ export interface WaClientEventMap {
|
|
|
1071
1117
|
readonly auth_paired: (event: {
|
|
1072
1118
|
readonly credentials: WaAuthCredentials;
|
|
1073
1119
|
}) => void;
|
|
1120
|
+
/**
|
|
1121
|
+
* The server is forcing a passkey (Shortcake) to link this device – emitted
|
|
1122
|
+
* when the server pushes the prologue request. `hasSigner` is `true` when a
|
|
1123
|
+
* `signPasskeyAssertion` is configured (the handshake proceeds) and `false`
|
|
1124
|
+
* when none is set (the request is acked but the link cannot complete
|
|
1125
|
+
* headless, so the user should be told a passkey is required).
|
|
1126
|
+
*/
|
|
1127
|
+
readonly auth_passkey_required: (event: {
|
|
1128
|
+
readonly hasSigner: boolean;
|
|
1129
|
+
}) => void;
|
|
1074
1130
|
/**
|
|
1075
1131
|
* Connection-state transitions: `'open'` (handshake + auth complete),
|
|
1076
1132
|
* `'connecting'`, or `'close'` (with a `reason` and optional `code`). The
|
|
@@ -1084,6 +1140,13 @@ export interface WaClientEventMap {
|
|
|
1084
1140
|
* `options.quote` for threads).
|
|
1085
1141
|
*/
|
|
1086
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;
|
|
1087
1150
|
/**
|
|
1088
1151
|
* A decrypted addon (poll vote, reaction, edit, comment, ...) attached to
|
|
1089
1152
|
* a previous message. Fires unless `addons.autoDecrypt` is explicitly
|
|
@@ -1143,11 +1206,20 @@ export interface WaClientEventMap {
|
|
|
1143
1206
|
/** Profile/group/community picture change notification – the new picture must still be fetched explicitly. */
|
|
1144
1207
|
readonly picture: (event: WaPictureEvent) => void;
|
|
1145
1208
|
/**
|
|
1146
|
-
* A parsed app-state mutation
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
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.
|
|
1213
|
+
*/
|
|
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.
|
|
1149
1221
|
*/
|
|
1150
|
-
readonly
|
|
1222
|
+
readonly mutation_send: WaAppStateMutationListener;
|
|
1151
1223
|
/**
|
|
1152
1224
|
* One chunk of history-sync data, fired both during the initial
|
|
1153
1225
|
* bootstrap that the primary device pushes after pairing and for any
|
|
@@ -1185,6 +1257,17 @@ export interface WaClientEventMap {
|
|
|
1185
1257
|
* `mobileTransport`.
|
|
1186
1258
|
*/
|
|
1187
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;
|
|
1188
1271
|
/** **debug** – the success node closing the noise handshake; emitted before `connection: { status: 'open' }`. */
|
|
1189
1272
|
readonly debug_connection_success: (event: {
|
|
1190
1273
|
readonly node: BinaryNode;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { buildCommsConfig, loadOrCreateCredentials, persistCredentials } from './credentials-flow.js';
|
|
2
2
|
import { WaPairingFlow } from './pairing/WaPairingFlow.js';
|
|
3
3
|
import { WaQrFlow } from './pairing/WaQrFlow.js';
|
|
4
|
-
import {
|
|
4
|
+
import { WaShortcakeFlow } from './pairing/WaShortcakeFlow.js';
|
|
5
|
+
import { resolveWaDeviceIdentity, WA_NOTIFICATION_TYPES } from '../protocol/constants.js';
|
|
6
|
+
import { buildAckNode } from '../transport/node/builders/global.js';
|
|
7
|
+
import { resolveDevicePropsPlatformType } from '../transport/noise/WaClientPayload.js';
|
|
5
8
|
import { uint8Equal } from '../util/bytes.js';
|
|
6
9
|
import { toError } from '../util/primitives.js';
|
|
7
|
-
import { getRuntimeOsDisplayName } from '../util/runtime.js';
|
|
8
10
|
/**
|
|
9
11
|
* Owns the auth/pairing lifecycle and persistence of {@link WaAuthCredentials}.
|
|
10
12
|
* Exposed as `client.auth` on a {@link WaClient}.
|
|
@@ -17,12 +19,8 @@ import { getRuntimeOsDisplayName } from '../util/runtime.js';
|
|
|
17
19
|
export class WaAuthClient {
|
|
18
20
|
constructor(options, deps) {
|
|
19
21
|
this.versionOverride = null;
|
|
20
|
-
|
|
21
|
-
const device =
|
|
22
|
-
browser: deviceBrowser,
|
|
23
|
-
osDisplayName: options.deviceOsDisplayName ?? getRuntimeOsDisplayName(),
|
|
24
|
-
platform: options.devicePlatform ?? getWaCompanionPlatformId(deviceBrowser)
|
|
25
|
-
});
|
|
22
|
+
this.mobileAppVersionOverride = null;
|
|
23
|
+
const device = resolveWaDeviceIdentity(options);
|
|
26
24
|
this.options = Object.freeze({
|
|
27
25
|
...options,
|
|
28
26
|
deviceBrowser: device.browser,
|
|
@@ -59,6 +57,23 @@ export class WaAuthClient {
|
|
|
59
57
|
},
|
|
60
58
|
dangerous: options.dangerous
|
|
61
59
|
});
|
|
60
|
+
this.socket = deps.socket;
|
|
61
|
+
const signAssertion = deps.callbacks?.signPasskeyAssertion;
|
|
62
|
+
this.shortcakeFlow = signAssertion
|
|
63
|
+
? new WaShortcakeFlow({
|
|
64
|
+
logger: this.logger,
|
|
65
|
+
socket: {
|
|
66
|
+
sendNode: deps.socket.sendNode,
|
|
67
|
+
query: (node, timeoutMs) => deps.socket.query(node, timeoutMs)
|
|
68
|
+
},
|
|
69
|
+
signAssertion,
|
|
70
|
+
auth: {
|
|
71
|
+
getCredentials: () => this.credentials,
|
|
72
|
+
updateCredentials: this.updateCredentials.bind(this)
|
|
73
|
+
},
|
|
74
|
+
deviceType: resolveDevicePropsPlatformType(device.browser)
|
|
75
|
+
})
|
|
76
|
+
: null;
|
|
62
77
|
}
|
|
63
78
|
/**
|
|
64
79
|
* Returns a snapshot of auth readiness flags (connection, registration,
|
|
@@ -104,12 +119,15 @@ export class WaAuthClient {
|
|
|
104
119
|
this.logger.trace('auth client building comms config');
|
|
105
120
|
const override = this.versionOverride;
|
|
106
121
|
this.versionOverride = null;
|
|
122
|
+
const mobileAppVersionOverride = this.mobileAppVersionOverride;
|
|
123
|
+
this.mobileAppVersionOverride = null;
|
|
107
124
|
return buildCommsConfig(this.logger, this.requireCredentials(), socketOptions, {
|
|
108
125
|
deviceBrowser: this.options.deviceBrowser,
|
|
109
126
|
deviceOsDisplayName: this.options.deviceOsDisplayName,
|
|
110
127
|
requireFullSync: this.options.requireFullSync,
|
|
111
128
|
version: override ?? this.options.version,
|
|
112
129
|
mobileTransport: this.options.mobileTransport,
|
|
130
|
+
mobileAppVersionOverride: mobileAppVersionOverride ?? undefined,
|
|
113
131
|
noiseTrustedRootCa: overrides.noiseTrustedRootCa,
|
|
114
132
|
disableNoiseCertificateChainVerification: overrides.disableNoiseCertificateChainVerification ??
|
|
115
133
|
this.options.dangerous?.disableNoiseCertificateChainVerification
|
|
@@ -125,12 +143,24 @@ export class WaAuthClient {
|
|
|
125
143
|
setNextConnectVersion(version) {
|
|
126
144
|
this.versionOverride = version;
|
|
127
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
|
+
}
|
|
128
157
|
/** Clears the in-memory QR and pairing sessions without touching storage. */
|
|
129
158
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
130
159
|
async clearTransientState() {
|
|
131
160
|
this.logger.trace('auth client clear transient state');
|
|
132
161
|
this.qrFlow.clear();
|
|
133
162
|
this.pairingFlow.clearSession();
|
|
163
|
+
this.shortcakeFlow?.clearSession();
|
|
134
164
|
}
|
|
135
165
|
/**
|
|
136
166
|
* Wipes credentials from memory **and** from the auth store. Does
|
|
@@ -289,9 +319,35 @@ export class WaAuthClient {
|
|
|
289
319
|
}
|
|
290
320
|
/** Dispatcher: returns `true` when `node` is a link-code companion notification. */
|
|
291
321
|
async handleLinkCodeNotification(node) {
|
|
322
|
+
const type = node.attrs.type;
|
|
323
|
+
if (type === WA_NOTIFICATION_TYPES.PASSKEY_PROLOGUE_REQUEST ||
|
|
324
|
+
type === WA_NOTIFICATION_TYPES.CRSC_CONTINUATION) {
|
|
325
|
+
return this.runHandled(() => this.handleShortcakeNotification(node));
|
|
326
|
+
}
|
|
292
327
|
this.logger.trace('auth client handleLinkCodeNotification', { id: node.attrs.id });
|
|
293
328
|
return this.runHandled(() => this.pairingFlow.handleLinkCodeNotification(node));
|
|
294
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Handles the server-forced passkey (Shortcake) prologue. With a configured
|
|
332
|
+
* `signPasskeyAssertion` the full handshake runs; without one we just ack
|
|
333
|
+
* and warn instead of silently stalling – which is what happens today when
|
|
334
|
+
* the server demands a passkey after a successful pairing-code
|
|
335
|
+
* `companion_finish`.
|
|
336
|
+
*/
|
|
337
|
+
async handleShortcakeNotification(node) {
|
|
338
|
+
if (node.attrs.type === WA_NOTIFICATION_TYPES.PASSKEY_PROLOGUE_REQUEST) {
|
|
339
|
+
this.callbacks.onPasskeyRequired?.(this.shortcakeFlow !== null);
|
|
340
|
+
}
|
|
341
|
+
if (this.shortcakeFlow) {
|
|
342
|
+
return this.shortcakeFlow.handleIncomingNotification(node);
|
|
343
|
+
}
|
|
344
|
+
if (node.attrs.type === WA_NOTIFICATION_TYPES.PASSKEY_PROLOGUE_REQUEST) {
|
|
345
|
+
this.logger.warn('auth client: server requested passkey prologue but no signPasskeyAssertion configured', { id: node.attrs.id });
|
|
346
|
+
await this.socket.sendNode(buildAckNode({ kind: 'notification', node }));
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
295
351
|
/** Dispatcher: returns `true` when `node` is a companion-registration refresh notification. */
|
|
296
352
|
async handleCompanionRegRefreshNotification(node) {
|
|
297
353
|
this.logger.trace('auth client handleCompanionRegRefreshNotification', {
|