-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathadmin-tools.ts
More file actions
142 lines (123 loc) · 4.3 KB
/
Copy pathadmin-tools.ts
File metadata and controls
142 lines (123 loc) · 4.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import {
type Db,
getDecryptedSecret,
setEncryptedSecret,
withTenant,
} from "@chatman-media/conversation-engine";
import { tenantSecrets } from "@chatman-media/storage";
import { and, eq } from "drizzle-orm";
import { Hono } from "hono";
/**
* Per-tenant agentic tool configuration endpoints.
*
* Сейчас поддерживается один тип tool: "booking_link" (Calendly / Cal.com / любой URL).
* Ссылка бронирования хранится в tenant_secrets.key = "tool_booking_url" в виде
* plaintext (не sensitive, но хранится там ради единообразия).
*
* Endpoints:
* GET /api/admin/tools — список включённых tools (без secret-значений)
* POST /api/admin/tools/booking — настроить/обновить ссылку бронирования
* DELETE /api/admin/tools/booking — отключить tool бронирования
*/
export interface AdminToolsRoutesOpts {
db: Db;
masterKeyHex: string;
/**
* Hot-reload hook — если задан, вызывается после POST/DELETE.
* Используется чтобы invalidate кеш resolveTools в llm-bootstrap
* без рестарта сервера.
*/
onReload?: (tenantId: number) => void;
}
const BOOKING_URL_KEY = "tool_booking_url";
export function makeAdminToolsRoutes(opts: AdminToolsRoutesOpts): Hono {
const app = new Hono();
/**
* GET /api/admin/tools
* Returns list of configured tools for the tenant.
* Response: { tools: Array<{ name, enabled, config }> }
*/
app.get("/api/admin/tools", async (c) => {
const tenantId = c.var.tenantId;
const rows = await withTenant(opts.db, tenantId, async (tx) => {
return tx
.select({ key: tenantSecrets.key })
.from(tenantSecrets)
.where(and(eq(tenantSecrets.tenantId, tenantId), eq(tenantSecrets.key, BOOKING_URL_KEY)));
});
const hasBookingUrl = rows.length > 0;
return c.json({
tools: [
{
name: "booking_link",
displayName: "Ссылка бронирования (Calendly / Cal.com)",
description:
"Бот предлагает ссылку для записи на звонок/демо когда лид сам об этом просит.",
enabled: hasBookingUrl,
},
],
});
});
/**
* GET /api/admin/tools/booking
* Returns the current booking URL (decrypted) for the tenant.
*/
app.get("/api/admin/tools/booking", async (c) => {
const tenantId = c.var.tenantId;
const url = await getDecryptedSecret({
db: opts.db,
tenantId,
key: BOOKING_URL_KEY,
masterKeyHex: opts.masterKeyHex,
});
if (!url) return c.json({ enabled: false, url: null });
return c.json({ enabled: true, url });
});
/**
* POST /api/admin/tools/booking
* Body: { url: string }
* Validates URL format, stores in tenant_secrets.
*/
app.post("/api/admin/tools/booking", async (c) => {
const tenantId = c.var.tenantId;
const body = await c.req.json().catch(() => ({}));
const url = typeof body?.url === "string" ? body.url.trim() : "";
if (!url) {
return c.json({ error: "url required" }, 400);
}
// Basic URL validation
try {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) {
return c.json({ error: "url must use http or https" }, 400);
}
} catch {
return c.json({ error: "invalid url format" }, 400);
}
await setEncryptedSecret({
db: opts.db,
tenantId,
key: BOOKING_URL_KEY,
value: url,
masterKeyHex: opts.masterKeyHex,
nowEpoch: Math.floor(Date.now() / 1000),
});
opts.onReload?.(tenantId);
return c.json({ ok: true, url });
});
/**
* DELETE /api/admin/tools/booking
* Removes the booking URL — disables the tool for this tenant.
*/
app.delete("/api/admin/tools/booking", async (c) => {
const tenantId = c.var.tenantId;
await withTenant(opts.db, tenantId, async (tx) => {
return tx
.delete(tenantSecrets)
.where(and(eq(tenantSecrets.tenantId, tenantId), eq(tenantSecrets.key, BOOKING_URL_KEY)));
});
opts.onReload?.(tenantId);
return c.json({ ok: true });
});
return app;
}