@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.
@@ -146,6 +146,17 @@ function createTelegramApi(config) {
146
146
  message_id: messageId,
147
147
  reaction: [{ type: "emoji", emoji }]
148
148
  });
149
+ },
150
+ async setWebhook(url, options) {
151
+ await call("setWebhook", {
152
+ url,
153
+ // Only what this channel interprets — see the matching comment on getUpdates.
154
+ allowed_updates: ["message"],
155
+ ...options?.secretToken ? { secret_token: options.secretToken } : {}
156
+ });
157
+ },
158
+ async deleteWebhook() {
159
+ await call("deleteWebhook");
149
160
  }
150
161
  };
151
162
  }
@@ -166,56 +177,6 @@ function createConsoleLogger(level = "info") {
166
177
  };
167
178
  }
168
179
 
169
- // src/channels/gates.ts
170
- function matches(re, text) {
171
- return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
172
- }
173
- function isBlockedByAllowlist(msg, config) {
174
- return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
175
- }
176
- function decideChannelAction(msg, config) {
177
- if (msg.fromBot || !msg.text.trim()) return "ignore";
178
- if (!msg.isDirectMessage) {
179
- if (isBlockedByAllowlist(msg, config)) return "ignore";
180
- if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
181
- if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
182
- if (config.mutedChats.has(msg.chatId)) return "ignore";
183
- return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
184
- }
185
- return "reply";
186
- }
187
- function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
188
- const log = /* @__PURE__ */ new Map();
189
- let lastSweep = now();
190
- function sweep(current) {
191
- if (current - lastSweep < windowMs) return;
192
- lastSweep = current;
193
- const cutoff = current - windowMs;
194
- for (const [key, entries] of log) {
195
- if (!entries.some((t) => t > cutoff)) log.delete(key);
196
- }
197
- }
198
- function allow(key) {
199
- const current = now();
200
- sweep(current);
201
- const cutoff = current - windowMs;
202
- const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
203
- if (entries.length >= maxPerWindow) {
204
- if (entries.length > 0) log.set(key, entries);
205
- else log.delete(key);
206
- return false;
207
- }
208
- entries.push(current);
209
- log.set(key, entries);
210
- return true;
211
- }
212
- allow.size = () => log.size;
213
- return allow;
214
- }
215
- function underReplyRateLimit(msg, limiters) {
216
- return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
217
- }
218
-
219
180
  // src/core/guardrails.ts
