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