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