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