220
181
  function detectLeakage(text) {
221
182
  const flags = [
@@ -387,6 +348,56 @@ ${ctx.join("\n\n")}`
387
348
  return { system, messages, context: ctx };
388
349
  }
389
350
 
351
+ // src/channels/gates.ts
352
+ function matches(re, text) {
353
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
354
+ }
355
+ function isBlockedByAllowlist(msg, config) {
356
+ return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
357
+ }
358
+ function decideChannelAction(msg, config) {
359
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
360
+ if (!msg.isDirectMessage) {
361
+ if (isBlockedByAllowlist(msg, config)) return "ignore";
362
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
363
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
364
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
365
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
366
+ }
367
+ return "reply";
368
+ }
369
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
370
+ const log = /* @__PURE__ */ new Map();
371
+ let lastSweep = now();
372
+ function sweep(current) {
373
+ if (current - lastSweep < windowMs) return;
374
+ lastSweep = current;
375
+ const cutoff = current - windowMs;
376
+ for (const [key, entries] of log) {
377
+ if (!entries.some((t) => t > cutoff)) log.delete(key);
378
+ }
379
+ }
380
+ function allow(key) {
381
+ const current = now();
382
+ sweep(current);
383
+ const cutoff = current - windowMs;
384
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
385
+ if (entries.length >= maxPerWindow) {
386
+ if (entries.length > 0) log.set(key, entries);
387
+ else log.delete(key);
388
+ return false;
389
+ }
390
+ entries.push(current);
391
+ log.set(key, entries);
392
+ return true;
393
+ }
394
+ allow.size = () => log.size;
395
+ return allow;
396
+ }
397
+ function underReplyRateLimit(msg, limiters) {
398
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
399
+ }
400
+
390
401
  // src/channels/pipeline.ts
391
402
  var DEFAULT_HISTORY_LIMIT = 20;
392
403
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
@@ -399,6 +410,16 @@ async function resolvePersonaLayer(config, ctx) {
399
410
  return void 0;
400
411
  }
401
412
  }
413
+ async function isHistoryEnabled(config, sender) {
414
+ const predicate = config.history?.historyEnabledFor;
415
+ if (!predicate) return true;
416
+ try {
417
+ return await predicate(sender);
418
+ } catch (error) {
419
+ config.logger?.error("historyEnabledFor threw; disabling history for this turn", error);
420
+ return false;
421
+ }
422
+ }
402
423
  function createInboundPipeline(deps, config) {
403
424
  const mutedChats = /* @__PURE__ */ new Set();
404
425
  const now = config.now ?? Date.now;
@@ -438,12 +459,15 @@ function createInboundPipeline(deps, config) {
438
459
  }
439
460
  const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
440
461
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
441
- const priorTurns = config.history ? await config.history.store.load(
462
+ const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
463
+ const priorTurns = config.history && historyEnabled ? await config.history.store.load(
442
464
  conversationId,
443
465
  config.history.limit ?? DEFAULT_HISTORY_LIMIT
444
466
  ) : [];
445
467
  const messages = [...priorTurns, { role: "user", content: msg.text }];
446
- await config.history?.store.append(conversationId, { role: "user", content: msg.text });
468
+ if (historyEnabled) {
469
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
470
+ }
447
471
  const { system } = await prepareChat({
448
472
  store: deps.store,
449
473
  prompts: deps.prompts,
@@ -475,7 +499,9 @@ function createInboundPipeline(deps, config) {
475
499
  if (!content) {
476
500
  return { action: "ignore" };
477
501
  }
478
- await config.history?.store.append(conversationId, { role: "assistant", content });
502
+ if (historyEnabled) {
503
+ await config.history?.store.append(conversationId, { role: "assistant", content });
504
+ }
479
505
  await turn.reply.sendAnswer(msg.chatId, content);
480
506
  return { action: "reply", content };
481
507
  };
@@ -524,6 +550,41 @@ function telegramSenderKey(senderId) {
524
550
  return `tg:${senderId}`;
525
551
  }
526
552
 
553
+ // src/channels/telegram/handler.ts
554
+ function createTelegramUpdateHandler(deps) {
555
+ const { api, me, pipeline, allowedChats, logger, label } = deps;
556
+ const loggedUnallowedChats = /* @__PURE__ */ new Set();
557
+ return async function handleUpdate(update) {
558
+ const message = update.message;
559
+ const msg = toChannelMessage(update, me);
560
+ if (!msg || !message) return;
561
+ if (isBlockedByAllowlist(msg, { allowedChats })) {
562
+ if (!loggedUnallowedChats.has(msg.chatId)) {
563
+ loggedUnallowedChats.add(msg.chatId);
564
+ logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
565
+ }
566
+ }
567
+ const reply = {
568
+ // Threaded onto the incoming message: in a busy group an untethered
569
+ // answer reads as a non-sequitur.
570
+ sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
571
+ sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
572
+ };
573
+ await pipeline(msg, {
574
+ reply,
575
+ conversationId: msg.chatId,
576
+ sender: telegramSenderKey(msg.senderId)
577
+ });
578
+ };
579
+ }
580
+ function createTelegramSender(api) {
581
+ return {
582
+ sendText: (chatId, text) => api.sendMessage(chatId, text),
583
+ sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
584
+ sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
585
+ };
586
+ }
587
+
527
588
  // src/channels/telegram/poll.ts
528
589
  var BASE_DELAY_MS = 1e3;
529
590
  var MAX_DELAY_MS = 6e4;
@@ -594,12 +655,7 @@ function createTelegramChannel(config) {
594
655
  });
595
656
  const me = await api.getMe();
596
657
  const label = `Telegram[${me.username ?? me.id}]`;
597
- const sender = {
598
- sendText: (chatId, text) => api.sendMessage(chatId, text),
599
- sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
600
- sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
601
- };
602
- deps.senders.register(channelName, sender);
658
+ deps.senders.register(channelName, createTelegramSender(api));
603
659
  senders = deps.senders;
604
660
  registered = true;
605
661
  const pipeline = createInboundPipeline(
@@ -626,29 +682,14 @@ function createTelegramChannel(config) {
626
682
  logger: log
627
683
  }
628
684
  );
629
- const loggedUnallowedChats = /* @__PURE__ */ new Set();
630
- async function handleUpdate(update) {
631
- const message = update.message;
632
- const msg = toChannelMessage(update, me);
633
- if (!msg || !message) return;
634
- if (isBlockedByAllowlist(msg, { allowedChats })) {
635
- if (!loggedUnallowedChats.has(msg.chatId)) {
636
- loggedUnallowedChats.add(msg.chatId);
637
- log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
638
- }
639
- }
640
- const reply = {
641
- // Threaded onto the incoming message: in a busy group an untethered
642
- // answer reads as a non-sequitur.
643
- sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
644
- sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
645
- };
646
- await pipeline(msg, {
647
- reply,
648
- conversationId: msg.chatId,
649
- sender: telegramSenderKey(msg.senderId)
650
- });
651
- }
685
+ const handleUpdate = createTelegramUpdateHandler({
686
+ api,
687
+ me,
688
+ pipeline,
689
+ allowedChats,
690
+ logger: log,
691
+ label
692
+ });
652
693
  abort = new AbortController();
653
694
  const signal = abort.signal;
654
695
  void runLongPoll({
@@ -673,12 +714,205 @@ function createTelegramChannel(config) {
673
714
  }
674
715
  };
675
716
  }
717
+
718
+ // src/channels/telegram/webhook.ts
719
+ import { timingSafeEqual } from "node:crypto";
720
+
721
+ // node_modules/hono/dist/http-exception.js
722
+ var HTTPException = class extends Error {
723
+ res;
724
+ status;
725
+ /**
726
+ * Creates an instance of `HTTPException`.
727
+ * @param status - HTTP status code for the exception. Defaults to 500.
728
+ * @param options - Additional options for the exception.
729
+ */
730
+ constructor(status = 500, options) {
731
+ super(options?.message, { cause: options?.cause });
732
+ this.res = options?.res;
733
+ this.status = status;
734
+ }
735
+ /**
736
+ * Returns the response object associated with the exception.
737
+ * If a response object is not provided, a new response is created with the error message and status code.
738
+ * @returns The response object.
739
+ */
740
+ getResponse() {
741
+ if (this.res) {
742
+ const newResponse = new Response(this.res.body, {
743
+ status: this.status,
744
+ headers: this.res.headers
745
+ });
746
+ return newResponse;
747
+ }
748
+ return new Response(this.message, {
749
+ status: this.status
750
+ });
751
+ }
752
+ };
753
+
754
+ // node_modules/hono/dist/middleware/body-limit/index.js
755
+ var ERROR_MESSAGE = "Payload Too Large";
756
+ var bodyLimit = (options) => {
757
+ const onError = options.onError || (() => {
758
+ const res = new Response(ERROR_MESSAGE, {
759
+ status: 413
760
+ });
761
+ throw new HTTPException(413, { res });
762
+ });
763
+ const maxSize = options.maxSize;
764
+ return async function bodyLimit2(c, next) {
765
+ if (!c.req.raw.body) {
766
+ return next();
767
+ }
768
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
769
+ const hasContentLength = c.req.raw.headers.has("content-length");
770
+ if (hasContentLength && !hasTransferEncoding) {
771
+ const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
772
+ return contentLength > maxSize ? onError(c) : next();
773
+ }
774
+ let size = 0;
775
+ const chunks = [];
776
+ const rawReader = c.req.raw.body.getReader();
777
+ for (; ; ) {
778
+ const { done, value } = await rawReader.read();
779
+ if (done) {
780
+ break;
781
+ }
782
+ size += value.length;
783
+ if (size > maxSize) {
784
+ return onError(c);
785
+ }
786
+ chunks.push(value);
787
+ }
788
+ const requestInit = {
789
+ body: new ReadableStream({
790
+ start(controller) {
791
+ for (const chunk of chunks) {
792
+ controller.enqueue(chunk);
793
+ }
794
+ controller.close();
795
+ }
796
+ }),
797
+ duplex: "half"
798
+ };
799
+ c.req.raw = new Request(c.req.raw, requestInit);
800
+ return next();
801
+ };
802
+ };
803
+
804
+ // src/middleware/bodyLimit.ts
805
+ var DEFAULT_MAX_REQUEST_BYTES = 262144;
806
+ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
807
+ return bodyLimit({
808
+ maxSize,
809
+ onError: (c) => c.json(toError("Request body too large"), 413)
810
+ });
811
+ }
812
+
813
+ // src/channels/telegram/webhook.ts
814
+ var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
815
+ function secureCompare(a, b) {
816
+ const bufA = Buffer.from(a);
817
+ const bufB = Buffer.from(b);
818
+ return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
819
+ }
820
+ var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
821
+ function createTelegramWebhookRoute(config) {
822
+ const webhookSecret = config.webhookSecret;
823
+ if (!webhookSecret?.trim()) {
824
+ throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
825
+ }
826
+ if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
827
+ throw new Error(
828
+ "Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
829
+ );
830
+ }
831
+ const channelName = config.name ?? "telegram";
832
+ const allowedChats = config.allowedChats ?? [];
833
+ const path = config.path ?? "/webhooks/telegram";
834
+ return async (app, deps) => {
835
+ const log = config.logger ?? deps.logger ?? createConsoleLogger();
836
+ const api = config.api ?? createTelegramApi({
837
+ botToken: config.botToken,
838
+ fetch: config.fetch,
839
+ baseUrl: config.apiBaseUrl
840
+ });
841
+ const me = await api.getMe();
842
+ const label = `Telegram[${me.username ?? me.id}] (webhook)`;
843
+ deps.senders.register(channelName, createTelegramSender(api));
844
+ const pipeline = createInboundPipeline(
845
+ { client: deps.client, store: deps.store, prompts: deps.prompts },
846
+ {
847
+ channel: channelName,
848
+ answerFn: config.answerFn ?? deps.config.answerFn,
849
+ bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
850
+ rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
851
+ rerankContext: config.rerankContext ?? deps.config.rerankContext,
852
+ transformReply: config.transformReply ?? deps.config.transformReply,
853
+ model: config.model,
854
+ channelHint: config.channelHint ?? "Channel: Telegram.",
855
+ personaResolver: config.personaResolver,
856
+ history: config.history,
857
+ allowedChats,
858
+ muteRegex: config.muteRegex,
859
+ unmuteRegex: config.unmuteRegex,
860
+ muteReply: config.muteReply,
861
+ unmuteReply: config.unmuteReply,
862
+ dmRateLimit: config.dmRateLimit,
863
+ groupRateLimit: config.groupRateLimit,
864
+ now: config.now,
865
+ logger: log
866
+ }
867
+ );
868
+ const handleUpdate = createTelegramUpdateHandler({
869
+ api,
870
+ me,
871
+ pipeline,
872
+ allowedChats,
873
+ logger: log,
874
+ label
875
+ });
876
+ app.post(
877
+ path,
878
+ chatBodyLimit(deps.config.server?.maxRequestBytes),
879
+ // Reject oversized bodies first
880
+ async (c) => {
881
+ const provided = c.req.header(SECRET_TOKEN_HEADER);
882
+ if (!provided || !secureCompare(provided, webhookSecret)) {
883
+ log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
884
+ return c.text("", 403);
885
+ }
886
+ let body;
887
+ try {
888
+ body = await c.req.json();
889
+ } catch {
890
+ return c.text("", 400);
891
+ }
892
+ if (typeof body !== "object" || body === null) {
893
+ return c.text("", 400);
894
+ }
895
+ const update = body;
896
+ try {
897
+ await handleUpdate(update);
898
+ } catch (error) {
899
+ log.warn(`${label}: update handling failed:`, error);
900
+ }
901
+ return c.text("", 200);
902
+ }
903
+ );
904
+ log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
905
+ };
906
+ }
676
907
  export {
677
908
  TELEGRAM_API_BASE,
678
909
  TELEGRAM_TEXT_LIMIT,
679
910
  TelegramApiError,
680
911
  createTelegramApi,
681
912
  createTelegramChannel,
913
+ createTelegramSender,
914
+ createTelegramUpdateHandler,
915
+ createTelegramWebhookRoute,
682
916
  mentionsBot,
683
917
  messageEntities,
684
918
  messageText,
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Telegram Bot API transport: a `CustomRoutes` mount that receives updates
3
+ * over HTTPS instead of long-polling `getUpdates` (see `./channel` for that
4
+ * transport). Update handling is identical between the two — both build on
5
+ * `./handler`'s shared `createTelegramUpdateHandler`/`createTelegramSender` —
6
+ * so choosing one over the other is purely an infrastructure decision: a
7
+ * webhook needs a publicly reachable HTTPS endpoint and no open connection,
8
+ * long-polling needs neither but holds one request open at a time. See
9
+ * docs/telegram.md for the full comparison and `TelegramApi.setWebhook` for
10
+ * registering the URL with Telegram once this route is mounted.
11
+ *
12
+ * ```ts
13
+ * await createServer({
14
+ * ...,
15
+ * customRoutes: createTelegramWebhookRoute({
16
+ * botToken: process.env.TELEGRAM_BOT_TOKEN as string,
17
+ * webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET as string,
18
+ * }),
19
+ * });
20
+ * ```
21
+ */
22
+ import type { AnswerFn, TransformReply } from "../../core/answer";
23
+ import type { BucketsFor } from "../../core/buckets";
24
+ import { type Logger } from "../../core/logger";
25
+ import type { RerankContext, RewriteQuery } from "../../core/pipeline";
26
+ import type { HistoryStore } from "../../history/types";
27
+ import type { CustomRoutes } from "../../types";
28
+ import { type TelegramApi } from "./api";
29
+ export interface TelegramWebhookConfig {
30
+ /** From @BotFather. A credential — pass it from the environment, never commit it. */
31
+ botToken: string;
32
+ /**
33
+ * Shared secret Telegram is told about via `setWebhook({ secretToken })`
34
+ * and then echoes back on every POST as `X-Telegram-Bot-Api-Secret-Token`.
35
+ * Required: without one there is no way to tell a genuine Telegram POST
36
+ * from a forged one, so this fails closed by construction — a missing or
37
+ * blank value throws rather than falling back to accepting every request.
38
+ */
39
+ webhookSecret: string;
40
+ /** Where the route is mounted. @default "/webhooks/telegram" */
41
+ path?: string;
42
+ /** Channel and sender-registry name. Override to run more than one bot in one process. @default "telegram" */
43
+ name?: string;
44
+ /** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
45
+ allowedChats?: string[];
46
+ answerFn?: AnswerFn;
47
+ bucketsFor?: BucketsFor;
48
+ rewriteQuery?: RewriteQuery;
49
+ rerankContext?: RerankContext;
50
+ transformReply?: TransformReply;
51
+ model?: string;
52
+ /** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. @default "Channel: Telegram." */
53
+ channelHint?: string;
54
+ /** A throw/rejection is treated as "no persona" for that turn. `sender` is the namespaced `tg:<id>` key. */
55
+ personaResolver?: (ctx: {
56
+ sender: string;
57
+ text: string;
58
+ }) => string | undefined | Promise<string | undefined>;
59
+ /** Off by default — the channel stays single-turn until a store is configured. */
60
+ history?: {
61
+ store: HistoryStore;
62
+ /** Most recent turns to load per reply. @default 20 */
63
+ limit?: number;
64
+ historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
65
+ };
66
+ muteRegex?: RegExp;
67
+ unmuteRegex?: RegExp;
68
+ muteReply?: string;
69
+ unmuteReply?: string;
70
+ dmRateLimit?: {
71
+ max: number;
72
+ windowMs: number;
73
+ };
74
+ groupRateLimit?: {
75
+ max: number;
76
+ windowMs: number;
77
+ };
78
+ now?: () => number;
79
+ /** Overridable for tests and for hosts routing through a proxy; defaults to `globalThis.fetch`. */
80
+ fetch?: typeof fetch;
81
+ /** Self-hosted Bot API server origin. */
82
+ apiBaseUrl?: string;
83
+ /** Overridable for tests, which fake the Bot API instead of calling it; defaults to a `fetch` client over {@link TelegramWebhookConfig.botToken}. */
84
+ api?: TelegramApi;
85
+ logger?: Logger;
86
+ }
87
+ /**
88
+ * Builds a `CustomRoutes` mount that receives Telegram updates over HTTPS.
89
+ * Throws synchronously if `webhookSecret` is missing, blank, or outside the
90
+ * charset Telegram accepts for it — before the route ever exists, rather
91
+ * than mounting an endpoint that would silently accept everything or reject
92
+ * every request.
93
+ */
94
+ export declare function createTelegramWebhookRoute(config: TelegramWebhookConfig): CustomRoutes;
95
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAMjF,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KACpE,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qJAAqJ;IACrJ,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,GAAG,YAAY,CAkHtF"}
@@ -193,6 +193,12 @@ export interface WhatsAppInboundConfig {
193
193
  store: HistoryStore;
194
194
  /** Most recent turns to load per reply. @default 20 */
195
195
  limit?: number;
196
+ /**
197
+ * Excludes a sender from history entirely — see
198
+ * `InboundPipelineConfig.history.historyEnabledFor` in `./channels`.
199
+ * @default every sender is enabled
200
+ */
201
+ historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
196
202
  };
197
203
  now?: () => number;
198
204
  /** Logger for gate/dispatch diagnostics. Default: a console logger writing to stderr. */
@@ -1 +1 @@
1
- {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"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"}
@@ -852,6 +852,16 @@ async function resolvePersonaLayer(config, ctx) {
852
852
  return void 0;
853
853
  }
854
854
  }
855
+ async function isHistoryEnabled(config, sender) {
856
+ const predicate = config.history?.historyEnabledFor;
857
+ if (!predicate) return true;
858
+ try {
859
+ return await predicate(sender);
860
+ } catch (error) {
861
+ config.logger?.error("historyEnabledFor threw; disabling history for this turn", error);
862
+ return false;
863
+ }
864
+ }
855
865
  function createInboundPipeline(deps, config) {
856
866
  const mutedChats = /* @__PURE__ */ new Set();
857
867
  const now = config.now ?? Date.now;
@@ -891,12 +901,15 @@ function createInboundPipeline(deps, config) {
891
901
  }
892
902
  const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
893
903
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
894
- const priorTurns = config.history ? await config.history.store.load(
904
+ const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
905
+ const priorTurns = config.history && historyEnabled ? await config.history.store.load(
895
906
  conversationId,
896
907
  config.history.limit ?? DEFAULT_HISTORY_LIMIT
897
908
  ) : [];
898
909
  const messages = [...priorTurns, { role: "user", content: msg.text }];
899
- await config.history?.store.append(conversationId, { role: "user", content: msg.text });
910
+ if (historyEnabled) {
911
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
912
+ }
900
913
  const { system } = await prepareChat({
901
914
  store: deps.store,
902
915
  prompts: deps.prompts,
@@ -928,7 +941,9 @@ function createInboundPipeline(deps, config) {
928
941
  if (!content) {
929
942
  return { action: "ignore" };
930
943
  }
931
- await config.history?.store.append(conversationId, { role: "assistant", content });
944
+ if (historyEnabled) {
945
+ await config.history?.store.append(conversationId, { role: "assistant", content });
946
+ }
932
947
  await turn.reply.sendAnswer(msg.chatId, content);
933
948
  return { action: "reply", content };
934
949
  };