@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,810 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/channels/whatsapp/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ LEASE_HEARTBEAT_MS: () => LEASE_HEARTBEAT_MS,
34
+ LEASE_STALE_MS: () => LEASE_STALE_MS,
35
+ LEASE_WAIT_MS: () => LEASE_WAIT_MS,
36
+ acquireSessionLease: () => acquireSessionLease,
37
+ canAcquireLease: () => canAcquireLease,
38
+ createTursoWaLeaseStore: () => createTursoWaLeaseStore,
39
+ createWhatsAppChannel: () => createWhatsAppChannel,
40
+ createWhatsAppInboundHandler: () => createWhatsAppInboundHandler,
41
+ decrypt: () => decrypt,
42
+ encrypt: () => encrypt,
43
+ extractText: () => extractText,
44
+ isGroupJid: () => isGroupJid,
45
+ jidToPhoneNumber: () => jidToPhoneNumber,
46
+ jidsMatch: () => jidsMatch,
47
+ loadBaileys: () => loadBaileys,
48
+ messageContext: () => messageContext,
49
+ reconnectDelayMs: () => reconnectDelayMs,
50
+ senderNameFor: () => senderNameFor,
51
+ senderPhoneFor: () => senderPhoneFor,
52
+ shutdownWaSessions: () => shutdownWaSessions,
53
+ useTursoAuthState: () => useTursoAuthState
54
+ });
55
+ module.exports = __toCommonJS(index_exports);
56
+
57
+ // src/channels/whatsapp/crypto.ts
58
+ var import_node_crypto = require("node:crypto");
59
+ var IV_LENGTH = 12;
60
+ var TAG_LENGTH = 16;
61
+ function deriveKey(secret) {
62
+ return (0, import_node_crypto.createHash)("sha256").update(secret, "utf-8").digest();
63
+ }
64
+ function encrypt(plaintext, secret) {
65
+ const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
66
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", deriveKey(secret), iv);
67
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
68
+ return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
69
+ }
70
+ function decrypt(payload, secret) {
71
+ const raw = Buffer.from(payload, "base64");
72
+ const iv = raw.subarray(0, IV_LENGTH);
73
+ const tag = raw.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
74
+ const ciphertext = raw.subarray(IV_LENGTH + TAG_LENGTH);
75
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveKey(secret), iv);
76
+ decipher.setAuthTag(tag);
77
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
78
+ }
79
+
80
+ // src/channels/whatsapp/authState.ts
81
+ var schemaReady = /* @__PURE__ */ new WeakMap();
82
+ function ensureAuthSchema(client) {
83
+ let ready = schemaReady.get(client);
84
+ if (!ready) {
85
+ ready = client.execute("CREATE TABLE IF NOT EXISTS wa_auth (id TEXT PRIMARY KEY, value TEXT NOT NULL)").catch((error) => {
86
+ schemaReady.delete(client);
87
+ throw error;
88
+ });
89
+ schemaReady.set(client, ready);
90
+ }
91
+ return ready;
92
+ }
93
+ async function useTursoAuthState(db, secret, sessionId, runtime) {
94
+ await ensureAuthSchema(db);
95
+ const rowId = (id) => sessionId === "default" ? id : `s:${sessionId}/${id}`;
96
+ async function read(id) {
97
+ const result = await db.execute({
98
+ sql: "SELECT value FROM wa_auth WHERE id = ?",
99
+ args: [rowId(id)]
100
+ });
101
+ const row = result.rows[0];
102
+ if (!row) return null;
103
+ return JSON.parse(decrypt(row.value, secret), runtime.bufferJSON.reviver);
104
+ }
105
+ async function write(id, value) {
106
+ const payload = encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
107
+ await db.execute({
108
+ sql: "INSERT OR REPLACE INTO wa_auth (id, value) VALUES (?, ?)",
109
+ args: [rowId(id), payload]
110
+ });
111
+ }
112
+ async function remove(id) {
113
+ await db.execute({ sql: "DELETE FROM wa_auth WHERE id = ?", args: [rowId(id)] });
114
+ }
115
+ const creds = await read("creds") ?? runtime.initAuthCreds();
116
+ return {
117
+ state: {
118
+ creds,
119
+ keys: {
120
+ get: async (type, ids) => {
121
+ const data = {};
122
+ await Promise.all(
123
+ ids.map(async (id) => {
124
+ let value = await read(`${type}-${id}`);
125
+ if (type === "app-state-sync-key" && value) {
126
+ value = runtime.appStateSyncKeyFromObject(
127
+ value
128
+ );
129
+ }
130
+ if (value) data[id] = value;
131
+ })
132
+ );
133
+ return data;
134
+ },
135
+ set: async (data) => {
136
+ const jobs = [];
137
+ for (const category of Object.keys(data)) {
138
+ const entries = data[category];
139
+ if (!entries) continue;
140
+ for (const id of Object.keys(entries)) {
141
+ const value = entries[id];
142
+ jobs.push(value ? write(`${category}-${id}`, value) : remove(`${category}-${id}`));
143
+ }
144
+ }
145
+ await Promise.all(jobs);
146
+ }
147
+ }
148
+ },
149
+ saveCreds: () => write("creds", creds),
150
+ clear: async () => {
151
+ if (sessionId === "default") {
152
+ await db.execute("DELETE FROM wa_auth WHERE id NOT LIKE 's:%'");
153
+ } else {
154
+ await db.execute({
155
+ sql: "DELETE FROM wa_auth WHERE id LIKE ?",
156
+ args: [`s:${sessionId}/%`]
157
+ });
158
+ }
159
+ }
160
+ };
161
+ }
162
+
163
+ // src/channels/whatsapp/baileys.ts
164
+ function wrapMissingBaileysError(cause) {
165
+ return new Error(
166
+ "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.",
167
+ { cause }
168
+ );
169
+ }
170
+ var cached;
171
+ function loadBaileys() {
172
+ if (!cached) {
173
+ cached = import("@whiskeysockets/baileys").catch((error) => {
174
+ cached = void 0;
175
+ throw wrapMissingBaileysError(error);
176
+ });
177
+ }
178
+ return cached;
179
+ }
180
+ function silentLogger() {
181
+ const noop = () => void 0;
182
+ const logger = {
183
+ level: "silent",
184
+ trace: noop,
185
+ debug: noop,
186
+ info: noop,
187
+ warn: noop,
188
+ error: noop,
189
+ fatal: noop
190
+ };
191
+ logger.child = () => logger;
192
+ return logger;
193
+ }
194
+
195
+ // src/channels/whatsapp/lease.ts
196
+ var LEASE_STALE_MS = 9e4;
197
+ var LEASE_HEARTBEAT_MS = 3e4;
198
+ var LEASE_WAIT_MS = 3e4;
199
+ function canAcquireLease(existing, instanceId, now, staleMs = LEASE_STALE_MS) {
200
+ if (!existing) return true;
201
+ if (existing.instanceId === instanceId) return true;
202
+ return now - existing.heartbeatAt >= staleMs;
203
+ }
204
+ var schemaReady2 = /* @__PURE__ */ new WeakMap();
205
+ function ensureWaLeaseSchema(client) {
206
+ let ready = schemaReady2.get(client);
207
+ if (!ready) {
208
+ ready = client.execute(
209
+ `CREATE TABLE IF NOT EXISTS wa_lease (
210
+ session_id TEXT PRIMARY KEY,
211
+ instance_id TEXT NOT NULL,
212
+ heartbeat_at INTEGER NOT NULL
213
+ )`
214
+ ).catch((error) => {
215
+ schemaReady2.delete(client);
216
+ throw error;
217
+ });
218
+ schemaReady2.set(client, ready);
219
+ }
220
+ return ready;
221
+ }
222
+ function createTursoWaLeaseStore(client) {
223
+ return {
224
+ async tryAcquire(sessionId, instanceId, now, staleMs) {
225
+ await ensureWaLeaseSchema(client);
226
+ const result = await client.execute({
227
+ sql: `INSERT INTO wa_lease (session_id, instance_id, heartbeat_at) VALUES (?, ?, ?)
228
+ ON CONFLICT(session_id) DO UPDATE SET
229
+ instance_id = excluded.instance_id,
230
+ heartbeat_at = excluded.heartbeat_at
231
+ WHERE wa_lease.instance_id = excluded.instance_id
232
+ OR wa_lease.heartbeat_at <= excluded.heartbeat_at - ?
233
+ RETURNING instance_id`,
234
+ args: [sessionId, instanceId, now, staleMs]
235
+ });
236
+ const row = result.rows[0];
237
+ return Boolean(row) && row.instance_id === instanceId;
238
+ },
239
+ async release(sessionId, instanceId) {
240
+ await ensureWaLeaseSchema(client);
241
+ await client.execute({
242
+ sql: "DELETE FROM wa_lease WHERE session_id = ? AND instance_id = ?",
243
+ args: [sessionId, instanceId]
244
+ });
245
+ }
246
+ };
247
+ }
248
+
249
+ // src/channels/whatsapp/channel.ts
250
+ var RECONNECT_DELAY_MS = 5e3;
251
+ var RECONNECT_MAX_DELAY_MS = 10 * 60 * 1e3;
252
+ function reconnectDelayMs(failures) {
253
+ return Math.min(RECONNECT_DELAY_MS * 2 ** Math.min(failures, 20), RECONNECT_MAX_DELAY_MS);
254
+ }
255
+ async function shutdownWaSessions(sessions) {
256
+ await Promise.all(
257
+ [...sessions.values()].map(async (handle) => {
258
+ handle.stopHeartbeat();
259
+ handle.sock.end(void 0);
260
+ await handle.release().catch(() => void 0);
261
+ })
262
+ );
263
+ }
264
+ async function loseSession(sessionId, sessions) {
265
+ const handle = sessions.get(sessionId);
266
+ if (!handle) return;
267
+ sessions.delete(sessionId);
268
+ await shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]]));
269
+ }
270
+ async function acquireSessionLease(sessionId, deps) {
271
+ const { leaseStore, instanceId, sessions, isStopped, connect } = deps;
272
+ const now = deps.now ?? Date.now;
273
+ const staleMs = deps.staleMs ?? LEASE_STALE_MS;
274
+ const heartbeatMs = deps.heartbeatMs ?? LEASE_HEARTBEAT_MS;
275
+ const waitMs = deps.waitMs ?? LEASE_WAIT_MS;
276
+ const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
277
+ if (isStopped()) return;
278
+ const retry = () => {
279
+ schedule(
280
+ () => void acquireSessionLease(sessionId, deps).catch(
281
+ (error) => console.error(`WhatsApp[${sessionId}]: lease re-check failed:`, error)
282
+ ),
283
+ waitMs
284
+ );
285
+ };
286
+ let acquired;
287
+ try {
288
+ acquired = await leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs);
289
+ } catch (error) {
290
+ console.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
291
+ retry();
292
+ return;
293
+ }
294
+ if (!acquired) {
295
+ console.warn(`WhatsApp[${sessionId}]: lease held by another instance, re-checking later`);
296
+ retry();
297
+ return;
298
+ }
299
+ const heartbeat = setInterval(() => {
300
+ void leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs).then((stillHeld) => {
301
+ if (stillHeld || isStopped()) return;
302
+ console.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
303
+ void loseSession(sessionId, sessions).then(retry);
304
+ }).catch((error) => console.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
305
+ }, heartbeatMs);
306
+ if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
307
+ sessions.set(sessionId, {
308
+ sock: { end: () => void 0 },
309
+ stopHeartbeat: () => clearInterval(heartbeat),
310
+ release: () => leaseStore.release(sessionId, instanceId)
311
+ });
312
+ try {
313
+ await connect(sessionId);
314
+ } catch (error) {
315
+ console.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
316
+ await loseSession(sessionId, sessions);
317
+ retry();
318
+ }
319
+ }
320
+ function senderNameFor(channelName, sessionId) {
321
+ return sessionId === "default" ? channelName : `${channelName}:${sessionId}`;
322
+ }
323
+ function createWhatsAppChannel(config) {
324
+ const channelName = "whatsapp";
325
+ const sessionIds = config.sessionIds && config.sessionIds.length > 0 ? config.sessionIds : ["default"];
326
+ const now = config.now ?? Date.now;
327
+ const schedule = config.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
328
+ const staleMs = config.staleMs ?? LEASE_STALE_MS;
329
+ const heartbeatMs = config.heartbeatMs ?? LEASE_HEARTBEAT_MS;
330
+ const waitMs = config.waitMs ?? LEASE_WAIT_MS;
331
+ const loadBaileys2 = config.loadBaileys ?? loadBaileys;
332
+ let stopped = false;
333
+ const instanceId = crypto.randomUUID();
334
+ const sessions = /* @__PURE__ */ new Map();
335
+ const failureCounts = /* @__PURE__ */ new Map();
336
+ const registeredSenderNames = /* @__PURE__ */ new Map();
337
+ let senders;
338
+ return {
339
+ name: channelName,
340
+ async start(deps) {
341
+ const db = deps.db;
342
+ const leaseStore = createTursoWaLeaseStore(db);
343
+ senders = deps.senders;
344
+ await loadBaileys2();
345
+ async function connect(sessionId) {
346
+ if (stopped || !sessions.has(sessionId)) return;
347
+ const baileys = await loadBaileys2();
348
+ const runtime = {
349
+ bufferJSON: baileys.BufferJSON,
350
+ initAuthCreds: baileys.initAuthCreds,
351
+ appStateSyncKeyFromObject: (value) => baileys.proto.Message.AppStateSyncKeyData.fromObject(value)
352
+ };
353
+ const { state, saveCreds } = await useTursoAuthState(
354
+ db,
355
+ config.sessionSecret,
356
+ sessionId,
357
+ runtime
358
+ );
359
+ if (!state.creds.registered) {
360
+ console.warn(
361
+ `WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
362
+ );
363
+ schedule(() => void connect(sessionId).catch(() => void 0), 6e4);
364
+ return;
365
+ }
366
+ const { version } = await baileys.fetchLatestBaileysVersion();
367
+ const logger = silentLogger();
368
+ const sock = baileys.makeWASocket({
369
+ version,
370
+ auth: {
371
+ creds: state.creds,
372
+ // biome-ignore lint/suspicious/noExplicitAny: silentLogger is structurally pino-compatible; Baileys' logger type is not exported
373
+ keys: baileys.makeCacheableSignalKeyStore(state.keys, logger)
374
+ },
375
+ // biome-ignore lint/suspicious/noExplicitAny: silentLogger is structurally pino-compatible; Baileys' logger type is not exported
376
+ logger,
377
+ markOnlineOnConnect: false,
378
+ syncFullHistory: false
379
+ });
380
+ const leaseHandle = sessions.get(sessionId);
381
+ if (leaseHandle) leaseHandle.sock = sock;
382
+ sock.ev.on(
383
+ "creds.update",
384
+ () => void saveCreds().catch(
385
+ (error) => console.warn(`WhatsApp[${sessionId}]: creds save failed:`, error)
386
+ )
387
+ );
388
+ sock.ev.on("connection.update", (update) => {
389
+ const { connection, lastDisconnect } = update;
390
+ if (connection === "open") {
391
+ failureCounts.set(sessionId, 0);
392
+ console.log(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
393
+ const senderName = senderNameFor(channelName, sessionId);
394
+ const sender = {
395
+ sendText: async (chatId, text) => {
396
+ await sock.sendMessage(chatId, { text });
397
+ }
398
+ };
399
+ deps.senders.register(senderName, sender);
400
+ registeredSenderNames.set(sessionId, senderName);
401
+ }
402
+ if (connection === "close") {
403
+ const senderName = registeredSenderNames.get(sessionId);
404
+ if (senderName) {
405
+ deps.senders.unregister(senderName);
406
+ registeredSenderNames.delete(sessionId);
407
+ }
408
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
409
+ if (statusCode === baileys.DisconnectReason.loggedOut) {
410
+ console.error(
411
+ `WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
412
+ );
413
+ void loseSession(sessionId, sessions);
414
+ return;
415
+ }
416
+ if (!stopped && sessions.has(sessionId)) {
417
+ const delay = reconnectDelayMs(failureCounts.get(sessionId) ?? 0);
418
+ failureCounts.set(sessionId, (failureCounts.get(sessionId) ?? 0) + 1);
419
+ console.warn(
420
+ `WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
421
+ );
422
+ schedule(
423
+ () => void connect(sessionId).catch(
424
+ (error) => console.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
425
+ ),
426
+ delay
427
+ );
428
+ }
429
+ }
430
+ });
431
+ if (config.onMessage) {
432
+ sock.ev.on("messages.upsert", async ({ messages, type }) => {
433
+ if (type !== "notify") return;
434
+ for (const message of messages) {
435
+ try {
436
+ await config.onMessage?.({ sessionId, sock, message });
437
+ } catch (error) {
438
+ console.warn(`WhatsApp[${sessionId}]: onMessage handler failed:`, error);
439
+ }
440
+ }
441
+ });
442
+ }
443
+ }
444
+ for (const sessionId of sessionIds) {
445
+ void acquireSessionLease(sessionId, {
446
+ leaseStore,
447
+ instanceId,
448
+ sessions,
449
+ isStopped: () => stopped,
450
+ connect,
451
+ now,
452
+ staleMs,
453
+ heartbeatMs,
454
+ waitMs,
455
+ schedule
456
+ }).catch((error) => console.error(`WhatsApp[${sessionId}]: failed to start:`, error));
457
+ }
458
+ },
459
+ async stop() {
460
+ stopped = true;
461
+ for (const senderName of registeredSenderNames.values()) {
462
+ senders?.unregister(senderName);
463
+ }
464
+ registeredSenderNames.clear();
465
+ await shutdownWaSessions(sessions);
466
+ }
467
+ };
468
+ }
469
+
470
+ // src/core/guardrails.ts
471
+ function detectLeakage(text) {
472
+ const flags = [
473
+ /system prompt/i,
474
+ /hidden instruction/i,
475
+ /here (are|is) (my|the) rules/i,
476
+ /BEGIN SYSTEM PROMPT/i,
477
+ /tool instructions/i
478
+ ];
479
+ return flags.some((rx) => rx.test(text));
480
+ }
481
+ function scrubOutput(text) {
482
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
483
+ }
484
+ function guardOutput(text) {
485
+ if (detectLeakage(text)) {
486
+ return "Sorry, I can't share internal instructions. How else can I help?";
487
+ }
488
+ return scrubOutput(text);
489
+ }
490
+
491
+ // src/core/llm.ts
492
+ var DEFAULT_MODEL = "gpt-4o";
493
+ async function completeOnce({
494
+ client,
495
+ system,
496
+ messages,
497
+ temperature = 0.2,
498
+ model = DEFAULT_MODEL
499
+ }) {
500
+ const res = await client.chat.completions.create({
501
+ model,
502
+ temperature,
503
+ messages: [{ role: "system", content: system }, ...messages]
504
+ });
505
+ const text = res.choices[0]?.message?.content ?? "";
506
+ return {
507
+ content: guardOutput(text),
508
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
509
+ };
510
+ }
511
+
512
+ // src/core/answer.ts
513
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
514
+ async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
515
+ const result = await answerFn({ system, messages, mode, sender });
516
+ const raw = typeof result === "string" ? result : result?.content;
517
+ const usage = typeof result === "string" ? void 0 : result?.usage;
518
+ return {
519
+ content: guardOutput(typeof raw === "string" ? raw : ""),
520
+ usage: usage ?? noUsage()
521
+ };
522
+ }
523
+ async function answerOnce({
524
+ answerFn,
525
+ client,
526
+ system,
527
+ messages,
528
+ mode,
529
+ sender,
530
+ temperature,
531
+ model
532
+ }) {
533
+ if (answerFn) {
534
+ return runAnswerFn(answerFn, { system, messages, mode, sender });
535
+ }
536
+ return completeOnce({ client, system, messages, temperature, model });
537
+ }
538
+
539
+ // src/core/buckets.ts
540
+ function defaultBuckets(mode) {
541
+ return ["base", mode];
542
+ }
543
+ function isIdentified(sender) {
544
+ return typeof sender === "string" && sender.trim().length > 0;
545
+ }
546
+ function normalize(buckets) {
547
+ const out = [];
548
+ for (const raw of buckets) {
549
+ if (typeof raw !== "string") continue;
550
+ const bucket = raw.trim();
551
+ if (bucket && !out.includes(bucket)) out.push(bucket);
552
+ }
553
+ return out;
554
+ }
555
+ async function resolveBuckets({
556
+ mode,
557
+ sender,
558
+ bucketsFor
559
+ }) {
560
+ if (!bucketsFor) return void 0;
561
+ const identified = isIdentified(sender);
562
+ const requested = await bucketsFor(identified ? { mode, sender } : { mode });
563
+ if (!Array.isArray(requested)) return void 0;
564
+ const cleaned = normalize(requested);
565
+ if (identified) return cleaned;
566
+ const ceiling = new Set(defaultBuckets(mode));
567
+ return cleaned.filter((bucket) => ceiling.has(bucket));
568
+ }
569
+
570
+ // src/core/pipeline.ts
571
+ var MODE_SETTINGS = {
572
+ public: { topK: 6, label: "Context" },
573
+ private: { topK: 8, label: "Internal Context" }
574
+ };
575
+ async function prepareChat({
576
+ store,
577
+ prompts,
578
+ mode,
579
+ messages,
580
+ personaLayer,
581
+ channelHint,
582
+ buckets
583
+ }) {
584
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
585
+ if (!lastUserMsg) {
586
+ throw new Error("no user message found in conversation");
587
+ }
588
+ const { topK, label } = MODE_SETTINGS[mode];
589
+ const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
590
+ const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
591
+ const hint = channelHint?.trim();
592
+ const system = [
593
+ prompts.baseSystemRules,
594
+ persona,
595
+ ...hint ? [hint] : [],
596
+ `${label}:
597
+ ${ctx.join("\n\n")}`
598
+ ].join("\n\n");
599
+ return { system, messages };
600
+ }
601
+
602
+ // src/channels/gates.ts
603
+ function matches(re, text) {
604
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
605
+ }
606
+ function decideChannelAction(msg, config) {
607
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
608
+ if (!msg.isDirectMessage) {
609
+ if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
610
+ return "ignore";
611
+ }
612
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
613
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
614
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
615
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
616
+ }
617
+ return "reply";
618
+ }
619
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
620
+ const log = /* @__PURE__ */ new Map();
621
+ return function allow(key) {
622
+ const cutoff = now() - windowMs;
623
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
624
+ if (entries.length >= maxPerWindow) {
625
+ if (entries.length > 0) log.set(key, entries);
626
+ else log.delete(key);
627
+ return false;
628
+ }
629
+ entries.push(now());
630
+ log.set(key, entries);
631
+ return true;
632
+ };
633
+ }
634
+ function underReplyRateLimit(msg, limiters) {
635
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
636
+ }
637
+ function isFromAnySession(id, registry) {
638
+ if (!id) return false;
639
+ for (const ids of registry.values()) {
640
+ if (ids.includes(id)) return true;
641
+ }
642
+ return false;
643
+ }
644
+ function isEffectivelyFromSelf(msg, registry) {
645
+ return msg.fromBot || isFromAnySession(msg.senderId, registry);
646
+ }
647
+
648
+ // src/channels/whatsapp/inbound.ts
649
+ function isGroupJid(jid) {
650
+ return jid.endsWith("@g.us");
651
+ }
652
+ function jidsMatch(a, b) {
653
+ if (!a || !b) return false;
654
+ const bare = (jid) => jid.split("@")[0]?.split(":")[0] ?? jid;
655
+ return bare(a) === bare(b);
656
+ }
657
+ function jidToPhoneNumber(jid) {
658
+ const bare = jid.split("@")[0]?.split(":")[0] ?? "";
659
+ return bare.startsWith("+") ? bare : `+${bare}`;
660
+ }
661
+ function normalizeJid(jid) {
662
+ const [user = "", domain] = jid.split("@");
663
+ const bareUser = user.split(":")[0] ?? user;
664
+ return domain ? `${bareUser}@${domain}` : bareUser;
665
+ }
666
+ async function senderPhoneFor(sock, message, chatJid) {
667
+ const key = message.key;
668
+ let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
669
+ (candidate) => typeof candidate === "string" && candidate.length > 0
670
+ ) ?? chatJid;
671
+ if (jid.endsWith("@lid")) {
672
+ try {
673
+ const mapped = await sock.signalRepository?.lidMapping?.getPNForLID?.(jid);
674
+ if (mapped) jid = mapped;
675
+ } catch {
676
+ }
677
+ }
678
+ return jid.endsWith("@lid") ? `lid:${jid.split("@")[0]?.split(":")[0] ?? jid}` : jidToPhoneNumber(jid);
679
+ }
680
+ function messageContext(message) {
681
+ const content = message.message;
682
+ const contextInfo = content?.extendedTextMessage?.contextInfo ?? content?.imageMessage?.contextInfo ?? content?.videoMessage?.contextInfo ?? content?.documentMessage?.contextInfo;
683
+ return {
684
+ mentionedJids: contextInfo?.mentionedJid ?? [],
685
+ quotedParticipantJid: contextInfo?.participant ?? void 0
686
+ };
687
+ }
688
+ function extractText(message) {
689
+ const content = message.message;
690
+ if (!content) return "";
691
+ return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
692
+ }
693
+ var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
694
+ var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
695
+ async function resolvePersonaLayer(config, ctx) {
696
+ if (!config.personaResolver) return void 0;
697
+ try {
698
+ return await config.personaResolver(ctx);
699
+ } catch {
700
+ return void 0;
701
+ }
702
+ }
703
+ function createWhatsAppInboundHandler(config) {
704
+ const mutedChats = /* @__PURE__ */ new Set();
705
+ const now = config.now ?? Date.now;
706
+ const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
707
+ const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
708
+ const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
709
+ const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
710
+ return async function handleWhatsAppMessage(event) {
711
+ const { sessionId, sock, message } = event;
712
+ try {
713
+ const chatId = message.key.remoteJid ?? "";
714
+ if (!chatId || chatId === "status@broadcast") return;
715
+ const text = extractText(message);
716
+ const context = messageContext(message);
717
+ const senderId = message.key.participant ?? chatId;
718
+ const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
719
+ config.registry.set(sessionId, ownIds);
720
+ const fromBot = isEffectivelyFromSelf(
721
+ { fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
722
+ config.registry
723
+ );
724
+ const msg = {
725
+ chatId,
726
+ senderId,
727
+ text,
728
+ isDirectMessage: !isGroupJid(chatId),
729
+ mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
730
+ isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
731
+ fromBot
732
+ };
733
+ const action = decideChannelAction(msg, {
734
+ allowedChats: config.allowedChats ?? [],
735
+ mutedChats,
736
+ muteRegex: config.muteRegex,
737
+ unmuteRegex: config.unmuteRegex
738
+ });
739
+ if (action === "mute") {
740
+ mutedChats.add(chatId);
741
+ if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
742
+ return;
743
+ }
744
+ if (action === "unmute") {
745
+ mutedChats.delete(chatId);
746
+ if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
747
+ return;
748
+ }
749
+ if (action !== "reply") return;
750
+ if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
751
+ return;
752
+ }
753
+ const senderPhone = await senderPhoneFor(sock, message, chatId);
754
+ const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
755
+ const buckets = await resolveBuckets({
756
+ mode: "public",
757
+ sender: senderPhone,
758
+ bucketsFor: config.bucketsFor
759
+ });
760
+ const messages = [{ role: "user", content: text }];
761
+ const { system } = await prepareChat({
762
+ store: config.store,
763
+ prompts: config.prompts,
764
+ mode: "public",
765
+ messages,
766
+ personaLayer,
767
+ channelHint: config.channelHint,
768
+ buckets
769
+ });
770
+ const { content } = await answerOnce({
771
+ answerFn: config.answerFn,
772
+ client: config.client,
773
+ system,
774
+ messages,
775
+ mode: "public",
776
+ sender: senderPhone,
777
+ model: config.model
778
+ });
779
+ if (content) {
780
+ await sock.sendMessage(chatId, { text: content }, { quoted: message });
781
+ }
782
+ } catch (error) {
783
+ console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
784
+ }
785
+ };
786
+ }
787
+ // Annotate the CommonJS export names for ESM import in node:
788
+ 0 && (module.exports = {
789
+ LEASE_HEARTBEAT_MS,
790
+ LEASE_STALE_MS,
791
+ LEASE_WAIT_MS,
792
+ acquireSessionLease,
793
+ canAcquireLease,
794
+ createTursoWaLeaseStore,
795
+ createWhatsAppChannel,
796
+ createWhatsAppInboundHandler,
797
+ decrypt,
798
+ encrypt,
799
+ extractText,
800
+ isGroupJid,
801
+ jidToPhoneNumber,
802
+ jidsMatch,
803
+ loadBaileys,
804
+ messageContext,
805
+ reconnectDelayMs,
806
+ senderNameFor,
807
+ senderPhoneFor,
808
+ shutdownWaSessions,
809
+ useTursoAuthState
810
+ });