@diegoaltoworks/chatter 0.57.0 → 0.59.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.
@@ -25,6 +25,9 @@ __export(index_exports, {
25
25
  TelegramApiError: () => TelegramApiError,
26
26
  createTelegramApi: () => createTelegramApi,
27
27
  createTelegramChannel: () => createTelegramChannel,
28
+ createTelegramSender: () => createTelegramSender,
29
+ createTelegramUpdateHandler: () => createTelegramUpdateHandler,
30
+ createTelegramWebhookRoute: () => createTelegramWebhookRoute,
28
31
  mentionsBot: () => mentionsBot,
29
32
  messageEntities: () => messageEntities,
30
33
  messageText: () => messageText,
@@ -188,6 +191,17 @@ function createTelegramApi(config) {
188
191
  message_id: messageId,
189
192
  reaction: [{ type: "emoji", emoji }]
190
193
  });
194
+ },
195
+ async setWebhook(url, options) {
196
+ await call("setWebhook", {
197
+ url,
198
+ // Only what this channel interprets — see the matching comment on getUpdates.
199
+ allowed_updates: ["message"],
200
+ ...options?.secretToken ? { secret_token: options.secretToken } : {}
201
+ });
202
+ },
203
+ async deleteWebhook() {
204
+ await call("deleteWebhook");
191
205
  }
192
206
  };
193
207
  }
@@ -208,56 +222,6 @@ function createConsoleLogger(level = "info") {
208
222
  };
209
223
  }
210
224
 
211
- // src/channels/gates.ts
212
- function matches(re, text) {
213
- return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
214
- }
215
- function isBlockedByAllowlist(msg, config) {
216
- return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
217
- }
218
- function decideChannelAction(msg, config) {
219
- if (msg.fromBot || !msg.text.trim()) return "ignore";
220
- if (!msg.isDirectMessage) {
221
- if (isBlockedByAllowlist(msg, config)) return "ignore";
222
- if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
223
- if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
224
- if (config.mutedChats.has(msg.chatId)) return "ignore";
225
- return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
226
- }
227
- return "reply";
228
- }
229
- function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
230
- const log = /* @__PURE__ */ new Map();
231
- let lastSweep = now();
232
- function sweep(current) {
233
- if (current - lastSweep < windowMs) return;
234
- lastSweep = current;
235
- const cutoff = current - windowMs;
236
- for (const [key, entries] of log) {
237
- if (!entries.some((t) => t > cutoff)) log.delete(key);
238
- }
239
- }
240
- function allow(key) {
241
- const current = now();
242
- sweep(current);
243
- const cutoff = current - windowMs;
244
- const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
245
- if (entries.length >= maxPerWindow) {
246
- if (entries.length > 0) log.set(key, entries);
247
- else log.delete(key);
248
- return false;
249
- }
250
- entries.push(current);
251
- log.set(key, entries);
252
- return true;
253
- }
254
- allow.size = () => log.size;
255
- return allow;
256
- }
257
- function underReplyRateLimit(msg, limiters) {
258
- return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
259
- }
260
-
261
225
  // src/core/guardrails.ts
