@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.
Files changed (91) hide show
  1. package/README.md +139 -0
  2. package/index.js +58 -0
  3. package/package.json +64 -0
  4. package/src/data/connectionTypes.json +18587 -0
  5. package/src/engine/index.js +183 -0
  6. package/src/engine/llm.js +71 -0
  7. package/src/engine/promptBuilder.js +38 -0
  8. package/src/index.js +156 -0
  9. package/src/middleware/auth.js +63 -0
  10. package/src/providers/embedding/index.js +69 -0
  11. package/src/providers/llm/index.js +136 -0
  12. package/src/routes/admin.js +686 -0
  13. package/src/routes/agents.js +193 -0
  14. package/src/routes/apiKeys.js +55 -0
  15. package/src/routes/audio.js +111 -0
  16. package/src/routes/auth.js +83 -0
  17. package/src/routes/chat.js +381 -0
  18. package/src/routes/connectors.js +435 -0
  19. package/src/routes/dashboard.js +244 -0
  20. package/src/routes/documents.js +443 -0
  21. package/src/routes/embed.js +103 -0
  22. package/src/routes/marketplace.js +49 -0
  23. package/src/routes/models.js +81 -0
  24. package/src/routes/oauth.js +423 -0
  25. package/src/routes/projects.js +238 -0
  26. package/src/routes/settings.js +45 -0
  27. package/src/routes/setup.js +42 -0
  28. package/src/routes/sso.js +218 -0
  29. package/src/routes/superadmin.js +51 -0
  30. package/src/routes/templates.js +75 -0
  31. package/src/routes/threads.js +53 -0
  32. package/src/routes/workspaces.js +464 -0
  33. package/src/telemetry/bootstrap.js +39 -0
  34. package/src/telemetry/registration.js +16 -0
  35. package/src/utils/activityLog.js +16 -0
  36. package/src/utils/agentChain.js +141 -0
  37. package/src/utils/buildVisualization.js +102 -0
  38. package/src/utils/dlpScanner.js +57 -0
  39. package/src/utils/ingestionQueue.js +240 -0
  40. package/src/utils/prepareConnectors.js +66 -0
  41. package/src/utils/rag/_settings.js +6 -0
  42. package/src/utils/rag/chroma.js +82 -0
  43. package/src/utils/rag/lancedb.js +103 -0
  44. package/src/utils/rag/milvus.js +116 -0
  45. package/src/utils/rag/pgvector.js +104 -0
  46. package/src/utils/rag/pinecone.js +71 -0
  47. package/src/utils/rag/qdrant.js +106 -0
  48. package/src/utils/rag/weaviate.js +137 -0
  49. package/src/utils/rag/zilliz.js +115 -0
  50. package/src/utils/scheduler.js +168 -0
  51. package/src/utils/tier.js +106 -0
  52. package/src/utils/tools/adapters/_template.js +74 -0
  53. package/src/utils/tools/adapters/box.js +71 -0
  54. package/src/utils/tools/adapters/confluence.js +80 -0
  55. package/src/utils/tools/adapters/database.js +215 -0
  56. package/src/utils/tools/adapters/dropbox.js +74 -0
  57. package/src/utils/tools/adapters/elasticsearch.js +90 -0
  58. package/src/utils/tools/adapters/filesystem.js +197 -0
  59. package/src/utils/tools/adapters/freshdesk.js +87 -0
  60. package/src/utils/tools/adapters/gdrive.js +326 -0
  61. package/src/utils/tools/adapters/github.js +169 -0
  62. package/src/utils/tools/adapters/gmail.js +157 -0
  63. package/src/utils/tools/adapters/graphql.js +73 -0
  64. package/src/utils/tools/adapters/hubspot.js +101 -0
  65. package/src/utils/tools/adapters/image-gen.js +120 -0
  66. package/src/utils/tools/adapters/jira.js +86 -0
  67. package/src/utils/tools/adapters/kafka.js +126 -0
  68. package/src/utils/tools/adapters/ldap.js +118 -0
  69. package/src/utils/tools/adapters/mcp-client.js +138 -0
  70. package/src/utils/tools/adapters/mongodb.js +119 -0
  71. package/src/utils/tools/adapters/mqtt.js +106 -0
  72. package/src/utils/tools/adapters/music-gen.js +118 -0
  73. package/src/utils/tools/adapters/notion.js +93 -0
  74. package/src/utils/tools/adapters/ocr.js +107 -0
  75. package/src/utils/tools/adapters/onedrive.js +72 -0
  76. package/src/utils/tools/adapters/redis.js +104 -0
  77. package/src/utils/tools/adapters/rest-api.js +119 -0
  78. package/src/utils/tools/adapters/s3.js +142 -0
  79. package/src/utils/tools/adapters/search.js +80 -0
  80. package/src/utils/tools/adapters/sftp.js +121 -0
  81. package/src/utils/tools/adapters/shell.js +97 -0
  82. package/src/utils/tools/adapters/slack.js +83 -0
  83. package/src/utils/tools/adapters/speech.js +160 -0
  84. package/src/utils/tools/adapters/ssh.js +113 -0
  85. package/src/utils/tools/adapters/video-gen.js +152 -0
  86. package/src/utils/tools/adapters/web3.js +111 -0
  87. package/src/utils/tools/adapters/zendesk.js +82 -0
  88. package/src/utils/tools/adapters/zoho-mail.js +246 -0
  89. package/src/utils/tools/registry.js +229 -0
  90. package/src/utils/vectorStore.js +68 -0
  91. package/src/utils/workflowEngine.js +4 -0
