@openclaw/crabline 0.1.10 → 0.1.12
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/README.md +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +12 -2
- package/dist/src/openclaw/artifact-generation.js +436 -15
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +40 -3
- package/dist/src/openclaw/private-file.js +2070 -95
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +696 -187
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +352 -29
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +532 -42
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1241 -22
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1193 -118
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { ProtocolAddress, SessionCipher, SessionRecord } from "libsignal";
|
|
3
|
-
import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, } from "./whatsapp-wire/crypto.js";
|
|
3
|
+
import { aesDecryptGCM, aesEncryptGCM, Curve, encodeBigEndian, ensureSignalPublicKey, generateSignalKeyPair, hkdf, NOISE_MODE, NOISE_WA_HEADER, sha256, signedKeyPair, scrubSignalPublicKey, } from "./whatsapp-wire/crypto.js";
|
|
4
4
|
import { decodeBinaryNode, encodeBinaryNode, S_WHATSAPP_NET, } from "./whatsapp-wire/binary-node.js";
|
|
5
5
|
import { decodeHandshakeMessage, encodeHandshakeMessage } from "./whatsapp-wire/handshake.js";
|
|
6
6
|
import { KEY_BUNDLE_TYPE, xmppPreKey, xmppSignedPreKey } from "./whatsapp-wire/signal.js";
|
|
7
7
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8
8
|
import { closeWebSocketServer } from "./websocket.js";
|
|
9
|
-
import { canonicalizeWhatsAppUserCorrelationJid } from "./whatsapp-jid.js";
|
|
9
|
+
import { canonicalizeWhatsAppChatCorrelationJid, canonicalizeWhatsAppUserCorrelationJid, } from "./whatsapp-jid.js";
|
|
10
10
|
// Keep the local server independent from Baileys at runtime. Tests use Baileys
|
|
11
11
|
// as a black-box client to verify this narrow WhatsApp Web wire subset.
|
|
12
12
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
@@ -20,9 +20,17 @@ export const MAX_WHATSAPP_NOISE_FRAMES_PER_MESSAGE = 1_024;
|
|
|
20
20
|
export const WHATSAPP_WEBSOCKET_SEND_TIMEOUT_MS = 5_000;
|
|
21
21
|
const MAX_PENDING_WEBSOCKET_BYTES = 8 * 1024 * 1024;
|
|
22
22
|
const MAX_PENDING_WEBSOCKET_MESSAGES = 32;
|
|
23
|
+
const MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENTS = 10_000;
|
|
24
|
+
const MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENT_AGE_MS = 5 * 60 * 1_000;
|
|
25
|
+
const MAX_WHATSAPP_RECENT_ACKNOWLEDGEMENTS = 10_000;
|
|
26
|
+
const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
|
|
23
27
|
export const MAX_WHATSAPP_WEBSOCKET_FRAGMENTS = 1_024;
|
|
24
28
|
export const MAX_WHATSAPP_SIGNAL_BUNDLES = 1_024;
|
|
29
|
+
export const MAX_WHATSAPP_SIGNAL_PREKEYS_PER_BUNDLE = 32;
|
|
30
|
+
export const MAX_WHATSAPP_SIGNAL_SESSIONS_PER_BUNDLE = 32;
|
|
25
31
|
export const MAX_WHATSAPP_WEBSOCKET_CLOSE_REASON_BYTES = 123;
|
|
32
|
+
export const WHATSAPP_MESSAGE_ACCEPTANCE_TIMEOUT_MS = 5_000;
|
|
33
|
+
export const WHATSAPP_SIGNAL_PREKEY_RESERVATION_TTL_MS = 5 * 60 * 1_000;
|
|
26
34
|
const WHATSAPP_NOISE_CERT_CHAIN = Buffer.from("CncKMwjjAhADGiCRKg7Kg1iu4CSulwLBaxX51Tefw6VXGgZqcr5OEbXIRiDQ04bOBijQjZ/TBhJA34Bj82jAHhLpCWBNVBlGnFDieamd8+138S57uMt9ke9mrn5r4+VepwBPKEgHjob6bR70rlCmWDkxZv+CfVjIAxJ2CjIIAxAAGiAcUamsMDmUxsjQuS6hh4pTNHZZnMWZ++o1mX2aqQzOYiCAka6+Bij/3rfcBhJAJw8pRkhTn+1IcOJQVN1OlZg6uikYnCumyO7acFVVX3U3QPXsGSq2TCbCbWrebSC593Su43EgprIDlfU8ZgWFBw==", "base64");
|
|
27
35
|
export function resolveMaxPendingWhatsAppInboundMessages(value) {
|
|
28
36
|
const resolved = value ?? MAX_PENDING_INBOUND_MESSAGES;
|
|
@@ -31,27 +39,271 @@ export function resolveMaxPendingWhatsAppInboundMessages(value) {
|
|
|
31
39
|
}
|
|
32
40
|
return resolved;
|
|
33
41
|
}
|
|
42
|
+
async function awaitWithAbort(operation, signal) {
|
|
43
|
+
if (!signal) {
|
|
44
|
+
return await operation;
|
|
45
|
+
}
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
return await new Promise((resolve, reject) => {
|
|
48
|
+
const abort = () => reject(signal.reason);
|
|
49
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
50
|
+
void operation.then((value) => {
|
|
51
|
+
signal.removeEventListener("abort", abort);
|
|
52
|
+
resolve(value);
|
|
53
|
+
}, (error) => {
|
|
54
|
+
signal.removeEventListener("abort", abort);
|
|
55
|
+
reject(error);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
34
59
|
export class WhatsAppSignalBundleStore {
|
|
35
60
|
maxBundles;
|
|
61
|
+
maxPendingAcknowledgements;
|
|
62
|
+
maxRecentAcknowledgements;
|
|
63
|
+
maxPendingAcknowledgementAgeMs;
|
|
64
|
+
now;
|
|
65
|
+
#acknowledgedMessageIds = new Map();
|
|
36
66
|
#bundles = new Map();
|
|
37
67
|
#lidByPhoneNumber = new Map();
|
|
68
|
+
#maxPreKeysPerBundle;
|
|
69
|
+
#maxSessionsPerBundle;
|
|
70
|
+
#maxTerminalAcceptances;
|
|
71
|
+
#messagePreKeyProtections = new Map();
|
|
72
|
+
#messageAcceptanceTimeoutMs;
|
|
73
|
+
#pendingAcknowledgements = new Map();
|
|
74
|
+
#pendingTransactions = new Map();
|
|
75
|
+
#persistedMessageIds = new Map();
|
|
76
|
+
#preKeyReservationTtlMs;
|
|
38
77
|
#sessions = new Map();
|
|
39
|
-
|
|
78
|
+
#terminalAcceptances = new Map();
|
|
79
|
+
constructor(maxBundles = MAX_WHATSAPP_SIGNAL_BUNDLES, maxPendingAcknowledgements = MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENTS, maxRecentAcknowledgements = MAX_WHATSAPP_RECENT_ACKNOWLEDGEMENTS, maxPendingAcknowledgementAgeMs = MAX_WHATSAPP_PENDING_ACKNOWLEDGEMENT_AGE_MS, now = Date.now, options = {}) {
|
|
40
80
|
this.maxBundles = maxBundles;
|
|
81
|
+
this.maxPendingAcknowledgements = maxPendingAcknowledgements;
|
|
82
|
+
this.maxRecentAcknowledgements = maxRecentAcknowledgements;
|
|
83
|
+
this.maxPendingAcknowledgementAgeMs = maxPendingAcknowledgementAgeMs;
|
|
84
|
+
this.now = now;
|
|
85
|
+
const maxPreKeysPerBundle = options.maxPreKeysPerBundle ?? MAX_WHATSAPP_SIGNAL_PREKEYS_PER_BUNDLE;
|
|
86
|
+
const maxSessionsPerBundle = options.maxSessionsPerBundle ?? MAX_WHATSAPP_SIGNAL_SESSIONS_PER_BUNDLE;
|
|
87
|
+
const messageAcceptanceTimeoutMs = options.messageAcceptanceTimeoutMs ?? WHATSAPP_MESSAGE_ACCEPTANCE_TIMEOUT_MS;
|
|
88
|
+
const preKeyReservationTtlMs = options.preKeyReservationTtlMs ?? WHATSAPP_SIGNAL_PREKEY_RESERVATION_TTL_MS;
|
|
41
89
|
if (!Number.isSafeInteger(maxBundles) || maxBundles < 1) {
|
|
42
90
|
throw new Error("WhatsApp maxSignalBundles must be a positive safe integer.");
|
|
43
91
|
}
|
|
92
|
+
if (!Number.isSafeInteger(maxSessionsPerBundle) || maxSessionsPerBundle < 1) {
|
|
93
|
+
throw new Error("WhatsApp maxSignalSessionsPerBundle must be a positive safe integer.");
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isSafeInteger(maxPreKeysPerBundle) || maxPreKeysPerBundle < 1) {
|
|
96
|
+
throw new Error("WhatsApp maxSignalPreKeysPerBundle must be a positive safe integer.");
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isSafeInteger(messageAcceptanceTimeoutMs) ||
|
|
99
|
+
messageAcceptanceTimeoutMs < 1 ||
|
|
100
|
+
messageAcceptanceTimeoutMs > MAX_NODE_TIMER_DELAY_MS) {
|
|
101
|
+
throw new Error(`WhatsApp messageAcceptanceTimeoutMs must be a positive integer no greater than ${MAX_NODE_TIMER_DELAY_MS}.`);
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isSafeInteger(preKeyReservationTtlMs) || preKeyReservationTtlMs < 1) {
|
|
104
|
+
throw new Error("WhatsApp preKeyReservationTtlMs must be a positive safe integer.");
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isSafeInteger(maxPendingAcknowledgements) || maxPendingAcknowledgements < 1) {
|
|
107
|
+
throw new Error("WhatsApp maxPendingAcknowledgements must be a positive safe integer.");
|
|
108
|
+
}
|
|
109
|
+
if (!Number.isSafeInteger(maxRecentAcknowledgements) || maxRecentAcknowledgements < 1) {
|
|
110
|
+
throw new Error("WhatsApp maxRecentAcknowledgements must be a positive safe integer.");
|
|
111
|
+
}
|
|
112
|
+
if (!Number.isSafeInteger(maxPendingAcknowledgementAgeMs) ||
|
|
113
|
+
maxPendingAcknowledgementAgeMs < 1) {
|
|
114
|
+
throw new Error("WhatsApp maxPendingAcknowledgementAgeMs must be a positive safe integer.");
|
|
115
|
+
}
|
|
116
|
+
this.#maxPreKeysPerBundle = maxPreKeysPerBundle;
|
|
117
|
+
this.#maxSessionsPerBundle = maxSessionsPerBundle;
|
|
118
|
+
this.#maxTerminalAcceptances = Math.min(Number.MAX_SAFE_INTEGER, maxPendingAcknowledgements + maxRecentAcknowledgements);
|
|
119
|
+
this.#messageAcceptanceTimeoutMs = messageAcceptanceTimeoutMs;
|
|
120
|
+
this.#preKeyReservationTtlMs = preKeyReservationTtlMs;
|
|
44
121
|
}
|
|
45
122
|
get size() {
|
|
46
123
|
return this.#bundles.size;
|
|
47
124
|
}
|
|
125
|
+
get lidMappingSize() {
|
|
126
|
+
return this.#lidByPhoneNumber.size;
|
|
127
|
+
}
|
|
128
|
+
get sessionCount() {
|
|
129
|
+
let count = 0;
|
|
130
|
+
for (const sessions of this.#sessions.values()) {
|
|
131
|
+
count += sessions.size;
|
|
132
|
+
}
|
|
133
|
+
return count;
|
|
134
|
+
}
|
|
135
|
+
async acceptMessageOnce(messageKey, operation, options = {}) {
|
|
136
|
+
this.#recoverExpiredPendingAcknowledgements();
|
|
137
|
+
const pendingAcceptance = this.#pendingAcknowledgements.get(messageKey);
|
|
138
|
+
if (pendingAcceptance) {
|
|
139
|
+
return await pendingAcceptance.acceptance;
|
|
140
|
+
}
|
|
141
|
+
const terminalAcceptance = this.#terminalAcceptances.get(messageKey);
|
|
142
|
+
if (terminalAcceptance) {
|
|
143
|
+
this.#terminalAcceptances.delete(messageKey);
|
|
144
|
+
this.#terminalAcceptances.set(messageKey, terminalAcceptance);
|
|
145
|
+
throw terminalAcceptance.error;
|
|
146
|
+
}
|
|
147
|
+
if (this.#acknowledgedMessageIds.delete(messageKey)) {
|
|
148
|
+
this.#acknowledgedMessageIds.set(messageKey, true);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (this.#terminalAcceptances.size + this.#pendingAcknowledgements.size >=
|
|
152
|
+
this.#maxTerminalAcceptances) {
|
|
153
|
+
throw new Error(`WhatsApp terminal acceptance limit exceeded (${this.#maxTerminalAcceptances}).`);
|
|
154
|
+
}
|
|
155
|
+
if (this.#pendingAcknowledgements.size >= this.maxPendingAcknowledgements) {
|
|
156
|
+
throw new Error(`WhatsApp pending acknowledgement limit exceeded (${this.maxPendingAcknowledgements}).`);
|
|
157
|
+
}
|
|
158
|
+
const timeoutMs = options.timeoutMs ?? this.#messageAcceptanceTimeoutMs;
|
|
159
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_NODE_TIMER_DELAY_MS) {
|
|
160
|
+
throw new Error(`WhatsApp message acceptance timeout must be a positive integer no greater than ${MAX_NODE_TIMER_DELAY_MS}.`);
|
|
161
|
+
}
|
|
162
|
+
options.signal?.throwIfAborted();
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const operationPromise = Promise.resolve().then(() => operation(controller.signal));
|
|
165
|
+
let terminalFenced = false;
|
|
166
|
+
const fenceTerminalAcceptance = (error) => {
|
|
167
|
+
if (terminalFenced) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
terminalFenced = true;
|
|
171
|
+
controller.abort(error);
|
|
172
|
+
const terminalDrain = Promise.all([
|
|
173
|
+
operationPromise.then(() => undefined, () => undefined),
|
|
174
|
+
Promise.resolve()
|
|
175
|
+
.then(() => options.terminalDrain?.())
|
|
176
|
+
.then((disposition) => disposition ?? "retryable", () => "retryable"),
|
|
177
|
+
]).then(([, disposition]) => disposition);
|
|
178
|
+
this.#rememberTerminalAcceptance(messageKey, error, terminalDrain);
|
|
179
|
+
};
|
|
180
|
+
const abortFromOwner = () => {
|
|
181
|
+
const reason = options.signal?.reason;
|
|
182
|
+
fenceTerminalAcceptance(reason instanceof Error ? reason : new Error("WhatsApp message acceptance cancelled."));
|
|
183
|
+
};
|
|
184
|
+
if (options.signal?.aborted) {
|
|
185
|
+
abortFromOwner();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
options.signal?.addEventListener("abort", abortFromOwner, { once: true });
|
|
189
|
+
}
|
|
190
|
+
const timeout = setTimeout(() => {
|
|
191
|
+
const error = new Error("WhatsApp message acceptance timed out.");
|
|
192
|
+
fenceTerminalAcceptance(error);
|
|
193
|
+
try {
|
|
194
|
+
options.onTerminalTimeout?.();
|
|
195
|
+
}
|
|
196
|
+
catch { }
|
|
197
|
+
}, timeoutMs);
|
|
198
|
+
timeout.unref();
|
|
199
|
+
let pendingAcknowledgement;
|
|
200
|
+
const acceptance = Promise.resolve().then(async () => {
|
|
201
|
+
try {
|
|
202
|
+
const accepted = await awaitWithAbort(operationPromise, controller.signal);
|
|
203
|
+
if (!accepted) {
|
|
204
|
+
if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
|
|
205
|
+
this.#pendingAcknowledgements.delete(messageKey);
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (pendingAcknowledgement.acknowledged) {
|
|
210
|
+
if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
|
|
211
|
+
this.#pendingAcknowledgements.delete(messageKey);
|
|
212
|
+
}
|
|
213
|
+
this.#rememberAcknowledgedMessage(messageKey);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
pendingAcknowledgement.acceptedAt = this.now();
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (this.#pendingAcknowledgements.get(messageKey) === pendingAcknowledgement) {
|
|
222
|
+
this.#pendingAcknowledgements.delete(messageKey);
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
clearTimeout(timeout);
|
|
228
|
+
options.signal?.removeEventListener("abort", abortFromOwner);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
pendingAcknowledgement = {
|
|
232
|
+
acceptance,
|
|
233
|
+
acceptedAt: undefined,
|
|
234
|
+
acknowledged: false,
|
|
235
|
+
};
|
|
236
|
+
this.#pendingAcknowledgements.set(messageKey, pendingAcknowledgement);
|
|
237
|
+
return await acceptance;
|
|
238
|
+
}
|
|
239
|
+
markMessageAcknowledged(peerJid, messageId) {
|
|
240
|
+
const peer = canonicalizeWhatsAppChatCorrelationJid(peerJid);
|
|
241
|
+
if (peer && messageId) {
|
|
242
|
+
const messageKey = `${peer}\0${messageId}`;
|
|
243
|
+
this.#recoverExpiredPendingAcknowledgements();
|
|
244
|
+
const pendingAcknowledgement = this.#pendingAcknowledgements.get(messageKey);
|
|
245
|
+
if (!pendingAcknowledgement) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (pendingAcknowledgement.acceptedAt === undefined) {
|
|
249
|
+
pendingAcknowledgement.acknowledged = true;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.#pendingAcknowledgements.delete(messageKey);
|
|
253
|
+
this.#rememberAcknowledgedMessage(messageKey);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
hasPersistedMessage(messageKey) {
|
|
257
|
+
return this.#persistedMessageIds.has(messageKey);
|
|
258
|
+
}
|
|
259
|
+
clearPersistedMessage(messageKey) {
|
|
260
|
+
this.#persistedMessageIds.delete(messageKey);
|
|
261
|
+
this.releaseMessagePreKeyProtection(messageKey);
|
|
262
|
+
}
|
|
263
|
+
releaseMessagePreKeyProtection(messageKey) {
|
|
264
|
+
const protections = this.#messagePreKeyProtections.get(messageKey);
|
|
265
|
+
if (!protections) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.#messagePreKeyProtections.delete(messageKey);
|
|
269
|
+
for (const [identityKey, preKeyIds] of protections) {
|
|
270
|
+
const bundle = this.#bundles.get(identityKey);
|
|
271
|
+
if (!bundle) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const preKeyId of preKeyIds) {
|
|
275
|
+
const references = bundle.protectedPreKeyIds.get(preKeyId) ?? 0;
|
|
276
|
+
if (references <= 1) {
|
|
277
|
+
bundle.protectedPreKeyIds.delete(preKeyId);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
bundle.protectedPreKeyIds.set(preKeyId, references - 1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
48
285
|
associateLid(phoneNumberJid, lidJid) {
|
|
49
286
|
const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
|
|
50
287
|
const lid = canonicalizeWhatsAppUserCorrelationJid(lidJid);
|
|
51
288
|
if (!phoneNumber?.endsWith("@s.whatsapp.net") || !lid?.endsWith("@lid")) {
|
|
52
289
|
throw new Error("Invalid WhatsApp PN/LID signal mapping.");
|
|
53
290
|
}
|
|
54
|
-
|
|
291
|
+
const phoneNumberKey = signalBundleIdentityKey(phoneNumber);
|
|
292
|
+
this.#lidByPhoneNumber.delete(phoneNumberKey);
|
|
293
|
+
this.#lidByPhoneNumber.set(phoneNumberKey, signalBundleIdentityKey(lid));
|
|
294
|
+
if (this.#lidByPhoneNumber.size > this.maxBundles) {
|
|
295
|
+
const oldestPhoneNumber = this.#lidByPhoneNumber.keys().next().value;
|
|
296
|
+
if (oldestPhoneNumber !== undefined) {
|
|
297
|
+
this.#lidByPhoneNumber.delete(oldestPhoneNumber);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
resolveAssociatedLid(phoneNumberJid) {
|
|
302
|
+
const phoneNumber = canonicalizeWhatsAppUserCorrelationJid(phoneNumberJid);
|
|
303
|
+
if (!phoneNumber?.endsWith("@s.whatsapp.net")) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
return this.#lidByPhoneNumber.get(signalBundleIdentityKey(phoneNumber));
|
|
55
307
|
}
|
|
56
308
|
resolveMany(jids) {
|
|
57
309
|
const uniqueNewIdentities = new Set();
|
|
@@ -68,74 +320,326 @@ export class WhatsAppSignalBundleStore {
|
|
|
68
320
|
}
|
|
69
321
|
}
|
|
70
322
|
if (this.#bundles.size + uniqueNewIdentities.size > this.maxBundles) {
|
|
323
|
+
// Published bundle identities stay stable for the store lifetime.
|
|
71
324
|
throw new Error(`WhatsApp signal bundle limit exceeded (${this.maxBundles}).`);
|
|
72
325
|
}
|
|
73
|
-
|
|
326
|
+
const stagedNewBundles = new Map();
|
|
327
|
+
const stagedReservations = new Map();
|
|
328
|
+
const reservationNow = this.now();
|
|
329
|
+
const stagedForIdentity = (identityKey) => {
|
|
330
|
+
let bundle = this.#bundles.get(identityKey) ?? stagedNewBundles.get(identityKey);
|
|
331
|
+
if (!bundle) {
|
|
332
|
+
bundle = this.#createBundle();
|
|
333
|
+
stagedNewBundles.set(identityKey, bundle);
|
|
334
|
+
}
|
|
335
|
+
let staged = stagedReservations.get(identityKey);
|
|
336
|
+
if (!staged) {
|
|
337
|
+
const preKeys = new Map(bundle.preKeys);
|
|
338
|
+
const preKeyReservations = new Map(bundle.preKeyReservations);
|
|
339
|
+
for (const [preKeyId, reservedAt] of preKeyReservations) {
|
|
340
|
+
if (reservationNow - reservedAt >= this.#preKeyReservationTtlMs &&
|
|
341
|
+
!bundle.protectedPreKeyIds.has(preKeyId)) {
|
|
342
|
+
preKeyReservations.delete(preKeyId);
|
|
343
|
+
preKeys.delete(preKeyId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
staged = {
|
|
347
|
+
availablePreKeyIds: bundle.availablePreKeyIds.filter((id) => preKeys.has(id)),
|
|
348
|
+
bundle,
|
|
349
|
+
nextPreKeyId: bundle.nextPreKeyId,
|
|
350
|
+
preKeyReservations,
|
|
351
|
+
preKeys,
|
|
352
|
+
};
|
|
353
|
+
stagedReservations.set(identityKey, staged);
|
|
354
|
+
}
|
|
355
|
+
return staged;
|
|
356
|
+
};
|
|
357
|
+
const requestedPreKeys = new Map();
|
|
358
|
+
for (const identityKey of identityKeys) {
|
|
359
|
+
requestedPreKeys.set(identityKey, (requestedPreKeys.get(identityKey) ?? 0) + 1);
|
|
360
|
+
}
|
|
361
|
+
for (const [identityKey, requested] of requestedPreKeys) {
|
|
362
|
+
const staged = stagedForIdentity(identityKey);
|
|
363
|
+
const available = staged.availablePreKeyIds.length;
|
|
364
|
+
const creatable = Math.max(0, this.#maxPreKeysPerBundle - staged.preKeys.size - staged.bundle.pendingPreKeyIds.size);
|
|
365
|
+
if (requested > available + creatable) {
|
|
366
|
+
throw new Error(`WhatsApp Signal prekey reservation limit exceeded (${this.#maxPreKeysPerBundle}).`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const reservedBundles = identityKeys.map((identityKey) => {
|
|
370
|
+
const staged = stagedForIdentity(identityKey);
|
|
371
|
+
const { bundle } = staged;
|
|
372
|
+
let preKeyId = staged.availablePreKeyIds.shift();
|
|
373
|
+
if (preKeyId === undefined) {
|
|
374
|
+
const created = this.#createPreKey(staged.preKeys, staged.nextPreKeyId, staged.bundle.pendingPreKeyIds);
|
|
375
|
+
preKeyId = created.id;
|
|
376
|
+
staged.preKeys.set(created.id, created.keyPair);
|
|
377
|
+
staged.nextPreKeyId = created.nextPreKeyId;
|
|
378
|
+
}
|
|
379
|
+
const preKey = staged.preKeys.get(preKeyId);
|
|
380
|
+
if (!preKey) {
|
|
381
|
+
throw new Error("WhatsApp Signal prekey reservation is unavailable.");
|
|
382
|
+
}
|
|
383
|
+
staged.preKeyReservations.set(preKeyId, reservationNow);
|
|
384
|
+
return {
|
|
385
|
+
identityKey: bundle.identityKey,
|
|
386
|
+
preKey,
|
|
387
|
+
preKeyId,
|
|
388
|
+
registrationId: bundle.registrationId,
|
|
389
|
+
signedPreKey: bundle.signedPreKey,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
for (const [identityKey, bundle] of stagedNewBundles) {
|
|
393
|
+
this.#bundles.set(identityKey, bundle);
|
|
394
|
+
}
|
|
395
|
+
for (const staged of stagedReservations.values()) {
|
|
396
|
+
staged.bundle.availablePreKeyIds = staged.availablePreKeyIds;
|
|
397
|
+
staged.bundle.nextPreKeyId = staged.nextPreKeyId;
|
|
398
|
+
staged.bundle.preKeyReservations = staged.preKeyReservations;
|
|
399
|
+
staged.bundle.preKeys = staged.preKeys;
|
|
400
|
+
}
|
|
401
|
+
return reservedBundles;
|
|
74
402
|
}
|
|
75
403
|
async decryptDirectMessage(params) {
|
|
404
|
+
const result = await this.transactDirectMessage({
|
|
405
|
+
...params,
|
|
406
|
+
accept: async (plaintext) => plaintext,
|
|
407
|
+
});
|
|
408
|
+
if (result.status === "decrypt-failed") {
|
|
409
|
+
throw result.error;
|
|
410
|
+
}
|
|
411
|
+
return result.status === "accepted" ? result.value : undefined;
|
|
412
|
+
}
|
|
413
|
+
async transactDirectMessage(params) {
|
|
414
|
+
params.signal?.throwIfAborted();
|
|
76
415
|
const recipientJid = canonicalizeWhatsAppUserCorrelationJid(params.recipientJid);
|
|
77
416
|
const recipientIdentityKey = recipientJid ? signalBundleIdentityKey(recipientJid) : undefined;
|
|
78
417
|
const mappedLidIdentityKey = recipientIdentityKey
|
|
79
|
-
? this
|
|
418
|
+
? this.resolveAssociatedLid(recipientIdentityKey)
|
|
80
419
|
: undefined;
|
|
81
420
|
const identityKey = mappedLidIdentityKey && this.#bundles.has(mappedLidIdentityKey)
|
|
82
421
|
? mappedLidIdentityKey
|
|
83
422
|
: recipientIdentityKey;
|
|
84
423
|
const bundle = identityKey ? this.#bundles.get(identityKey) : undefined;
|
|
85
424
|
if (!identityKey || !bundle) {
|
|
86
|
-
return
|
|
425
|
+
return { status: "unavailable" };
|
|
87
426
|
}
|
|
88
427
|
const remoteAddress = signalProtocolAddress(params.remoteJid);
|
|
89
428
|
if (!remoteAddress) {
|
|
90
|
-
return
|
|
429
|
+
return { status: "unavailable" };
|
|
91
430
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
431
|
+
const address = new ProtocolAddress(remoteAddress.name, remoteAddress.deviceId);
|
|
432
|
+
// Capacity checks, staged writes, and commits stay atomic per recipient bundle.
|
|
433
|
+
return await this.#runTransaction(identityKey, async () => {
|
|
434
|
+
params.signal?.throwIfAborted();
|
|
435
|
+
const sessions = this.#sessions.get(identityKey) ?? new Map();
|
|
436
|
+
const sessionLimitError = new Error("WhatsApp Signal session limit exceeded.");
|
|
437
|
+
const maxSessionsPerBundle = this.#maxSessionsPerBundle;
|
|
438
|
+
let stagedNewSessionCount = 0;
|
|
439
|
+
const stagedPreKeyRemovals = new Set();
|
|
440
|
+
const stagedSessions = new Map();
|
|
441
|
+
const storage = {
|
|
442
|
+
async loadSession(id) {
|
|
443
|
+
const session = stagedSessions.get(id) ?? sessions.get(id);
|
|
444
|
+
return session ? cloneSignalSession(session) : undefined;
|
|
445
|
+
},
|
|
446
|
+
async storeSession(id, session) {
|
|
447
|
+
if (!sessions.has(id) && !stagedSessions.has(id)) {
|
|
448
|
+
if (sessions.size + stagedNewSessionCount >= maxSessionsPerBundle) {
|
|
449
|
+
throw sessionLimitError;
|
|
450
|
+
}
|
|
451
|
+
stagedNewSessionCount += 1;
|
|
452
|
+
}
|
|
453
|
+
stagedSessions.set(id, cloneSignalSession(session));
|
|
454
|
+
},
|
|
455
|
+
isTrustedIdentity: () => true,
|
|
456
|
+
loadPreKey: async (id) => {
|
|
457
|
+
const preKey = bundle.preKeys.get(Number(id));
|
|
458
|
+
if (preKey && params.messageKey) {
|
|
459
|
+
this.#protectMessagePreKey(params.messageKey, identityKey, Number(id));
|
|
460
|
+
}
|
|
461
|
+
return preKey ? signalKeyPair(preKey) : undefined;
|
|
462
|
+
},
|
|
463
|
+
removePreKey(id) {
|
|
464
|
+
stagedPreKeyRemovals.add(id);
|
|
465
|
+
},
|
|
466
|
+
loadSignedPreKey: () => signalKeyPair(bundle.signedPreKey.keyPair),
|
|
467
|
+
getOurRegistrationId: () => bundle.registrationId,
|
|
468
|
+
getOurIdentity: () => signalKeyPair(bundle.identityKey),
|
|
469
|
+
};
|
|
470
|
+
const cipher = new SessionCipher(storage, address);
|
|
471
|
+
let plaintext;
|
|
472
|
+
try {
|
|
473
|
+
plaintext =
|
|
474
|
+
params.type === "pkmsg"
|
|
475
|
+
? await cipher.decryptPreKeyWhisperMessage(params.ciphertext)
|
|
476
|
+
: await cipher.decryptWhisperMessage(params.ciphertext);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (error === sessionLimitError) {
|
|
480
|
+
return { status: "rejected" };
|
|
107
481
|
}
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
482
|
+
return { error, status: "decrypt-failed" };
|
|
483
|
+
}
|
|
484
|
+
params.signal?.throwIfAborted();
|
|
485
|
+
let stagedNextPreKeyId = bundle.nextPreKeyId;
|
|
486
|
+
const stagedPreKeyIds = new Set();
|
|
487
|
+
const replacementPreKeys = [...stagedPreKeyRemovals]
|
|
488
|
+
.filter((id) => bundle.preKeys.has(id))
|
|
489
|
+
.map(() => {
|
|
490
|
+
const unavailableIds = new Set([...bundle.pendingPreKeyIds, ...stagedPreKeyIds]);
|
|
491
|
+
const created = this.#createPreKey(bundle.preKeys, stagedNextPreKeyId, unavailableIds);
|
|
492
|
+
stagedNextPreKeyId = created.nextPreKeyId;
|
|
493
|
+
stagedPreKeyIds.add(created.id);
|
|
494
|
+
return created;
|
|
495
|
+
});
|
|
496
|
+
for (const replacementPreKey of replacementPreKeys) {
|
|
497
|
+
bundle.pendingPreKeyIds.add(replacementPreKey.id);
|
|
498
|
+
}
|
|
499
|
+
bundle.nextPreKeyId = stagedNextPreKeyId;
|
|
500
|
+
try {
|
|
501
|
+
const accepted = await awaitWithAbort(Promise.resolve().then(() => params.accept(plaintext)), params.signal);
|
|
502
|
+
if (accepted === undefined) {
|
|
503
|
+
return { status: "rejected" };
|
|
504
|
+
}
|
|
505
|
+
params.signal?.throwIfAborted();
|
|
506
|
+
for (const [id, session] of stagedSessions) {
|
|
507
|
+
sessions.set(id, session);
|
|
508
|
+
}
|
|
509
|
+
if (stagedSessions.size > 0) {
|
|
510
|
+
this.#sessions.set(identityKey, sessions);
|
|
511
|
+
}
|
|
512
|
+
for (const id of stagedPreKeyRemovals) {
|
|
513
|
+
bundle.preKeyReservations.delete(id);
|
|
514
|
+
bundle.preKeys.delete(id);
|
|
515
|
+
}
|
|
516
|
+
for (const replacementPreKey of replacementPreKeys) {
|
|
517
|
+
bundle.preKeys.set(replacementPreKey.id, replacementPreKey.keyPair);
|
|
518
|
+
bundle.availablePreKeyIds.push(replacementPreKey.id);
|
|
519
|
+
}
|
|
520
|
+
return { status: "accepted", value: accepted };
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
for (const replacementPreKey of replacementPreKeys) {
|
|
524
|
+
bundle.pendingPreKeyIds.delete(replacementPreKey.id);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
#createBundle() {
|
|
530
|
+
const identityKey = generateSignalKeyPair();
|
|
531
|
+
return {
|
|
532
|
+
availablePreKeyIds: [],
|
|
131
533
|
identityKey,
|
|
132
|
-
|
|
133
|
-
|
|
534
|
+
nextPreKeyId: 1,
|
|
535
|
+
pendingPreKeyIds: new Set(),
|
|
536
|
+
preKeyReservations: new Map(),
|
|
537
|
+
preKeys: new Map(),
|
|
538
|
+
protectedPreKeyIds: new Map(),
|
|
134
539
|
registrationId: 1,
|
|
135
540
|
signedPreKey: signedKeyPair(identityKey, 1),
|
|
136
541
|
};
|
|
137
|
-
|
|
138
|
-
|
|
542
|
+
}
|
|
543
|
+
#createPreKey(preKeys, startingId, stagedIds = new Set()) {
|
|
544
|
+
let id = startingId;
|
|
545
|
+
while (preKeys.has(id) || stagedIds.has(id)) {
|
|
546
|
+
id = id === 0xff_ff_ff ? 1 : id + 1;
|
|
547
|
+
if (id === startingId) {
|
|
548
|
+
throw new Error("WhatsApp Signal prekey identifier space exhausted.");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
keyPair: generateSignalKeyPair(),
|
|
554
|
+
nextPreKeyId: id === 0xff_ff_ff ? 1 : id + 1,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
#recoverExpiredPendingAcknowledgements() {
|
|
558
|
+
const now = this.now();
|
|
559
|
+
for (const [messageKey, pendingAcknowledgement] of this.#pendingAcknowledgements) {
|
|
560
|
+
const { acceptedAt } = pendingAcknowledgement;
|
|
561
|
+
if (acceptedAt === undefined) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (now - acceptedAt < this.maxPendingAcknowledgementAgeMs) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
this.#pendingAcknowledgements.delete(messageKey);
|
|
568
|
+
this.#rememberAcknowledgedMessage(messageKey);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
#rememberAcknowledgedMessage(messageKey) {
|
|
572
|
+
this.#acknowledgedMessageIds.delete(messageKey);
|
|
573
|
+
this.#acknowledgedMessageIds.set(messageKey, true);
|
|
574
|
+
if (this.#acknowledgedMessageIds.size > this.maxRecentAcknowledgements) {
|
|
575
|
+
const oldestMessageKey = this.#acknowledgedMessageIds.keys().next().value;
|
|
576
|
+
if (oldestMessageKey !== undefined) {
|
|
577
|
+
this.#acknowledgedMessageIds.delete(oldestMessageKey);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
#rememberPersistedMessage(messageKey) {
|
|
582
|
+
this.#persistedMessageIds.delete(messageKey);
|
|
583
|
+
this.#persistedMessageIds.set(messageKey, true);
|
|
584
|
+
if (this.#persistedMessageIds.size > this.maxRecentAcknowledgements) {
|
|
585
|
+
const oldestMessageKey = this.#persistedMessageIds.keys().next().value;
|
|
586
|
+
if (oldestMessageKey !== undefined) {
|
|
587
|
+
this.#persistedMessageIds.delete(oldestMessageKey);
|
|
588
|
+
this.releaseMessagePreKeyProtection(oldestMessageKey);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
#protectMessagePreKey(messageKey, identityKey, preKeyId) {
|
|
593
|
+
let protections = this.#messagePreKeyProtections.get(messageKey);
|
|
594
|
+
if (!protections) {
|
|
595
|
+
protections = new Map();
|
|
596
|
+
this.#messagePreKeyProtections.set(messageKey, protections);
|
|
597
|
+
}
|
|
598
|
+
let preKeyIds = protections.get(identityKey);
|
|
599
|
+
if (!preKeyIds) {
|
|
600
|
+
preKeyIds = new Set();
|
|
601
|
+
protections.set(identityKey, preKeyIds);
|
|
602
|
+
}
|
|
603
|
+
if (preKeyIds.has(preKeyId)) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
preKeyIds.add(preKeyId);
|
|
607
|
+
const bundle = this.#bundles.get(identityKey);
|
|
608
|
+
if (bundle) {
|
|
609
|
+
bundle.protectedPreKeyIds.set(preKeyId, (bundle.protectedPreKeyIds.get(preKeyId) ?? 0) + 1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
#rememberTerminalAcceptance(messageKey, error, drain) {
|
|
613
|
+
const terminalAcceptance = { error };
|
|
614
|
+
this.#terminalAcceptances.set(messageKey, terminalAcceptance);
|
|
615
|
+
void drain.then((disposition) => {
|
|
616
|
+
if (this.#terminalAcceptances.get(messageKey) === terminalAcceptance) {
|
|
617
|
+
this.#terminalAcceptances.delete(messageKey);
|
|
618
|
+
}
|
|
619
|
+
if (disposition === "acknowledged") {
|
|
620
|
+
this.#rememberAcknowledgedMessage(messageKey);
|
|
621
|
+
}
|
|
622
|
+
else if (disposition === "persisted") {
|
|
623
|
+
this.#rememberPersistedMessage(messageKey);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async #runTransaction(key, operation) {
|
|
628
|
+
return await this.#runSerialized(this.#pendingTransactions, key, operation);
|
|
629
|
+
}
|
|
630
|
+
async #runSerialized(pending, key, operation) {
|
|
631
|
+
const previous = pending.get(key) ?? Promise.resolve();
|
|
632
|
+
const result = previous.catch(() => undefined).then(operation);
|
|
633
|
+
const settled = result.then(() => undefined, () => undefined);
|
|
634
|
+
pending.set(key, settled);
|
|
635
|
+
try {
|
|
636
|
+
return await result;
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
if (pending.get(key) === settled) {
|
|
640
|
+
pending.delete(key);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
139
643
|
}
|
|
140
644
|
}
|
|
141
645
|
export function createSerializedMessageHandler(processMessage, onError, options = {}) {
|
|
@@ -382,6 +886,7 @@ class BaileysNoiseServer {
|
|
|
382
886
|
class WhatsAppBaileysWebSocketSession {
|
|
383
887
|
socket;
|
|
384
888
|
params;
|
|
889
|
+
#acceptanceAbort = new AbortController();
|
|
385
890
|
#handshakeState = "client-hello";
|
|
386
891
|
#handleSerializedMessage;
|
|
387
892
|
#noise = new BaileysNoiseServer();
|
|
@@ -399,7 +904,12 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
399
904
|
return this.#handshakeState === "open" && this.socket.readyState === WebSocket.OPEN;
|
|
400
905
|
}
|
|
401
906
|
handleMessage(data) {
|
|
402
|
-
|
|
907
|
+
return this.#handleSerializedMessage(data);
|
|
908
|
+
}
|
|
909
|
+
abortAcceptance(reason) {
|
|
910
|
+
if (!this.#acceptanceAbort.signal.aborted) {
|
|
911
|
+
this.#acceptanceAbort.abort(reason);
|
|
912
|
+
}
|
|
403
913
|
}
|
|
404
914
|
async deliverInboundMessage(message) {
|
|
405
915
|
if (!this.isOpen) {
|
|
@@ -450,32 +960,33 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
450
960
|
}
|
|
451
961
|
if (node.tag === "message") {
|
|
452
962
|
const peer = requireAttr(node, "to");
|
|
453
|
-
const
|
|
963
|
+
const messageId = requireAttr(node, "id");
|
|
964
|
+
const accepted = await persistAcceptedBaileysMessage({
|
|
965
|
+
acceptanceSignal: this.#acceptanceAbort.signal,
|
|
966
|
+
appendEvent: this.params.appendEvent,
|
|
454
967
|
node,
|
|
968
|
+
onAcceptanceTimeout: () => {
|
|
969
|
+
const close = resolveWhatsAppWebSocketClose(new Error("WhatsApp message acceptance timed out."));
|
|
970
|
+
this.socket.close(close.code, close.reason);
|
|
971
|
+
},
|
|
972
|
+
path: this.params.path,
|
|
455
973
|
remoteJid: this.params.selfJid,
|
|
456
974
|
signalBundles: this.params.signalBundles,
|
|
457
975
|
});
|
|
976
|
+
if (!accepted) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
458
979
|
await this.#sendNode({
|
|
459
980
|
attrs: {
|
|
460
981
|
class: "message",
|
|
461
982
|
from: peer,
|
|
462
|
-
id:
|
|
983
|
+
id: messageId,
|
|
463
984
|
to: this.params.selfJid,
|
|
464
985
|
...(node.attrs.type ? { type: node.attrs.type } : {}),
|
|
465
986
|
},
|
|
466
987
|
tag: "ack",
|
|
467
988
|
});
|
|
468
|
-
|
|
469
|
-
await this.params.appendEvent({
|
|
470
|
-
accepted: true,
|
|
471
|
-
at: new Date().toISOString(),
|
|
472
|
-
body: normalizedMessage,
|
|
473
|
-
method: "WEBSOCKET",
|
|
474
|
-
path: this.params.path,
|
|
475
|
-
query: {},
|
|
476
|
-
type: "api",
|
|
477
|
-
});
|
|
478
|
-
}
|
|
989
|
+
this.params.signalBundles.markMessageAcknowledged(peer, messageId);
|
|
479
990
|
}
|
|
480
991
|
}
|
|
481
992
|
#createIqResult(node) {
|
|
@@ -621,7 +1132,11 @@ class WhatsAppBaileysWebSocketSession {
|
|
|
621
1132
|
content: [
|
|
622
1133
|
{ attrs: {}, content: encodeBigEndian(bundle.registrationId), tag: "registration" },
|
|
623
1134
|
{ attrs: {}, content: KEY_BUNDLE_TYPE, tag: "type" },
|
|
624
|
-
{
|
|
1135
|
+
{
|
|
1136
|
+
attrs: {},
|
|
1137
|
+
content: scrubSignalPublicKey(bundle.identityKey.public),
|
|
1138
|
+
tag: "identity",
|
|
1139
|
+
},
|
|
625
1140
|
xmppSignedPreKey(bundle.signedPreKey),
|
|
626
1141
|
xmppPreKey(bundle.preKey, bundle.preKeyId),
|
|
627
1142
|
],
|
|
@@ -742,8 +1257,9 @@ function truncateWebSocketCloseReason(reason) {
|
|
|
742
1257
|
return result;
|
|
743
1258
|
}
|
|
744
1259
|
export function attachWhatsAppBaileysWebSocketServer(params) {
|
|
745
|
-
const signalBundles = new WhatsAppSignalBundleStore();
|
|
1260
|
+
const signalBundles = new WhatsAppSignalBundleStore(undefined, undefined, undefined, undefined, undefined, { messageAcceptanceTimeoutMs: params.messageAcceptanceTimeoutMs });
|
|
746
1261
|
const sessions = new Set();
|
|
1262
|
+
const pendingSessionMessages = new Set();
|
|
747
1263
|
const pendingMessages = [];
|
|
748
1264
|
const maxPendingInboundMessages = resolveMaxPendingWhatsAppInboundMessages(params.maxPendingInboundMessages);
|
|
749
1265
|
let pendingReservations = 0;
|
|
@@ -809,20 +1325,39 @@ export function attachWhatsAppBaileysWebSocketServer(params) {
|
|
|
809
1325
|
signalBundles,
|
|
810
1326
|
});
|
|
811
1327
|
sessions.add(session);
|
|
812
|
-
socket.once("close", () =>
|
|
1328
|
+
socket.once("close", () => {
|
|
1329
|
+
session.abortAcceptance(new Error("WhatsApp WebSocket closed."));
|
|
1330
|
+
sessions.delete(session);
|
|
1331
|
+
});
|
|
813
1332
|
socket.on("error", () => {
|
|
1333
|
+
session.abortAcceptance(new Error("WhatsApp WebSocket failed."));
|
|
814
1334
|
sessions.delete(session);
|
|
815
1335
|
socket.terminate();
|
|
816
1336
|
});
|
|
817
|
-
socket.on("message", (data) =>
|
|
1337
|
+
socket.on("message", (data) => {
|
|
1338
|
+
const pending = session.handleMessage(data);
|
|
1339
|
+
pendingSessionMessages.add(pending);
|
|
1340
|
+
void pending.then(() => pendingSessionMessages.delete(pending), () => pendingSessionMessages.delete(pending));
|
|
1341
|
+
});
|
|
818
1342
|
});
|
|
819
1343
|
return {
|
|
820
1344
|
async close() {
|
|
821
1345
|
closing = true;
|
|
822
1346
|
params.httpServer.off("upgrade", handleUpgrade);
|
|
823
1347
|
pendingMessages.length = 0;
|
|
1348
|
+
for (const session of sessions) {
|
|
1349
|
+
session.abortAcceptance(new Error("WhatsApp WebSocket server is shutting down."));
|
|
1350
|
+
}
|
|
824
1351
|
await closeWebSocketServer(wss);
|
|
1352
|
+
const messageResults = await Promise.allSettled([...pendingSessionMessages]);
|
|
825
1353
|
await flushPromise;
|
|
1354
|
+
const messageErrors = messageResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1355
|
+
if (messageErrors.length === 1) {
|
|
1356
|
+
throw messageErrors[0];
|
|
1357
|
+
}
|
|
1358
|
+
if (messageErrors.length > 1) {
|
|
1359
|
+
throw new AggregateError(messageErrors, "WhatsApp WebSocket message drain failed.");
|
|
1360
|
+
}
|
|
826
1361
|
},
|
|
827
1362
|
prepareInboundMessage(message) {
|
|
828
1363
|
if (pendingMessages.length + pendingReservations >= maxPendingInboundMessages) {
|
|
@@ -909,45 +1444,133 @@ function removeNoiseIntroHeader(chunk) {
|
|
|
909
1444
|
function children(node) {
|
|
910
1445
|
return Array.isArray(node.content) ? node.content : [];
|
|
911
1446
|
}
|
|
912
|
-
async function
|
|
1447
|
+
export async function persistAcceptedBaileysMessage(params) {
|
|
913
1448
|
const peer = canonicalizeWhatsAppUserCorrelationJid(params.node.attrs.to ?? "");
|
|
914
1449
|
const messageId = params.node.attrs.id;
|
|
915
1450
|
if (!peer || !messageId) {
|
|
916
|
-
return
|
|
917
|
-
}
|
|
918
|
-
const
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
const messageKey = `${peer}\0${messageId}`;
|
|
1454
|
+
const wasPreviouslyPersisted = params.signalBundles.hasPersistedMessage(messageKey);
|
|
1455
|
+
let persistenceSucceeded = wasPreviouslyPersisted;
|
|
1456
|
+
let operationSettled;
|
|
1457
|
+
const operationSettlement = new Promise((resolve) => {
|
|
1458
|
+
operationSettled = resolve;
|
|
924
1459
|
});
|
|
925
|
-
|
|
1460
|
+
let requiresSignalCommit = wasPreviouslyPersisted;
|
|
1461
|
+
let signalCommitSucceeded = false;
|
|
1462
|
+
const pendingAcceptanceOperations = new Set();
|
|
1463
|
+
const trackAcceptanceOperation = (operation) => {
|
|
1464
|
+
pendingAcceptanceOperations.add(operation);
|
|
1465
|
+
void operation.then(() => {
|
|
1466
|
+
persistenceSucceeded = true;
|
|
1467
|
+
pendingAcceptanceOperations.delete(operation);
|
|
1468
|
+
}, () => pendingAcceptanceOperations.delete(operation));
|
|
1469
|
+
return operation;
|
|
1470
|
+
};
|
|
1471
|
+
const drainAcceptanceOperations = async () => {
|
|
1472
|
+
while (pendingAcceptanceOperations.size > 0) {
|
|
1473
|
+
await Promise.allSettled([...pendingAcceptanceOperations]);
|
|
1474
|
+
}
|
|
1475
|
+
await operationSettlement;
|
|
1476
|
+
if (!persistenceSucceeded) {
|
|
1477
|
+
params.signalBundles.releaseMessagePreKeyProtection(messageKey);
|
|
1478
|
+
return "retryable";
|
|
1479
|
+
}
|
|
1480
|
+
if (!requiresSignalCommit || signalCommitSucceeded) {
|
|
1481
|
+
params.signalBundles.releaseMessagePreKeyProtection(messageKey);
|
|
1482
|
+
return "acknowledged";
|
|
1483
|
+
}
|
|
1484
|
+
return "persisted";
|
|
1485
|
+
};
|
|
1486
|
+
const accepted = await params.signalBundles.acceptMessageOnce(messageKey, async (acceptanceSignal) => {
|
|
926
1487
|
try {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1488
|
+
acceptanceSignal.throwIfAborted();
|
|
1489
|
+
const candidates = encryptedMessageCandidates(params.node);
|
|
1490
|
+
requiresSignalCommit = candidates.length > 0;
|
|
1491
|
+
if (candidates.length === 0) {
|
|
1492
|
+
if (!wasPreviouslyPersisted) {
|
|
1493
|
+
await awaitWithAbort(trackAcceptanceOperation(params.appendEvent({
|
|
1494
|
+
accepted: true,
|
|
1495
|
+
at: new Date().toISOString(),
|
|
1496
|
+
body: sanitizeNodeForJson(params.node),
|
|
1497
|
+
method: "WEBSOCKET",
|
|
1498
|
+
path: params.path,
|
|
1499
|
+
query: {},
|
|
1500
|
+
type: "api",
|
|
1501
|
+
})), acceptanceSignal);
|
|
1502
|
+
}
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
const remoteCorrelationJid = canonicalizeWhatsAppUserCorrelationJid(params.remoteJid);
|
|
1506
|
+
candidates.sort((left, right) => {
|
|
1507
|
+
const leftIsSelf = canonicalizeWhatsAppUserCorrelationJid(left.recipientJid) === remoteCorrelationJid;
|
|
1508
|
+
const rightIsSelf = canonicalizeWhatsAppUserCorrelationJid(right.recipientJid) === remoteCorrelationJid;
|
|
1509
|
+
return Number(leftIsSelf) - Number(rightIsSelf);
|
|
932
1510
|
});
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1511
|
+
for (const candidate of candidates) {
|
|
1512
|
+
const result = await params.signalBundles.transactDirectMessage({
|
|
1513
|
+
accept: async (decrypted) => {
|
|
1514
|
+
let text;
|
|
1515
|
+
try {
|
|
1516
|
+
text = readWhatsAppConversation(unpadRandomMax16(decrypted));
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
text = undefined;
|
|
1520
|
+
}
|
|
1521
|
+
const message = text
|
|
1522
|
+
? {
|
|
1523
|
+
key: {
|
|
1524
|
+
fromMe: true,
|
|
1525
|
+
id: messageId,
|
|
1526
|
+
remoteJid: peer,
|
|
1527
|
+
},
|
|
1528
|
+
message: { conversation: text },
|
|
1529
|
+
messageTimestamp: Math.floor(Date.now() / 1000),
|
|
1530
|
+
}
|
|
1531
|
+
: undefined;
|
|
1532
|
+
if (!wasPreviouslyPersisted) {
|
|
1533
|
+
await awaitWithAbort(trackAcceptanceOperation(params.appendEvent({
|
|
1534
|
+
accepted: true,
|
|
1535
|
+
at: new Date().toISOString(),
|
|
1536
|
+
body: message ?? sanitizeNodeForJson(params.node),
|
|
1537
|
+
method: "WEBSOCKET",
|
|
1538
|
+
path: params.path,
|
|
1539
|
+
query: {},
|
|
1540
|
+
type: "api",
|
|
1541
|
+
})), acceptanceSignal);
|
|
1542
|
+
}
|
|
1543
|
+
return true;
|
|
940
1544
|
},
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1545
|
+
ciphertext: candidate.ciphertext,
|
|
1546
|
+
messageKey,
|
|
1547
|
+
recipientJid: candidate.recipientJid,
|
|
1548
|
+
remoteJid: params.remoteJid,
|
|
1549
|
+
signal: acceptanceSignal,
|
|
1550
|
+
type: candidate.type,
|
|
1551
|
+
});
|
|
1552
|
+
if (result.status === "accepted") {
|
|
1553
|
+
signalCommitSucceeded = true;
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
944
1556
|
}
|
|
1557
|
+
return false;
|
|
945
1558
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1559
|
+
finally {
|
|
1560
|
+
if (!acceptanceSignal.aborted && !signalCommitSucceeded) {
|
|
1561
|
+
params.signalBundles.releaseMessagePreKeyProtection(messageKey);
|
|
1562
|
+
}
|
|
1563
|
+
operationSettled();
|
|
948
1564
|
}
|
|
1565
|
+
}, {
|
|
1566
|
+
onTerminalTimeout: params.onAcceptanceTimeout,
|
|
1567
|
+
signal: params.acceptanceSignal,
|
|
1568
|
+
terminalDrain: drainAcceptanceOperations,
|
|
1569
|
+
});
|
|
1570
|
+
if (accepted) {
|
|
1571
|
+
params.signalBundles.clearPersistedMessage(messageKey);
|
|
949
1572
|
}
|
|
950
|
-
return
|
|
1573
|
+
return accepted;
|
|
951
1574
|
}
|
|
952
1575
|
function encryptedMessageCandidates(node) {
|
|
953
1576
|
const result = [];
|
|
@@ -973,29 +1596,29 @@ function encryptedMessageCandidates(node) {
|
|
|
973
1596
|
function signalKeyPair(pair) {
|
|
974
1597
|
return {
|
|
975
1598
|
privKey: Buffer.from(pair.private),
|
|
976
|
-
pubKey: pair.public
|
|
977
|
-
? Buffer.from(pair.public)
|
|
978
|
-
: Buffer.concat([KEY_BUNDLE_TYPE, pair.public]),
|
|
1599
|
+
pubKey: ensureSignalPublicKey(pair.public),
|
|
979
1600
|
};
|
|
980
1601
|
}
|
|
981
1602
|
function cloneSignalSession(session) {
|
|
982
1603
|
return SessionRecord.deserialize(session.serialize());
|
|
983
1604
|
}
|
|
984
1605
|
export function signalBundleIdentityKey(jid) {
|
|
985
|
-
return jid.replace(
|
|
1606
|
+
return jid.replace(/(?:_\d+)?(?::\d+)?(?=@)/u, "");
|
|
986
1607
|
}
|
|
987
1608
|
function signalProtocolAddress(jid) {
|
|
988
|
-
const match = /^(\d{7,15})(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
|
|
1609
|
+
const match = /^(\d{7,15})(?:_(\d+))?(?::(\d+))?@(s\.whatsapp\.net|lid)$/iu.exec(jid);
|
|
989
1610
|
if (!match) {
|
|
990
1611
|
return undefined;
|
|
991
1612
|
}
|
|
992
|
-
const deviceId = Number(match[
|
|
1613
|
+
const deviceId = Number(match[3] ?? "0");
|
|
993
1614
|
if (!Number.isSafeInteger(deviceId) || deviceId < 0) {
|
|
994
1615
|
return undefined;
|
|
995
1616
|
}
|
|
1617
|
+
const agent = match[2];
|
|
1618
|
+
const server = match[4].toLowerCase();
|
|
996
1619
|
return {
|
|
997
1620
|
deviceId,
|
|
998
|
-
name: match[
|
|
1621
|
+
name: agent !== undefined ? `${match[1]}_${agent}` : server === "lid" ? `${match[1]}_1` : match[1],
|
|
999
1622
|
};
|
|
1000
1623
|
}
|
|
1001
1624
|
function unpadRandomMax16(value) {
|
|
@@ -1030,6 +1653,7 @@ function readWhatsAppConversation(message) {
|
|
|
1030
1653
|
: undefined;
|
|
1031
1654
|
return nestedMessage ? readWhatsAppConversation(nestedMessage) : undefined;
|
|
1032
1655
|
}
|
|
1656
|
+
const MAX_WHATSAPP_PROTOBUF_GROUP_DEPTH = 64;
|
|
1033
1657
|
function readProtobufLengthDelimitedFields(value) {
|
|
1034
1658
|
const fields = new Map();
|
|
1035
1659
|
let offset = 0;
|
|
@@ -1055,20 +1679,66 @@ function readProtobufLengthDelimitedFields(value) {
|
|
|
1055
1679
|
continue;
|
|
1056
1680
|
}
|
|
1057
1681
|
if (wireType === 0) {
|
|
1058
|
-
offset =
|
|
1682
|
+
offset = skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, 0);
|
|
1059
1683
|
continue;
|
|
1060
1684
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1685
|
+
offset = skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, 0);
|
|
1686
|
+
}
|
|
1687
|
+
return fields;
|
|
1688
|
+
}
|
|
1689
|
+
function skipWhatsAppProtobufField(value, offset, fieldNumber, wireType, groupDepth) {
|
|
1690
|
+
if (wireType === 0) {
|
|
1691
|
+
return readProtobufVarint(value, offset).offset;
|
|
1692
|
+
}
|
|
1693
|
+
if (wireType === 1) {
|
|
1694
|
+
const end = offset + 8;
|
|
1695
|
+
if (end > value.length) {
|
|
1696
|
+
throw new Error("Invalid WhatsApp protobuf fixed64 field.");
|
|
1064
1697
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1698
|
+
return end;
|
|
1699
|
+
}
|
|
1700
|
+
if (wireType === 2) {
|
|
1701
|
+
const length = readProtobufVarint(value, offset);
|
|
1702
|
+
const end = length.offset + length.value;
|
|
1703
|
+
if (end > value.length) {
|
|
1704
|
+
throw new Error("Invalid WhatsApp protobuf length.");
|
|
1068
1705
|
}
|
|
1069
|
-
|
|
1706
|
+
return end;
|
|
1070
1707
|
}
|
|
1071
|
-
|
|
1708
|
+
if (wireType === 3) {
|
|
1709
|
+
if (groupDepth >= MAX_WHATSAPP_PROTOBUF_GROUP_DEPTH) {
|
|
1710
|
+
throw new Error("WhatsApp protobuf group nesting exceeds the supported depth.");
|
|
1711
|
+
}
|
|
1712
|
+
let nestedOffset = offset;
|
|
1713
|
+
while (nestedOffset < value.length) {
|
|
1714
|
+
const tag = readProtobufVarint(value, nestedOffset);
|
|
1715
|
+
nestedOffset = tag.offset;
|
|
1716
|
+
const nestedFieldNumber = tag.value >>> 3;
|
|
1717
|
+
const nestedWireType = tag.value & 7;
|
|
1718
|
+
if (nestedFieldNumber < 1) {
|
|
1719
|
+
throw new Error("Invalid WhatsApp protobuf field.");
|
|
1720
|
+
}
|
|
1721
|
+
if (nestedWireType === 4) {
|
|
1722
|
+
if (nestedFieldNumber !== fieldNumber) {
|
|
1723
|
+
throw new Error("Mismatched WhatsApp protobuf end group.");
|
|
1724
|
+
}
|
|
1725
|
+
return nestedOffset;
|
|
1726
|
+
}
|
|
1727
|
+
nestedOffset = skipWhatsAppProtobufField(value, nestedOffset, nestedFieldNumber, nestedWireType, groupDepth + 1);
|
|
1728
|
+
}
|
|
1729
|
+
throw new Error("Unterminated WhatsApp protobuf group.");
|
|
1730
|
+
}
|
|
1731
|
+
if (wireType === 4) {
|
|
1732
|
+
throw new Error("Unexpected WhatsApp protobuf end group.");
|
|
1733
|
+
}
|
|
1734
|
+
if (wireType === 5) {
|
|
1735
|
+
const end = offset + 4;
|
|
1736
|
+
if (end > value.length) {
|
|
1737
|
+
throw new Error("Invalid WhatsApp protobuf fixed32 field.");
|
|
1738
|
+
}
|
|
1739
|
+
return end;
|
|
1740
|
+
}
|
|
1741
|
+
throw new Error(`Unsupported WhatsApp protobuf wire type: ${wireType}.`);
|
|
1072
1742
|
}
|
|
1073
1743
|
function readProtobufVarint(value, start) {
|
|
1074
1744
|
let result = 0;
|