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