@diegoaltoworks/chatter 0.45.0 → 0.48.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 (120) hide show
  1. package/README.md +19 -5
  2. package/dist/auth/apikeys.d.ts.map +1 -1
  3. package/dist/auth/secretStrength.d.ts +8 -0
  4. package/dist/auth/secretStrength.d.ts.map +1 -0
  5. package/dist/bin/create-apikey.js +22 -3
  6. package/dist/bin/wa-pair.js +59 -11
  7. package/dist/channels/gates.d.ts +9 -0
  8. package/dist/channels/gates.d.ts.map +1 -1
  9. package/dist/channels/index.d.ts +1 -0
  10. package/dist/channels/index.d.ts.map +1 -1
  11. package/dist/channels/index.js +252 -8
  12. package/dist/channels/index.mjs +250 -8
  13. package/dist/channels/pipeline.d.ts +106 -0
  14. package/dist/channels/pipeline.d.ts.map +1 -0
  15. package/dist/channels/senders.d.ts +2 -1
  16. package/dist/channels/senders.d.ts.map +1 -1
  17. package/dist/channels/whatsapp/channel.d.ts +4 -0
  18. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  19. package/dist/channels/whatsapp/crypto.d.ts +17 -3
  20. package/dist/channels/whatsapp/crypto.d.ts.map +1 -1
  21. package/dist/channels/whatsapp/inbound.d.ts +34 -4
  22. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  23. package/dist/channels/whatsapp/index.d.ts +2 -1
  24. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  25. package/dist/channels/whatsapp/index.js +395 -159
  26. package/dist/channels/whatsapp/index.mjs +394 -160
  27. package/dist/channels/whatsapp/router.d.ts +107 -0
  28. package/dist/channels/whatsapp/router.d.ts.map +1 -0
  29. package/dist/core/answer.d.ts +5 -2
  30. package/dist/core/answer.d.ts.map +1 -1
  31. package/dist/core/logger.d.ts +23 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/messages.d.ts +8 -0
  34. package/dist/core/messages.d.ts.map +1 -1
  35. package/dist/core/pipeline.d.ts +2 -0
  36. package/dist/core/pipeline.d.ts.map +1 -1
  37. package/dist/core/retrieval.d.ts +4 -0
  38. package/dist/core/retrieval.d.ts.map +1 -1
  39. package/dist/core/serve-static.d.ts +38 -0
  40. package/dist/core/serve-static.d.ts.map +1 -0
  41. package/dist/flows/engine.d.ts +3 -0
  42. package/dist/flows/engine.d.ts.map +1 -1
  43. package/dist/flows/index.d.ts +1 -0
  44. package/dist/flows/index.d.ts.map +1 -1
  45. package/dist/flows/index.js +91 -25
  46. package/dist/flows/index.mjs +91 -26
  47. package/dist/flows/intent.d.ts +2 -1
  48. package/dist/flows/intent.d.ts.map +1 -1
  49. package/dist/flows/loader.d.ts +5 -3
  50. package/dist/flows/loader.d.ts.map +1 -1
  51. package/dist/flows/types.d.ts +9 -0
  52. package/dist/flows/types.d.ts.map +1 -1
  53. package/dist/history/index.d.ts +17 -0
  54. package/dist/history/index.d.ts.map +1 -0
  55. package/dist/history/index.js +100 -0
  56. package/dist/history/index.mjs +73 -0
  57. package/dist/history/tursoStore.d.ts +26 -0
  58. package/dist/history/tursoStore.d.ts.map +1 -0
  59. package/dist/history/types.d.ts +18 -0
  60. package/dist/history/types.d.ts.map +1 -0
  61. package/dist/index.d.ts +5 -2
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1202 -32462
  64. package/dist/index.mjs +1118 -32414
  65. package/dist/mcp-server/cost-tracker.d.ts +11 -6
  66. package/dist/mcp-server/cost-tracker.d.ts.map +1 -1
  67. package/dist/mcp-server/logger.d.ts +5 -2
  68. package/dist/mcp-server/logger.d.ts.map +1 -1
  69. package/dist/mcp-server/sdk.d.ts +23 -0
  70. package/dist/mcp-server/sdk.d.ts.map +1 -0
  71. package/dist/mcp-server/tool-input.d.ts +12 -0
  72. package/dist/mcp-server/tool-input.d.ts.map +1 -0
  73. package/dist/mcp-server/types.d.ts +10 -3
  74. package/dist/mcp-server/types.d.ts.map +1 -1
  75. package/dist/mcp-server.d.ts +1 -2
  76. package/dist/mcp-server.d.ts.map +1 -1
  77. package/dist/mcp-server.js +192 -111
  78. package/dist/mcp-server.mjs +179 -98
  79. package/dist/middleware/auth.d.ts.map +1 -1
  80. package/dist/middleware/bodyLimit.d.ts +11 -0
  81. package/dist/middleware/bodyLimit.d.ts.map +1 -0
  82. package/dist/middleware/clientKey.d.ts +15 -0
  83. package/dist/middleware/clientKey.d.ts.map +1 -0
  84. package/dist/middleware/cors.d.ts.map +1 -1
  85. package/dist/middleware/jwt.d.ts +0 -3
  86. package/dist/middleware/jwt.d.ts.map +1 -1
  87. package/dist/middleware/originMatch.d.ts +27 -0
  88. package/dist/middleware/originMatch.d.ts.map +1 -0
  89. package/dist/middleware/ratelimit.d.ts +0 -6
  90. package/dist/middleware/ratelimit.d.ts.map +1 -1
  91. package/dist/middleware/referrer.d.ts +5 -1
  92. package/dist/middleware/referrer.d.ts.map +1 -1
  93. package/dist/middleware/securityHeaders.d.ts +20 -0
  94. package/dist/middleware/securityHeaders.d.ts.map +1 -0
  95. package/dist/middleware/session.d.ts +2 -1
  96. package/dist/middleware/session.d.ts.map +1 -1
  97. package/dist/middleware/windowBucket.d.ts +19 -0
  98. package/dist/middleware/windowBucket.d.ts.map +1 -0
  99. package/dist/personas/index.js +20 -1
  100. package/dist/personas/index.mjs +20 -1
  101. package/dist/personas/resolver.d.ts +3 -0
  102. package/dist/personas/resolver.d.ts.map +1 -1
  103. package/dist/routes/demo.d.ts.map +1 -1
  104. package/dist/routes/openai.d.ts.map +1 -1
  105. package/dist/routes/private.d.ts.map +1 -1
  106. package/dist/routes/public.d.ts.map +1 -1
  107. package/dist/scheduler/compose.d.ts +2 -1
  108. package/dist/scheduler/compose.d.ts.map +1 -1
  109. package/dist/scheduler/index.js +29 -9
  110. package/dist/scheduler/index.mjs +29 -9
  111. package/dist/scheduler/scheduler.d.ts +3 -0
  112. package/dist/scheduler/scheduler.d.ts.map +1 -1
  113. package/dist/scheduler/tick.d.ts +2 -0
  114. package/dist/scheduler/tick.d.ts.map +1 -1
  115. package/dist/server.d.ts.map +1 -1
  116. package/dist/server.js +726 -538
  117. package/dist/server.mjs +708 -527
  118. package/dist/types.d.ts +87 -0
  119. package/dist/types.d.ts.map +1 -1
  120. package/package.json +30 -9
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  createWhatsAppChannel: () => createWhatsAppChannel,
40
40
  createWhatsAppImageHandler: () => createWhatsAppImageHandler,
