@okrlinkhub/agent-factory 3.0.3 → 3.1.1

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.
@@ -1,7 +1,8 @@
1
1
  import { v } from "convex/values";
2
2
  import { internal } from "./_generated/api.js";
3
- import { action, mutation, query } from "./_generated/server.js";
3
+ import { action, internalMutation, mutation, query } from "./_generated/server.js";
4
4
  import type { MutationCtx, QueryCtx } from "./_generated/server.js";
5
+ import type { Id } from "./_generated/dataModel.js";
5
6
 
6
7
  const bindingStatusValidator = v.union(v.literal("active"), v.literal("revoked"));
7
8
  const bindingSourceValidator = v.union(
@@ -26,6 +27,7 @@ const bindingViewValidator = v.object({
26
27
  consumerUserId: v.string(),
27
28
  agentKey: v.string(),
28
29
  conversationId: v.string(),
30
+ botIdentity: v.union(v.null(), v.string()),
29
31
  status: bindingStatusValidator,
30
32
  source: bindingSourceValidator,
31
33
  telegramUserId: v.union(v.null(), v.string()),
@@ -39,6 +41,7 @@ const pairingCodeViewValidator = v.object({
39
41
  code: v.string(),
40
42
  consumerUserId: v.string(),
41
43
  agentKey: v.string(),
44
+ botIdentity: v.union(v.null(), v.string()),
42
45
  status: pairingStatusValidator,
43
46
  createdAt: v.number(),
44
47
  expiresAt: v.number(),
@@ -51,6 +54,8 @@ const telegramWebhookStatusValidator = v.object({
51
54
  ok: v.boolean(),
52
55
  webhookUrl: v.string(),
53
56
  currentUrl: v.union(v.null(), v.string()),
57
+ botIdentity: v.union(v.null(), v.string()),
58
+ secretTokenConfigured: v.boolean(),
54
59
  isReady: v.boolean(),
55
60
  pendingUpdateCount: v.number(),
56
61
  lastErrorMessage: v.union(v.null(), v.string()),
@@ -93,6 +98,7 @@ const webhookReadinessValidator = v.object({
93
98
  const onboardingStateValidator = v.object({
94
99
  agentKey: v.string(),
95
100
  telegramUsername: v.union(v.null(), v.string()),
101
+ botIdentity: v.union(v.null(), v.string()),
96
102
  tokenSecretRef: v.union(v.null(), v.string()),
97
103
  tokenImported: v.boolean(),
98
104
  webhookReady: v.boolean(),
@@ -149,6 +155,7 @@ type OnboardingNextAction =
149
155
  type OnboardingState = {
150
156
  agentKey: string;
151
157
  telegramUsername: string | null;
158
+ botIdentity: string | null;
152
159
  tokenSecretRef: string | null;
153
160
  tokenImported: boolean;
154
161
  webhookReady: boolean;
@@ -161,6 +168,7 @@ type OnboardingState = {
161
168
  type UpsertBindingArgs = {
162
169
  consumerUserId: string;
163
170
  agentKey: string;
171
+ botIdentity?: string;
164
172
  source?: BindingSource;
165
173
  telegramUserId?: string;
166
174
  telegramChatId?: string;
@@ -168,10 +176,14 @@ type UpsertBindingArgs = {
168
176
  nowMs?: number;
169
177
  };
170
178
 
179
+ const TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX = "af_v1_";
180
+ const LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX = "af:v1:";
181
+
171
182
  export const configureTelegramWebhook = action({
172
183
  args: {
173
184
  convexSiteUrl: v.string(),
174
185
  secretRef: v.optional(v.string()),
186
+ agentKey: v.optional(v.string()),
175
187
  },
176
188
  returns: telegramWebhookStatusValidator,
177
189
  handler: async (ctx, args) => {
@@ -199,6 +211,16 @@ export const configureTelegramWebhook = action({
199
211
 
200
212
  const webhookUrl = `${normalizedSiteUrl}/agent-factory/telegram/webhook`;
201
213
  const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(token)}`;
214
+ const telegramBot = await fetchTelegramBotProfile(token);
215
+ const webhookSecretToken = buildTelegramWebhookSecretToken(telegramBot.botIdentity);
216
+ const agentKey = args.agentKey?.trim();
217
+ if (agentKey) {
218
+ await ctx.runMutation(internal.identity.syncAgentProfileTelegramBotIdentity, {
219
+ agentKey,
220
+ botIdentity: telegramBot.botIdentity,
221
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
222
+ });
223
+ }
202
224
 
203
225
  const setWebhookResponse = await fetch(`${telegramApiBaseUrl}/setWebhook`, {
204
226
  method: "POST",
@@ -207,6 +229,7 @@ export const configureTelegramWebhook = action({
207
229
  },
208
230
  body: JSON.stringify({
209
231
  url: webhookUrl,
232
+ secret_token: webhookSecretToken,
210
233
  }),
211
234
  });
212
235
 
@@ -259,6 +282,8 @@ export const configureTelegramWebhook = action({
259
282
  ok: true,
260
283
  webhookUrl,
261
284
  currentUrl,
285
+ botIdentity: telegramBot.botIdentity,
286
+ secretTokenConfigured: true,
262
287
  isReady,
263
288
  pendingUpdateCount,
264
289
  lastErrorMessage,
@@ -284,6 +309,7 @@ export const createPairingCode = mutation({
284
309
  export const consumePairingCode = mutation({
285
310
  args: {
286
311
  code: v.string(),
312
+ botIdentity: v.optional(v.string()),
287
313
  telegramUserId: v.string(),
288
314
  telegramChatId: v.string(),
289
315
  nowMs: v.optional(v.number()),
@@ -305,10 +331,18 @@ export const consumePairingCode = mutation({
305
331
  await ctx.db.patch(pairing._id, { status: "expired" });
306
332
  throw new Error("Pairing code expired");
307
333
  }
334
+ const providedBotIdentity = args.botIdentity?.trim() || null;
335
+ if (!providedBotIdentity) {
336
+ throw new Error("Missing bot identity for Telegram pairing");
337
+ }
338
+ if (pairing.botIdentity && pairing.botIdentity !== providedBotIdentity) {
339
+ throw new Error("Pairing code belongs to a different Telegram bot");
340
+ }
308
341
 
309
342
  await upsertBinding(ctx, {
310
343
  consumerUserId: pairing.consumerUserId,
311
344
  agentKey: pairing.agentKey,
345
+ botIdentity: providedBotIdentity,
312
346
  source: "telegram_pairing",
313
347
  telegramUserId: args.telegramUserId,
314
348
  telegramChatId: args.telegramChatId,
@@ -318,6 +352,7 @@ export const consumePairingCode = mutation({
318
352
  await ctx.db.patch(pairing._id, {
319
353
  status: "used",
320
354
  usedAt: nowMs,
355
+ botIdentity: providedBotIdentity,
321
356
  telegramUserId: args.telegramUserId,
322
357
  telegramChatId: args.telegramChatId,
323
358
  });
@@ -326,6 +361,7 @@ export const consumePairingCode = mutation({
326
361
  code: pairing.code,
327
362
  consumerUserId: pairing.consumerUserId,
328
363
  agentKey: pairing.agentKey,
364
+ botIdentity: providedBotIdentity,
329
365
  status: "used" as const,
330
366
  createdAt: pairing.createdAt,
331
367
  expiresAt: pairing.expiresAt,
@@ -355,6 +391,7 @@ export const getPairingCodeStatus = query({
355
391
  code: pairing.code,
356
392
  consumerUserId: pairing.consumerUserId,
357
393
  agentKey: pairing.agentKey,
394
+ botIdentity: pairing.botIdentity ?? null,
358
395
  status: "expired" as const,
359
396
  createdAt: pairing.createdAt,
360
397
  expiresAt: pairing.expiresAt,
@@ -368,6 +405,7 @@ export const getPairingCodeStatus = query({
368
405
  code: pairing.code,
369
406
  consumerUserId: pairing.consumerUserId,
370
407
  agentKey: pairing.agentKey,
408
+ botIdentity: pairing.botIdentity ?? null,
371
409
  status: pairing.status,
372
410
  createdAt: pairing.createdAt,
373
411
  expiresAt: pairing.expiresAt,
@@ -382,6 +420,7 @@ export const bindUserAgent = mutation({
382
420
  args: {
383
421
  consumerUserId: v.string(),
384
422
  agentKey: v.string(),
423
+ botIdentity: v.optional(v.string()),
385
424
  source: v.optional(bindingSourceValidator),
386
425
  telegramUserId: v.optional(v.string()),
387
426
  telegramChatId: v.optional(v.string()),
@@ -442,6 +481,7 @@ export const resolveAgentForUser = query({
442
481
 
443
482
  export const resolveAgentForTelegram = query({
444
483
  args: {
484
+ botIdentity: v.optional(v.string()),
445
485
  telegramUserId: v.optional(v.string()),
446
486
  telegramChatId: v.optional(v.string()),
447
487
  },
@@ -451,6 +491,7 @@ export const resolveAgentForTelegram = query({
451
491
  conversationId: v.union(v.null(), v.string()),
452
492
  }),
453
493
  handler: async (ctx, args) => {
494
+ const botIdentity = args.botIdentity?.trim() || null;
454
495
  let active:
455
496
  | {
456
497
  consumerUserId: string;
@@ -459,7 +500,45 @@ export const resolveAgentForTelegram = query({
459
500
  }
460
501
  | null = null;
461
502
 
462
- if (args.telegramUserId) {
503
+ if (botIdentity && args.telegramUserId) {
504
+ const byUser = await ctx.db
505
+ .query("identityBindings")
506
+ .withIndex("by_botIdentity_and_telegramUserId_and_status", (q) =>
507
+ q
508
+ .eq("botIdentity", botIdentity)
509
+ .eq("telegramUserId", args.telegramUserId)
510
+ .eq("status", "active"),
511
+ )
512
+ .first();
513
+ if (byUser) {
514
+ active = {
515
+ consumerUserId: byUser.consumerUserId,
516
+ agentKey: byUser.agentKey,
517
+ conversationId: byUser.conversationId,
518
+ };
519
+ }
520
+ }
521
+
522
+ if (!active && botIdentity && args.telegramChatId) {
523
+ const byChat = await ctx.db
524
+ .query("identityBindings")
525
+ .withIndex("by_botIdentity_and_telegramChatId_and_status", (q) =>
526
+ q
527
+ .eq("botIdentity", botIdentity)
528
+ .eq("telegramChatId", args.telegramChatId)
529
+ .eq("status", "active"),
530
+ )
531
+ .first();
532
+ if (byChat) {
533
+ active = {
534
+ consumerUserId: byChat.consumerUserId,
535
+ agentKey: byChat.agentKey,
536
+ conversationId: byChat.conversationId,
537
+ };
538
+ }
539
+ }
540
+
541
+ if (!active && !botIdentity && args.telegramUserId) {
463
542
  const byUser = await ctx.db
464
543
  .query("identityBindings")
465
544
  .withIndex("by_telegramUserId_and_status", (q) =>
@@ -475,7 +554,7 @@ export const resolveAgentForTelegram = query({
475
554
  }
476
555
  }
477
556
 
478
- if (!active && args.telegramChatId) {
557
+ if (!active && !botIdentity && args.telegramChatId) {
479
558
  const byChat = await ctx.db
480
559
  .query("identityBindings")
481
560
  .withIndex("by_telegramChatId_and_status", (q) =>
@@ -518,6 +597,7 @@ export const getUserAgentBinding = query({
518
597
  consumerUserId: active.consumerUserId,
519
598
  agentKey: active.agentKey,
520
599
  conversationId: active.conversationId,
600
+ botIdentity: active.botIdentity ?? null,
521
601
  status: active.status,
522
602
  source: active.source,
523
603
  telegramUserId: active.telegramUserId ?? null,
@@ -644,6 +724,7 @@ export const getUserAgentPairingStatus = query({
644
724
  code: pairing.code,
645
725
  consumerUserId: pairing.consumerUserId,
646
726
  agentKey: pairing.agentKey,
727
+ botIdentity: pairing.botIdentity ?? null,
647
728
  status:
648
729
  pairing.status === "pending" && pairing.expiresAt <= nowMs
649
730
  ? "expired"
@@ -659,7 +740,7 @@ export const getUserAgentPairingStatus = query({
659
740
  },
660
741
  });
661
742
 
662
- export const importTelegramTokenForAgent = mutation({
743
+ export const importTelegramTokenForAgent = action({
663
744
  args: {
664
745
  consumerUserId: v.string(),
665
746
  agentKey: v.string(),
@@ -670,34 +751,31 @@ export const importTelegramTokenForAgent = mutation({
670
751
  secretId: v.id("secrets"),
671
752
  secretRef: v.string(),
672
753
  version: v.number(),
754
+ botIdentity: v.string(),
755
+ telegramUsername: v.union(v.null(), v.string()),
673
756
  }),
674
- handler: async (ctx, args) => {
675
- const details = await buildUserAgentDetails(
676
- ctx,
677
- args.consumerUserId,
678
- args.agentKey,
679
- Date.now(),
680
- );
681
- if (details.latestBinding === null && details.latestPairing === null) {
682
- throw new Error("Agent is not yet associated with the provided consumerUserId");
683
- }
684
- const profile = await ctx.db
685
- .query("agentProfiles")
686
- .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
687
- .unique();
688
- if (!profile) {
689
- throw new Error(`Agent profile '${args.agentKey}' not found`);
690
- }
691
- const secretRef = resolveTelegramSecretRef(profile, args.agentKey);
692
- if (!profile.secretsRef.includes(secretRef)) {
693
- await ctx.db.patch(profile._id, {
694
- secretsRef: [...profile.secretsRef, secretRef],
695
- });
757
+ handler: async (
758
+ ctx,
759
+ args,
760
+ ): Promise<{
761
+ secretId: Id<"secrets">;
762
+ secretRef: string;
763
+ version: number;
764
+ botIdentity: string;
765
+ telegramUsername: string | null;
766
+ }> => {
767
+ const plaintextValue = args.plaintextValue.trim();
768
+ if (!plaintextValue) {
769
+ throw new Error("Telegram token is required");
696
770
  }
697
- return await importPlaintextSecretRecord(ctx, {
698
- secretRef,
699
- plaintextValue: args.plaintextValue,
771
+ const telegramBot = await fetchTelegramBotProfile(plaintextValue);
772
+ return await ctx.runMutation(internal.identity.persistImportedTelegramTokenForAgent, {
773
+ consumerUserId: args.consumerUserId,
774
+ agentKey: args.agentKey,
775
+ plaintextValue,
700
776
  metadata: args.metadata,
777
+ botIdentity: telegramBot.botIdentity,
778
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
701
779
  });
702
780
  },
703
781
  });
@@ -775,6 +853,7 @@ export const getUserAgentOnboardingState = query({
775
853
  : "pending";
776
854
  const webhookReady =
777
855
  tokenImported &&
856
+ details.botIdentity !== null &&
778
857
  details.latestBinding?.status === "active" &&
779
858
  details.latestBinding.source === "telegram_pairing";
780
859
  const pairingCode =
@@ -783,6 +862,8 @@ export const getUserAgentOnboardingState = query({
783
862
  : null;
784
863
  const nextAction: OnboardingNextAction = !tokenImported
785
864
  ? "import_token"
865
+ : details.botIdentity === null
866
+ ? "import_token"
786
867
  : !webhookReady
787
868
  ? "configure_webhook"
788
869
  : pairingStatus === "pending"
@@ -793,6 +874,7 @@ export const getUserAgentOnboardingState = query({
793
874
  const result: OnboardingState = {
794
875
  agentKey: args.agentKey,
795
876
  telegramUsername: details.telegramUsername,
877
+ botIdentity: details.botIdentity,
796
878
  tokenSecretRef: details.telegramTokenSecretRef,
797
879
  tokenImported,
798
880
  webhookReady,
@@ -852,6 +934,200 @@ export const getWebhookReadiness = action({
852
934
  },
853
935
  });
854
936
 
937
+ export const syncAgentProfileTelegramBotIdentity = internalMutation({
938
+ args: {
939
+ agentKey: v.string(),
940
+ botIdentity: v.string(),
941
+ telegramUsername: v.optional(v.string()),
942
+ },
943
+ returns: v.null(),
944
+ handler: async (ctx, args) => {
945
+ const agentKey = args.agentKey.trim();
946
+ const botIdentity = args.botIdentity.trim();
947
+ if (!agentKey || !botIdentity) {
948
+ throw new Error("agentKey and botIdentity are required");
949
+ }
950
+ const profile = await ctx.db
951
+ .query("agentProfiles")
952
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", agentKey))
953
+ .unique();
954
+ if (!profile) {
955
+ throw new Error(`Agent profile '${agentKey}' not found`);
956
+ }
957
+ await ensureUniqueBotIdentityForAgent(ctx, agentKey, botIdentity);
958
+ await ctx.db.patch(profile._id, {
959
+ botIdentity,
960
+ });
961
+ return null;
962
+ },
963
+ });
964
+
965
+ export const persistImportedTelegramTokenForAgent = internalMutation({
966
+ args: {
967
+ consumerUserId: v.string(),
968
+ agentKey: v.string(),
969
+ plaintextValue: v.string(),
970
+ metadata: v.optional(v.record(v.string(), v.string())),
971
+ botIdentity: v.string(),
972
+ telegramUsername: v.optional(v.string()),
973
+ },
974
+ returns: v.object({
975
+ secretId: v.id("secrets"),
976
+ secretRef: v.string(),
977
+ version: v.number(),
978
+ botIdentity: v.string(),
979
+ telegramUsername: v.union(v.null(), v.string()),
980
+ }),
981
+ handler: async (ctx, args) => {
982
+ const details = await buildUserAgentDetails(
983
+ ctx,
984
+ args.consumerUserId,
985
+ args.agentKey,
986
+ Date.now(),
987
+ );
988
+ if (details.latestBinding === null && details.latestPairing === null) {
989
+ throw new Error("Agent is not yet associated with the provided consumerUserId");
990
+ }
991
+ const profile = await ctx.db
992
+ .query("agentProfiles")
993
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
994
+ .unique();
995
+ if (!profile) {
996
+ throw new Error(`Agent profile '${args.agentKey}' not found`);
997
+ }
998
+ await ensureUniqueBotIdentityForAgent(ctx, args.agentKey, args.botIdentity);
999
+ const secretRef = resolveTelegramSecretRef(profile, args.agentKey);
1000
+ const nextSecretsRef = profile.secretsRef.includes(secretRef)
1001
+ ? profile.secretsRef
1002
+ : [...profile.secretsRef, secretRef];
1003
+ await ctx.db.patch(profile._id, {
1004
+ secretsRef: nextSecretsRef,
1005
+ botIdentity: args.botIdentity,
1006
+ });
1007
+ const result = await importPlaintextSecretRecord(ctx, {
1008
+ secretRef,
1009
+ plaintextValue: args.plaintextValue,
1010
+ metadata: {
1011
+ ...(args.metadata ?? {}),
1012
+ telegramBotId: args.botIdentity,
1013
+ ...(args.telegramUsername ? { telegramUsername: args.telegramUsername } : {}),
1014
+ },
1015
+ });
1016
+ return {
1017
+ ...result,
1018
+ botIdentity: args.botIdentity,
1019
+ telegramUsername: args.telegramUsername ?? null,
1020
+ };
1021
+ },
1022
+ });
1023
+
1024
+ export const reconcileTelegramBotIdentityForAgent = action({
1025
+ args: {
1026
+ agentKey: v.string(),
1027
+ secretRef: v.optional(v.string()),
1028
+ },
1029
+ returns: v.object({
1030
+ agentKey: v.string(),
1031
+ secretRef: v.union(v.null(), v.string()),
1032
+ botIdentity: v.string(),
1033
+ telegramUsername: v.union(v.null(), v.string()),
1034
+ }),
1035
+ handler: async (ctx, args) => {
1036
+ const secretRef = args.secretRef?.trim() || `telegram.botToken.${args.agentKey.trim()}`;
1037
+ const token = await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
1038
+ secretRef,
1039
+ });
1040
+ if (!token) {
1041
+ throw new Error(`Missing Telegram token. Import an active '${secretRef}' secret first.`);
1042
+ }
1043
+ const telegramBot = await fetchTelegramBotProfile(token);
1044
+ await ctx.runMutation(internal.identity.syncAgentProfileTelegramBotIdentity, {
1045
+ agentKey: args.agentKey,
1046
+ botIdentity: telegramBot.botIdentity,
1047
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
1048
+ });
1049
+ return {
1050
+ agentKey: args.agentKey,
1051
+ secretRef,
1052
+ botIdentity: telegramBot.botIdentity,
1053
+ telegramUsername: telegramBot.telegramUsername,
1054
+ };
1055
+ },
1056
+ });
1057
+
1058
+ export const softResetTelegramBindingsMissingBotIdentity = mutation({
1059
+ args: {
1060
+ nowMs: v.optional(v.number()),
1061
+ revokeActiveBindings: v.optional(v.boolean()),
1062
+ expirePendingPairings: v.optional(v.boolean()),
1063
+ },
1064
+ returns: v.object({
1065
+ revokedBindings: v.number(),
1066
+ annotatedBindings: v.number(),
1067
+ expiredPairings: v.number(),
1068
+ pendingPairingsMissingBotIdentity: v.number(),
1069
+ legacyBindingsMissingBotIdentity: v.number(),
1070
+ profilesMissingBotIdentity: v.number(),
1071
+ }),
1072
+ handler: async (ctx, args) => {
1073
+ const nowMs = args.nowMs ?? Date.now();
1074
+ const revokeActiveBindings = args.revokeActiveBindings ?? true;
1075
+ const expirePendingPairings = args.expirePendingPairings ?? true;
1076
+ const [bindings, pairingCodes, profiles] = await Promise.all([
1077
+ ctx.db.query("identityBindings").collect(),
1078
+ ctx.db.query("pairingCodes").collect(),
1079
+ ctx.db.query("agentProfiles").collect(),
1080
+ ]);
1081
+ let revokedBindings = 0;
1082
+ let annotatedBindings = 0;
1083
+ let expiredPairings = 0;
1084
+ const legacyBindings = bindings.filter(
1085
+ (binding) => binding.source === "telegram_pairing" && !binding.botIdentity,
1086
+ );
1087
+ for (const binding of legacyBindings) {
1088
+ const nextMetadata = {
1089
+ ...(binding.metadata ?? {}),
1090
+ softResetReason: "missing_bot_identity",
1091
+ softResetAt: String(nowMs),
1092
+ softResetMode: "telegram_bot_identity_v1",
1093
+ };
1094
+ if (revokeActiveBindings && binding.status === "active") {
1095
+ await ctx.db.patch(binding._id, {
1096
+ status: "revoked",
1097
+ revokedAt: nowMs,
1098
+ metadata: nextMetadata,
1099
+ });
1100
+ revokedBindings += 1;
1101
+ } else {
1102
+ await ctx.db.patch(binding._id, {
1103
+ metadata: nextMetadata,
1104
+ });
1105
+ annotatedBindings += 1;
1106
+ }
1107
+ }
1108
+ const pendingPairingsMissingBotIdentity = pairingCodes.filter(
1109
+ (pairing) => pairing.status === "pending" && !pairing.botIdentity,
1110
+ );
1111
+ for (const pairing of pendingPairingsMissingBotIdentity) {
1112
+ if (expirePendingPairings) {
1113
+ await ctx.db.patch(pairing._id, {
1114
+ status: "expired",
1115
+ });
1116
+ expiredPairings += 1;
1117
+ }
1118
+ }
1119
+ const profilesMissingBotIdentity = profiles.filter((profile) => !profile.botIdentity).length;
1120
+ return {
1121
+ revokedBindings,
1122
+ annotatedBindings,
1123
+ expiredPairings,
1124
+ pendingPairingsMissingBotIdentity: pendingPairingsMissingBotIdentity.length,
1125
+ legacyBindingsMissingBotIdentity: legacyBindings.length,
1126
+ profilesMissingBotIdentity,
1127
+ };
1128
+ },
1129
+ });
1130
+
855
1131
  function generatePairingCode() {
856
1132
  return Math.random().toString(36).slice(2, 12).toUpperCase();
857
1133
  }
@@ -1007,6 +1283,12 @@ async function buildUserAgentDetails(
1007
1283
  pairings.find((pairing) => pairing.status === "pending") ?? null;
1008
1284
  const telegramTokenSecretRef = resolveTelegramSecretRef(profile, agentKey);
1009
1285
  const telegramUsername = deriveTelegramUsername(latestBinding, telegramTokenSecretRef);
1286
+ const botIdentity =
1287
+ profile?.botIdentity?.trim() ||
1288
+ latestBinding?.botIdentity?.trim() ||
1289
+ latestPairing?.botIdentity?.trim() ||
1290
+ latestPendingPairing?.botIdentity?.trim() ||
1291
+ null;
1010
1292
  const displayName = deriveDisplayName(latestBinding);
1011
1293
  const status = deriveUserAgentStatus({
1012
1294
  latestBinding,
@@ -1018,6 +1300,7 @@ async function buildUserAgentDetails(
1018
1300
  latestBinding,
1019
1301
  latestPairing,
1020
1302
  latestPendingPairing,
1303
+ botIdentity,
1021
1304
  telegramTokenSecretRef,
1022
1305
  telegramUsername,
1023
1306
  displayName,
@@ -1117,6 +1400,75 @@ async function fetchTelegramWebhookInfo(token: string) {
1117
1400
  };
1118
1401
  }
1119
1402
 
1403
+ async function fetchTelegramBotProfile(token: string) {
1404
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(token)}`;
1405
+ const response = await fetch(`${telegramApiBaseUrl}/getMe`);
1406
+ const payload = (await response.json().catch(() => ({}))) as {
1407
+ ok?: boolean;
1408
+ description?: string;
1409
+ result?: {
1410
+ id?: number | string;
1411
+ username?: string;
1412
+ };
1413
+ };
1414
+ if (!response.ok || payload.ok !== true) {
1415
+ throw new Error(
1416
+ `Telegram getMe failed: ${
1417
+ typeof payload.description === "string" ? payload.description : "unknown error"
1418
+ }`,
1419
+ );
1420
+ }
1421
+ const rawBotIdentity = payload.result?.id;
1422
+ const botIdentity =
1423
+ rawBotIdentity === undefined || rawBotIdentity === null ? "" : String(rawBotIdentity).trim();
1424
+ if (!botIdentity) {
1425
+ throw new Error("Telegram getMe did not return a bot id");
1426
+ }
1427
+ const telegramUsername =
1428
+ typeof payload.result?.username === "string" && payload.result.username.trim().length > 0
1429
+ ? payload.result.username.trim()
1430
+ : null;
1431
+ return {
1432
+ botIdentity,
1433
+ telegramUsername,
1434
+ };
1435
+ }
1436
+
1437
+ function buildTelegramWebhookSecretToken(botIdentity: string) {
1438
+ return `${TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX}${botIdentity}`;
1439
+ }
1440
+
1441
+ export function parseTelegramWebhookSecretToken(secretToken: string | null | undefined) {
1442
+ const value = secretToken?.trim() ?? "";
1443
+ const prefix = value.startsWith(TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX)
1444
+ ? TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX
1445
+ : value.startsWith(LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX)
1446
+ ? LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX
1447
+ : null;
1448
+ if (!prefix) {
1449
+ return null;
1450
+ }
1451
+ const botIdentity = value.slice(prefix.length).trim();
1452
+ return botIdentity.length > 0 ? botIdentity : null;
1453
+ }
1454
+
1455
+ async function ensureUniqueBotIdentityForAgent(
1456
+ ctx: QueryCtx | MutationCtx,
1457
+ agentKey: string,
1458
+ botIdentity: string,
1459
+ ) {
1460
+ const collisions = await ctx.db
1461
+ .query("agentProfiles")
1462
+ .withIndex("by_botIdentity", (q) => q.eq("botIdentity", botIdentity))
1463
+ .collect();
1464
+ const conflictingProfile = collisions.find((profile) => profile.agentKey !== agentKey) ?? null;
1465
+ if (conflictingProfile) {
1466
+ throw new Error(
1467
+ `Telegram bot identity '${botIdentity}' is already assigned to agent '${conflictingProfile.agentKey}'`,
1468
+ );
1469
+ }
1470
+ }
1471
+
1120
1472
  function encryptSecretValue(plaintext: string): string {
1121
1473
  const units = Array.from(plaintext);
1122
1474
  return units
@@ -1148,6 +1500,12 @@ async function createPairingCodeRecord(
1148
1500
  if (!profile || !profile.enabled) {
1149
1501
  throw new Error(`Agent profile '${args.agentKey}' not found or disabled`);
1150
1502
  }
1503
+ const botIdentity = profile.botIdentity?.trim() || null;
1504
+ if (!botIdentity) {
1505
+ throw new Error(
1506
+ `Agent '${args.agentKey}' is missing botIdentity. Import and verify the Telegram token first.`,
1507
+ );
1508
+ }
1151
1509
 
1152
1510
  const pendingCodes = await ctx.db
1153
1511
  .query("pairingCodes")
@@ -1183,6 +1541,7 @@ async function createPairingCodeRecord(
1183
1541
  code,
1184
1542
  consumerUserId: args.consumerUserId,
1185
1543
  agentKey: args.agentKey,
1544
+ botIdentity,
1186
1545
  status: "pending",
1187
1546
  createdAt: nowMs,
1188
1547
  expiresAt,
@@ -1192,6 +1551,7 @@ async function createPairingCodeRecord(
1192
1551
  code,
1193
1552
  consumerUserId: args.consumerUserId,
1194
1553
  agentKey: args.agentKey,
1554
+ botIdentity,
1195
1555
  status: "pending" as const,
1196
1556
  createdAt: nowMs,
1197
1557
  expiresAt,
@@ -1311,12 +1671,16 @@ async function buildTelegramAgentReadiness(
1311
1671
  : false;
1312
1672
  const webhookReady =
1313
1673
  hasTelegramToken &&
1674
+ details.botIdentity !== null &&
1314
1675
  details.latestBinding?.status === "active" &&
1315
1676
  details.latestBinding.source === "telegram_pairing";
1316
1677
  const issues = [...providerReadiness.issues];
1317
1678
  if (!hasTelegramToken) {
1318
1679
  issues.push("missing_telegram_token");
1319
1680
  }
1681
+ if (!details.botIdentity) {
1682
+ issues.push("missing_bot_identity");
1683
+ }
1320
1684
  if (!webhookReady) {
1321
1685
  issues.push("webhook_not_verified");
1322
1686
  }
@@ -1342,6 +1706,10 @@ async function upsertBinding(
1342
1706
  if (!profile) {
1343
1707
  throw new Error(`Agent profile '${args.agentKey}' not found`);
1344
1708
  }
1709
+ const botIdentity = args.botIdentity?.trim() || profile.botIdentity?.trim() || null;
1710
+ if ((args.telegramUserId || args.telegramChatId) && !botIdentity) {
1711
+ throw new Error(`Agent '${args.agentKey}' is missing botIdentity`);
1712
+ }
1345
1713
 
1346
1714
  const activeForUser = await ctx.db
1347
1715
  .query("identityBindings")
@@ -1353,7 +1721,22 @@ async function upsertBinding(
1353
1721
  await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1354
1722
  }
1355
1723
 
1356
- if (args.telegramUserId) {
1724
+ if (args.telegramUserId && botIdentity) {
1725
+ const byTelegramUser = await ctx.db
1726
+ .query("identityBindings")
1727
+ .withIndex("by_botIdentity_and_telegramUserId_and_status", (q) =>
1728
+ q
1729
+ .eq("botIdentity", botIdentity)
1730
+ .eq("telegramUserId", args.telegramUserId)
1731
+ .eq("status", "active"),
1732
+ )
1733
+ .collect();
1734
+ for (const row of byTelegramUser) {
1735
+ if (row.consumerUserId !== args.consumerUserId) {
1736
+ await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1737
+ }
1738
+ }
1739
+ } else if (args.telegramUserId) {
1357
1740
  const byTelegramUser = await ctx.db
1358
1741
  .query("identityBindings")
1359
1742
  .withIndex("by_telegramUserId_and_status", (q) =>
@@ -1367,7 +1750,22 @@ async function upsertBinding(
1367
1750
  }
1368
1751
  }
1369
1752
 
1370
- if (args.telegramChatId) {
1753
+ if (args.telegramChatId && botIdentity) {
1754
+ const byTelegramChat = await ctx.db
1755
+ .query("identityBindings")
1756
+ .withIndex("by_botIdentity_and_telegramChatId_and_status", (q) =>
1757
+ q
1758
+ .eq("botIdentity", botIdentity)
1759
+ .eq("telegramChatId", args.telegramChatId)
1760
+ .eq("status", "active"),
1761
+ )
1762
+ .collect();
1763
+ for (const row of byTelegramChat) {
1764
+ if (row.consumerUserId !== args.consumerUserId) {
1765
+ await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1766
+ }
1767
+ }
1768
+ } else if (args.telegramChatId) {
1371
1769
  const byTelegramChat = await ctx.db
1372
1770
  .query("identityBindings")
1373
1771
  .withIndex("by_telegramChatId_and_status", (q) =>
@@ -1385,6 +1783,7 @@ async function upsertBinding(
1385
1783
  consumerUserId: args.consumerUserId,
1386
1784
  agentKey: args.agentKey,
1387
1785
  conversationId: buildUserAgentConversationId(args.consumerUserId, args.agentKey),
1786
+ botIdentity: botIdentity ?? undefined,
1388
1787
  status: "active",
1389
1788
  source: args.source ?? "api",
1390
1789
  telegramUserId: args.telegramUserId,
@@ -1402,6 +1801,7 @@ async function upsertBinding(
1402
1801
  consumerUserId: created.consumerUserId,
1403
1802
  agentKey: created.agentKey,
1404
1803
  conversationId: created.conversationId,
1804
+ botIdentity: created.botIdentity ?? null,
1405
1805
  status: created.status,
1406
1806
  source: created.source,
1407
1807
  telegramUserId: created.telegramUserId ?? null,