@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.
- package/README.md +50 -0
- package/dist/client/bridge.d.ts +1 -0
- package/dist/client/bridge.d.ts.map +1 -1
- package/dist/client/bridge.js.map +1 -1
- package/dist/client/index.d.ts +26 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +43 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +80 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/identity.d.ts +60 -2
- package/dist/component/identity.d.ts.map +1 -1
- package/dist/component/identity.js +378 -32
- package/dist/component/identity.js.map +1 -1
- package/dist/component/lib.d.ts +1 -1
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +1 -1
- package/dist/component/lib.js.map +1 -1
- package/dist/component/queue.d.ts +11 -9
- package/dist/component/queue.d.ts.map +1 -1
- package/dist/component/queue.js +3 -0
- package/dist/component/queue.js.map +1 -1
- package/dist/component/schema.d.ts +38 -28
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +15 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +5 -1
- package/src/client/bridge.ts +1 -0
- package/src/client/index.ts +52 -3
- package/src/component/_generated/component.ts +116 -8
- package/src/component/identity.ts +431 -31
- package/src/component/lib.test.ts +8 -0
- package/src/component/lib.ts +2 -0
- package/src/component/queue.ts +5 -0
- package/src/component/schema.ts +15 -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,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 =
|
|
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 (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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,
|