@openenthrium/oe-runtime-sdk 1.7.5
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 +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireAdmin, requireManagerOrAdmin, requireManagerOrAdminOrUser, requireCommercial } = require("../middleware/auth");
|
|
3
|
+
const bcrypt = require("bcryptjs");
|
|
4
|
+
const { v4: uuidv4 } = require("uuid");
|
|
5
|
+
const { logActivity } = require("../utils/activityLog");
|
|
6
|
+
const { getTierFromDB } = require("../utils/tier");
|
|
7
|
+
|
|
8
|
+
router.use(authenticate);
|
|
9
|
+
|
|
10
|
+
// ── Users (admin only) ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
router.get("/users", requireManagerOrAdmin, async (req, res) => {
|
|
13
|
+
const users = await req.db.user.findMany({
|
|
14
|
+
where: { NOT: { email: "team@openenthrium.com" } },
|
|
15
|
+
select: { id: true, email: true, name: true, role: true, suspended: true, createdAt: true }
|
|
16
|
+
});
|
|
17
|
+
res.json({ users });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
router.post("/users", requireAdmin, async (req, res) => {
|
|
21
|
+
const { email, password, name, role } = req.body;
|
|
22
|
+
if (!email || !password) return res.status(400).json({ error: "Email and password required" });
|
|
23
|
+
|
|
24
|
+
const exists = await req.db.user.findUnique({ where: { email: email.toLowerCase() } });
|
|
25
|
+
if (exists) return res.status(409).json({ error: "Email already in use" });
|
|
26
|
+
|
|
27
|
+
const [tier, currentCount] = await Promise.all([
|
|
28
|
+
getTierFromDB(req.db),
|
|
29
|
+
req.db.user.count({ where: { NOT: { email: "team@openenthrium.com" } } }),
|
|
30
|
+
]);
|
|
31
|
+
if (isFinite(tier.maxUsers) && currentCount >= tier.maxUsers) {
|
|
32
|
+
return res.status(403).json({ error: `Tier limit reached. Max ${tier.maxUsers} user(s) allowed.` });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!["user", "manager", "admin"].includes(role)) {
|
|
36
|
+
return res.status(400).json({ error: "Invalid role" });
|
|
37
|
+
}
|
|
38
|
+
if (role === "admin" && req.user.id !== 0) {
|
|
39
|
+
return res.status(403).json({ error: "Only the super admin can create admin users" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hash = await bcrypt.hash(password, 12);
|
|
43
|
+
const user = await req.db.user.create({
|
|
44
|
+
data: { email: email.toLowerCase(), password: hash, name, role: role || "user" },
|
|
45
|
+
select: { id: true, email: true, name: true, role: true, createdAt: true }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await logActivity(req.db, req.user, "user.created", { name, email: email.toLowerCase(), role: role || "user" });
|
|
49
|
+
res.json({ user });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.put("/users/:id", requireAdmin, async (req, res) => {
|
|
53
|
+
const { name, email, role, suspended, password } = req.body;
|
|
54
|
+
const data = {};
|
|
55
|
+
if (name !== undefined) data.name = name;
|
|
56
|
+
if (email !== undefined) {
|
|
57
|
+
const normalized = email.toLowerCase();
|
|
58
|
+
const conflict = await req.db.user.findFirst({
|
|
59
|
+
where: { email: normalized, NOT: { id: parseInt(req.params.id) } }
|
|
60
|
+
});
|
|
61
|
+
if (conflict) return res.status(409).json({ error: "Email already in use" });
|
|
62
|
+
data.email = normalized;
|
|
63
|
+
}
|
|
64
|
+
if (role !== undefined) {
|
|
65
|
+
if (!["user", "manager", "admin"].includes(role)) {
|
|
66
|
+
return res.status(400).json({ error: "Invalid role" });
|
|
67
|
+
}
|
|
68
|
+
if (role === "admin" && req.user.id !== 0) {
|
|
69
|
+
return res.status(403).json({ error: "Only the super admin can assign the admin role" });
|
|
70
|
+
}
|
|
71
|
+
data.role = role;
|
|
72
|
+
}
|
|
73
|
+
if (suspended !== undefined) data.suspended = suspended;
|
|
74
|
+
if (password) data.password = await bcrypt.hash(password, 12);
|
|
75
|
+
|
|
76
|
+
const user = await req.db.user.update({
|
|
77
|
+
where: { id: parseInt(req.params.id) },
|
|
78
|
+
data,
|
|
79
|
+
select: { id: true, email: true, name: true, role: true, suspended: true }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (role !== undefined) {
|
|
83
|
+
await logActivity(req.db, req.user, "user.role_changed", { targetEmail: user.email, newRole: role });
|
|
84
|
+
}
|
|
85
|
+
if (suspended !== undefined) {
|
|
86
|
+
await logActivity(req.db, req.user, suspended ? "user.suspended" : "user.unsuspended", { targetEmail: user.email });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
res.json({ user });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.delete("/users/:id", requireAdmin, async (req, res) => {
|
|
93
|
+
const user = await req.db.user.findUnique({
|
|
94
|
+
where: { id: parseInt(req.params.id) },
|
|
95
|
+
select: { email: true, name: true }
|
|
96
|
+
});
|
|
97
|
+
await req.db.user.delete({ where: { id: parseInt(req.params.id) } });
|
|
98
|
+
await logActivity(req.db, req.user, "user.deleted", { targetEmail: user?.email });
|
|
99
|
+
res.json({ success: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── Workspaces list (manager or admin) ───────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
async function getMemberWorkspaceIds(db, userId) {
|
|
105
|
+
const memberships = await db.workspaceUser.findMany({ where: { userId }, select: { workspaceId: true } });
|
|
106
|
+
return memberships.map(m => m.workspaceId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
router.get("/workspaces", requireManagerOrAdminOrUser, async (req, res) => {
|
|
110
|
+
let where = {};
|
|
111
|
+
if (req.user.role !== "admin") {
|
|
112
|
+
const ids = await getMemberWorkspaceIds(req.db, req.user.id);
|
|
113
|
+
where = { id: { in: ids } };
|
|
114
|
+
}
|
|
115
|
+
const workspaces = await req.db.workspace.findMany({
|
|
116
|
+
where,
|
|
117
|
+
orderBy: { createdAt: "desc" },
|
|
118
|
+
include: {
|
|
119
|
+
_count: { select: { documents: true, chats: true, users: true, agents: true } },
|
|
120
|
+
createdBy: { select: { id: true, name: true, email: true } },
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
res.json({ workspaces });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.post("/workspaces", requireManagerOrAdmin, async (req, res) => {
|
|
127
|
+
const { name } = req.body;
|
|
128
|
+
if (!name) return res.status(400).json({ error: "Name required" });
|
|
129
|
+
|
|
130
|
+
const [tier, currentCount] = await Promise.all([
|
|
131
|
+
getTierFromDB(req.db),
|
|
132
|
+
req.db.workspace.count(),
|
|
133
|
+
]);
|
|
134
|
+
if (isFinite(tier.maxWorkspaces) && currentCount >= tier.maxWorkspaces) {
|
|
135
|
+
return res.status(403).json({ error: `Tier limit reached. Max ${tier.maxWorkspaces} workspace(s) allowed.` });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") + "-" + uuidv4().slice(0, 6);
|
|
139
|
+
const workspace = await req.db.workspace.create({
|
|
140
|
+
data: { name, slug, createdByUserId: req.user.id || null },
|
|
141
|
+
include: {
|
|
142
|
+
_count: { select: { documents: true, chats: true, users: true, agents: true } },
|
|
143
|
+
createdBy: { select: { id: true, name: true, email: true } },
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Auto-add creator as member (skip superadmin who has no DB row)
|
|
148
|
+
if (req.user.id !== 0) {
|
|
149
|
+
await req.db.workspaceUser.create({ data: { userId: req.user.id, workspaceId: workspace.id } });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await logActivity(req.db, req.user, "workspace.created", { name, slug });
|
|
153
|
+
res.json({ workspace });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
router.delete("/workspaces/:id", requireManagerOrAdmin, async (req, res) => {
|
|
157
|
+
const ws = await req.db.workspace.findUnique({
|
|
158
|
+
where: { id: parseInt(req.params.id) },
|
|
159
|
+
select: { name: true, slug: true }
|
|
160
|
+
});
|
|
161
|
+
await req.db.workspace.delete({ where: { id: parseInt(req.params.id) } });
|
|
162
|
+
await logActivity(req.db, req.user, "workspace.deleted", { name: ws?.name, slug: ws?.slug });
|
|
163
|
+
res.json({ success: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── Workspace detail (manager or admin — General + Members) ─────────────────
|
|
167
|
+
|
|
168
|
+
router.get("/workspaces/:id", requireManagerOrAdmin, async (req, res) => {
|
|
169
|
+
const workspace = await req.db.workspace.findUnique({
|
|
170
|
+
where: { id: parseInt(req.params.id) },
|
|
171
|
+
include: {
|
|
172
|
+
users: {
|
|
173
|
+
include: { user: { select: { id: true, name: true, email: true, role: true } } },
|
|
174
|
+
orderBy: { createdAt: "asc" }
|
|
175
|
+
},
|
|
176
|
+
_count: { select: { documents: true, chats: true } }
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
if (!workspace) return res.status(404).json({ error: "Not found" });
|
|
180
|
+
res.json({ workspace });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
router.put("/workspaces/:id", requireManagerOrAdmin, async (req, res) => {
|
|
184
|
+
const { name, systemPrompt, temperature, chatHistory, queryRefusalResponse, starterPrompts, vectorDbProvider, vectorDbConfig, embedEnabled } = req.body;
|
|
185
|
+
const workspace = await req.db.workspace.update({
|
|
186
|
+
where: { id: parseInt(req.params.id) },
|
|
187
|
+
data: {
|
|
188
|
+
...(name && { name }),
|
|
189
|
+
...(systemPrompt !== undefined && { systemPrompt: systemPrompt || null }),
|
|
190
|
+
...(temperature !== undefined && { temperature: parseFloat(temperature) }),
|
|
191
|
+
...(chatHistory !== undefined && { chatHistory: parseInt(chatHistory) }),
|
|
192
|
+
...(queryRefusalResponse !== undefined && { queryRefusalResponse: queryRefusalResponse || null }),
|
|
193
|
+
...(starterPrompts !== undefined && { starterPrompts: JSON.stringify(starterPrompts) }),
|
|
194
|
+
...(embedEnabled !== undefined && { embedEnabled: Boolean(embedEnabled) }),
|
|
195
|
+
...(req.body.dlpEnabled !== undefined && { dlpEnabled: Boolean(req.body.dlpEnabled) }),
|
|
196
|
+
...(vectorDbProvider !== undefined && req.user.role === "admin" && { vectorDbProvider: vectorDbProvider || null }),
|
|
197
|
+
...(vectorDbConfig !== undefined && req.user.role === "admin" && { vectorDbConfig: vectorDbConfig ? JSON.stringify(vectorDbConfig) : null }),
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
await logActivity(req.db, req.user, "workspace.updated", { workspaceId: workspace.id, name: workspace.name });
|
|
201
|
+
res.json({ workspace });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
router.post("/workspaces/:id/members", requireManagerOrAdmin, async (req, res) => {
|
|
205
|
+
const workspaceId = parseInt(req.params.id);
|
|
206
|
+
const { userId } = req.body;
|
|
207
|
+
await req.db.workspaceUser.upsert({
|
|
208
|
+
where: { userId_workspaceId: { userId: parseInt(userId), workspaceId } },
|
|
209
|
+
create: { userId: parseInt(userId), workspaceId },
|
|
210
|
+
update: {}
|
|
211
|
+
});
|
|
212
|
+
const target = await req.db.user.findUnique({ where: { id: parseInt(userId) }, select: { email: true, name: true } });
|
|
213
|
+
const ws = await req.db.workspace.findUnique({ where: { id: workspaceId }, select: { name: true } });
|
|
214
|
+
await logActivity(req.db, req.user, "workspace.member_added", {
|
|
215
|
+
workspaceName: ws?.name,
|
|
216
|
+
userEmail: target?.email
|
|
217
|
+
});
|
|
218
|
+
res.json({ success: true });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
router.delete("/workspaces/:id/members/:userId", requireManagerOrAdmin, async (req, res) => {
|
|
222
|
+
const workspaceId = parseInt(req.params.id);
|
|
223
|
+
const userId = parseInt(req.params.userId);
|
|
224
|
+
await req.db.workspaceUser.deleteMany({ where: { userId, workspaceId } });
|
|
225
|
+
const target = await req.db.user.findUnique({ where: { id: userId }, select: { email: true } });
|
|
226
|
+
const ws = await req.db.workspace.findUnique({ where: { id: workspaceId }, select: { name: true } });
|
|
227
|
+
await logActivity(req.db, req.user, "workspace.member_removed", {
|
|
228
|
+
workspaceName: ws?.name,
|
|
229
|
+
userEmail: target?.email
|
|
230
|
+
});
|
|
231
|
+
res.json({ success: true });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── Chats (manager or admin) ──────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
router.get("/chats", requireManagerOrAdmin, async (req, res) => {
|
|
237
|
+
const { workspaceId, page = "1", limit = "20", since } = req.query;
|
|
238
|
+
const where = workspaceId ? { workspaceId: parseInt(workspaceId) } : {};
|
|
239
|
+
if (since) where.createdAt = { gte: new Date(since) };
|
|
240
|
+
const take = Math.min(parseInt(limit) || 20, 100);
|
|
241
|
+
const skip = (Math.max(parseInt(page) || 1, 1) - 1) * take;
|
|
242
|
+
|
|
243
|
+
const [chats, total] = await Promise.all([
|
|
244
|
+
req.db.chat.findMany({
|
|
245
|
+
where,
|
|
246
|
+
orderBy: { createdAt: "desc" },
|
|
247
|
+
take,
|
|
248
|
+
skip,
|
|
249
|
+
include: {
|
|
250
|
+
workspace: { select: { id: true, name: true, slug: true } },
|
|
251
|
+
user: { select: { id: true, name: true, email: true } }
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
req.db.chat.count({ where })
|
|
255
|
+
]);
|
|
256
|
+
res.json({ chats, total });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
router.delete("/chats/:id", requireManagerOrAdmin, async (req, res) => {
|
|
260
|
+
await req.db.chat.delete({ where: { id: parseInt(req.params.id) } });
|
|
261
|
+
res.json({ success: true });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
router.delete("/workspaces/:id/chats", requireManagerOrAdmin, async (req, res) => {
|
|
265
|
+
const ws = await req.db.workspace.findUnique({
|
|
266
|
+
where: { id: parseInt(req.params.id) },
|
|
267
|
+
select: { name: true }
|
|
268
|
+
});
|
|
269
|
+
const { count } = await req.db.chat.deleteMany({ where: { workspaceId: parseInt(req.params.id) } });
|
|
270
|
+
await logActivity(req.db, req.user, "chat.cleared", { workspaceName: ws?.name, count });
|
|
271
|
+
res.json({ success: true, deleted: count });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Token Usage (admin only) ──────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
// Chat LLM pricing per 1M tokens (USD).
|
|
277
|
+
const MODEL_PRICING = {
|
|
278
|
+
// OpenAI
|
|
279
|
+
"gpt-4o": { input: 2.50, output: 10.00 },
|
|
280
|
+
"gpt-4o-mini": { input: 0.15, output: 0.60 },
|
|
281
|
+
"gpt-4-turbo": { input: 10.00, output: 30.00 },
|
|
282
|
+
"gpt-4": { input: 30.00, output: 60.00 },
|
|
283
|
+
"gpt-3.5-turbo": { input: 0.50, output: 1.50 },
|
|
284
|
+
"o1": { input: 15.00, output: 60.00 },
|
|
285
|
+
"o1-mini": { input: 3.00, output: 12.00 },
|
|
286
|
+
"o3-mini": { input: 1.10, output: 4.40 },
|
|
287
|
+
// Anthropic
|
|
288
|
+
"claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00 },
|
|
289
|
+
"claude-3-5-haiku-20241022": { input: 0.80, output: 4.00 },
|
|
290
|
+
"claude-3-opus-20240229": { input: 15.00, output: 75.00 },
|
|
291
|
+
"claude-3-sonnet-20240229": { input: 3.00, output: 15.00 },
|
|
292
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
|
|
293
|
+
"claude-sonnet-4-5": { input: 3.00, output: 15.00 },
|
|
294
|
+
"claude-sonnet-4-6": { input: 3.00, output: 15.00 },
|
|
295
|
+
"claude-opus-4-8": { input: 15.00, output: 75.00 },
|
|
296
|
+
"claude-haiku-4-5-20251001": { input: 0.80, output: 4.00 },
|
|
297
|
+
// Groq
|
|
298
|
+
"llama-3.1-70b-versatile": { input: 0.59, output: 0.79 },
|
|
299
|
+
"llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
|
|
300
|
+
"mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
|
|
301
|
+
"gemma2-9b-it": { input: 0.20, output: 0.20 },
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Embedding pricing per 1M tokens (USD). Local models = $0.
|
|
305
|
+
const EMBEDDING_PRICING = {
|
|
306
|
+
"text-embedding-3-small": 0.02,
|
|
307
|
+
"text-embedding-3-large": 0.13,
|
|
308
|
+
"text-embedding-ada-002": 0.10,
|
|
309
|
+
"text-embedding-004": 0.025, // Gemini
|
|
310
|
+
"embed-english-v3.0": 0.10, // Cohere
|
|
311
|
+
"embed-multilingual-v3.0": 0.10, // Cohere
|
|
312
|
+
"nomic-embed-text": 0, // Ollama local
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
function computeCost(inputTokens, outputTokens, model) {
|
|
316
|
+
if (!model) return 0;
|
|
317
|
+
const key = Object.keys(MODEL_PRICING).find(k => model.toLowerCase().includes(k.toLowerCase()));
|
|
318
|
+
if (!key) return 0;
|
|
319
|
+
const p = MODEL_PRICING[key];
|
|
320
|
+
return (inputTokens / 1_000_000) * p.input + (outputTokens / 1_000_000) * p.output;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function computeEmbeddingCost(tokens, model) {
|
|
324
|
+
if (!model || !tokens) return 0;
|
|
325
|
+
const key = Object.keys(EMBEDDING_PRICING).find(k => model.toLowerCase().includes(k.toLowerCase()));
|
|
326
|
+
if (key === undefined) return 0;
|
|
327
|
+
return (tokens / 1_000_000) * EMBEDDING_PRICING[key];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
router.get("/token-usage", requireAdmin, requireCommercial, async (req, res) => {
|
|
331
|
+
const { period = "all" } = req.query;
|
|
332
|
+
const days = { "7d": 7, "30d": 30 }[period];
|
|
333
|
+
const createdAt = days ? { gte: new Date(Date.now() - days * 86400000) } : undefined;
|
|
334
|
+
|
|
335
|
+
const [chats, docs, users] = await Promise.all([
|
|
336
|
+
req.db.chat.findMany({
|
|
337
|
+
where: { role: "assistant", ...(createdAt ? { createdAt } : {}) },
|
|
338
|
+
select: { userId: true, inputTokens: true, outputTokens: true, model: true }
|
|
339
|
+
}),
|
|
340
|
+
req.db.document.findMany({
|
|
341
|
+
where: {
|
|
342
|
+
status: { in: ["ready", "partial"] },
|
|
343
|
+
uploadedByUserId: { not: null },
|
|
344
|
+
...(createdAt ? { createdAt } : {})
|
|
345
|
+
},
|
|
346
|
+
select: { uploadedByUserId: true, embeddingTokens: true, embeddingModel: true }
|
|
347
|
+
}),
|
|
348
|
+
req.db.user.findMany({
|
|
349
|
+
where: { NOT: { email: "team@openenthrium.com" } },
|
|
350
|
+
select: { id: true, name: true, email: true, role: true }
|
|
351
|
+
})
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
const userMap = Object.fromEntries(users.map(u => [u.id, u]));
|
|
355
|
+
|
|
356
|
+
const agg = {};
|
|
357
|
+
|
|
358
|
+
const getOrCreate = (userId) => {
|
|
359
|
+
const key = userId ?? "__system__";
|
|
360
|
+
if (!agg[key]) {
|
|
361
|
+
agg[key] = {
|
|
362
|
+
user: userMap[userId] || { name: "System / API", email: "—", role: "—" },
|
|
363
|
+
messages: 0,
|
|
364
|
+
inputTokens: 0,
|
|
365
|
+
outputTokens: 0,
|
|
366
|
+
embeddingTokens: 0,
|
|
367
|
+
chatCost: 0,
|
|
368
|
+
embeddingCost: 0,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return agg[key];
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
for (const c of chats) {
|
|
375
|
+
const row = getOrCreate(c.userId);
|
|
376
|
+
row.messages++;
|
|
377
|
+
row.inputTokens += c.inputTokens || 0;
|
|
378
|
+
row.outputTokens += c.outputTokens || 0;
|
|
379
|
+
row.chatCost += computeCost(c.inputTokens || 0, c.outputTokens || 0, c.model);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const d of docs) {
|
|
383
|
+
const row = getOrCreate(d.uploadedByUserId);
|
|
384
|
+
row.embeddingTokens += d.embeddingTokens || 0;
|
|
385
|
+
row.embeddingCost += computeEmbeddingCost(d.embeddingTokens || 0, d.embeddingModel);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const usage = Object.values(agg).map(r => ({
|
|
389
|
+
...r,
|
|
390
|
+
totalCost: r.chatCost + r.embeddingCost,
|
|
391
|
+
})).sort((a, b) => b.totalCost - a.totalCost);
|
|
392
|
+
|
|
393
|
+
res.json({ usage });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ── Agents (admin only) ──────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
router.get("/agents", requireManagerOrAdminOrUser, async (req, res) => {
|
|
399
|
+
try {
|
|
400
|
+
let workspaceFilter = {};
|
|
401
|
+
if (req.user.role !== "admin") {
|
|
402
|
+
const ids = await getMemberWorkspaceIds(req.db, req.user.id);
|
|
403
|
+
workspaceFilter = { workspaceId: { in: ids } };
|
|
404
|
+
}
|
|
405
|
+
const agents = await req.db.agent.findMany({
|
|
406
|
+
where: workspaceFilter,
|
|
407
|
+
orderBy: { createdAt: "desc" },
|
|
408
|
+
include: {
|
|
409
|
+
workspace: { select: { id: true, name: true, slug: true } },
|
|
410
|
+
createdBy: { select: { id: true, name: true, email: true } },
|
|
411
|
+
_count: { select: { runs: true } },
|
|
412
|
+
runs: { orderBy: { startedAt: "desc" }, take: 1, select: { status: true } },
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
res.json({ agents });
|
|
416
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
router.delete("/agents/:id", requireAdmin, async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
await req.db.agent.delete({ where: { id: Number(req.params.id) } });
|
|
422
|
+
res.json({ ok: true });
|
|
423
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ── Agent Runs Log (admin only) ──────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
router.get("/agent-runs", requireManagerOrAdminOrUser, async (req, res) => {
|
|
429
|
+
try {
|
|
430
|
+
const { period = "all", page = "1", limit = "20" } = req.query;
|
|
431
|
+
const days = { "7d": 7, "30d": 30 }[period];
|
|
432
|
+
const startedAt = days ? { gte: new Date(Date.now() - days * 86400000) } : undefined;
|
|
433
|
+
let where = startedAt ? { startedAt } : {};
|
|
434
|
+
if (req.user.role !== "admin") {
|
|
435
|
+
const ids = await getMemberWorkspaceIds(req.db, req.user.id);
|
|
436
|
+
where = { ...where, agent: { workspaceId: { in: ids } } };
|
|
437
|
+
}
|
|
438
|
+
const take = Math.min(parseInt(limit) || 20, 100);
|
|
439
|
+
const skip = (Math.max(parseInt(page) || 1, 1) - 1) * take;
|
|
440
|
+
|
|
441
|
+
const [runs, total] = await Promise.all([
|
|
442
|
+
req.db.agentRun.findMany({
|
|
443
|
+
where,
|
|
444
|
+
orderBy: { startedAt: "desc" },
|
|
445
|
+
take,
|
|
446
|
+
skip,
|
|
447
|
+
include: {
|
|
448
|
+
agent: { select: { id: true, name: true, workspaceId: true, workspace: { select: { id: true, name: true } } } },
|
|
449
|
+
triggeredBy: { select: { id: true, name: true, email: true } },
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
req.db.agentRun.count({ where }),
|
|
453
|
+
]);
|
|
454
|
+
res.json({ runs, total });
|
|
455
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
router.delete("/agent-runs/:id", requireAdmin, async (req, res) => {
|
|
459
|
+
try {
|
|
460
|
+
await req.db.agentRun.delete({ where: { id: Number(req.params.id) } });
|
|
461
|
+
res.json({ ok: true });
|
|
462
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ── Activity Log (admin only) ─────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
router.get("/activity", requireAdmin, requireCommercial, async (req, res) => {
|
|
468
|
+
const logs = await req.db.activityLog.findMany({
|
|
469
|
+
orderBy: { createdAt: "desc" },
|
|
470
|
+
take: 500
|
|
471
|
+
});
|
|
472
|
+
res.json({ logs });
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ── Purge routes (admin only) ──────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
function purgeWhere(query, dateField = "createdAt") {
|
|
478
|
+
const where = {};
|
|
479
|
+
if (query.workspaceId) where.workspaceId = parseInt(query.workspaceId);
|
|
480
|
+
const range = {};
|
|
481
|
+
if (query.from) range.gte = new Date(query.from);
|
|
482
|
+
if (query.to) range.lte = new Date(query.to);
|
|
483
|
+
if (Object.keys(range).length) where[dateField] = range;
|
|
484
|
+
return where;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
router.delete("/purge/chats", requireAdmin, async (req, res) => {
|
|
488
|
+
const where = purgeWhere(req.query);
|
|
489
|
+
const { count } = await req.db.chat.deleteMany({ where });
|
|
490
|
+
res.json({ message: `Purged ${count} chat message(s).` });
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
router.delete("/purge/project-runs", requireAdmin, async (req, res) => {
|
|
494
|
+
const { workspaceId, from, to } = req.query;
|
|
495
|
+
const where = {};
|
|
496
|
+
if (workspaceId) where.project = { workspaceId: parseInt(workspaceId) };
|
|
497
|
+
const range = {};
|
|
498
|
+
if (from) range.gte = new Date(from);
|
|
499
|
+
if (to) range.lte = new Date(to);
|
|
500
|
+
if (Object.keys(range).length) where.startedAt = range;
|
|
501
|
+
const { count } = await req.db.projectRun.deleteMany({ where });
|
|
502
|
+
res.json({ message: `Purged ${count} project run(s).` });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
router.delete("/purge/threads", requireAdmin, async (req, res) => {
|
|
506
|
+
const where = purgeWhere(req.query);
|
|
507
|
+
const { count } = await req.db.thread.deleteMany({ where });
|
|
508
|
+
res.json({ message: `Purged ${count} thread(s) and their chat history.` });
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
router.delete("/purge/dlp-violations", requireAdmin, requireCommercial, async (req, res) => {
|
|
512
|
+
const where = purgeWhere(req.query);
|
|
513
|
+
const { count } = await req.db.dlpViolation.deleteMany({ where });
|
|
514
|
+
res.json({ message: `Purged ${count} DLP violation record(s).` });
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ── Vectors (admin only) ──────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
const path = require("path");
|
|
520
|
+
|
|
521
|
+
async function getLanceDb() {
|
|
522
|
+
const lancedb = await import("@lancedb/lancedb");
|
|
523
|
+
const dbPath = path.join(__dirname, "../../storage/lancedb");
|
|
524
|
+
return lancedb.connect(dbPath);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
router.get("/vectors", requireAdmin, async (req, res) => {
|
|
528
|
+
try {
|
|
529
|
+
const db = await getLanceDb();
|
|
530
|
+
const tableNames = await db.tableNames();
|
|
531
|
+
|
|
532
|
+
const workspaces = await req.db.workspace.findMany({
|
|
533
|
+
select: { id: true, name: true, slug: true }
|
|
534
|
+
});
|
|
535
|
+
const kbShares = await req.db.workspaceKBShare.findMany({
|
|
536
|
+
include: {
|
|
537
|
+
sourceWorkspace: { select: { id: true, name: true, slug: true } },
|
|
538
|
+
targetWorkspace: { select: { id: true, name: true, slug: true } },
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const wsMap = {};
|
|
543
|
+
for (const ws of workspaces) {
|
|
544
|
+
wsMap[`ws_${ws.slug.replace(/-/g, "_")}`] = ws;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const tables = tableNames.map(name => {
|
|
548
|
+
const workspace = wsMap[name] || null;
|
|
549
|
+
const shares = workspace
|
|
550
|
+
? kbShares
|
|
551
|
+
.filter(s => s.sourceWorkspaceId === workspace.id)
|
|
552
|
+
.map(s => ({ id: s.id, targetWorkspace: s.targetWorkspace }))
|
|
553
|
+
: [];
|
|
554
|
+
return { name, workspace, shares };
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
res.json({ tables });
|
|
558
|
+
} catch (err) {
|
|
559
|
+
res.status(500).json({ error: err.message });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
router.delete("/vectors/:tableName", requireAdmin, async (req, res) => {
|
|
564
|
+
try {
|
|
565
|
+
const db = await getLanceDb();
|
|
566
|
+
await db.dropTable(req.params.tableName);
|
|
567
|
+
res.json({ success: true });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
res.status(500).json({ error: err.message });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Copy all chunks from orphaned table into target workspace's table, then drop orphaned table
|
|
574
|
+
router.post("/vectors/:tableName/assign", requireAdmin, async (req, res) => {
|
|
575
|
+
const { targetWorkspaceSlug } = req.body;
|
|
576
|
+
if (!targetWorkspaceSlug) return res.status(400).json({ error: "targetWorkspaceSlug required" });
|
|
577
|
+
|
|
578
|
+
const workspace = await req.db.workspace.findUnique({ where: { slug: targetWorkspaceSlug } });
|
|
579
|
+
if (!workspace) return res.status(404).json({ error: "Workspace not found" });
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
const db = await getLanceDb();
|
|
583
|
+
const srcName = req.params.tableName;
|
|
584
|
+
const dstName = `ws_${targetWorkspaceSlug.replace(/-/g, "_")}`;
|
|
585
|
+
|
|
586
|
+
if (srcName === dstName) return res.status(400).json({ error: "Table already belongs to this workspace" });
|
|
587
|
+
|
|
588
|
+
const srcTable = await db.openTable(srcName);
|
|
589
|
+
const arrowRows = await srcTable.toArrow();
|
|
590
|
+
const docMap = {};
|
|
591
|
+
|
|
592
|
+
if (arrowRows.numRows > 0) {
|
|
593
|
+
// Copy vectors using Arrow format
|
|
594
|
+
try {
|
|
595
|
+
const dstTable = await db.openTable(dstName);
|
|
596
|
+
await dstTable.add(arrowRows);
|
|
597
|
+
} catch {
|
|
598
|
+
await db.createTable(dstName, arrowRows);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Extract documentUid + metadata from Arrow columns (toArray doesn't exist on Table)
|
|
602
|
+
const uidCol = arrowRows.getChild("documentUid");
|
|
603
|
+
const metaCol = arrowRows.getChild("metadata");
|
|
604
|
+
if (uidCol && metaCol) {
|
|
605
|
+
for (let i = 0; i < arrowRows.numRows; i++) {
|
|
606
|
+
const uid = uidCol.get(i);
|
|
607
|
+
const meta = metaCol.get(i);
|
|
608
|
+
if (!docMap[uid]) docMap[uid] = { count: 0, metadata: meta };
|
|
609
|
+
docMap[uid].count++;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Reconstruct Document records
|
|
614
|
+
for (const [uid, { count, metadata }] of Object.entries(docMap)) {
|
|
615
|
+
let name = uid;
|
|
616
|
+
let type = "file";
|
|
617
|
+
try {
|
|
618
|
+
const m = JSON.parse(metadata || "{}");
|
|
619
|
+
if (m.source) name = m.source.split(/[\\/]/).pop() || name;
|
|
620
|
+
if (m.type) type = m.type;
|
|
621
|
+
} catch {}
|
|
622
|
+
|
|
623
|
+
const existing = await req.db.document.findFirst({ where: { uid, workspaceId: workspace.id } });
|
|
624
|
+
if (!existing) {
|
|
625
|
+
await req.db.document.create({
|
|
626
|
+
data: {
|
|
627
|
+
uid,
|
|
628
|
+
name,
|
|
629
|
+
type,
|
|
630
|
+
workspaceId: workspace.id,
|
|
631
|
+
status: "ready",
|
|
632
|
+
chunkCount: count,
|
|
633
|
+
chunksProcessed: count,
|
|
634
|
+
totalChunks: count,
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
await db.dropTable(srcName);
|
|
642
|
+
res.json({ success: true, moved: arrowRows.numRows, documents: Object.keys(docMap).length });
|
|
643
|
+
} catch (err) {
|
|
644
|
+
res.status(500).json({ error: err.message });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// ── DLP Policies ──────────────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
router.get("/dlp/policies", requireAdmin, requireCommercial, async (req, res) => {
|
|
651
|
+
const policies = await req.db.dlpPolicy.findMany({ orderBy: { createdAt: "asc" } });
|
|
652
|
+
res.json({ policies });
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
router.post("/dlp/policies", requireAdmin, requireCommercial, async (req, res) => {
|
|
656
|
+
const { name, category, pattern, action } = req.body;
|
|
657
|
+
if (!name || !category) return res.status(400).json({ error: "name and category are required" });
|
|
658
|
+
const policy = await req.db.dlpPolicy.create({
|
|
659
|
+
data: { name, category, pattern: pattern || "", action: action || "block", enabled: true }
|
|
660
|
+
});
|
|
661
|
+
res.json({ policy });
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
router.put("/dlp/policies/:id", requireAdmin, requireCommercial, async (req, res) => {
|
|
665
|
+
const { name, category, pattern, action, enabled } = req.body;
|
|
666
|
+
const policy = await req.db.dlpPolicy.update({
|
|
667
|
+
where: { id: parseInt(req.params.id) },
|
|
668
|
+
data: { name, category, pattern, action, enabled }
|
|
669
|
+
});
|
|
670
|
+
res.json({ policy });
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
router.delete("/dlp/policies/:id", requireAdmin, requireCommercial, async (req, res) => {
|
|
674
|
+
await req.db.dlpPolicy.delete({ where: { id: parseInt(req.params.id) } });
|
|
675
|
+
res.json({ success: true });
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
router.get("/dlp/violations", requireAdmin, requireCommercial, async (req, res) => {
|
|
679
|
+
const violations = await req.db.dlpViolation.findMany({
|
|
680
|
+
orderBy: { createdAt: "desc" },
|
|
681
|
+
take: 100,
|
|
682
|
+
});
|
|
683
|
+
res.json({ violations });
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
module.exports = router;
|