@diegoaltoworks/chatter 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/wa-pair.js +320 -0
- package/dist/channels/whatsapp/authState.d.ts +39 -0
- package/dist/channels/whatsapp/authState.d.ts.map +1 -0
- package/dist/channels/whatsapp/authState.test.d.ts +2 -0
- package/dist/channels/whatsapp/authState.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/baileys.d.ts +21 -0
- package/dist/channels/whatsapp/baileys.d.ts.map +1 -0
- package/dist/channels/whatsapp/baileys.test.d.ts +2 -0
- package/dist/channels/whatsapp/baileys.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/channel.d.ts +95 -0
- package/dist/channels/whatsapp/channel.d.ts.map +1 -0
- package/dist/channels/whatsapp/channel.test.d.ts +2 -0
- package/dist/channels/whatsapp/channel.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/crypto.d.ts +10 -0
- package/dist/channels/whatsapp/crypto.d.ts.map +1 -0
- package/dist/channels/whatsapp/crypto.test.d.ts +2 -0
- package/dist/channels/whatsapp/crypto.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/index.d.ts +33 -0
- package/dist/channels/whatsapp/index.d.ts.map +1 -0
- package/dist/channels/whatsapp/index.js +478 -0
- package/dist/channels/whatsapp/index.mjs +428 -0
- package/dist/channels/whatsapp/lease.d.ts +42 -0
- package/dist/channels/whatsapp/lease.d.ts.map +1 -0
- package/dist/channels/whatsapp/lease.test.d.ts +2 -0
- package/dist/channels/whatsapp/lease.test.d.ts.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/crypto.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp web-client transport — a {@link Channel} built on Baileys
|
|
3
|
+
* (`@whiskeysockets/baileys`, an OPTIONAL peer dependency; nothing in core or
|
|
4
|
+
* `./channels` imports it, so `bun install` and every core import work
|
|
5
|
+
* without it).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { createWhatsAppChannel } from "@diegoaltoworks/chatter/whatsapp";
|
|
9
|
+
*
|
|
10
|
+
* const channel = createWhatsAppChannel({
|
|
11
|
+
* sessionSecret: process.env.WA_SESSION_SECRET as string,
|
|
12
|
+
* sessionIds: (process.env.WA_SESSION_IDS ?? "default").split(","),
|
|
13
|
+
* onMessage: async ({ sessionId, sock, message }) => {
|
|
14
|
+
* // apply ./channels' decideChannelAction, resolve the reply, sock.sendMessage(...)
|
|
15
|
+
* },
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* await createServer({ ..., channels: [channel] });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ToS note: Baileys is an unofficial WhatsApp client. A linked number can be
|
|
22
|
+
* banned — use one you can afford to lose, and keep this channel opt-in.
|
|
23
|
+
*
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
export type { AuthStateRuntime, TursoAuthStateResult } from "./authState";
|
|
27
|
+
export { useTursoAuthState } from "./authState";
|
|
28
|
+
export type { Baileys } from "./baileys";
|
|
29
|
+
export { loadBaileys } from "./baileys";
|
|
30
|
+
export { acquireSessionLease, createWhatsAppChannel, type LeaseGatedConnectDeps, reconnectDelayMs, senderNameFor, shutdownWaSessions, type WaSessionHandle, type WhatsAppChannelConfig, type WhatsAppMessageEvent, } from "./channel";
|
|
31
|
+
export { decrypt, encrypt } from "./crypto";
|
|
32
|
+
export { canAcquireLease, createTursoWaLeaseStore, LEASE_HEARTBEAT_MS, LEASE_STALE_MS, LEASE_WAIT_MS, type WaLeaseRow, type WaLeaseStore, } from "./lease";
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|