41
41
  createWhatsAppInboundHandler: () => createWhatsAppInboundHandler,
42
+ createWhatsAppMessageRouter: () => createWhatsAppMessageRouter,
42
43
  decrypt: () => decrypt,
43
44
  drawSubject: () => drawSubject,
44
45
  encrypt: () => encrypt,
@@ -50,6 +51,7 @@ __export(index_exports, {
50
51
  loadBaileys: () => loadBaileys,
51
52
  messageContext: () => messageContext,
52
53
  reconnectDelayMs: () => reconnectDelayMs,
54
+ resolveWaMessage: () => resolveWaMessage,
53
55
  senderNameFor: () => senderNameFor,
54
56
  senderPhoneFor: () => senderPhoneFor,
55
57
  shutdownWaSessions: () => shutdownWaSessions,
@@ -60,26 +62,59 @@ module.exports = __toCommonJS(index_exports);
60
62
 
61
63
  // src/channels/whatsapp/crypto.ts
62
64
  var import_node_crypto = require("node:crypto");
65
+ var import_node_util = require("node:util");
66
+ var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
63
67
  var IV_LENGTH = 12;
64
68
  var TAG_LENGTH = 16;
65
- function deriveKey(secret) {
69
+ var SALT_LENGTH = 16;
70
+ var KEY_LENGTH = 32;
71
+ var VERSION = 1;
72
+ function deriveLegacyKey(secret) {
66
73
  return (0, import_node_crypto.createHash)("sha256").update(secret, "utf-8").digest();
67
74
  }
68
- function encrypt(plaintext, secret) {
75
+ async function deriveKey(secret, salt) {
76
+ return await scryptAsync(secret, salt, KEY_LENGTH);
77
+ }
78
+ async function encrypt(plaintext, secret) {
79
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LENGTH);
69
80
  const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
70
- const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", deriveKey(secret), iv);
81
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", await deriveKey(secret, salt), iv);
71
82
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
72
- return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
83
+ return Buffer.concat([
84
+ Buffer.from([VERSION]),
85
+ salt,
86
+ iv,
87
+ cipher.getAuthTag(),
88
+ ciphertext
89
+ ]).toString("base64");
73
90
  }
74
- function decrypt(payload, secret) {
75
- const raw = Buffer.from(payload, "base64");
91
+ async function decryptVersioned(raw, secret) {
92
+ const salt = raw.subarray(1, 1 + SALT_LENGTH);
93
+ const iv = raw.subarray(1 + SALT_LENGTH, 1 + SALT_LENGTH + IV_LENGTH);
94
+ const tag = raw.subarray(1 + SALT_LENGTH + IV_LENGTH, 1 + SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
95
+ const ciphertext = raw.subarray(1 + SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
96
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", await deriveKey(secret, salt), iv);
97
+ decipher.setAuthTag(tag);
98
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
99
+ }
100
+ function decryptLegacy(raw, secret) {
76
101
  const iv = raw.subarray(0, IV_LENGTH);
77
102
  const tag = raw.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
78
103
  const ciphertext = raw.subarray(IV_LENGTH + TAG_LENGTH);
79
- const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveKey(secret), iv);
104
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveLegacyKey(secret), iv);
80
105
  decipher.setAuthTag(tag);
81
106
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
82
107
  }
108
+ async function decrypt(payload, secret) {
109
+ const raw = Buffer.from(payload, "base64");
110
+ if (raw[0] === VERSION) {
111
+ try {
112
+ return await decryptVersioned(raw, secret);
113
+ } catch {
114
+ }
115
+ }
116
+ return decryptLegacy(raw, secret);
117
+ }
83
118
 
84
119
  // src/channels/whatsapp/authState.ts
85
120
  var schemaReady = /* @__PURE__ */ new WeakMap();
@@ -104,10 +139,10 @@ async function useTursoAuthState(db, secret, sessionId, runtime) {
104
139
  });
105
140
  const row = result.rows[0];
106
141
  if (!row) return null;
107
- return JSON.parse(decrypt(row.value, secret), runtime.bufferJSON.reviver);
142
+ return JSON.parse(await decrypt(row.value, secret), runtime.bufferJSON.reviver);
108
143
  }
109
144
  async function write(id, value) {
110
- const payload = encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
145
+ const payload = await encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
111
146
  await db.execute({
112
147
  sql: "INSERT OR REPLACE INTO wa_auth (id, value) VALUES (?, ?)",
113
148
  args: [rowId(id), payload]
@@ -196,6 +231,35 @@ function silentLogger() {
196
231
  return logger;
197
232
  }
198
233
 
234
+ // src/auth/secretStrength.ts
235
+ var MIN_SECRET_LENGTH = 16;
236
+ function assertStrongSecret(secret, label) {
237
+ if (!secret) {
238
+ throw new Error(`${label} is required and cannot be empty.`);
239
+ }
240
+ if (secret.length < MIN_SECRET_LENGTH) {
241
+ throw new Error(
242
+ `${label} is too weak: must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length}). Generate one with \`openssl rand -base64 32\`.`
243
+ );
244
+ }
245
+ }
246
+
247
+ // src/core/logger.ts
248
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
249
+ function createConsoleLogger(level = "info") {
250
+ const threshold = LEVEL_ORDER[level];
251
+ const write = (at) => (...args) => {
252
+ if (LEVEL_ORDER[at] < threshold) return;
253
+ console.error(`[${at}]`, ...args);
254
+ };
255
+ return {
256
+ debug: write("debug"),
257
+ info: write("info"),
258
+ warn: write("warn"),
259
+ error: write("error")
260
+ };
261
+ }
262
+
199
263
  // src/channels/whatsapp/lease.ts
200
264
  var LEASE_STALE_MS = 9e4;
201
265
  var LEASE_HEARTBEAT_MS = 3e4;
@@ -278,11 +342,12 @@ async function acquireSessionLease(sessionId, deps) {
278
342
  const heartbeatMs = deps.heartbeatMs ?? LEASE_HEARTBEAT_MS;
279
343
  const waitMs = deps.waitMs ?? LEASE_WAIT_MS;
280
344
  const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
345
+ const logger = deps.logger ?? createConsoleLogger();
281
346
  if (isStopped()) return;
282
347
  const retry = () => {
283
348
  schedule(
284
349
  () => void acquireSessionLease(sessionId, deps).catch(
285
- (error) => console.error(`WhatsApp[${sessionId}]: lease re-check failed:`, error)
350
+ (error) => logger.error(`WhatsApp[${sessionId}]: lease re-check failed:`, error)
286
351
  ),
287
352
  waitMs
288
353
  );
@@ -291,21 +356,21 @@ async function acquireSessionLease(sessionId, deps) {
291
356
  try {
292
357
  acquired = await leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs);
293
358
  } catch (error) {
294
- console.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
359
+ logger.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
295
360
  retry();
296
361
  return;
297
362
  }
298
363
  if (!acquired) {
299
- console.warn(`WhatsApp[${sessionId}]: lease held by another instance, re-checking later`);
364
+ logger.warn(`WhatsApp[${sessionId}]: lease held by another instance, re-checking later`);
300
365
  retry();
301
366
  return;
302
367
  }
303
368
  const heartbeat = setInterval(() => {
304
369
  void leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs).then((stillHeld) => {
305
370
  if (stillHeld || isStopped()) return;
306
- console.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
371
+ logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
307
372
  void loseSession(sessionId, sessions).then(retry);
308
- }).catch((error) => console.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
373
+ }).catch((error) => logger.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
309
374
  }, heartbeatMs);
310
375
  if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
311
376
  sessions.set(sessionId, {
@@ -316,7 +381,7 @@ async function acquireSessionLease(sessionId, deps) {
316
381
  try {
317
382
  await connect(sessionId);
318
383
  } catch (error) {
319
- console.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
384
+ logger.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
320
385
  await loseSession(sessionId, sessions);
321
386
  retry();
322
387
  }
@@ -325,6 +390,7 @@ function senderNameFor(channelName, sessionId) {
325
390
  return sessionId === "default" ? channelName : `${channelName}:${sessionId}`;
326
391
  }
327
392
  function createWhatsAppChannel(config) {
393
+ assertStrongSecret(config.sessionSecret, "WhatsApp sessionSecret (e.g. WA_SESSION_SECRET)");
328
394
  const channelName = "whatsapp";
329
395
  const sessionIds = config.sessionIds && config.sessionIds.length > 0 ? config.sessionIds : ["default"];
330
396
  const now = config.now ?? Date.now;
@@ -342,6 +408,7 @@ function createWhatsAppChannel(config) {
342
408
  return {
343
409
  name: channelName,
344
410
  async start(deps) {
411
+ const log = config.logger ?? deps.logger ?? createConsoleLogger();
345
412
  const db = deps.db;
346
413
  const leaseStore = createTursoWaLeaseStore(db);
347
414
  senders = deps.senders;
@@ -361,7 +428,7 @@ function createWhatsAppChannel(config) {
361
428
  runtime
362
429
  );
363
430
  if (!state.creds.registered) {
364
- console.warn(
431
+ log.warn(
365
432
  `WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
366
433
  );
367
434
  schedule(() => void connect(sessionId).catch(() => void 0), 6e4);
@@ -386,14 +453,14 @@ function createWhatsAppChannel(config) {
386
453
  sock.ev.on(
387
454
  "creds.update",
388
455
  () => void saveCreds().catch(
389
- (error) => console.warn(`WhatsApp[${sessionId}]: creds save failed:`, error)
456
+ (error) => log.warn(`WhatsApp[${sessionId}]: creds save failed:`, error)
390
457
  )
391
458
  );
392
459
  sock.ev.on("connection.update", (update) => {
393
460
  const { connection, lastDisconnect } = update;
394
461
  if (connection === "open") {
395
462
  failureCounts.set(sessionId, 0);
396
- console.log(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
463
+ log.info(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
397
464
  const senderName = senderNameFor(channelName, sessionId);
398
465
  const sender = {
399
466
  sendText: async (chatId, text) => {
@@ -411,7 +478,7 @@ function createWhatsAppChannel(config) {
411
478
  }
412
479
  const statusCode = lastDisconnect?.error?.output?.statusCode;
413
480
  if (statusCode === baileys.DisconnectReason.loggedOut) {
414
- console.error(
481
+ log.error(
415
482
  `WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
416
483
  );
417
484
  void loseSession(sessionId, sessions);
@@ -420,12 +487,12 @@ function createWhatsAppChannel(config) {
420
487
  if (!stopped && sessions.has(sessionId)) {
421
488
  const delay = reconnectDelayMs(failureCounts.get(sessionId) ?? 0);
422
489
  failureCounts.set(sessionId, (failureCounts.get(sessionId) ?? 0) + 1);
423
- console.warn(
490
+ log.warn(
424
491
  `WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
425
492
  );
426
493
  schedule(
427
494
  () => void connect(sessionId).catch(
428
- (error) => console.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
495
+ (error) => log.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
429
496
  ),
430
497
  delay
431
498
  );
@@ -439,7 +506,7 @@ function createWhatsAppChannel(config) {
439
506
  try {
440
507
  await config.onMessage?.({ sessionId, sock, message });
441
508
  } catch (error) {
442
- console.warn(`WhatsApp[${sessionId}]: onMessage handler failed:`, error);
509
+ log.warn(`WhatsApp[${sessionId}]: onMessage handler failed:`, error);
443
510
  }
444
511
  }
445
512
  });
@@ -456,8 +523,9 @@ function createWhatsAppChannel(config) {
456
523
  staleMs,
457
524
  heartbeatMs,
458
525
  waitMs,
459
- schedule
460
- }).catch((error) => console.error(`WhatsApp[${sessionId}]: failed to start:`, error));
526
+ schedule,
527
+ logger: log
528
+ }).catch((error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error));
461
529
  }
462
530
  },
463
531
  async stop() {
@@ -536,6 +604,53 @@ function createWhatsAppImageHandler(deps) {
536
604
  };
537
605
  }
538
606
 
607
+ // src/channels/gates.ts
608
+ function matches(re, text) {
609
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
610
+ }
611
+ function isBlockedByAllowlist(msg, config) {
612
+ return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
613
+ }
614
+ function decideChannelAction(msg, config) {
615
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
616
+ if (!msg.isDirectMessage) {
617
+ if (isBlockedByAllowlist(msg, config)) return "ignore";
618
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
619
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
620
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
621
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
622
+ }
623
+ return "reply";
624
+ }
625
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
626
+ const log = /* @__PURE__ */ new Map();
627
+ return function allow(key) {
628
+ const cutoff = now() - windowMs;
629
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
630
+ if (entries.length >= maxPerWindow) {
631
+ if (entries.length > 0) log.set(key, entries);
632
+ else log.delete(key);
633
+ return false;
634
+ }
635
+ entries.push(now());
636
+ log.set(key, entries);
637
+ return true;
638
+ };
639
+ }
640
+ function underReplyRateLimit(msg, limiters) {
641
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
642
+ }
643
+ function isFromAnySession(id, registry) {
644
+ if (!id) return false;
645
+ for (const ids of registry.values()) {
646
+ if (ids.includes(id)) return true;
647
+ }
648
+ return false;
649
+ }
650
+ function isEffectivelyFromSelf(msg, registry) {
651
+ return msg.fromBot || isFromAnySession(msg.senderId, registry);
652
+ }
653
+
539
654
  // src/core/guardrails.ts
540
655
  function detectLeakage(text) {
541
656
  const flags = [
@@ -580,8 +695,8 @@ async function completeOnce({
580
695
 
581
696
  // src/core/answer.ts
582
697
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
583
- async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
584
- const result = await answerFn({ system, messages, mode, sender });
698
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
699
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
585
700
  const raw = typeof result === "string" ? result : result?.content;
586
701
  const usage = typeof result === "string" ? void 0 : result?.usage;
587
702
  return {
@@ -596,11 +711,12 @@ async function answerOnce({
596
711
  messages,
597
712
  mode,
598
713
  sender,
714
+ conversationId,
599
715
  temperature,
600
716
  model
601
717
  }) {
602
718
  if (answerFn) {
603
- return runAnswerFn(answerFn, { system, messages, mode, sender });
719
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
604
720
  }
605
721
  return completeOnce({ client, system, messages, temperature, model });
606
722
  }
@@ -636,6 +752,11 @@ async function resolveBuckets({
636
752
  return cleaned.filter((bucket) => ceiling.has(bucket));
637
753
  }
638
754
 
755
+ // src/core/messages.ts
756
+ function lastUserMessage(messages) {
757
+ return [...messages].reverse().find((m) => m.role === "user");
758
+ }
759
+
639
760
  // src/core/pipeline.ts
640
761
  var MODE_SETTINGS = {
641
762
  public: { topK: 6, label: "Context" },
@@ -650,7 +771,7 @@ async function prepareChat({
650
771
  channelHint,
651
772
  buckets
652
773
  }) {
653
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
774
+ const lastUserMsg = lastUserMessage(messages);
654
775
  if (!lastUserMsg) {
655
776
  throw new Error("no user message found in conversation");
656
777
  }
@@ -665,54 +786,93 @@ async function prepareChat({
665
786
  `${label}:
666
787
  ${ctx.join("\n\n")}`
667
788
  ].join("\n\n");
668
- return { system, messages };
789
+ return { system, messages, context: ctx };
669
790
  }
670
791
 
671
- // src/channels/gates.ts
672
- function matches(re, text) {
673
- return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
674
- }
675
- function decideChannelAction(msg, config) {
676
- if (msg.fromBot || !msg.text.trim()) return "ignore";
677
- if (!msg.isDirectMessage) {
678
- if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
679
- return "ignore";
680
- }
681
- if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
682
- if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
683
- if (config.mutedChats.has(msg.chatId)) return "ignore";
684
- return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
792
+ // src/channels/pipeline.ts
793
+ var DEFAULT_HISTORY_LIMIT = 20;
794
+ var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
795
+ var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
796
+ async function resolvePersonaLayer(config, ctx) {
797
+ if (!config.personaResolver) return void 0;
798
+ try {
799
+ return await config.personaResolver(ctx);
800
+ } catch {
801
+ return void 0;
685
802
  }
686
- return "reply";
687
803
  }
688
- function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
689
- const log = /* @__PURE__ */ new Map();
690
- return function allow(key) {
691
- const cutoff = now() - windowMs;
692
- const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
693
- if (entries.length >= maxPerWindow) {
694
- if (entries.length > 0) log.set(key, entries);
695
- else log.delete(key);
696
- return false;
804
+ function createInboundPipeline(deps, config) {
805
+ const mutedChats = /* @__PURE__ */ new Set();
806
+ const now = config.now ?? Date.now;
807
+ const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
808
+ const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
809
+ const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
810
+ const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
811
+ const mode = "public";
812
+ return async function handleInboundMessage(msg, turn) {
813
+ const allowedChats = config.allowedChats ?? [];
814
+ const action = decideChannelAction(msg, {
815
+ allowedChats,
816
+ mutedChats,
817
+ muteRegex: config.muteRegex,
818
+ unmuteRegex: config.unmuteRegex
819
+ });
820
+ if (action === "mute") {
821
+ mutedChats.add(msg.chatId);
822
+ if (config.muteReply) await turn.reply.sendGateReply(msg.chatId, config.muteReply);
823
+ return { action: "mute" };
697
824
  }
698
- entries.push(now());
699
- log.set(key, entries);
700
- return true;
825
+ if (action === "unmute") {
826
+ mutedChats.delete(msg.chatId);
827
+ if (config.unmuteReply) await turn.reply.sendGateReply(msg.chatId, config.unmuteReply);
828
+ return { action: "unmute" };
829
+ }
830
+ if (action === "ignore") {
831
+ return { action: "ignore" };
832
+ }
833
+ if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
834
+ return { action: "rate-limited" };
835
+ }
836
+ const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
837
+ const conversationId = turn.conversationId ?? msg.chatId;
838
+ if (turn.intercept && await turn.intercept(sender)) {
839
+ return { action: "intercepted" };
840
+ }
841
+ const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
842
+ const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
843
+ const priorTurns = config.history ? await config.history.store.load(
844
+ conversationId,
845
+ config.history.limit ?? DEFAULT_HISTORY_LIMIT
846
+ ) : [];
847
+ const messages = [...priorTurns, { role: "user", content: msg.text }];
848
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
849
+ const { system } = await prepareChat({
850
+ store: deps.store,
851
+ prompts: deps.prompts,
852
+ mode,
853
+ messages,
854
+ personaLayer,
855
+ channelHint: config.channelHint,
856
+ buckets
857
+ });
858
+ const { content } = await answerOnce({
859
+ answerFn: config.answerFn,
860
+ client: deps.client,
861
+ system,
862
+ messages,
863
+ mode,
864
+ sender,
865
+ conversationId,
866
+ model: config.model
867
+ });
868
+ if (!content) {
869
+ return { action: "ignore" };
870
+ }
871
+ await config.history?.store.append(conversationId, { role: "assistant", content });
872
+ await turn.reply.sendAnswer(msg.chatId, content);
873
+ return { action: "reply", content };
701
874
  };
702
875
  }
703
- function underReplyRateLimit(msg, limiters) {
704
- return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
705
- }
706
- function isFromAnySession(id, registry) {
707
- if (!id) return false;
708
- for (const ids of registry.values()) {
709
- if (ids.includes(id)) return true;
710
- }
711
- return false;
712
- }
713
- function isEffectivelyFromSelf(msg, registry) {
714
- return msg.fromBot || isFromAnySession(msg.senderId, registry);
715
- }
716
876
 
717
877
  // src/channels/whatsapp/inbound.ts
718
878
  function isGroupJid(jid) {
@@ -729,11 +889,6 @@ function jidToPhoneNumber(jid) {
729
889
  const bare = bareUser(jid);
730
890
  return bare.startsWith("+") ? bare : `+${bare}`;
731
891
  }
732
- function normalizeJid(jid) {
733
- const [user = "", domain] = jid.split("@");
734
- const withoutDevice = user.split(":")[0] ?? user;
735
- return domain ? `${withoutDevice}@${domain}` : withoutDevice;
736
- }
737
892
  async function senderPhoneFor(sock, message, chatJid) {
738
893
  const key = message.key;
739
894
  let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
@@ -761,6 +916,11 @@ function extractText(message) {
761
916
  if (!content) return "";
762
917
  return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
763
918
  }
919
+ function normalizeJid(jid) {
920
+ const [user = "", domain] = jid.split("@");
921
+ const withoutDevice = user.split(":")[0] ?? user;
922
+ return domain ? `${withoutDevice}@${domain}` : withoutDevice;
923
+ }
764
924
  function stripOwnMentions(text, ownIds) {
765
925
  const ownUsers = new Set(ownIds.map(bareUser).filter(Boolean));
766
926
  if (!text || ownUsers.size === 0) return text;
@@ -774,109 +934,183 @@ function stripOwnMentions(text, ownIds) {
774
934
  const cleaned = stripped.trim();
775
935
  return cleaned ? cleaned : text;
776
936
  }
777
- var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
778
- var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
779
- async function resolvePersonaLayer(config, ctx) {
780
- if (!config.personaResolver) return void 0;
781
- try {
782
- return await config.personaResolver(ctx);
783
- } catch {
784
- return void 0;
785
- }
937
+ function resolveWaMessage(event, registry) {
938
+ const { sessionId, sock, message } = event;
939
+ const chatId = message.key.remoteJid ?? "";
940
+ const rawText = extractText(message);
941
+ const context = messageContext(message);
942
+ const senderId = message.key.participant ?? chatId;
943
+ const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
944
+ registry.set(sessionId, ownIds);
945
+ const text = stripOwnMentions(rawText, ownIds);
946
+ const fromBot = isEffectivelyFromSelf(
947
+ { fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
948
+ registry
949
+ );
950
+ const msg = {
951
+ chatId,
952
+ senderId,
953
+ text,
954
+ isDirectMessage: !isGroupJid(chatId),
955
+ mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
956
+ isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
957
+ fromBot
958
+ };
959
+ return { msg, ownIds };
786
960
  }
787
961
  function createWhatsAppInboundHandler(config) {
788
- const mutedChats = /* @__PURE__ */ new Set();
789
- const now = config.now ?? Date.now;
790
- const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
791
- const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
792
- const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
793
- const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
962
+ const loggedUnallowedChats = /* @__PURE__ */ new Set();
963
+ const allowedChats = config.allowedChats ?? [];
964
+ const personaResolver = config.personaResolver;
965
+ const images = config.images;
966
+ const logger = config.logger ?? createConsoleLogger();
967
+ const pipeline = createInboundPipeline(
968
+ { client: config.client, store: config.store, prompts: config.prompts },
969
+ {
970
+ answerFn: config.answerFn,
971
+ bucketsFor: config.bucketsFor,
972
+ model: config.model,
973
+ channelHint: config.channelHint,
974
+ personaResolver: personaResolver ? ({ sender, text }) => personaResolver({ senderPhone: sender, text }) : void 0,
975
+ history: config.history,
976
+ allowedChats,
977
+ muteRegex: config.muteRegex,
978
+ unmuteRegex: config.unmuteRegex,
979
+ muteReply: config.muteReply,
980
+ unmuteReply: config.unmuteReply,
981
+ dmRateLimit: config.dmRateLimit,
982
+ groupRateLimit: config.groupRateLimit,
983
+ now: config.now
984
+ }
985
+ );
794
986
  return async function handleWhatsAppMessage(event) {
795
987
  const { sessionId, sock, message } = event;
796
988
  try {
797
989
  const chatId = message.key.remoteJid ?? "";
798
990
  if (!chatId || chatId === "status@broadcast") return;
799
- const rawText = extractText(message);
800
- const context = messageContext(message);
801
- const senderId = message.key.participant ?? chatId;
802
- const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
803
- config.registry.set(sessionId, ownIds);
804
- const text = stripOwnMentions(rawText, ownIds);
805
- const fromBot = isEffectivelyFromSelf(
806
- { fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
807
- config.registry
808
- );
809
- const msg = {
810
- chatId,
811
- senderId,
812
- text,
813
- isDirectMessage: !isGroupJid(chatId),
814
- mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
815
- isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
816
- fromBot
817
- };
818
- const action = decideChannelAction(msg, {
819
- allowedChats: config.allowedChats ?? [],
820
- mutedChats,
821
- muteRegex: config.muteRegex,
822
- unmuteRegex: config.unmuteRegex
823
- });
824
- if (action === "mute") {
825
- mutedChats.add(chatId);
826
- if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
827
- return;
828
- }
829
- if (action === "unmute") {
830
- mutedChats.delete(chatId);
831
- if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
832
- return;
833
- }
834
- if (action !== "reply") return;
835
- if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
836
- return;
991
+ const { msg } = resolveWaMessage(event, config.registry);
992
+ const text = msg.text;
993
+ if (isBlockedByAllowlist(msg, { allowedChats })) {
994
+ const dedupKey = `${sessionId}:${chatId}`;
995
+ if (!loggedUnallowedChats.has(dedupKey)) {
996
+ loggedUnallowedChats.add(dedupKey);
997
+ logger.warn(`WhatsApp[${sessionId}]: skipped group ${chatId} - not in allowedChats`);
998
+ }
837
999
  }
838
- const senderPhone = await senderPhoneFor(sock, message, chatId);
839
- if (config.images) {
840
- const handled = await config.images({
1000
+ const reply = {
1001
+ sendAnswer: async (targetChatId, replyText2) => {
1002
+ await sock.sendMessage(targetChatId, { text: replyText2 }, { quoted: message });
1003
+ },
1004
+ sendGateReply: async (targetChatId, replyText2) => {
1005
+ await sock.sendMessage(targetChatId, { text: replyText2 });
1006
+ }
1007
+ };
1008
+ await pipeline(msg, {
1009
+ reply,
1010
+ conversationId: chatId,
1011
+ sender: () => senderPhoneFor(sock, message, chatId),
1012
+ intercept: images ? (senderPhone) => images({
841
1013
  sock,
842
1014
  message,
843
1015
  chatId,
844
1016
  senderId: senderPhone,
845
1017
  text,
846
1018
  hasPhoto: Boolean(message.message?.imageMessage)
847
- });
848
- if (handled) return;
849
- }
850
- const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
851
- const buckets = await resolveBuckets({
852
- mode: "public",
853
- sender: senderPhone,
854
- bucketsFor: config.bucketsFor
855
- });
856
- const messages = [{ role: "user", content: text }];
857
- const { system } = await prepareChat({
858
- store: config.store,
859
- prompts: config.prompts,
860
- mode: "public",
861
- messages,
862
- personaLayer,
863
- channelHint: config.channelHint,
864
- buckets
1019
+ }) : void 0
865
1020
  });
866
- const { content } = await answerOnce({
867
- answerFn: config.answerFn,
868
- client: config.client,
869
- system,
870
- messages,
871
- mode: "public",
872
- sender: senderPhone,
873
- model: config.model
1021
+ } catch (error) {
1022
+ logger.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
1023
+ }
1024
+ };
1025
+ }
1026
+
1027
+ // src/channels/whatsapp/router.ts
1028
+ async function logDetectorError(config, logger, name, error) {
1029
+ if (!config.onDetectorError) {
1030
+ logger.warn(`WhatsApp router: detector "${name}" failed:`, error);
1031
+ return;
1032
+ }
1033
+ try {
1034
+ await config.onDetectorError(name, error);
1035
+ } catch (callbackError) {
1036
+ logger.warn(`WhatsApp router: onDetectorError itself failed:`, callbackError);
1037
+ }
1038
+ }
1039
+ function isChatIneligible(msg, allowedChats) {
1040
+ return !msg.isDirectMessage && allowedChats.length > 0 && !allowedChats.includes(msg.chatId);
1041
+ }
1042
+ function fireParallelDetector(config, logger, detector, ctx) {
1043
+ void (async () => {
1044
+ try {
1045
+ if (await detector.test(ctx)) {
1046
+ await detector.handle(ctx);
1047
+ }
1048
+ } catch (error) {
1049
+ await logDetectorError(config, logger, detector.name, error);
1050
+ }
1051
+ })();
1052
+ }
1053
+ function createWhatsAppMessageRouter(config) {
1054
+ const allowedChats = config.allowedChats ?? [];
1055
+ const mutedChats = config.mutedChats ?? /* @__PURE__ */ new Set();
1056
+ const logger = config.logger ?? createConsoleLogger();
1057
+ const replaceDetectors = config.detectors.filter(
1058
+ (d) => d.mode === "replace"
1059
+ );
1060
+ const parallelDetectors = config.detectors.filter(
1061
+ (d) => d.mode === "parallel"
1062
+ );
1063
+ return async function routeWhatsAppMessage(event) {
1064
+ const { sock, message } = event;
1065
+ let ctx;
1066
+ try {
1067
+ const chatId = message.key.remoteJid ?? "";
1068
+ if (!chatId || chatId === "status@broadcast") return;
1069
+ const { msg, ownIds } = resolveWaMessage(event, config.registry);
1070
+ if (msg.fromBot || isChatIneligible(msg, allowedChats)) return;
1071
+ const action = decideChannelAction(msg, {
1072
+ allowedChats,
1073
+ mutedChats,
1074
+ muteRegex: config.muteRegex,
1075
+ unmuteRegex: config.unmuteRegex
874
1076
  });
875
- if (content) {
876
- await sock.sendMessage(chatId, { text: content }, { quoted: message });
1077
+ ctx = {
1078
+ event,
1079
+ sock,
1080
+ message,
1081
+ msg,
1082
+ ownIds,
1083
+ senderId: msg.senderId,
1084
+ text: msg.text,
1085
+ action
1086
+ };
1087
+ } catch (error) {
1088
+ await logDetectorError(config, logger, "resolve", error);
1089
+ return;
1090
+ }
1091
+ for (const detector of parallelDetectors) {
1092
+ fireParallelDetector(config, logger, detector, ctx);
1093
+ }
1094
+ for (const detector of replaceDetectors) {
1095
+ let matched;
1096
+ try {
1097
+ matched = await detector.test(ctx);
1098
+ } catch (error) {
1099
+ await logDetectorError(config, logger, detector.name, error);
1100
+ continue;
877
1101
  }
1102
+ if (!matched) continue;
1103
+ try {
1104
+ await detector.handle(ctx);
1105
+ } catch (error) {
1106
+ await logDetectorError(config, logger, detector.name, error);
1107
+ }
1108
+ return;
1109
+ }
1110
+ try {
1111
+ await config.fallback(event);
878
1112
  } catch (error) {
879
- console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
1113
+ await logDetectorError(config, logger, "fallback", error);
880
1114
  }
881
1115
  };
882
1116
  }
@@ -891,6 +1125,7 @@ function createWhatsAppInboundHandler(config) {
891
1125
  createWhatsAppChannel,
892
1126
  createWhatsAppImageHandler,
893
1127
  createWhatsAppInboundHandler,
1128
+ createWhatsAppMessageRouter,
894
1129
  decrypt,
895
1130
  drawSubject,
896
1131
  encrypt,
@@ -902,6 +1137,7 @@ function createWhatsAppInboundHandler(config) {
902
1137
  loadBaileys,
903
1138
  messageContext,
904
1139
  reconnectDelayMs,
1140
+ resolveWaMessage,
905
1141
  senderNameFor,
906
1142
  senderPhoneFor,
907
1143
  shutdownWaSessions,