@diegoaltoworks/chatter 0.28.0 → 0.30.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.
Files changed (30) hide show
  1. package/dist/bin/wa-pair.js +320 -0
  2. package/dist/channels/whatsapp/authState.d.ts +39 -0
  3. package/dist/channels/whatsapp/authState.d.ts.map +1 -0
  4. package/dist/channels/whatsapp/authState.test.d.ts +2 -0
  5. package/dist/channels/whatsapp/authState.test.d.ts.map +1 -0
  6. package/dist/channels/whatsapp/baileys.d.ts +21 -0
  7. package/dist/channels/whatsapp/baileys.d.ts.map +1 -0
  8. package/dist/channels/whatsapp/baileys.test.d.ts +2 -0
  9. package/dist/channels/whatsapp/baileys.test.d.ts.map +1 -0
  10. package/dist/channels/whatsapp/channel.d.ts +95 -0
  11. package/dist/channels/whatsapp/channel.d.ts.map +1 -0
  12. package/dist/channels/whatsapp/channel.test.d.ts +2 -0
  13. package/dist/channels/whatsapp/channel.test.d.ts.map +1 -0
  14. package/dist/channels/whatsapp/crypto.d.ts +10 -0
  15. package/dist/channels/whatsapp/crypto.d.ts.map +1 -0
  16. package/dist/channels/whatsapp/crypto.test.d.ts +2 -0
  17. package/dist/channels/whatsapp/crypto.test.d.ts.map +1 -0
  18. package/dist/channels/whatsapp/inbound.d.ts +131 -0
  19. package/dist/channels/whatsapp/inbound.d.ts.map +1 -0
  20. package/dist/channels/whatsapp/inbound.test.d.ts +2 -0
  21. package/dist/channels/whatsapp/inbound.test.d.ts.map +1 -0
  22. package/dist/channels/whatsapp/index.d.ts +34 -0
  23. package/dist/channels/whatsapp/index.d.ts.map +1 -0
  24. package/dist/channels/whatsapp/index.js +810 -0
  25. package/dist/channels/whatsapp/index.mjs +753 -0
  26. package/dist/channels/whatsapp/lease.d.ts +42 -0
  27. package/dist/channels/whatsapp/lease.d.ts.map +1 -0
  28. package/dist/channels/whatsapp/lease.test.d.ts +2 -0
  29. package/dist/channels/whatsapp/lease.test.d.ts.map +1 -0
  30. package/package.json +23 -4
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // bin/wa-pair.ts
27
+ var import_client = require("@libsql/client");
28
+
29
+ // src/channels/whatsapp/crypto.ts
30
+ var import_node_crypto = require("node:crypto");
31
+ var IV_LENGTH = 12;
32
+ var TAG_LENGTH = 16;
33
+ function deriveKey(secret) {
34
+ return (0, import_node_crypto.createHash)("sha256").update(secret, "utf-8").digest();
35
+ }
36
+ function encrypt(plaintext, secret) {
37
+ const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
38
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", deriveKey(secret), iv);
39
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
40
+ return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
41
+ }
42
+ function decrypt(payload, secret) {
43
+ const raw = Buffer.from(payload, "base64");
44
+ const iv = raw.subarray(0, IV_LENGTH);
45
+ const tag = raw.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
46
+ const ciphertext = raw.subarray(IV_LENGTH + TAG_LENGTH);
47
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveKey(secret), iv);
48
+ decipher.setAuthTag(tag);
49
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
50
+ }
51
+
52
+ // src/channels/whatsapp/authState.ts
53
+ var schemaReady = /* @__PURE__ */ new WeakMap();
54
+ function ensureAuthSchema(client) {
55
+ let ready = schemaReady.get(client);
56
+ if (!ready) {
57
+ ready = client.execute("CREATE TABLE IF NOT EXISTS wa_auth (id TEXT PRIMARY KEY, value TEXT NOT NULL)").catch((error) => {
58
+ schemaReady.delete(client);
59
+ throw error;
60
+ });
61
+ schemaReady.set(client, ready);
62
+ }
63
+ return ready;
64
+ }
65
+ async function useTursoAuthState(db2, secret, sessionId2, runtime) {
66
+ await ensureAuthSchema(db2);
67
+ const rowId = (id) => sessionId2 === "default" ? id : `s:${sessionId2}/${id}`;
68
+ async function read(id) {
69
+ const result = await db2.execute({
70
+ sql: "SELECT value FROM wa_auth WHERE id = ?",
71
+ args: [rowId(id)]
72
+ });
73
+ const row = result.rows[0];
74
+ if (!row) return null;
75
+ return JSON.parse(decrypt(row.value, secret), runtime.bufferJSON.reviver);
76
+ }
77
+ async function write(id, value) {
78
+ const payload = encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
79
+ await db2.execute({
80
+ sql: "INSERT OR REPLACE INTO wa_auth (id, value) VALUES (?, ?)",
81
+ args: [rowId(id), payload]
82
+ });
83
+ }
84
+ async function remove(id) {
85
+ await db2.execute({ sql: "DELETE FROM wa_auth WHERE id = ?", args: [rowId(id)] });
86
+ }
87
+ const creds = await read("creds") ?? runtime.initAuthCreds();
88
+ return {
89
+ state: {
90
+ creds,
91
+ keys: {
92
+ get: async (type, ids) => {
93
+ const data = {};
94
+ await Promise.all(
95
+ ids.map(async (id) => {
96
+ let value = await read(`${type}-${id}`);
97
+ if (type === "app-state-sync-key" && value) {
98
+ value = runtime.appStateSyncKeyFromObject(
99
+ value
100
+ );
101
+ }
102
+ if (value) data[id] = value;
103
+ })
104
+ );
105
+ return data;
106
+ },
107
+ set: async (data) => {
108
+ const jobs = [];
109
+ for (const category of Object.keys(data)) {
110
+ const entries = data[category];
111
+ if (!entries) continue;
112
+ for (const id of Object.keys(entries)) {
113
+ const value = entries[id];
114
+ jobs.push(value ? write(`${category}-${id}`, value) : remove(`${category}-${id}`));
115
+ }
116
+ }
117
+ await Promise.all(jobs);
118
+ }
119
+ }
120
+ },
121
+ saveCreds: () => write("creds", creds),
122
+ clear: async () => {
123
+ if (sessionId2 === "default") {
124
+ await db2.execute("DELETE FROM wa_auth WHERE id NOT LIKE 's:%'");
125
+ } else {
126
+ await db2.execute({
127
+ sql: "DELETE FROM wa_auth WHERE id LIKE ?",
128
+ args: [`s:${sessionId2}/%`]
129
+ });
130
+ }
131
+ }
132
+ };
133
+ }
134
+
135
+ // src/channels/whatsapp/baileys.ts
136
+ function wrapMissingBaileysError(cause) {
137
+ return new Error(
138
+ "The WhatsApp channel ('@diegoaltoworks/chatter/whatsapp') requires the optional peer dependency '@whiskeysockets/baileys', which is not installed. Install it with `bun add @whiskeysockets/baileys` (or npm/pnpm/yarn) to use this channel.",
139
+ { cause }
140
+ );
141
+ }
142
+ var cached;
143
+ function loadBaileys() {
144
+ if (!cached) {
145
+ cached = import("@whiskeysockets/baileys").catch((error) => {
146
+ cached = void 0;
147
+ throw wrapMissingBaileysError(error);
148
+ });
149
+ }
150
+ return cached;
151
+ }
152
+
153
+ // bin/wa-pair.ts
154
+ var args = process.argv.slice(2);
155
+ function flagValue(name) {
156
+ const index = args.indexOf(name);
157
+ return index >= 0 ? args[index + 1] : void 0;
158
+ }
159
+ if (args.includes("--help") || args.includes("-h")) {
160
+ console.log(`
161
+ WhatsApp pairing CLI
162
+
163
+ Usage:
164
+ bun run wa-pair [sessionId] [options]
165
+
166
+ Arguments:
167
+ sessionId Which session to pair (default: "default"). Use a
168
+ distinct id per WhatsApp number for multi-session.
169
+
170
+ Options:
171
+ --code <phoneNumber> Pair via pairing code instead of QR (digits only,
172
+ e.g. 447700900123). For headless setups.
173
+ --reset Wipe the stored session first, then pair fresh.
174
+ --help, -h Show this help.
175
+
176
+ Environment:
177
+ TURSO_URL, TURSO_AUTH_TOKEN Required. The database auth state is stored in.
178
+ WA_SESSION_SECRET Required. Encrypts the stored session at rest.
179
+ `);
180
+ process.exit(0);
181
+ }
182
+ var sessionId = args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--code") ?? "default";
183
+ if (args.includes("--code") && !flagValue("--code")) {
184
+ console.error("\u274C --code requires a phone number, e.g. --code 447700900123");
185
+ process.exit(1);
186
+ }
187
+ var phoneNumber = flagValue("--code")?.replace(/[^0-9]/g, "");
188
+ var reset = args.includes("--reset");
189
+ var sessionSecret = process.env.WA_SESSION_SECRET;
190
+ if (!sessionSecret) {
191
+ console.error("\u274C WA_SESSION_SECRET is required (any strong passphrase; keep it stable).");
192
+ process.exit(1);
193
+ }
194
+ if (!process.env.TURSO_URL) {
195
+ console.error("\u274C TURSO_URL is required.");
196
+ process.exit(1);
197
+ }
198
+ var db = (0, import_client.createClient)({
199
+ url: process.env.TURSO_URL,
200
+ authToken: process.env.TURSO_AUTH_TOKEN || ""
201
+ });
202
+ var MAX_RECONNECTS = 12;
203
+ var codeRequested = false;
204
+ var reconnects = 0;
205
+ var didReset = false;
206
+ async function start() {
207
+ const baileys = await loadBaileys().catch((error) => {
208
+ console.error(`\u274C ${error.message}`);
209
+ process.exit(1);
210
+ });
211
+ const runtime = {
212
+ bufferJSON: baileys.BufferJSON,
213
+ initAuthCreds: baileys.initAuthCreds,
214
+ appStateSyncKeyFromObject: (value) => baileys.proto.Message.AppStateSyncKeyData.fromObject(value)
215
+ };
216
+ const { state, saveCreds, clear } = await useTursoAuthState(
217
+ db,
218
+ sessionSecret,
219
+ sessionId,
220
+ runtime
221
+ );
222
+ if (reset && !didReset) {
223
+ didReset = true;
224
+ await clear();
225
+ console.log(`Session "${sessionId}" wiped. Pairing fresh...`);
226
+ return start();
227
+ }
228
+ if (state.creds.registered && !codeRequested) {
229
+ console.log(
230
+ `Already paired. Session "${sessionId}" is connected-ready. Run with --reset to re-pair.`
231
+ );
232
+ process.exit(0);
233
+ }
234
+ const { version } = await baileys.fetchLatestBaileysVersion();
235
+ const noop = () => void 0;
236
+ const logger = {
237
+ level: "silent",
238
+ trace: noop,
239
+ debug: noop,
240
+ info: noop,
241
+ warn: noop,
242
+ error: noop,
243
+ fatal: noop
244
+ };
245
+ logger.child = () => logger;
246
+ const sock = baileys.makeWASocket({
247
+ version,
248
+ auth: {
249
+ creds: state.creds,
250
+ // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
251
+ keys: baileys.makeCacheableSignalKeyStore(state.keys, logger)
252
+ },
253
+ // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
254
+ logger,
255
+ markOnlineOnConnect: false
256
+ });
257
+ sock.ev.on("creds.update", saveCreds);
258
+ sock.ev.on("connection.update", async (update) => {
259
+ const { connection, lastDisconnect, qr } = update;
260
+ if (qr && !phoneNumber) {
261
+ const qrcodeTerminal = await import("qrcode-terminal").catch(() => void 0);
262
+ if (!qrcodeTerminal) {
263
+ console.error(
264
+ "\u274C QR mode requires the optional peer dependency 'qrcode-terminal'. Install it with `bun add qrcode-terminal`, or re-run with --code <phoneNumber> for pairing-code mode instead."
265
+ );
266
+ process.exit(1);
267
+ }
268
+ console.log(
269
+ "\nScan this QR code: WhatsApp -> Settings -> Linked Devices -> Link a Device.\n"
270
+ );
271
+ qrcodeTerminal.default.generate(qr, { small: true });
272
+ }
273
+ if (connection === "open") {
274
+ console.log(
275
+ `
276
+ \u2705 Paired session "${sessionId}" as ${sock.user?.id}. Session saved (encrypted).`
277
+ );
278
+ console.log("The running server will pick this session up on its next connect.");
279
+ process.exit(0);
280
+ }
281
+ if (connection === "close") {
282
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
283
+ if (codeRequested && reconnects < MAX_RECONNECTS) {
284
+ reconnects++;
285
+ console.log(
286
+ `Connection closed (${statusCode ?? "?"}) - reconnecting (code STILL VALID, keep going on the phone) (${reconnects}/${MAX_RECONNECTS})...`
287
+ );
288
+ setTimeout(() => void start(), 2e3);
289
+ return;
290
+ }
291
+ if (statusCode === baileys.DisconnectReason.loggedOut) {
292
+ console.error("\u274C Pairing rejected (logged out). Run wa-pair again for a fresh code.");
293
+ } else {
294
+ console.error(`\u274C Connection closed (${statusCode ?? "?"}). Run wa-pair again.`);
295
+ }
296
+ process.exit(1);
297
+ }
298
+ });
299
+ if (phoneNumber && !codeRequested && !state.creds.registered) {
300
+ setTimeout(async () => {
301
+ try {
302
+ const code = await sock.requestPairingCode(phoneNumber);
303
+ codeRequested = true;
304
+ console.log("\n==============================================");
305
+ console.log(` Pairing code for +${phoneNumber}: ${code}`);
306
+ console.log("==============================================");
307
+ console.log("On the phone: WhatsApp -> Settings -> Linked Devices ->");
308
+ console.log("Link a Device -> 'Link with phone number instead' -> enter the code.");
309
+ console.log("Waiting for confirmation (leave this running)...");
310
+ } catch (error) {
311
+ console.error("\u274C Failed to request pairing code:", error);
312
+ process.exit(1);
313
+ }
314
+ }, 3e3);
315
+ }
316
+ }
317
+ start().catch((error) => {
318
+ console.error("\u274C Unexpected error:", error);
319
+ process.exit(1);
320
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Turso/libsql-backed Baileys auth state, encrypted at rest.
3
+ *
4
+ * Same contract as Baileys' own `useMultiFileAuthState`, but rows in a
5
+ * `wa_auth` table instead of files, AES-GCM encrypted (see `crypto.ts`) so
6
+ * session material — which grants full account access — is never stored
7
+ * readable.
8
+ *
9
+ * Only Baileys *types* are imported here (erased at compile time, so they add
10
+ * no runtime dependency); the few Baileys runtime values this module needs
11
+ * (`BufferJSON`, `initAuthCreds`, the app-state-sync-key constructor) are
12
+ * passed in by the caller, which already holds them from the dynamic import
13
+ * in `baileys.ts` — see that module for why the import is dynamic.
14
+ */
15
+ import type { Client } from "@libsql/client";
16
+ import type { AuthenticationCreds, AuthenticationState, SignalDataTypeMap } from "@whiskeysockets/baileys";
17
+ export interface TursoAuthStateResult {
18
+ state: AuthenticationState;
19
+ saveCreds: () => Promise<void>;
20
+ /** Wipe the stored session (unlink / re-pair). */
21
+ clear: () => Promise<void>;
22
+ }
23
+ /** The slice of the dynamically-loaded Baileys module this file needs at runtime. */
24
+ export interface AuthStateRuntime {
25
+ bufferJSON: {
26
+ replacer: (key: string, value: unknown) => unknown;
27
+ reviver: (key: string, value: unknown) => unknown;
28
+ };
29
+ initAuthCreds: () => AuthenticationCreds;
30
+ appStateSyncKeyFromObject: (value: Record<string, unknown>) => SignalDataTypeMap["app-state-sync-key"];
31
+ }
32
+ /**
33
+ * Sessions share the `wa_auth` table, namespaced by row-id prefix. The
34
+ * "default" session uses UNPREFIXED ids — exactly a single-session layout —
35
+ * so an existing default session needs no migration when multi-session
36
+ * support is introduced.
37
+ */
38
+ export declare function useTursoAuthState(db: Client, secret: string, sessionId: string, runtime: AuthStateRuntime): Promise<TursoAuthStateResult>;
39
+ //# sourceMappingURL=authState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authState.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/authState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,kDAAkD;IAClD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE;QACV,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QACnD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;KACnD,CAAC;IACF,aAAa,EAAE,MAAM,mBAAmB,CAAC;IACzC,yBAAyB,EAAE,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,iBAAiB,CAAC,oBAAoB,CAAC,CAAC;CAC9C;AAiBD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CA6E/B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=authState.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authState.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/authState.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The one place `./whatsapp` touches the `@whiskeysockets/baileys` package at
3
+ * runtime. It's an OPTIONAL peer dependency — `bun install` and every core
4
+ * import must work without it — so nothing in this subpath may `import` it
5
+ * statically; every other module here takes what it needs as a value (see
6
+ * `AuthStateRuntime` in `authState.ts`) or a type-only import (erased at
7
+ * compile time, so it costs nothing at runtime).
8
+ *
9
+ * `loadBaileys()` is the single dynamic import, called lazily when a channel
10
+ * actually starts, so a missing package fails with an actionable message
11
+ * instead of a raw "Cannot find module" — or, worse, at unrelated core
12
+ * imports that never touch WhatsApp at all.
13
+ */
14
+ import type * as BaileysModule from "@whiskeysockets/baileys";
15
+ export type Baileys = typeof BaileysModule;
16
+ /** Wraps a failed `import("@whiskeysockets/baileys")` in an actionable message. Exported separately so the message content is unit-testable without simulating a real missing module. */
17
+ export declare function wrapMissingBaileysError(cause: unknown): Error;
18
+ export declare function loadBaileys(): Promise<Baileys>;
19
+ /** A silent, structurally pino-compatible logger — Baileys logs very verbosely by default. */
20
+ export declare function silentLogger(): unknown;
21
+ //# sourceMappingURL=baileys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baileys.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/baileys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,aAAa,MAAM,yBAAyB,CAAC;AAE9D,MAAM,MAAM,OAAO,GAAG,OAAO,aAAa,CAAC;AAE3C,yLAAyL;AACzL,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO7D;AAID,wBAAgB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ9C;AAED,8FAA8F;AAC9F,wBAAgB,YAAY,IAAI,OAAO,CAatC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=baileys.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baileys.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/baileys.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * WhatsApp web-client transport: a {@link Channel} that runs one Baileys
3
+ * socket per configured session id (one WhatsApp number each), auth state
4
+ * encrypted in the host's own database (`deps.db` — no second connection
5
+ * opened), and a deploy lease so two overlapping instances can never hold the
6
+ * same session's socket at once (WhatsApp treats a second connection as a
7
+ * takeover and logs the first one out).
8
+ *
9
+ * Message INTERPRETATION — mention/reply detection, allowlists, loop guards —
10
+ * is deliberately not this module's job; it hands every raw inbound message
11
+ * to `config.onMessage` and lets the caller apply `./channels`' reply gates.
12
+ * This module owns only the connection lifecycle and outbound sending.
13
+ *
14
+ * ToS note: Baileys is an unofficial WhatsApp client. Linking a number
15
+ * carries a real ban risk — use a number you can afford to lose, and treat
16
+ * this channel as opt-in, never a default.
17
+ */
18
+ import type { WAMessage, WASocket } from "@whiskeysockets/baileys";
19
+ import type { Channel } from "../index";
20
+ import { type Baileys } from "./baileys";
21
+ import { type WaLeaseStore } from "./lease";
22
+ /** Exponential backoff: 5s, 10s, 20s ... capped at 10 minutes. Hammering
23
+ * WhatsApp every few seconds during an outage or ban aggravates ban scoring. */
24
+ export declare function reconnectDelayMs(failures: number): number;
25
+ export interface WhatsAppMessageEvent {
26
+ sessionId: string;
27
+ sock: WASocket;
28
+ message: WAMessage;
29
+ }
30
+ export interface WhatsAppChannelConfig {
31
+ /** Passphrase that encrypts auth state at rest (AES-256-GCM key material). Keep it stable across restarts and deploys — losing it means re-pairing. */
32
+ sessionSecret: string;
33
+ /** One Baileys connection per id, each a separate WhatsApp number. Defaults to `["default"]`, whose auth rows are unprefixed (a pre-multi-session deployment needs no migration). */
34
+ sessionIds?: string[];
35
+ /**
36
+ * Called for every raw inbound message on every session. This module does
37
+ * no interpretation of its own — no mention detection, no allowlists, no
38
+ * loop guard — so replies belong entirely to the handler; pair it with
39
+ * `./channels`' `decideChannelAction` and `isEffectivelyFromSelf`. A
40
+ * throwing/rejecting handler is caught and logged, never left to crash the
41
+ * socket's event loop.
42
+ */
43
+ onMessage?: (event: WhatsAppMessageEvent) => void | Promise<void>;
44
+ now?: () => number;
45
+ /** Overridable for tests; defaults to `setTimeout`. */
46
+ schedule?: (fn: () => void, ms: number) => void;
47
+ /** Overridable for tests, which fake the Baileys module instead of installing it; defaults to `loadBaileys` from `./baileys`. */
48
+ loadBaileys?: () => Promise<Baileys>;
49
+ staleMs?: number;
50
+ heartbeatMs?: number;
51
+ waitMs?: number;
52
+ }
53
+ /** A live, leased session: what `stop()` (or losing the lease) must tear down. */
54
+ export interface WaSessionHandle {
55
+ sock: {
56
+ end: (error?: Error) => void;
57
+ };
58
+ stopHeartbeat: () => void;
59
+ release: () => Promise<void>;
60
+ }
61
+ /**
62
+ * Tears down every session in `sessions`: stop renewing, close the socket,
63
+ * THEN release the lease — releasing before the socket closes could let
64
+ * another instance connect while this one is still on the wire. Sessions run
65
+ * independently (`Promise.all`); one release rejecting must not block or skip
66
+ * the rest.
67
+ */
68
+ export declare function shutdownWaSessions(sessions: Map<string, WaSessionHandle>): Promise<void>;
69
+ export interface LeaseGatedConnectDeps {
70
+ leaseStore: WaLeaseStore;
71
+ instanceId: string;
72
+ sessions: Map<string, WaSessionHandle>;
73
+ isStopped: () => boolean;
74
+ /** The actual Baileys connect — registers its own reconnect-on-close loop; this only gates its FIRST call. */
75
+ connect: (sessionId: string) => Promise<void>;
76
+ now?: () => number;
77
+ staleMs?: number;
78
+ heartbeatMs?: number;
79
+ waitMs?: number;
80
+ schedule?: (fn: () => void, ms: number) => void;
81
+ }
82
+ /**
83
+ * Acquire `sessionId`'s lease before connecting — two instances must never
84
+ * run concurrent Baileys clients for the same session. Not holding it (or a
85
+ * database blip while trying) means wait and re-check instead of connecting.
86
+ * Once held, a heartbeat renews it for as long as the session runs; if a
87
+ * heartbeat ever finds the lease gone (this instance went stale and another
88
+ * took over), the session is torn down and this re-enters the same
89
+ * wait-and-retry loop rather than staying connected alongside the new holder.
90
+ */
91
+ export declare function acquireSessionLease(sessionId: string, deps: LeaseGatedConnectDeps): Promise<void>;
92
+ /** Sender-registry name for a session — the default session stays unprefixed so single-number deployments see a plain `"whatsapp"` name. */
93
+ export declare function senderNameFor(channelName: string, sessionId: string): string;
94
+ export declare function createWhatsAppChannel(config: WhatsAppChannelConfig): Channel;
95
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAIxC,OAAO,EAAE,KAAK,OAAO,EAAmD,MAAM,WAAW,CAAC;AAC1F,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;AAKjB;gFACgF;AAChF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,uJAAuJ;IACvJ,aAAa,EAAE,MAAM,CAAC;IACtB,qLAAqL;IACrL,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,iIAAiI;IACjI,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;KAAE,CAAC;IACvC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;AAYD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,YAAY,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,8GAA8G;IAC9G,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED,4IAA4I;AAC5I,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CA8L5E"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=channel.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Encryption for persisted WhatsApp session material (AES-256-GCM).
3
+ *
4
+ * The stored auth state grants full account access, so it never touches the
5
+ * database in plaintext. The key derives from a caller-supplied secret (any
6
+ * strong passphrase, kept stable across restarts and deploys).
7
+ */
8
+ export declare function encrypt(plaintext: string, secret: string): string;
9
+ export declare function decrypt(payload: string, secret: string): string;
10
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQ/D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crypto.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/crypto.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * WhatsApp inbound: turns a raw Baileys message into a reply through
3
+ * chatter's own pipeline (`prepareChat` + `answerOnce`), gated by
4
+ * `./channels`' reply gates and loop guard. `./channel` hands every raw
5
+ * message to `config.onMessage` uninterpreted — this module is that
6
+ * interpretation.
7
+ *
8
+ * Message-shape parsing (text/caption extraction, contextInfo mentions and
9
+ * quoted-reply resolution) is genericized from the reference implementation:
10
+ * text AND contextInfo live in a DIFFERENT place per message shape — plain
11
+ * text carries both on `extendedTextMessage`, but a photo/video/document
12
+ * carries its caption on its own message type and its contextInfo there too.
13
+ * Missing that meant mentions in photo captions were invisible and group
14
+ * photo requests were silently ignored.
15
+ *
16
+ * `createWhatsAppInboundHandler` needs `ServerDependencies`
17
+ * (`client`/`store`/`prompts`), which only exists once `createServer` builds
18
+ * it — after `channels` (and therefore `onMessage`) must already be
19
+ * configured. Wire it up from `ChatterConfig.customRoutes`, which runs
20
+ * before channels start and receives the same deps:
21
+ *
22
+ * ```ts
23
+ * let handleInbound: ((event: WhatsAppMessageEvent) => Promise<void>) | undefined;
24
+ * const whatsapp = createWhatsAppChannel({
25
+ * sessionSecret: process.env.WA_SESSION_SECRET as string,
26
+ * onMessage: (event) => handleInbound?.(event),
27
+ * });
28
+ *
29
+ * await createServer({
30
+ * ...,
31
+ * channels: [whatsapp],
32
+ * customRoutes: async (app, deps) => {
33
+ * handleInbound = createWhatsAppInboundHandler({
34
+ * client: deps.client,
35
+ * store: deps.store,
36
+ * prompts: deps.prompts,
37
+ * answerFn: deps.config.answerFn,
38
+ * bucketsFor: deps.config.bucketsFor,
39
+ * registry: new Map(),
40
+ * });
41
+ * },
42
+ * });
43
+ * ```
44
+ */
45
+ import type { WAMessage, WASocket } from "@whiskeysockets/baileys";
46
+ import type OpenAI from "openai";
47
+ import { type AnswerFn } from "../../core/answer";
48
+ import { type BucketsFor } from "../../core/buckets";
49
+ import type { PromptLoader } from "../../core/prompts";
50
+ import type { VectorStore } from "../../core/retrieval";
51
+ import { type SessionIdentityRegistry } from "../gates";
52
+ import type { WhatsAppMessageEvent } from "./channel";
53
+ export declare function isGroupJid(jid: string): boolean;
54
+ /** "447700900123:17@s.whatsapp.net" (device-suffixed) and "447700900123@s.whatsapp.net" match. */
55
+ export declare function jidsMatch(a: string | undefined, b: string | undefined): boolean;
56
+ export declare function jidToPhoneNumber(jid: string): string;
57
+ /**
58
+ * Resolve the sender's real phone number. Modern WhatsApp identifies group
59
+ * senders (and some DM chats) by LID (anonymized `"12345@lid"`), which
60
+ * carries no relationship to the phone number — treating it as one silently
61
+ * keys per-person state on garbage. Prefer the `*Pn` fields Baileys attaches
62
+ * when available, then fall back to the socket's own LID mapping store.
63
+ *
64
+ * The returned string is a stable identity key, not guaranteed to be a
65
+ * dialable phone number: an unresolved LID returns an explicit `"lid:<id>"`
66
+ * marker rather than a phone-shaped fabrication (`jidToPhoneNumber("12345@lid")`
67
+ * would otherwise yield `"+12345"`, indistinguishable from a real number) —
68
+ * callers keying security or spend decisions off `sender` (`bucketsFor`,
69
+ * usage metering) must not mistake a LID numeral for a phone number.
70
+ */
71
+ export declare function senderPhoneFor(sock: WASocket, message: WAMessage, chatJid: string): Promise<string>;
72
+ export interface WaMessageContext {
73
+ mentionedJids: string[];
74
+ quotedParticipantJid?: string;
75
+ }
76
+ /**
77
+ * contextInfo (mentions, quoted message) lives in a DIFFERENT place per
78
+ * message shape: a plain text message carries it on `extendedTextMessage`,
79
+ * but a photo/video/document message carries it on its own type.
80
+ */
81
+ export declare function messageContext(message: WAMessage): WaMessageContext;
82
+ /** Text/caption across every shape that can carry one — plain text, a reply, or a photo/video/document caption. */
83
+ export declare function extractText(message: WAMessage): string;
84
+ export interface WhatsAppInboundConfig {
85
+ client: OpenAI;
86
+ store: VectorStore;
87
+ prompts: PromptLoader;
88
+ answerFn?: AnswerFn;
89
+ bucketsFor?: BucketsFor;
90
+ model?: string;
91
+ /**
92
+ * Own identities per session — share ONE registry across every session
93
+ * this process runs so a message from one session's own number never
94
+ * looks like a stranger to another (the loop-guard case a single
95
+ * session's `fromMe` flag can't catch). Kept in sync from every message,
96
+ * not just at connection open, because a session's LID identity can
97
+ * populate late.
98
+ */
99
+ registry: SessionIdentityRegistry;
100
+ /** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply regardless. */
101
+ allowedChats?: string[];
102
+ muteRegex?: RegExp;
103
+ unmuteRegex?: RegExp;
104
+ /** Neutral, overridable acknowledgements — this module ships no bot personality; unset = silent mute/unmute. */
105
+ muteReply?: string;
106
+ unmuteReply?: string;
107
+ dmRateLimit?: {
108
+ max: number;
109
+ windowMs: number;
110
+ };
111
+ groupRateLimit?: {
112
+ max: number;
113
+ windowMs: number;
114
+ };
115
+ /** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. */
116
+ channelHint?: string;
117
+ /**
118
+ * Optional persona layer for a sender/text pair — the resolution
119
+ * mechanism (registry, windowed rolls, prompt files) lives in its own
120
+ * module; this only plugs the result into `prepareChat`'s `personaLayer`.
121
+ * A throw/rejection is treated as "no persona" for that turn.
122
+ */
123
+ personaResolver?: (ctx: {
124
+ senderPhone: string;
125
+ text: string;
126
+ }) => string | undefined | Promise<string | undefined>;
127
+ now?: () => number;
128
+ }
129
+ /** Builds the `onMessage` handler for `createWhatsAppChannel` (see `./channel`) — see the module docstring for how to wire `ServerDependencies` into it. */
130
+ export declare function createWhatsAppInboundHandler(config: WhatsAppInboundConfig): (event: WhatsAppMessageEvent) => Promise<void>;
131
+ //# sourceMappingURL=inbound.d.ts.map