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