@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,6 +1,6 @@
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
  const bindingStatusValidator = v.union(v.literal("active"), v.literal("revoked"));
5
5
  const bindingSourceValidator = v.union(v.literal("manual"), v.literal("telegram_pairing"), v.literal("api"));
6
6
  const pairingStatusValidator = v.union(v.literal("pending"), v.literal("used"), v.literal("expired"));
@@ -9,6 +9,7 @@ const bindingViewValidator = v.object({
9
9
  consumerUserId: v.string(),
10
10
  agentKey: v.string(),
11
11
  conversationId: v.string(),
12
+ botIdentity: v.union(v.null(), v.string()),
12
13
  status: bindingStatusValidator,
13
14
  source: bindingSourceValidator,
14
15
  telegramUserId: v.union(v.null(), v.string()),
@@ -21,6 +22,7 @@ const pairingCodeViewValidator = v.object({
21
22
  code: v.string(),
22
23
  consumerUserId: v.string(),
23
24
  agentKey: v.string(),
25
+ botIdentity: v.union(v.null(), v.string()),
24
26
  status: pairingStatusValidator,
25
27
  createdAt: v.number(),
26
28
  expiresAt: v.number(),
@@ -32,6 +34,8 @@ const telegramWebhookStatusValidator = v.object({
32
34
  ok: v.boolean(),
33
35
  webhookUrl: v.string(),
34
36
  currentUrl: v.union(v.null(), v.string()),
37
+ botIdentity: v.union(v.null(), v.string()),
38
+ secretTokenConfigured: v.boolean(),
35
39
  isReady: v.boolean(),
36
40
  pendingUpdateCount: v.number(),
37
41
  lastErrorMessage: v.union(v.null(), v.string()),
@@ -70,6 +74,7 @@ const webhookReadinessValidator = v.object({
70
74
  const onboardingStateValidator = v.object({
71
75
  agentKey: v.string(),
72
76
  telegramUsername: v.union(v.null(), v.string()),
77
+ botIdentity: v.union(v.null(), v.string()),
73
78
  tokenSecretRef: v.union(v.null(), v.string()),
74
79
  tokenImported: v.boolean(),
75
80
  webhookReady: v.boolean(),
@@ -86,10 +91,13 @@ const operationalReadinessValidator = v.object({
86
91
  workerRuntimeConfigPresent: v.boolean(),
87
92
  issues: v.array(v.string()),
88
93
  });
94
+ const TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX = "af_v1_";
95
+ const LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX = "af:v1:";
89
96
  export const configureTelegramWebhook = action({
90
97
  args: {
91
98
  convexSiteUrl: v.string(),
92
99
  secretRef: v.optional(v.string()),
100
+ agentKey: v.optional(v.string()),
93
101
  },
94
102
  returns: telegramWebhookStatusValidator,
95
103
  handler: async (ctx, args) => {
@@ -112,6 +120,16 @@ export const configureTelegramWebhook = action({
112
120
  }
113
121
  const webhookUrl = `${normalizedSiteUrl}/agent-factory/telegram/webhook`;
114
122
  const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(token)}`;
123
+ const telegramBot = await fetchTelegramBotProfile(token);
124
+ const webhookSecretToken = buildTelegramWebhookSecretToken(telegramBot.botIdentity);
125
+ const agentKey = args.agentKey?.trim();
126
+ if (agentKey) {
127
+ await ctx.runMutation(internal.identity.syncAgentProfileTelegramBotIdentity, {
128
+ agentKey,
129
+ botIdentity: telegramBot.botIdentity,
130
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
131
+ });
132
+ }
115
133
  const setWebhookResponse = await fetch(`${telegramApiBaseUrl}/setWebhook`, {
116
134
  method: "POST",
117
135
  headers: {
@@ -119,6 +137,7 @@ export const configureTelegramWebhook = action({
119
137
  },
120
138
  body: JSON.stringify({
121
139
  url: webhookUrl,
140
+ secret_token: webhookSecretToken,
122
141
  }),
123
142
  });
124
143
  const setWebhookPayload = (await setWebhookResponse.json().catch(() => ({})));
@@ -151,6 +170,8 @@ export const configureTelegramWebhook = action({
151
170
  ok: true,
152
171
  webhookUrl,
153
172
  currentUrl,
173
+ botIdentity: telegramBot.botIdentity,
174
+ secretTokenConfigured: true,
154
175
  isReady,
155
176
  pendingUpdateCount,
156
177
  lastErrorMessage,
@@ -174,6 +195,7 @@ export const createPairingCode = mutation({
174
195
  export const consumePairingCode = mutation({
175
196
  args: {
176
197
  code: v.string(),
198
+ botIdentity: v.optional(v.string()),
177
199
  telegramUserId: v.string(),
178
200
  telegramChatId: v.string(),
179
201
  nowMs: v.optional(v.number()),
@@ -195,9 +217,17 @@ export const consumePairingCode = mutation({
195
217
  await ctx.db.patch(pairing._id, { status: "expired" });
196
218
  throw new Error("Pairing code expired");
197
219
  }
220
+ const providedBotIdentity = args.botIdentity?.trim() || null;
221
+ if (!providedBotIdentity) {
222
+ throw new Error("Missing bot identity for Telegram pairing");
223
+ }
224
+ if (pairing.botIdentity && pairing.botIdentity !== providedBotIdentity) {
225
+ throw new Error("Pairing code belongs to a different Telegram bot");
226
+ }
198
227
  await upsertBinding(ctx, {
199
228
  consumerUserId: pairing.consumerUserId,
200
229
  agentKey: pairing.agentKey,
230
+ botIdentity: providedBotIdentity,
201
231
  source: "telegram_pairing",
202
232
  telegramUserId: args.telegramUserId,
203
233
  telegramChatId: args.telegramChatId,
@@ -206,6 +236,7 @@ export const consumePairingCode = mutation({
206
236
  await ctx.db.patch(pairing._id, {
207
237
  status: "used",
208
238
  usedAt: nowMs,
239
+ botIdentity: providedBotIdentity,
209
240
  telegramUserId: args.telegramUserId,
210
241
  telegramChatId: args.telegramChatId,
211
242
  });
@@ -213,6 +244,7 @@ export const consumePairingCode = mutation({
213
244
  code: pairing.code,
214
245
  consumerUserId: pairing.consumerUserId,
215
246
  agentKey: pairing.agentKey,
247
+ botIdentity: providedBotIdentity,
216
248
  status: "used",
217
249
  createdAt: pairing.createdAt,
218
250
  expiresAt: pairing.expiresAt,
@@ -241,6 +273,7 @@ export const getPairingCodeStatus = query({
241
273
  code: pairing.code,
242
274
  consumerUserId: pairing.consumerUserId,
243
275
  agentKey: pairing.agentKey,
276
+ botIdentity: pairing.botIdentity ?? null,
244
277
  status: "expired",
245
278
  createdAt: pairing.createdAt,
246
279
  expiresAt: pairing.expiresAt,
@@ -253,6 +286,7 @@ export const getPairingCodeStatus = query({
253
286
  code: pairing.code,
254
287
  consumerUserId: pairing.consumerUserId,
255
288
  agentKey: pairing.agentKey,
289
+ botIdentity: pairing.botIdentity ?? null,
256
290
  status: pairing.status,
257
291
  createdAt: pairing.createdAt,
258
292
  expiresAt: pairing.expiresAt,
@@ -266,6 +300,7 @@ export const bindUserAgent = mutation({
266
300
  args: {
267
301
  consumerUserId: v.string(),
268
302
  agentKey: v.string(),
303
+ botIdentity: v.optional(v.string()),
269
304
  source: v.optional(bindingSourceValidator),
270
305
  telegramUserId: v.optional(v.string()),
271
306
  telegramChatId: v.optional(v.string()),
@@ -318,6 +353,7 @@ export const resolveAgentForUser = query({
318
353
  });
319
354
  export const resolveAgentForTelegram = query({
320
355
  args: {
356
+ botIdentity: v.optional(v.string()),
321
357
  telegramUserId: v.optional(v.string()),
322
358
  telegramChatId: v.optional(v.string()),
323
359
  },
@@ -327,8 +363,41 @@ export const resolveAgentForTelegram = query({
327
363
  conversationId: v.union(v.null(), v.string()),
328
364
  }),
329
365
  handler: async (ctx, args) => {
366
+ const botIdentity = args.botIdentity?.trim() || null;
330
367
  let active = null;
331
- if (args.telegramUserId) {
368
+ if (botIdentity && args.telegramUserId) {
369
+ const byUser = await ctx.db
370
+ .query("identityBindings")
371
+ .withIndex("by_botIdentity_and_telegramUserId_and_status", (q) => q
372
+ .eq("botIdentity", botIdentity)
373
+ .eq("telegramUserId", args.telegramUserId)
374
+ .eq("status", "active"))
375
+ .first();
376
+ if (byUser) {
377
+ active = {
378
+ consumerUserId: byUser.consumerUserId,
379
+ agentKey: byUser.agentKey,
380
+ conversationId: byUser.conversationId,
381
+ };
382
+ }
383
+ }
384
+ if (!active && botIdentity && args.telegramChatId) {
385
+ const byChat = await ctx.db
386
+ .query("identityBindings")
387
+ .withIndex("by_botIdentity_and_telegramChatId_and_status", (q) => q
388
+ .eq("botIdentity", botIdentity)
389
+ .eq("telegramChatId", args.telegramChatId)
390
+ .eq("status", "active"))
391
+ .first();
392
+ if (byChat) {
393
+ active = {
394
+ consumerUserId: byChat.consumerUserId,
395
+ agentKey: byChat.agentKey,
396
+ conversationId: byChat.conversationId,
397
+ };
398
+ }
399
+ }
400
+ if (!active && !botIdentity && args.telegramUserId) {
332
401
  const byUser = await ctx.db
333
402
  .query("identityBindings")
334
403
  .withIndex("by_telegramUserId_and_status", (q) => q.eq("telegramUserId", args.telegramUserId).eq("status", "active"))
@@ -341,7 +410,7 @@ export const resolveAgentForTelegram = query({
341
410
  };
342
411
  }
343
412
  }
344
- if (!active && args.telegramChatId) {
413
+ if (!active && !botIdentity && args.telegramChatId) {
345
414
  const byChat = await ctx.db
346
415
  .query("identityBindings")
347
416
  .withIndex("by_telegramChatId_and_status", (q) => q.eq("telegramChatId", args.telegramChatId).eq("status", "active"))
@@ -378,6 +447,7 @@ export const getUserAgentBinding = query({
378
447
  consumerUserId: active.consumerUserId,
379
448
  agentKey: active.agentKey,
380
449
  conversationId: active.conversationId,
450
+ botIdentity: active.botIdentity ?? null,
381
451
  status: active.status,
382
452
  source: active.source,
383
453
  telegramUserId: active.telegramUserId ?? null,
@@ -476,6 +546,7 @@ export const getUserAgentPairingStatus = query({
476
546
  code: pairing.code,
477
547
  consumerUserId: pairing.consumerUserId,
478
548
  agentKey: pairing.agentKey,
549
+ botIdentity: pairing.botIdentity ?? null,
479
550
  status: pairing.status === "pending" && pairing.expiresAt <= nowMs
480
551
  ? "expired"
481
552
  : pairing.status,
@@ -489,7 +560,7 @@ export const getUserAgentPairingStatus = query({
489
560
  return null;
490
561
  },
491
562
  });
492
- export const importTelegramTokenForAgent = mutation({
563
+ export const importTelegramTokenForAgent = action({
493
564
  args: {
494
565
  consumerUserId: v.string(),
495
566
  agentKey: v.string(),
@@ -500,29 +571,22 @@ export const importTelegramTokenForAgent = mutation({
500
571
  secretId: v.id("secrets"),
501
572
  secretRef: v.string(),
502
573
  version: v.number(),
574
+ botIdentity: v.string(),
575
+ telegramUsername: v.union(v.null(), v.string()),
503
576
  }),
504
577
  handler: async (ctx, args) => {
505
- const details = await buildUserAgentDetails(ctx, args.consumerUserId, args.agentKey, Date.now());
506
- if (details.latestBinding === null && details.latestPairing === null) {
507
- throw new Error("Agent is not yet associated with the provided consumerUserId");
578
+ const plaintextValue = args.plaintextValue.trim();
579
+ if (!plaintextValue) {
580
+ throw new Error("Telegram token is required");
508
581
  }
509
- const profile = await ctx.db
510
- .query("agentProfiles")
511
- .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
512
- .unique();
513
- if (!profile) {
514
- throw new Error(`Agent profile '${args.agentKey}' not found`);
515
- }
516
- const secretRef = resolveTelegramSecretRef(profile, args.agentKey);
517
- if (!profile.secretsRef.includes(secretRef)) {
518
- await ctx.db.patch(profile._id, {
519
- secretsRef: [...profile.secretsRef, secretRef],
520
- });
521
- }
522
- return await importPlaintextSecretRecord(ctx, {
523
- secretRef,
524
- plaintextValue: args.plaintextValue,
582
+ const telegramBot = await fetchTelegramBotProfile(plaintextValue);
583
+ return await ctx.runMutation(internal.identity.persistImportedTelegramTokenForAgent, {
584
+ consumerUserId: args.consumerUserId,
585
+ agentKey: args.agentKey,
586
+ plaintextValue,
525
587
  metadata: args.metadata,
588
+ botIdentity: telegramBot.botIdentity,
589
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
526
590
  });
527
591
  },
528
592
  });
@@ -592,6 +656,7 @@ export const getUserAgentOnboardingState = query({
592
656
  ? "expired"
593
657
  : "pending";
594
658
  const webhookReady = tokenImported &&
659
+ details.botIdentity !== null &&
595
660
  details.latestBinding?.status === "active" &&
596
661
  details.latestBinding.source === "telegram_pairing";
597
662
  const pairingCode = pairingStatus === "pending" && details.latestPendingPairing !== null
@@ -599,16 +664,19 @@ export const getUserAgentOnboardingState = query({
599
664
  : null;
600
665
  const nextAction = !tokenImported
601
666
  ? "import_token"
602
- : !webhookReady
603
- ? "configure_webhook"
604
- : pairingStatus === "pending"
605
- ? "complete_pairing"
606
- : pairingStatus === "used"
607
- ? "ready"
608
- : "create_pairing";
667
+ : details.botIdentity === null
668
+ ? "import_token"
669
+ : !webhookReady
670
+ ? "configure_webhook"
671
+ : pairingStatus === "pending"
672
+ ? "complete_pairing"
673
+ : pairingStatus === "used"
674
+ ? "ready"
675
+ : "create_pairing";
609
676
  const result = {
610
677
  agentKey: args.agentKey,
611
678
  telegramUsername: details.telegramUsername,
679
+ botIdentity: details.botIdentity,
612
680
  tokenSecretRef: details.telegramTokenSecretRef,
613
681
  tokenImported,
614
682
  webhookReady,
@@ -664,6 +732,188 @@ export const getWebhookReadiness = action({
664
732
  };
665
733
  },
666
734
  });
735
+ export const syncAgentProfileTelegramBotIdentity = internalMutation({
736
+ args: {
737
+ agentKey: v.string(),
738
+ botIdentity: v.string(),
739
+ telegramUsername: v.optional(v.string()),
740
+ },
741
+ returns: v.null(),
742
+ handler: async (ctx, args) => {
743
+ const agentKey = args.agentKey.trim();
744
+ const botIdentity = args.botIdentity.trim();
745
+ if (!agentKey || !botIdentity) {
746
+ throw new Error("agentKey and botIdentity are required");
747
+ }
748
+ const profile = await ctx.db
749
+ .query("agentProfiles")
750
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", agentKey))
751
+ .unique();
752
+ if (!profile) {
753
+ throw new Error(`Agent profile '${agentKey}' not found`);
754
+ }
755
+ await ensureUniqueBotIdentityForAgent(ctx, agentKey, botIdentity);
756
+ await ctx.db.patch(profile._id, {
757
+ botIdentity,
758
+ });
759
+ return null;
760
+ },
761
+ });
762
+ export const persistImportedTelegramTokenForAgent = internalMutation({
763
+ args: {
764
+ consumerUserId: v.string(),
765
+ agentKey: v.string(),
766
+ plaintextValue: v.string(),
767
+ metadata: v.optional(v.record(v.string(), v.string())),
768
+ botIdentity: v.string(),
769
+ telegramUsername: v.optional(v.string()),
770
+ },
771
+ returns: v.object({
772
+ secretId: v.id("secrets"),
773
+ secretRef: v.string(),
774
+ version: v.number(),
775
+ botIdentity: v.string(),
776
+ telegramUsername: v.union(v.null(), v.string()),
777
+ }),
778
+ handler: async (ctx, args) => {
779
+ const details = await buildUserAgentDetails(ctx, args.consumerUserId, args.agentKey, Date.now());
780
+ if (details.latestBinding === null && details.latestPairing === null) {
781
+ throw new Error("Agent is not yet associated with the provided consumerUserId");
782
+ }
783
+ const profile = await ctx.db
784
+ .query("agentProfiles")
785
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
786
+ .unique();
787
+ if (!profile) {
788
+ throw new Error(`Agent profile '${args.agentKey}' not found`);
789
+ }
790
+ await ensureUniqueBotIdentityForAgent(ctx, args.agentKey, args.botIdentity);
791
+ const secretRef = resolveTelegramSecretRef(profile, args.agentKey);
792
+ const nextSecretsRef = profile.secretsRef.includes(secretRef)
793
+ ? profile.secretsRef
794
+ : [...profile.secretsRef, secretRef];
795
+ await ctx.db.patch(profile._id, {
796
+ secretsRef: nextSecretsRef,
797
+ botIdentity: args.botIdentity,
798
+ });
799
+ const result = await importPlaintextSecretRecord(ctx, {
800
+ secretRef,
801
+ plaintextValue: args.plaintextValue,
802
+ metadata: {
803
+ ...(args.metadata ?? {}),
804
+ telegramBotId: args.botIdentity,
805
+ ...(args.telegramUsername ? { telegramUsername: args.telegramUsername } : {}),
806
+ },
807
+ });
808
+ return {
809
+ ...result,
810
+ botIdentity: args.botIdentity,
811
+ telegramUsername: args.telegramUsername ?? null,
812
+ };
813
+ },
814
+ });
815
+ export const reconcileTelegramBotIdentityForAgent = action({
816
+ args: {
817
+ agentKey: v.string(),
818
+ secretRef: v.optional(v.string()),
819
+ },
820
+ returns: v.object({
821
+ agentKey: v.string(),
822
+ secretRef: v.union(v.null(), v.string()),
823
+ botIdentity: v.string(),
824
+ telegramUsername: v.union(v.null(), v.string()),
825
+ }),
826
+ handler: async (ctx, args) => {
827
+ const secretRef = args.secretRef?.trim() || `telegram.botToken.${args.agentKey.trim()}`;
828
+ const token = await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
829
+ secretRef,
830
+ });
831
+ if (!token) {
832
+ throw new Error(`Missing Telegram token. Import an active '${secretRef}' secret first.`);
833
+ }
834
+ const telegramBot = await fetchTelegramBotProfile(token);
835
+ await ctx.runMutation(internal.identity.syncAgentProfileTelegramBotIdentity, {
836
+ agentKey: args.agentKey,
837
+ botIdentity: telegramBot.botIdentity,
838
+ telegramUsername: telegramBot.telegramUsername ?? undefined,
839
+ });
840
+ return {
841
+ agentKey: args.agentKey,
842
+ secretRef,
843
+ botIdentity: telegramBot.botIdentity,
844
+ telegramUsername: telegramBot.telegramUsername,
845
+ };
846
+ },
847
+ });
848
+ export const softResetTelegramBindingsMissingBotIdentity = mutation({
849
+ args: {
850
+ nowMs: v.optional(v.number()),
851
+ revokeActiveBindings: v.optional(v.boolean()),
852
+ expirePendingPairings: v.optional(v.boolean()),
853
+ },
854
+ returns: v.object({
855
+ revokedBindings: v.number(),
856
+ annotatedBindings: v.number(),
857
+ expiredPairings: v.number(),
858
+ pendingPairingsMissingBotIdentity: v.number(),
859
+ legacyBindingsMissingBotIdentity: v.number(),
860
+ profilesMissingBotIdentity: v.number(),
861
+ }),
862
+ handler: async (ctx, args) => {
863
+ const nowMs = args.nowMs ?? Date.now();
864
+ const revokeActiveBindings = args.revokeActiveBindings ?? true;
865
+ const expirePendingPairings = args.expirePendingPairings ?? true;
866
+ const [bindings, pairingCodes, profiles] = await Promise.all([
867
+ ctx.db.query("identityBindings").collect(),
868
+ ctx.db.query("pairingCodes").collect(),
869
+ ctx.db.query("agentProfiles").collect(),
870
+ ]);
871
+ let revokedBindings = 0;
872
+ let annotatedBindings = 0;
873
+ let expiredPairings = 0;
874
+ const legacyBindings = bindings.filter((binding) => binding.source === "telegram_pairing" && !binding.botIdentity);
875
+ for (const binding of legacyBindings) {
876
+ const nextMetadata = {
877
+ ...(binding.metadata ?? {}),
878
+ softResetReason: "missing_bot_identity",
879
+ softResetAt: String(nowMs),
880
+ softResetMode: "telegram_bot_identity_v1",
881
+ };
882
+ if (revokeActiveBindings && binding.status === "active") {
883
+ await ctx.db.patch(binding._id, {
884
+ status: "revoked",
885
+ revokedAt: nowMs,
886
+ metadata: nextMetadata,
887
+ });
888
+ revokedBindings += 1;
889
+ }
890
+ else {
891
+ await ctx.db.patch(binding._id, {
892
+ metadata: nextMetadata,
893
+ });
894
+ annotatedBindings += 1;
895
+ }
896
+ }
897
+ const pendingPairingsMissingBotIdentity = pairingCodes.filter((pairing) => pairing.status === "pending" && !pairing.botIdentity);
898
+ for (const pairing of pendingPairingsMissingBotIdentity) {
899
+ if (expirePendingPairings) {
900
+ await ctx.db.patch(pairing._id, {
901
+ status: "expired",
902
+ });
903
+ expiredPairings += 1;
904
+ }
905
+ }
906
+ const profilesMissingBotIdentity = profiles.filter((profile) => !profile.botIdentity).length;
907
+ return {
908
+ revokedBindings,
909
+ annotatedBindings,
910
+ expiredPairings,
911
+ pendingPairingsMissingBotIdentity: pendingPairingsMissingBotIdentity.length,
912
+ legacyBindingsMissingBotIdentity: legacyBindings.length,
913
+ profilesMissingBotIdentity,
914
+ };
915
+ },
916
+ });
667
917
  function generatePairingCode() {
668
918
  return Math.random().toString(36).slice(2, 12).toUpperCase();
669
919
  }
@@ -750,6 +1000,11 @@ async function buildUserAgentDetails(ctx, consumerUserId, agentKey, nowMs) {
750
1000
  const latestPendingPairing = pairings.find((pairing) => pairing.status === "pending") ?? null;
751
1001
  const telegramTokenSecretRef = resolveTelegramSecretRef(profile, agentKey);
752
1002
  const telegramUsername = deriveTelegramUsername(latestBinding, telegramTokenSecretRef);
1003
+ const botIdentity = profile?.botIdentity?.trim() ||
1004
+ latestBinding?.botIdentity?.trim() ||
1005
+ latestPairing?.botIdentity?.trim() ||
1006
+ latestPendingPairing?.botIdentity?.trim() ||
1007
+ null;
753
1008
  const displayName = deriveDisplayName(latestBinding);
754
1009
  const status = deriveUserAgentStatus({
755
1010
  latestBinding,
@@ -761,6 +1016,7 @@ async function buildUserAgentDetails(ctx, consumerUserId, agentKey, nowMs) {
761
1016
  latestBinding,
762
1017
  latestPairing,
763
1018
  latestPendingPairing,
1019
+ botIdentity,
764
1020
  telegramTokenSecretRef,
765
1021
  telegramUsername,
766
1022
  displayName,
@@ -831,6 +1087,52 @@ async function fetchTelegramWebhookInfo(token) {
831
1087
  : null,
832
1088
  };
833
1089
  }
1090
+ async function fetchTelegramBotProfile(token) {
1091
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(token)}`;
1092
+ const response = await fetch(`${telegramApiBaseUrl}/getMe`);
1093
+ const payload = (await response.json().catch(() => ({})));
1094
+ if (!response.ok || payload.ok !== true) {
1095
+ throw new Error(`Telegram getMe failed: ${typeof payload.description === "string" ? payload.description : "unknown error"}`);
1096
+ }
1097
+ const rawBotIdentity = payload.result?.id;
1098
+ const botIdentity = rawBotIdentity === undefined || rawBotIdentity === null ? "" : String(rawBotIdentity).trim();
1099
+ if (!botIdentity) {
1100
+ throw new Error("Telegram getMe did not return a bot id");
1101
+ }
1102
+ const telegramUsername = typeof payload.result?.username === "string" && payload.result.username.trim().length > 0
1103
+ ? payload.result.username.trim()
1104
+ : null;
1105
+ return {
1106
+ botIdentity,
1107
+ telegramUsername,
1108
+ };
1109
+ }
1110
+ function buildTelegramWebhookSecretToken(botIdentity) {
1111
+ return `${TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX}${botIdentity}`;
1112
+ }
1113
+ export function parseTelegramWebhookSecretToken(secretToken) {
1114
+ const value = secretToken?.trim() ?? "";
1115
+ const prefix = value.startsWith(TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX)
1116
+ ? TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX
1117
+ : value.startsWith(LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX)
1118
+ ? LEGACY_TELEGRAM_WEBHOOK_SECRET_TOKEN_PREFIX
1119
+ : null;
1120
+ if (!prefix) {
1121
+ return null;
1122
+ }
1123
+ const botIdentity = value.slice(prefix.length).trim();
1124
+ return botIdentity.length > 0 ? botIdentity : null;
1125
+ }
1126
+ async function ensureUniqueBotIdentityForAgent(ctx, agentKey, botIdentity) {
1127
+ const collisions = await ctx.db
1128
+ .query("agentProfiles")
1129
+ .withIndex("by_botIdentity", (q) => q.eq("botIdentity", botIdentity))
1130
+ .collect();
1131
+ const conflictingProfile = collisions.find((profile) => profile.agentKey !== agentKey) ?? null;
1132
+ if (conflictingProfile) {
1133
+ throw new Error(`Telegram bot identity '${botIdentity}' is already assigned to agent '${conflictingProfile.agentKey}'`);
1134
+ }
1135
+ }
834
1136
  function encryptSecretValue(plaintext) {
835
1137
  const units = Array.from(plaintext);
836
1138
  return units
@@ -852,6 +1154,10 @@ async function createPairingCodeRecord(ctx, args) {
852
1154
  if (!profile || !profile.enabled) {
853
1155
  throw new Error(`Agent profile '${args.agentKey}' not found or disabled`);
854
1156
  }
1157
+ const botIdentity = profile.botIdentity?.trim() || null;
1158
+ if (!botIdentity) {
1159
+ throw new Error(`Agent '${args.agentKey}' is missing botIdentity. Import and verify the Telegram token first.`);
1160
+ }
855
1161
  const pendingCodes = await ctx.db
856
1162
  .query("pairingCodes")
857
1163
  .withIndex("by_consumerUserId_and_status", (q) => q.eq("consumerUserId", args.consumerUserId).eq("status", "pending"))
@@ -881,6 +1187,7 @@ async function createPairingCodeRecord(ctx, args) {
881
1187
  code,
882
1188
  consumerUserId: args.consumerUserId,
883
1189
  agentKey: args.agentKey,
1190
+ botIdentity,
884
1191
  status: "pending",
885
1192
  createdAt: nowMs,
886
1193
  expiresAt,
@@ -889,6 +1196,7 @@ async function createPairingCodeRecord(ctx, args) {
889
1196
  code,
890
1197
  consumerUserId: args.consumerUserId,
891
1198
  agentKey: args.agentKey,
1199
+ botIdentity,
892
1200
  status: "pending",
893
1201
  createdAt: nowMs,
894
1202
  expiresAt,
@@ -984,12 +1292,16 @@ async function buildTelegramAgentReadiness(ctx, args) {
984
1292
  ? await hasActiveSecret(ctx, details.telegramTokenSecretRef)
985
1293
  : false;
986
1294
  const webhookReady = hasTelegramToken &&
1295
+ details.botIdentity !== null &&
987
1296
  details.latestBinding?.status === "active" &&
988
1297
  details.latestBinding.source === "telegram_pairing";
989
1298
  const issues = [...providerReadiness.issues];
990
1299
  if (!hasTelegramToken) {
991
1300
  issues.push("missing_telegram_token");
992
1301
  }
1302
+ if (!details.botIdentity) {
1303
+ issues.push("missing_bot_identity");
1304
+ }
993
1305
  if (!webhookReady) {
994
1306
  issues.push("webhook_not_verified");
995
1307
  }
@@ -1011,6 +1323,10 @@ async function upsertBinding(ctx, args) {
1011
1323
  if (!profile) {
1012
1324
  throw new Error(`Agent profile '${args.agentKey}' not found`);
1013
1325
  }
1326
+ const botIdentity = args.botIdentity?.trim() || profile.botIdentity?.trim() || null;
1327
+ if ((args.telegramUserId || args.telegramChatId) && !botIdentity) {
1328
+ throw new Error(`Agent '${args.agentKey}' is missing botIdentity`);
1329
+ }
1014
1330
  const activeForUser = await ctx.db
1015
1331
  .query("identityBindings")
1016
1332
  .withIndex("by_consumerUserId_and_status", (q) => q.eq("consumerUserId", args.consumerUserId).eq("status", "active"))
@@ -1018,7 +1334,21 @@ async function upsertBinding(ctx, args) {
1018
1334
  for (const row of activeForUser) {
1019
1335
  await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1020
1336
  }
1021
- if (args.telegramUserId) {
1337
+ if (args.telegramUserId && botIdentity) {
1338
+ const byTelegramUser = await ctx.db
1339
+ .query("identityBindings")
1340
+ .withIndex("by_botIdentity_and_telegramUserId_and_status", (q) => q
1341
+ .eq("botIdentity", botIdentity)
1342
+ .eq("telegramUserId", args.telegramUserId)
1343
+ .eq("status", "active"))
1344
+ .collect();
1345
+ for (const row of byTelegramUser) {
1346
+ if (row.consumerUserId !== args.consumerUserId) {
1347
+ await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1348
+ }
1349
+ }
1350
+ }
1351
+ else if (args.telegramUserId) {
1022
1352
  const byTelegramUser = await ctx.db
1023
1353
  .query("identityBindings")
1024
1354
  .withIndex("by_telegramUserId_and_status", (q) => q.eq("telegramUserId", args.telegramUserId).eq("status", "active"))
@@ -1029,7 +1359,21 @@ async function upsertBinding(ctx, args) {
1029
1359
  }
1030
1360
  }
1031
1361
  }
1032
- if (args.telegramChatId) {
1362
+ if (args.telegramChatId && botIdentity) {
1363
+ const byTelegramChat = await ctx.db
1364
+ .query("identityBindings")
1365
+ .withIndex("by_botIdentity_and_telegramChatId_and_status", (q) => q
1366
+ .eq("botIdentity", botIdentity)
1367
+ .eq("telegramChatId", args.telegramChatId)
1368
+ .eq("status", "active"))
1369
+ .collect();
1370
+ for (const row of byTelegramChat) {
1371
+ if (row.consumerUserId !== args.consumerUserId) {
1372
+ await ctx.db.patch(row._id, { status: "revoked", revokedAt: nowMs });
1373
+ }
1374
+ }
1375
+ }
1376
+ else if (args.telegramChatId) {
1033
1377
  const byTelegramChat = await ctx.db
1034
1378
  .query("identityBindings")
1035
1379
  .withIndex("by_telegramChatId_and_status", (q) => q.eq("telegramChatId", args.telegramChatId).eq("status", "active"))
@@ -1044,6 +1388,7 @@ async function upsertBinding(ctx, args) {
1044
1388
  consumerUserId: args.consumerUserId,
1045
1389
  agentKey: args.agentKey,
1046
1390
  conversationId: buildUserAgentConversationId(args.consumerUserId, args.agentKey),
1391
+ botIdentity: botIdentity ?? undefined,
1047
1392
  status: "active",
1048
1393
  source: args.source ?? "api",
1049
1394
  telegramUserId: args.telegramUserId,
@@ -1059,6 +1404,7 @@ async function upsertBinding(ctx, args) {
1059
1404
  consumerUserId: created.consumerUserId,
1060
1405
  agentKey: created.agentKey,
1061
1406
  conversationId: created.conversationId,
1407
+ botIdentity: created.botIdentity ?? null,
1062
1408
  status: created.status,
1063
1409
  source: created.source,
1064
1410
  telegramUserId: created.telegramUserId ?? null,