@@ -0,0 +1,435 @@
1
+ const router = require("express").Router();
2
+ const { authenticate, requireAdmin, requireManagerOrAdmin } = require("../middleware/auth");
3
+ const { canAddConnector, getTierFromDB } = require("../utils/tier");
4
+ const { logActivity } = require("../utils/activityLog");
5
+
6
+ const SUPPORTED_TYPES = [
7
+ "postgresql", "mysql", "mssql", "oracle", "mongodb",
8
+ "redis", "sqlite", "snowflake", "bigquery", "cockroachdb", "elasticsearch",
9
+ "rest-api", "gmail", "slack", "jira", "confluence", "notion", "hubspot",
10
+ "freshdesk", "zendesk", "github", "zoho-mail", "gdrive", "ssh",
11
+ "onedrive", "dropbox", "box",
12
+ // Search
13
+ "perplexity-search", "google-search", "bing-search",
14
+ // OCR
15
+ "azure-vision", "google-vision", "aws-textract", "tesseract-ocr",
16
+ // Image generation
17
+ "openai-image", "flux", "stable-diffusion", "ideogram",
18
+ // Speech & audio
19
+ "elevenlabs", "openai-tts", "azure-speech", "google-tts",
20
+ // Video generation
21
+ "runway", "kling", "pika",
22
+ // Music generation
23
+ "suno", "udio",
24
+ ];
25
+
26
+ function generateSlug(name) {
27
+ return (name || "").toLowerCase().replace(/[^a-z0-9]/g, "") || "connector";
28
+ }
29
+
30
+ // Public-to-all-authenticated: connector type catalog (needed by workspace users too)
31
+ router.get("/connection-masters", authenticate, async (req, res) => {
32
+ try {
33
+ const { implementedTypes } = require("../utils/tools/registry");
34
+ const masters = await req.db.connectionMaster.findMany({
35
+ orderBy: [{ category: "asc" }, { label: "asc" }],
36
+ });
37
+ res.json({ masters: masters.map(m => ({ ...m, implemented: implementedTypes.has(m.key) })) });
38
+ } catch (err) {
39
+ res.status(500).json({ error: err.message });
40
+ }
41
+ });
42
+
43
+ router.use(authenticate, requireManagerOrAdmin);
44
+
45
+ const SENSITIVE_AUTH_KEYS = new Set([
46
+ "password", "privateKey", "apiToken", "apiKey", "botToken", "appPassword",
47
+ "bearerToken", "integrationToken", "privateAppToken", "personalAccessToken",
48
+ "keyFileJson", "refreshToken", "accessToken", "clientSecret",
49
+ ]);
50
+
51
+ // List connectors for a workspace
52
+ router.get("/workspaces/:workspaceId/connectors", async (req, res) => {
53
+ try {
54
+ const workspaceId = parseInt(req.params.workspaceId);
55
+ const [connectors, totalConnectors, tier] = await Promise.all([
56
+ req.db.connector.findMany({
57
+ where: { workspaceId },
58
+ orderBy: { createdAt: "asc" },
59
+ select: { id: true, name: true, slug: true, type: true, config: true, authConfig: true, status: true, lastTestedAt: true, createdAt: true }
60
+ }),
61
+ req.db.connector.count(),
62
+ getTierFromDB(req.db),
63
+ ]);
64
+ // Return non-sensitive auth fields (host, port, username, email, etc.) for edit pre-fill
65
+ const sanitized = connectors.map(({ authConfig, ...c }) => {
66
+ const auth = authConfig ? JSON.parse(authConfig) : {};
67
+ const publicAuth = Object.fromEntries(Object.entries(auth).filter(([k]) => !SENSITIVE_AUTH_KEYS.has(k)));
68
+ return { ...c, publicAuth }; // slug is already in ...c
69
+ });
70
+ res.json({ connectors: sanitized, totalConnectors, tier: { maxConnectors: isFinite(tier.maxConnectors) ? tier.maxConnectors : null } });
71
+ } catch (err) {
72
+ console.error("[connectors] GET failed:", err.message);
73
+ res.status(500).json({ error: err.message });
74
+ }
75
+ });
76
+
77
+ // Check slug availability (legacy — kept for backward compat)
78
+ router.get("/connectors/check-slug", async (req, res) => {
79
+ const { slug, excludeId } = req.query;
80
+ if (!slug) return res.json({ available: false });
81
+ const clean = slug.toLowerCase().replace(/[^a-z0-9]/g, "");
82
+ if (!clean) return res.json({ available: false });
83
+ const existing = await req.db.connector.findUnique({ where: { slug: clean }, select: { id: true } });
84
+ const available = !existing || (excludeId && existing.id === parseInt(excludeId));
85
+ res.json({ available });
86
+ });
87
+
88
+ // Check name availability (globally unique across all workspaces)
89
+ router.get("/connectors/check-name", async (req, res) => {
90
+ const { name, excludeId } = req.query;
91
+ const clean = name?.trim();
92
+ if (!clean) return res.json({ available: false, suggestion: null });
93
+ const existing = await req.db.connector.findFirst({ where: { name: clean }, select: { id: true } });
94
+ const available = !existing || (excludeId && existing.id === parseInt(excludeId));
95
+ let suggestion = null;
96
+ if (!available) {
97
+ let suffix = 1;
98
+ while (await req.db.connector.findFirst({ where: { name: `${clean}-${suffix}` } })) suffix++;
99
+ suggestion = `${clean}-${suffix}`;
100
+ }
101
+ res.json({ available, suggestion });
102
+ });
103
+
104
+ // Create connector
105
+ router.post("/workspaces/:workspaceId/connectors", async (req, res) => {
106
+ const workspaceId = parseInt(req.params.workspaceId);
107
+ const { name, slug: providedSlug, type, config, authConfig } = req.body;
108
+
109
+ if (!name?.trim()) return res.status(400).json({ error: "Name required" });
110
+ let typeIsValid = SUPPORTED_TYPES.includes(type);
111
+ if (!typeIsValid) {
112
+ const master = await req.db.connectionMaster.findUnique({ where: { key: type }, select: { fields: true } });
113
+ typeIsValid = !!master?.fields;
114
+ }
115
+ if (!typeIsValid) return res.status(400).json({ error: `Unsupported connector type: ${type}` });
116
+
117
+ const existing = await req.db.connector.count();
118
+ if (!await canAddConnector(existing, req.db)) {
119
+ const tier = await getTierFromDB(req.db);
120
+ return res.status(403).json({ error: `Tier limit reached. Max ${tier.maxConnectors} connector(s) allowed across the instance.` });
121
+ }
122
+
123
+ // Auto-suffix name to keep it globally unique (mysql → mysql-1 → mysql-2 …)
124
+ let finalName = name.trim();
125
+ const baseName = finalName;
126
+ let nameSuffix = 1;
127
+ while (await req.db.connector.findFirst({ where: { name: finalName } })) {
128
+ finalName = `${baseName}-${nameSuffix++}`;
129
+ }
130
+ const slug = finalName; // slug mirrors name
131
+
132
+ const connector = await req.db.connector.create({
133
+ data: {
134
+ workspaceId,
135
+ name: finalName,
136
+ slug,
137
+ type,
138
+ config: config ? JSON.stringify(config) : null,
139
+ authConfig: authConfig ? JSON.stringify(authConfig) : null,
140
+ status: "active",
141
+ },
142
+ select: { id: true, name: true, slug: true, type: true, config: true, status: true, createdAt: true }
143
+ });
144
+
145
+ await logActivity(req.db, req.user, "connector.created", { workspaceId, name: connector.name, type });
146
+ res.json({ connector });
147
+ });
148
+
149
+ // Update connector
150
+ router.put("/workspaces/:workspaceId/connectors/:id", async (req, res) => {
151
+ try {
152
+ const id = parseInt(req.params.id);
153
+ const { name, slug, config, authConfig, status } = req.body;
154
+ const data = {};
155
+ if (name !== undefined) data.name = name.trim();
156
+ if (slug !== undefined) data.slug = slug ? slug.toLowerCase().replace(/[^a-z0-9]/g, "") || null : null;
157
+ if (config !== undefined) data.config = config ? JSON.stringify(config) : null;
158
+ if (status !== undefined) data.status = status;
159
+
160
+ if (authConfig !== undefined) {
161
+ if (!authConfig) {
162
+ data.authConfig = null;
163
+ } else {
164
+ // Merge with existing: blank fields in the edit form mean "keep existing"
165
+ const existing = await req.db.connector.findUnique({ where: { id }, select: { authConfig: true } });
166
+ const existingAuth = existing?.authConfig ? JSON.parse(existing.authConfig) : {};
167
+ const merged = { ...existingAuth };
168
+ for (const [k, v] of Object.entries(authConfig)) {
169
+ if (v !== "" && v !== null && v !== undefined) merged[k] = v;
170
+ }
171
+ data.authConfig = JSON.stringify(merged);
172
+ }
173
+ }
174
+
175
+ const connector = await req.db.connector.update({
176
+ where: { id },
177
+ data,
178
+ select: { id: true, name: true, slug: true, type: true, config: true, status: true, lastTestedAt: true }
179
+ });
180
+ res.json({ connector });
181
+ } catch (err) {
182
+ if (err.code === "P2002") {
183
+ return res.status(400).json({ error: "Slug is already in use by another connector. Choose a different slug." });
184
+ }
185
+ console.error("[connectors] PUT failed:", err.message);
186
+ res.status(500).json({ error: err.message });
187
+ }
188
+ });
189
+
190
+ // Test connector connection
191
+ router.post("/workspaces/:workspaceId/connectors/:id/test", async (req, res) => {
192
+ const id = parseInt(req.params.id);
193
+ const connector = await req.db.connector.findUnique({ where: { id } });
194
+ if (!connector) return res.status(404).json({ error: "Connector not found" });
195
+
196
+ const cfg = connector.config ? JSON.parse(connector.config) : {};
197
+ const auth = connector.authConfig ? JSON.parse(connector.authConfig) : {};
198
+
199
+ let success = false;
200
+ let message = "";
201
+
202
+ try {
203
+ if (connector.type === "postgresql") {
204
+ const { Pool } = require("pg");
205
+ const pool = new Pool({
206
+ connectionString: cfg.url || undefined,
207
+ host: cfg.host || "localhost",
208
+ port: parseInt(cfg.port || "5432"),
209
+ database: cfg.database || undefined,
210
+ user: auth.username || undefined,
211
+ password: auth.password || undefined,
212
+ ssl: cfg.ssl ? { rejectUnauthorized: false } : false,
213
+ connectionTimeoutMillis: 8000,
214
+ });
215
+ await pool.query("SELECT 1");
216
+ await pool.end();
217
+ success = true; message = "Connected successfully.";
218
+ } else if (connector.type === "mysql") {
219
+ const mysql = require("mysql2/promise");
220
+ const TIMEOUT_MS = 8000;
221
+ const connPromise = mysql.createConnection({
222
+ host: cfg.host || "localhost",
223
+ port: parseInt(cfg.port || "3306"),
224
+ database: cfg.database || undefined,
225
+ user: auth.username || undefined,
226
+ password: auth.password || undefined,
227
+ connectTimeout: TIMEOUT_MS,
228
+ });
229
+ const timeoutPromise = new Promise((_, reject) =>
230
+ setTimeout(() => reject(new Error("Connection timed out")), TIMEOUT_MS)
231
+ );
232
+ const conn = await Promise.race([connPromise, timeoutPromise]);
233
+ await conn.ping();
234
+ await conn.execute("SELECT 1");
235
+ await conn.end();
236
+ success = true; message = "Connected successfully.";
237
+ } else if (connector.type === "mssql") {
238
+ const mssql = require("mssql");
239
+ const pool = await mssql.connect({
240
+ server: cfg.host || "localhost",
241
+ port: parseInt(cfg.port || "1433"),
242
+ database: cfg.database || undefined,
243
+ user: auth.username || undefined,
244
+ password: auth.password || undefined,
245
+ options: {
246
+ encrypt: cfg.encrypt !== false,
247
+ trustServerCertificate: cfg.trustServerCertificate || false,
248
+ },
249
+ connectionTimeout: 8000,
250
+ requestTimeout: 8000,
251
+ });
252
+ await pool.request().query("SELECT 1 AS ok");
253
+ await pool.close();
254
+ success = true; message = "Connected successfully.";
255
+ } else if (connector.type === "oracle") {
256
+ const oracledb = require("oracledb");
257
+ const connectString = cfg.connectString ||
258
+ `${cfg.host || "localhost"}:${cfg.port || "1521"}/${cfg.serviceName || cfg.sid || "ORCL"}`;
259
+ const conn = await oracledb.getConnection({
260
+ user: auth.username || undefined, password: auth.password || undefined, connectString,
261
+ });
262
+ await conn.execute("SELECT 1 FROM DUAL");
263
+ await conn.close();
264
+ success = true; message = "Connected successfully.";
265
+ } else if (connector.type === "mongodb") {
266
+ const { MongoClient } = require("mongodb");
267
+ const { buildUri } = require("../utils/tools/adapters/mongodb");
268
+ const uri = buildUri(cfg, auth);
269
+ const client = new MongoClient(uri, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000 });
270
+ await client.connect();
271
+ await client.db(cfg.database || "admin").command({ ping: 1 });
272
+ await client.close();
273
+ success = true; message = "Connected successfully.";
274
+ } else if (connector.type === "gmail") {
275
+ const { google } = require("googleapis");
276
+ const { makeOAuth2Client } = require("../utils/tools/adapters/gmail");
277
+ if (!auth.refreshToken) throw new Error("Not connected — please reconnect via the Integrations tab.");
278
+ const oauth2 = await makeOAuth2Client(req.db);
279
+ oauth2.setCredentials({ access_token: auth.accessToken, refresh_token: auth.refreshToken, expiry_date: auth.expiresAt });
280
+ const gmail = google.gmail({ version: "v1", auth: oauth2 });
281
+ const profile = await gmail.users.getProfile({ userId: "me" });
282
+ success = true; message = `Connected as ${profile.data.emailAddress}`;
283
+ } else if (connector.type === "gdrive") {
284
+ const { google } = require("googleapis");
285
+ const { makeOAuth2Client } = require("../utils/tools/adapters/gmail");
286
+ if (!auth.refreshToken) throw new Error("Not connected — please reconnect via the Integrations tab.");
287
+ const oauth2 = await makeOAuth2Client(req.db, connector.workspaceId);
288
+ oauth2.setCredentials({ access_token: auth.accessToken, refresh_token: auth.refreshToken, expiry_date: auth.expiresAt });
289
+ const drive = google.drive({ version: "v3", auth: oauth2 });
290
+ const res = await drive.about.get({ fields: "user" });
291
+ success = true; message = `Connected as ${res.data.user.emailAddress}`;
292
+ } else if (["slack","jira","confluence","notion","hubspot","freshdesk","zendesk","github"].includes(connector.type)) {
293
+ const { ADAPTERS } = require("../utils/tools/registry");
294
+ const adapter = ADAPTERS[connector.type];
295
+ const ok = await adapter.testConnection(auth);
296
+ if (!ok) throw new Error("Connection test failed — check your credentials.");
297
+ success = true; message = "Connected successfully.";
298
+ } else if (connector.type === "zoho-mail") {
299
+ const nodemailer = require("nodemailer");
300
+ const port = parseInt(auth.smtpPort || "465");
301
+ const transport = nodemailer.createTransport({
302
+ host: auth.smtpHost || "smtp.zoho.com",
303
+ port,
304
+ secure: port === 465,
305
+ auth: { user: auth.email, pass: auth.appPassword },
306
+ connectionTimeout: 8000,
307
+ });
308
+ await transport.verify();
309
+ success = true; message = `Connected as ${auth.email}`;
310
+ } else if (connector.type === "ssh") {
311
+ const { Client } = require("ssh2");
312
+ await new Promise((resolve, reject) => {
313
+ const conn = new Client();
314
+ const timer = setTimeout(() => { conn.end(); reject(new Error("Connection timed out")); }, 10000);
315
+ conn.on("ready", () => { clearTimeout(timer); conn.end(); resolve(); });
316
+ conn.on("error", err => { clearTimeout(timer); reject(err); });
317
+ const cfg2 = { host: auth.host || cfg.host, port: parseInt(auth.port || cfg.port || "22"), username: auth.username || cfg.username };
318
+ if (auth.privateKey || cfg.privateKey) cfg2.privateKey = auth.privateKey || cfg.privateKey;
319
+ conn.connect(cfg2);
320
+ });
321
+ success = true; message = `Connected to ${auth.host || cfg.host} as ${auth.username || cfg.username}`;
322
+ } else if (connector.type === "redis") {
323
+ const Redis = require("ioredis");
324
+ const client = new Redis({
325
+ host: cfg.host || "localhost",
326
+ port: parseInt(cfg.port || "6379"),
327
+ password: auth.password || undefined,
328
+ db: parseInt(cfg.db || "0"),
329
+ tls: cfg.tls ? {} : undefined,
330
+ connectTimeout: 8000,
331
+ lazyConnect: true,
332
+ });
333
+ await client.connect();
334
+ await client.ping();
335
+ await client.quit();
336
+ success = true; message = "Connected successfully.";
337
+ } else if (connector.type === "sqlite") {
338
+ const Database = require("better-sqlite3");
339
+ const db = new Database(cfg.filename || ":memory:", { timeout: 8000 });
340
+ db.prepare("SELECT 1").get();
341
+ db.close();
342
+ success = true; message = `Connected to ${cfg.filename || ":memory:"}`;
343
+ } else if (connector.type === "cockroachdb") {
344
+ const { Pool } = require("pg");
345
+ const pool = new Pool({
346
+ host: cfg.host || "localhost",
347
+ port: parseInt(cfg.port || "26257"),
348
+ database: cfg.database || "defaultdb",
349
+ user: auth.username || undefined,
350
+ password: auth.password || undefined,
351
+ ssl: cfg.ssl ? { rejectUnauthorized: false } : false,
352
+ connectionTimeoutMillis: 8000,
353
+ });
354
+ await pool.query("SELECT 1");
355
+ await pool.end();
356
+ success = true; message = "Connected successfully.";
357
+ } else if (connector.type === "snowflake") {
358
+ const snowflake = require("snowflake-sdk");
359
+ await new Promise((resolve, reject) => {
360
+ const c = snowflake.createConnection({
361
+ account: cfg.account,
362
+ username: auth.username,
363
+ password: auth.password,
364
+ database: cfg.database,
365
+ schema: cfg.schema || "PUBLIC",
366
+ warehouse: cfg.warehouse,
367
+ role: cfg.role || undefined,
368
+ });
369
+ c.connect(err => {
370
+ if (err) { reject(err); return; }
371
+ c.execute({ sqlText: "SELECT 1", complete: (err2) => { c.destroy(() => {}); err2 ? reject(err2) : resolve(); } });
372
+ });
373
+ });
374
+ success = true; message = "Connected successfully.";
375
+ } else if (connector.type === "bigquery") {
376
+ const { BigQuery } = require("@google-cloud/bigquery");
377
+ const credentials = auth.keyFileJson ? JSON.parse(auth.keyFileJson) : undefined;
378
+ const bq = new BigQuery({ projectId: cfg.projectId, credentials });
379
+ await bq.query({ query: "SELECT 1", timeoutMs: 8000 });
380
+ success = true; message = `Connected to project ${cfg.projectId}`;
381
+ } else if (connector.type === "elasticsearch") {
382
+ const { Client } = require("@elastic/elasticsearch");
383
+ const esAuth = auth.apiKey
384
+ ? { auth: { apiKey: auth.apiKey } }
385
+ : (auth.username ? { auth: { username: auth.username, password: auth.password || "" } } : {});
386
+ const esClient = new Client({ node: cfg.node || "http://localhost:9200", ...esAuth, requestTimeout: 8000 });
387
+ const info = await esClient.info();
388
+ await esClient.close();
389
+ success = true; message = `Connected — Elasticsearch ${info.version?.number || ""}`;
390
+ } else if (connector.type === "onedrive") {
391
+ if (!auth.refreshToken && !auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
392
+ const { data } = await require("axios").get("https://graph.microsoft.com/v1.0/me/drive", { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
393
+ success = true; message = `Connected to OneDrive (${data.owner?.user?.displayName || data.driveType || "personal"})`;
394
+ } else if (connector.type === "dropbox") {
395
+ if (!auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
396
+ const { data } = await require("axios").post("https://api.dropboxapi.com/2/users/get_current_account", null, { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
397
+ success = true; message = `Connected as ${data.email || data.name?.display_name || "Dropbox user"}`;
398
+ } else if (connector.type === "box") {
399
+ if (!auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
400
+ const { data } = await require("axios").get("https://api.box.com/2.0/users/me", { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
401
+ success = true; message = `Connected as ${data.login || data.name || "Box user"}`;
402
+ } else if (connector.type === "rest-api") {
403
+ const axios = require("axios");
404
+ const baseUrl = (cfg.baseUrl || "").replace(/\/$/, "");
405
+ if (!baseUrl) throw new Error("Base URL not configured");
406
+ const headers = {};
407
+ if (auth.apiKey) headers[auth.headerName || "X-API-Key"] = auth.apiKey;
408
+ if (auth.bearerToken) headers["Authorization"] = `Bearer ${auth.bearerToken}`;
409
+ await axios.get(baseUrl + (cfg.healthPath || "/"), { headers, timeout: 8000 });
410
+ success = true; message = "API reachable.";
411
+ }
412
+ } catch (err) {
413
+ message = err.message || err.errors?.[0]?.message || err.code || String(err) || "Connection failed";
414
+ }
415
+
416
+ await req.db.connector.update({
417
+ where: { id },
418
+ data: { status: success ? "active" : "error", lastTestedAt: new Date() }
419
+ });
420
+
421
+ res.json({ success, message });
422
+ });
423
+
424
+ // Delete connector
425
+ router.delete("/workspaces/:workspaceId/connectors/:id", async (req, res) => {
426
+ const id = parseInt(req.params.id);
427
+ const connector = await req.db.connector.findUnique({ where: { id }, select: { name: true, type: true } });
428
+ if (!connector) return res.status(404).json({ error: "Not found" });
429
+ await req.db.connector.delete({ where: { id } });
430
+ await logActivity(req.db, req.user, "connector.deleted", { name: connector.name, type: connector.type });
431
+ res.json({ success: true });
432
+ });
433
+
434
+
435
+ module.exports = router;
@@ -0,0 +1,244 @@
1
+ const router = require("express").Router();
2
+ const { authenticate, requireCommercial } = require("../middleware/auth");
3
+ const { getTierFromDB, getAgentRunsThisMonth } = require("../utils/tier");
4
+
5
+ const EMBEDDING_RATES = {
6
+ "text-embedding-3-small": 0.02 / 1_000_000,
7
+ "text-embedding-3-large": 0.13 / 1_000_000,
8
+ "text-embedding-ada-002": 0.10 / 1_000_000,
9
+ };
10
+
11
+ const LLM_RATES = {
12
+ "gpt-4o": { in: 2.50 / 1e6, out: 10.00 / 1e6 },
13
+ "gpt-4o-mini": { in: 0.15 / 1e6, out: 0.60 / 1e6 },
14
+ "gpt-4-turbo": { in: 10.00 / 1e6, out: 30.00 / 1e6 },
15
+ "claude-3-5-sonnet-20241022": { in: 3.00 / 1e6, out: 15.00 / 1e6 },
16
+ "claude-3-5-haiku-20241022": { in: 0.80 / 1e6, out: 4.00 / 1e6 },
17
+ "claude-3-opus-20240229": { in: 15.00 / 1e6, out: 75.00 / 1e6 },
18
+ "claude-3-haiku-20240307": { in: 0.25 / 1e6, out: 1.25 / 1e6 },
19
+ };
20
+
21
+ function calcEmbeddingCost(tokens, model) {
22
+ return (tokens || 0) * (EMBEDDING_RATES[model] || 0.10 / 1_000_000);
23
+ }
24
+
25
+ function calcLLMCost(inputTokens, outputTokens, model) {
26
+ const r = LLM_RATES[model] || { in: 2.50 / 1e6, out: 10.00 / 1e6 };
27
+ return (inputTokens || 0) * r.in + (outputTokens || 0) * r.out;
28
+ }
29
+
30
+ function last30Days() {
31
+ return Array.from({ length: 30 }, (_, i) => {
32
+ const d = new Date();
33
+ d.setDate(d.getDate() - (29 - i));
34
+ return d.toISOString().slice(0, 10);
35
+ });
36
+ }
37
+
38
+ function groupByDay(items, dateField) {
39
+ const map = {};
40
+ for (const item of items) {
41
+ const day = new Date(item[dateField]).toISOString().slice(0, 10);
42
+ map[day] = (map[day] || 0) + 1;
43
+ }
44
+ return last30Days().map(d => ({ date: d, count: map[d] || 0 }));
45
+ }
46
+
47
+ function statusSeries(counts) {
48
+ return [
49
+ { name: "Ready", value: counts.ready || 0, color: "#22c55e" },
50
+ { name: "Failed", value: counts.failed || 0, color: "#ef4444" },
51
+ { name: "Partial", value: counts.partial || 0, color: "#f59e0b" },
52
+ { name: "Active", value: (counts.ingesting || 0) + (counts.queued || 0), color: "#6366f1" },
53
+ ].filter(s => s.value > 0);
54
+ }
55
+
56
+ // Manager stats — scoped to user's workspaces (admin sees all)
57
+ router.get("/manager", authenticate, async (req, res) => {
58
+ const db = req.db;
59
+
60
+ let workspaceIds;
61
+ if (req.user.role === "admin") {
62
+ const all = await db.workspace.findMany({ select: { id: true } });
63
+ workspaceIds = all.map(w => w.id);
64
+ } else {
65
+ const memberships = await db.workspaceUser.findMany({
66
+ where: { userId: req.user.id },
67
+ select: { workspaceId: true }
68
+ });
69
+ workspaceIds = memberships.map(m => m.workspaceId);
70
+ }
71
+
72
+ const thirtyDaysAgo = new Date();
73
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
74
+
75
+ const agentIds = (await db.agent.findMany({ where: { workspaceId: { in: workspaceIds } }, select: { id: true } })).map(a => a.id);
76
+
77
+ const [workspaces, documents, agentRuns] = await Promise.all([
78
+ db.workspace.findMany({
79
+ where: { id: { in: workspaceIds } },
80
+ include: { _count: { select: { documents: true, chats: true } } }
81
+ }),
82
+ db.document.findMany({
83
+ where: { workspaceId: { in: workspaceIds } },
84
+ select: { status: true, chunkCount: true, createdAt: true }
85
+ }),
86
+ agentIds.length ? db.agentRun.findMany({
87
+ where: { agentId: { in: agentIds }, startedAt: { gte: thirtyDaysAgo } },
88
+ select: { startedAt: true, status: true }
89
+ }) : Promise.resolve([]),
90
+ ]);
91
+
92
+ const statusCounts = {};
93
+ let totalVectors = 0;
94
+ for (const d of documents) {
95
+ statusCounts[d.status] = (statusCounts[d.status] || 0) + 1;
96
+ totalVectors += d.chunkCount || 0;
97
+ }
98
+
99
+ res.json({
100
+ workspaceCount: workspaces.length,
101
+ documentCount: documents.length,
102
+ vectorCount: totalVectors,
103
+ activeIngestions: (statusCounts.ingesting || 0) + (statusCounts.queued || 0),
104
+ agentRunCount: agentRuns.length,
105
+ agentRunSuccess: agentRuns.filter(r => r.status === "success").length,
106
+ agentRunErrors: agentRuns.filter(r => r.status === "error").length,
107
+ agentRunsByTrigger: [
108
+ { name: "Runs", value: agentRuns.length, color: "#6366f1" },
109
+ ].filter(s => s.value > 0),
110
+ documentsByStatus: statusSeries(statusCounts),
111
+ agentRunActivity: groupByDay(agentRuns, "startedAt"),
112
+ docsByWorkspace: workspaces
113
+ .map(ws => ({
114
+ name: ws.name.length > 18 ? ws.name.slice(0, 18) + "…" : ws.name,
115
+ documents: ws._count.documents,
116
+ chats: ws._count.chats,
117
+ }))
118
+ .sort((a, b) => b.documents - a.documents)
119
+ .slice(0, 8),
120
+ ingestActivity: groupByDay(
121
+ documents.filter(d => new Date(d.createdAt) >= thirtyDaysAgo),
122
+ "createdAt"
123
+ ),
124
+ });
125
+ });
126
+
127
+ // Admin stats — platform-wide
128
+ router.get("/admin", authenticate, async (req, res) => {
129
+ if (req.user.role !== "admin") return res.status(403).json({ error: "Admin only" });
130
+ const db = req.db;
131
+
132
+ const thirtyDaysAgo = new Date();
133
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
134
+
135
+ const tier = await getTierFromDB(db);
136
+
137
+ const [users, workspaces, documents, recentUserChats, allAssistantChats, agentRunsThisMonth, recentAgentRuns] = await Promise.all([
138
+ db.user.findMany({ select: { id: true, role: true, createdAt: true } }),
139
+ db.workspace.findMany({
140
+ select: { id: true, name: true, _count: { select: { documents: true, chats: true } } }
141
+ }),
142
+ db.document.findMany({
143
+ select: { status: true, chunkCount: true, embeddingTokens: true, embeddingModel: true, createdAt: true, size: true }
144
+ }),
145
+ db.chat.findMany({
146
+ where: { role: "user", createdAt: { gte: thirtyDaysAgo } },
147
+ select: { createdAt: true }
148
+ }),
149
+ db.chat.findMany({
150
+ where: { role: "assistant" },
151
+ select: { inputTokens: true, outputTokens: true, model: true }
152
+ }),
153
+ getAgentRunsThisMonth(db),
154
+ db.agentRun.findMany({
155
+ where: { startedAt: { gte: thirtyDaysAgo } },
156
+ select: { startedAt: true, status: true }
157
+ }),
158
+ ]);
159
+
160
+ // Document stats
161
+ const statusCounts = {};
162
+ let totalVectors = 0, totalEmbedTokens = 0, totalEmbedCost = 0, totalStorageBytes = 0;
163
+ for (const d of documents) {
164
+ statusCounts[d.status] = (statusCounts[d.status] || 0) + 1;
165
+ totalVectors += d.chunkCount || 0;
166
+ totalEmbedTokens += d.embeddingTokens || 0;
167
+ totalEmbedCost += calcEmbeddingCost(d.embeddingTokens, d.embeddingModel);
168
+ totalStorageBytes += d.size || 0;
169
+ }
170
+ const storageUsedGb = totalStorageBytes / (1024 ** 3);
171
+
172
+ // LLM stats
173
+ let llmInputTokens = 0, llmOutputTokens = 0, llmCost = 0;
174
+ for (const c of allAssistantChats) {
175
+ llmInputTokens += c.inputTokens || 0;
176
+ llmOutputTokens += c.outputTokens || 0;
177
+ llmCost += calcLLMCost(c.inputTokens, c.outputTokens, c.model);
178
+ }
179
+
180
+ res.json({
181
+ // Summary counts
182
+ userCount: users.length,
183
+ workspaceCount: workspaces.length,
184
+ documentCount: documents.length,
185
+ vectorCount: totalVectors,
186
+ chatCount: allAssistantChats.length,
187
+ activeIngestions: (statusCounts.ingesting || 0) + (statusCounts.queued || 0),
188
+ // Cost
189
+ embeddingTokens: totalEmbedTokens,
190
+ embeddingCostUsd: totalEmbedCost,
191
+ llmInputTokens,
192
+ llmOutputTokens,
193
+ llmCostUsd: llmCost,
194
+ totalCostUsd: totalEmbedCost + llmCost,
195
+ // Charts
196
+ documentsByStatus: statusSeries(statusCounts),
197
+ usersByRole: [
198
+ { name: "Admin", value: users.filter(u => u.role === "admin").length, color: "#6366f1" },
199
+ { name: "Manager", value: users.filter(u => u.role === "manager").length, color: "#f59e0b" },
200
+ { name: "User", value: users.filter(u => u.role === "user").length, color: "#22c55e" },
201
+ ].filter(u => u.value > 0),
202
+ topWorkspaces: workspaces
203
+ .sort((a, b) => b._count.chats - a._count.chats)
204
+ .slice(0, 8)
205
+ .map(ws => ({
206
+ name: ws.name.length > 16 ? ws.name.slice(0, 16) + "…" : ws.name,
207
+ chats: ws._count.chats,
208
+ documents: ws._count.documents,
209
+ })),
210
+ // Usage vs limits — commercial only; null means unlimited (Infinity can't serialize to JSON)
211
+ usage: (process.env.LICENSE_TYPE === "enterprise" && process.env.LICENSE_EDITION === "Open Enthrium Commercial" && process.env.LICENSE_PRICE === "custom") ? {
212
+ tierName: tier.name,
213
+ workspaceCount: workspaces.length,
214
+ workspaceLimit: isFinite(tier.maxWorkspaces) ? tier.maxWorkspaces : null,
215
+ userCount: users.length,
216
+ userLimit: isFinite(tier.maxUsers) ? tier.maxUsers : null,
217
+ connectorCount: 0,
218
+ connectorLimit: null,
219
+ agentRunsThisMonth,
220
+ agentRunsLimit: isFinite(tier.maxAgentRunsPerMonth) ? tier.maxAgentRunsPerMonth : null,
221
+ storageUsedGb: parseFloat(storageUsedGb.toFixed(3)),
222
+ storageUsedBytes: totalStorageBytes,
223
+ storageLimitGb: isFinite(tier.ingestionSpaceGb) ? tier.ingestionSpaceGb : null,
224
+ } : null,
225
+ agentRunCount: recentAgentRuns.length,
226
+ agentRunSuccess: recentAgentRuns.filter(r => r.status === "success").length,
227
+ agentRunErrors: recentAgentRuns.filter(r => r.status === "error").length,
228
+ agentRunsByTrigger: [
229
+ { name: "Runs", value: recentAgentRuns.length, color: "#6366f1" },
230
+ ].filter(s => s.value > 0),
231
+ chatActivity: groupByDay(recentUserChats, "createdAt"),
232
+ agentRunActivity: groupByDay(recentAgentRuns, "startedAt"),
233
+ ingestActivity: groupByDay(
234
+ documents.filter(d => new Date(d.createdAt) >= thirtyDaysAgo),
235
+ "createdAt"
236
+ ),
237
+ userGrowth: groupByDay(
238
+ users.filter(u => new Date(u.createdAt) >= thirtyDaysAgo),
239
+ "createdAt"
240
+ ),
241
+ });
242
+ });
243
+
244
+ module.exports = router;