262
226
  function detectLeakage(text) {
263
227
  const flags = [
@@ -429,6 +393,56 @@ ${ctx.join("\n\n")}`
429
393
  return { system, messages, context: ctx };
430
394
  }
431
395
 
396
+ // src/channels/gates.ts
397
+ function matches(re, text) {
398
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
399
+ }
400
+ function isBlockedByAllowlist(msg, config) {
401
+ return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
402
+ }
403
+ function decideChannelAction(msg, config) {
404
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
405
+ if (!msg.isDirectMessage) {
406
+ if (isBlockedByAllowlist(msg, config)) return "ignore";
407
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
408
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
409
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
410
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
411
+ }
412
+ return "reply";
413
+ }
414
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
415
+ const log = /* @__PURE__ */ new Map();
416
+ let lastSweep = now();
417
+ function sweep(current) {
418
+ if (current - lastSweep < windowMs) return;
419
+ lastSweep = current;
420
+ const cutoff = current - windowMs;
421
+ for (const [key, entries] of log) {
422
+ if (!entries.some((t) => t > cutoff)) log.delete(key);
423
+ }
424
+ }
425
+ function allow(key) {
426
+ const current = now();
427
+ sweep(current);
428
+ const cutoff = current - windowMs;
429
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
430
+ if (entries.length >= maxPerWindow) {
431
+ if (entries.length > 0) log.set(key, entries);
432
+ else log.delete(key);
433
+ return false;
434
+ }
435
+ entries.push(current);
436
+ log.set(key, entries);
437
+ return true;
438
+ }
439
+ allow.size = () => log.size;
440
+ return allow;
441
+ }
442
+ function underReplyRateLimit(msg, limiters) {
443
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
444
+ }
445
+
432
446
  // src/channels/pipeline.ts
433
447
  var DEFAULT_HISTORY_LIMIT = 20;
434
448
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
@@ -441,6 +455,16 @@ async function resolvePersonaLayer(config, ctx) {
441
455
  return void 0;
442
456
  }
443
457
  }
458
+ async function isHistoryEnabled(config, sender) {
459
+ const predicate = config.history?.historyEnabledFor;
460
+ if (!predicate) return true;
461
+ try {
462
+ return await predicate(sender);
463
+ } catch (error) {
464
+ config.logger?.error("historyEnabledFor threw; disabling history for this turn", error);
465
+ return false;
466
+ }
467
+ }
444
468
  function createInboundPipeline(deps, config) {
445
469
  const mutedChats = /* @__PURE__ */ new Set();
446
470
  const now = config.now ?? Date.now;
@@ -480,12 +504,15 @@ function createInboundPipeline(deps, config) {
480
504
  }
481
505
  const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
482
506
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
483
- const priorTurns = config.history ? await config.history.store.load(
507
+ const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
508
+ const priorTurns = config.history && historyEnabled ? await config.history.store.load(
484
509
  conversationId,
485
510
  config.history.limit ?? DEFAULT_HISTORY_LIMIT
486
511
  ) : [];
487
512
  const messages = [...priorTurns, { role: "user", content: msg.text }];
488
- await config.history?.store.append(conversationId, { role: "user", content: msg.text });
513
+ if (historyEnabled) {
514
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
515
+ }
489
516
  const { system } = await prepareChat({
490
517
  store: deps.store,
491
518
  prompts: deps.prompts,
@@ -517,7 +544,9 @@ function createInboundPipeline(deps, config) {
517
544
  if (!content) {
518
545
  return { action: "ignore" };
519
546
  }
520
- await config.history?.store.append(conversationId, { role: "assistant", content });
547
+ if (historyEnabled) {
548
+ await config.history?.store.append(conversationId, { role: "assistant", content });
549
+ }
521
550
  await turn.reply.sendAnswer(msg.chatId, content);
522
551
  return { action: "reply", content };
523
552
  };
@@ -566,6 +595,41 @@ function telegramSenderKey(senderId) {
566
595
  return `tg:${senderId}`;
567
596
  }
568
597
 
598
+ // src/channels/telegram/handler.ts
599
+ function createTelegramUpdateHandler(deps) {
600
+ const { api, me, pipeline, allowedChats, logger, label } = deps;
601
+ const loggedUnallowedChats = /* @__PURE__ */ new Set();
602
+ return async function handleUpdate(update) {
603
+ const message = update.message;
604
+ const msg = toChannelMessage(update, me);
605
+ if (!msg || !message) return;
606
+ if (isBlockedByAllowlist(msg, { allowedChats })) {
607
+ if (!loggedUnallowedChats.has(msg.chatId)) {
608
+ loggedUnallowedChats.add(msg.chatId);
609
+ logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
610
+ }
611
+ }
612
+ const reply = {
613
+ // Threaded onto the incoming message: in a busy group an untethered
614
+ // answer reads as a non-sequitur.
615
+ sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
616
+ sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
617
+ };
618
+ await pipeline(msg, {
619
+ reply,
620
+ conversationId: msg.chatId,
621
+ sender: telegramSenderKey(msg.senderId)
622
+ });
623
+ };
624
+ }
625
+ function createTelegramSender(api) {
626
+ return {
627
+ sendText: (chatId, text) => api.sendMessage(chatId, text),
628
+ sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
629
+ sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
630
+ };
631
+ }
632
+
569
633
  // src/channels/telegram/poll.ts
570
634
  var BASE_DELAY_MS = 1e3;
571
635
  var MAX_DELAY_MS = 6e4;
@@ -636,12 +700,7 @@ function createTelegramChannel(config) {
636
700
  });
637
701
  const me = await api.getMe();
638
702
  const label = `Telegram[${me.username ?? me.id}]`;
639
- const sender = {
640
- sendText: (chatId, text) => api.sendMessage(chatId, text),
641
- sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
642
- sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
643
- };
644
- deps.senders.register(channelName, sender);
703
+ deps.senders.register(channelName, createTelegramSender(api));
645
704
  senders = deps.senders;
646
705
  registered = true;
647
706
  const pipeline = createInboundPipeline(
@@ -668,29 +727,14 @@ function createTelegramChannel(config) {
668
727
  logger: log
669
728
  }
670
729
  );
671
- const loggedUnallowedChats = /* @__PURE__ */ new Set();
672
- async function handleUpdate(update) {
673
- const message = update.message;
674
- const msg = toChannelMessage(update, me);
675
- if (!msg || !message) return;
676
- if (isBlockedByAllowlist(msg, { allowedChats })) {
677
- if (!loggedUnallowedChats.has(msg.chatId)) {
678
- loggedUnallowedChats.add(msg.chatId);
679
- log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
680
- }
681
- }
682
- const reply = {
683
- // Threaded onto the incoming message: in a busy group an untethered
684
- // answer reads as a non-sequitur.
685
- sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
686
- sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
687
- };
688
- await pipeline(msg, {
689
- reply,
690
- conversationId: msg.chatId,
691
- sender: telegramSenderKey(msg.senderId)
692
- });
693
- }
730
+ const handleUpdate = createTelegramUpdateHandler({
731
+ api,
732
+ me,
733
+ pipeline,
734
+ allowedChats,
735
+ logger: log,
736
+ label
737
+ });
694
738
  abort = new AbortController();
695
739
  const signal = abort.signal;
696
740
  void runLongPoll({
@@ -715,6 +759,196 @@ function createTelegramChannel(config) {
715
759
  }
716
760
  };
717
761
  }
762
+
763
+ // src/channels/telegram/webhook.ts
764
+ var import_node_crypto = require("node:crypto");
765
+
766
+ // node_modules/hono/dist/http-exception.js
767
+ var HTTPException = class extends Error {
768
+ res;
769
+ status;
770
+ /**
771
+ * Creates an instance of `HTTPException`.
772
+ * @param status - HTTP status code for the exception. Defaults to 500.
773
+ * @param options - Additional options for the exception.
774
+ */
775
+ constructor(status = 500, options) {
776
+ super(options?.message, { cause: options?.cause });
777
+ this.res = options?.res;
778
+ this.status = status;
779
+ }
780
+ /**
781
+ * Returns the response object associated with the exception.
782
+ * If a response object is not provided, a new response is created with the error message and status code.
783
+ * @returns The response object.
784
+ */
785
+ getResponse() {
786
+ if (this.res) {
787
+ const newResponse = new Response(this.res.body, {
788
+ status: this.status,
789
+ headers: this.res.headers
790
+ });
791
+ return newResponse;
792
+ }
793
+ return new Response(this.message, {
794
+ status: this.status
795
+ });
796
+ }
797
+ };
798
+
799
+ // node_modules/hono/dist/middleware/body-limit/index.js
800
+ var ERROR_MESSAGE = "Payload Too Large";
801
+ var bodyLimit = (options) => {
802
+ const onError = options.onError || (() => {
803
+ const res = new Response(ERROR_MESSAGE, {
804
+ status: 413
805
+ });
806
+ throw new HTTPException(413, { res });
807
+ });
808
+ const maxSize = options.maxSize;
809
+ return async function bodyLimit2(c, next) {
810
+ if (!c.req.raw.body) {
811
+ return next();
812
+ }
813
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
814
+ const hasContentLength = c.req.raw.headers.has("content-length");
815
+ if (hasContentLength && !hasTransferEncoding) {
816
+ const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
817
+ return contentLength > maxSize ? onError(c) : next();
818
+ }
819
+ let size = 0;
820
+ const chunks = [];
821
+ const rawReader = c.req.raw.body.getReader();
822
+ for (; ; ) {
823
+ const { done, value } = await rawReader.read();
824
+ if (done) {
825
+ break;
826
+ }
827
+ size += value.length;
828
+ if (size > maxSize) {
829
+ return onError(c);
830
+ }
831
+ chunks.push(value);
832
+ }
833
+ const requestInit = {
834
+ body: new ReadableStream({
835
+ start(controller) {
836
+ for (const chunk of chunks) {
837
+ controller.enqueue(chunk);
838
+ }
839
+ controller.close();
840
+ }
841
+ }),
842
+ duplex: "half"
843
+ };
844
+ c.req.raw = new Request(c.req.raw, requestInit);
845
+ return next();
846
+ };
847
+ };
848
+
849
+ // src/middleware/bodyLimit.ts
850
+ var DEFAULT_MAX_REQUEST_BYTES = 262144;
851
+ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
852
+ return bodyLimit({
853
+ maxSize,
854
+ onError: (c) => c.json(toError("Request body too large"), 413)
855
+ });
856
+ }
857
+
858
+ // src/channels/telegram/webhook.ts
859
+ var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
860
+ function secureCompare(a, b) {
861
+ const bufA = Buffer.from(a);
862
+ const bufB = Buffer.from(b);
863
+ return bufA.length === bufB.length && (0, import_node_crypto.timingSafeEqual)(bufA, bufB);
864
+ }
865
+ var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
866
+ function createTelegramWebhookRoute(config) {
867
+ const webhookSecret = config.webhookSecret;
868
+ if (!webhookSecret?.trim()) {
869
+ throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
870
+ }
871
+ if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
872
+ throw new Error(
873
+ "Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
874
+ );
875
+ }
876
+ const channelName = config.name ?? "telegram";
877
+ const allowedChats = config.allowedChats ?? [];
878
+ const path = config.path ?? "/webhooks/telegram";
879
+ return async (app, deps) => {
880
+ const log = config.logger ?? deps.logger ?? createConsoleLogger();
881
+ const api = config.api ?? createTelegramApi({
882
+ botToken: config.botToken,
883
+ fetch: config.fetch,
884
+ baseUrl: config.apiBaseUrl
885
+ });
886
+ const me = await api.getMe();
887
+ const label = `Telegram[${me.username ?? me.id}] (webhook)`;
888
+ deps.senders.register(channelName, createTelegramSender(api));
889
+ const pipeline = createInboundPipeline(
890
+ { client: deps.client, store: deps.store, prompts: deps.prompts },
891
+ {
892
+ channel: channelName,
893
+ answerFn: config.answerFn ?? deps.config.answerFn,
894
+ bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
895
+ rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
896
+ rerankContext: config.rerankContext ?? deps.config.rerankContext,
897
+ transformReply: config.transformReply ?? deps.config.transformReply,
898
+ model: config.model,
899
+ channelHint: config.channelHint ?? "Channel: Telegram.",
900
+ personaResolver: config.personaResolver,
901
+ history: config.history,
902
+ allowedChats,
903
+ muteRegex: config.muteRegex,
904
+ unmuteRegex: config.unmuteRegex,
905
+ muteReply: config.muteReply,
906
+ unmuteReply: config.unmuteReply,
907
+ dmRateLimit: config.dmRateLimit,
908
+ groupRateLimit: config.groupRateLimit,
909
+ now: config.now,
910
+ logger: log
911
+ }
912
+ );
913
+ const handleUpdate = createTelegramUpdateHandler({
914
+ api,
915
+ me,
916
+ pipeline,
917
+ allowedChats,
918
+ logger: log,
919
+ label
920
+ });
921
+ app.post(
922
+ path,
923
+ chatBodyLimit(deps.config.server?.maxRequestBytes),
924
+ // Reject oversized bodies first
925
+ async (c) => {
926
+ const provided = c.req.header(SECRET_TOKEN_HEADER);
927
+ if (!provided || !secureCompare(provided, webhookSecret)) {
928
+ log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
929
+ return c.text("", 403);
930
+ }
931
+ let body;
932
+ try {
933
+ body = await c.req.json();
934
+ } catch {
935
+ return c.text("", 400);
936
+ }
937
+ if (typeof body !== "object" || body === null) {
938
+ return c.text("", 400);
939
+ }
940
+ const update = body;
941
+ try {
942
+ await handleUpdate(update);
943
+ } catch (error) {
944
+ log.warn(`${label}: update handling failed:`, error);
945
+ }
946
+ return c.text("", 200);
947
+ }
948
+ );
949
+ log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
950
+ };
951
+ }
718
952
  // Annotate the CommonJS export names for ESM import in node:
719
953
  0 && (module.exports = {
720
954
  TELEGRAM_API_BASE,
@@ -722,6 +956,9 @@ function createTelegramChannel(config) {
722
956
  TelegramApiError,
723
957
  createTelegramApi,
724
958
  createTelegramChannel,
959
+ createTelegramSender,
960
+ createTelegramUpdateHandler,
961
+ createTelegramWebhookRoute,
725
962
  mentionsBot,
726
963
  messageEntities,
727
964
  messageText,