@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,53 @@
1
+ const router = require("express").Router();
2
+ const { authenticate } = require("../middleware/auth");
3
+ const { v4: uuidv4 } = require("uuid");
4
+
5
+ router.get("/:slug", authenticate, async (req, res) => {
6
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
7
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
8
+ const threads = await req.db.thread.findMany({
9
+ where: { workspaceId: workspace.id },
10
+ orderBy: [{ pinned: "desc" }, { createdAt: "desc" }]
11
+ });
12
+ res.json({ threads });
13
+ });
14
+
15
+ router.post("/:slug", authenticate, async (req, res) => {
16
+ const { name } = req.body;
17
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
18
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
19
+ const thread = await req.db.thread.create({
20
+ data: { uid: uuidv4(), workspaceId: workspace.id, name: name || "New Thread" }
21
+ });
22
+ res.json({ thread });
23
+ });
24
+
25
+ router.put("/:slug/:uid/pin", authenticate, async (req, res) => {
26
+ const thread = await req.db.thread.findUnique({ where: { uid: req.params.uid } });
27
+ if (!thread) return res.status(404).json({ error: "Thread not found" });
28
+ const updated = await req.db.thread.update({
29
+ where: { uid: req.params.uid },
30
+ data: { pinned: !thread.pinned }
31
+ });
32
+ res.json({ thread: updated });
33
+ });
34
+
35
+ router.put("/:slug/:uid", authenticate, async (req, res) => {
36
+ const { name } = req.body;
37
+ if (!name?.trim()) return res.status(400).json({ error: "Name required" });
38
+ const thread = await req.db.thread.update({
39
+ where: { uid: req.params.uid },
40
+ data: { name: name.trim() }
41
+ });
42
+ res.json({ thread });
43
+ });
44
+
45
+ router.delete("/:slug/:uid", authenticate, async (req, res) => {
46
+ const thread = await req.db.thread.findUnique({ where: { uid: req.params.uid } });
47
+ if (!thread) return res.status(404).json({ error: "Thread not found" });
48
+ await req.db.chat.deleteMany({ where: { threadId: thread.id } });
49
+ await req.db.thread.delete({ where: { uid: req.params.uid } });
50
+ res.json({ success: true });
51
+ });
52
+
53
+ module.exports = router;
@@ -0,0 +1,464 @@
1
+ const router = require("express").Router();
2
+ const { authenticate, requireAdmin, requireManagerOrAdmin, requireCommercial } = require("../middleware/auth");
3
+ const { v4: uuidv4 } = require("uuid");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ // Check workspace membership — admins/managers pass through; regular users must be a member
8
+ async function wsMember(req, slug) {
9
+ const ws = await req.db.workspace.findUnique({ where: { slug } });
10
+ if (!ws) return null;
11
+ if (req.user.role === "admin" || req.user.role === "manager") return ws;
12
+ const m = await req.db.workspaceUser.findFirst({ where: { userId: req.user.id, workspaceId: ws.id } });
13
+ return m ? ws : false; // null = not found, false = not a member
14
+ }
15
+
16
+ // List workspaces for current user
17
+ router.get("/", authenticate, async (req, res) => {
18
+ const isAdmin = req.user.role === "admin";
19
+ const include = {
20
+ _count: { select: { documents: true, chats: true } },
21
+ createdBy: { select: { id: true, name: true, email: true } },
22
+ };
23
+ const workspaces = isAdmin
24
+ ? await req.db.workspace.findMany({ orderBy: { createdAt: "desc" }, include })
25
+ : await req.db.workspace.findMany({
26
+ where: { users: { some: { userId: req.user.id } } },
27
+ orderBy: { createdAt: "desc" },
28
+ include,
29
+ });
30
+ res.json({ workspaces });
31
+ });
32
+
33
+ // Agent runs across all workspaces the current user manages
34
+ router.get("/agent-runs", authenticate, requireManagerOrAdmin, async (req, res) => {
35
+ try {
36
+ const { period = "all" } = req.query;
37
+ const days = { "7d": 7, "30d": 30 }[period];
38
+ const startedAt = days ? { gte: new Date(Date.now() - days * 86400000) } : undefined;
39
+
40
+ const isAdmin = req.user.role === "admin";
41
+ let workspaceFilter = undefined;
42
+ if (!isAdmin) {
43
+ const memberships = await req.db.workspaceUser.findMany({ where: { userId: req.user.id }, select: { workspaceId: true } });
44
+ const workspaceIds = memberships.map(m => m.workspaceId);
45
+ workspaceFilter = { agent: { workspaceId: { in: workspaceIds } } };
46
+ }
47
+
48
+ const runs = await req.db.agentRun.findMany({
49
+ where: { ...(startedAt ? { startedAt } : {}), ...workspaceFilter },
50
+ orderBy: { startedAt: "desc" },
51
+ take: 200,
52
+ include: {
53
+ agent: { select: { id: true, name: true, workspaceId: true, workspace: { select: { id: true, name: true } } } },
54
+ triggeredBy: { select: { id: true, name: true, email: true } },
55
+ },
56
+ });
57
+ res.json({ runs });
58
+ } catch (err) { res.status(500).json({ error: err.message }); }
59
+ });
60
+
61
+ // ── Member-accessible project routes ─────────────────────────────────────────
62
+
63
+ // List projects (workspace member or manager/admin)
64
+ router.get("/:slug/projects", authenticate, async (req, res) => {
65
+ try {
66
+ const ws = await wsMember(req, req.params.slug);
67
+ if (ws === null) return res.status(404).json({ error: "Workspace not found" });
68
+ if (ws === false) return res.status(403).json({ error: "Access denied" });
69
+ const projects = await req.db.project.findMany({
70
+ where: { workspaceId: ws.id },
71
+ include: { _count: { select: { agents: true, runs: true } } },
72
+ orderBy: { createdAt: "desc" },
73
+ });
74
+ res.json({ projects });
75
+ } catch (err) { res.status(500).json({ error: err.message }); }
76
+ });
77
+
78
+ // List agents for a project (workspace member or manager/admin)
79
+ router.get("/:slug/projects/:projectId/agents", authenticate, async (req, res) => {
80
+ try {
81
+ const ws = await wsMember(req, req.params.slug);
82
+ if (!ws) return res.status(ws === null ? 404 : 403).json({ error: !ws ? "Access denied" : "Not found" });
83
+ const agents = await req.db.projectAgent.findMany({
84
+ where: { projectId: parseInt(req.params.projectId) },
85
+ orderBy: { createdAt: "asc" },
86
+ });
87
+ res.json({ agents });
88
+ } catch (err) { res.status(500).json({ error: err.message }); }
89
+ });
90
+
91
+ // Run agent — SSE (workspace member or manager/admin)
92
+ router.post("/:slug/projects/:projectId/agents/:agentId/run", authenticate, async (req, res) => {
93
+ try {
94
+ const ws = await wsMember(req, req.params.slug);
95
+ if (!ws) return res.status(403).json({ error: "Access denied" });
96
+
97
+ const { input = "" } = req.body;
98
+ const { getLLMConfig } = require("../providers/llm");
99
+ const yaml = require("js-yaml");
100
+ const engine = require("../engine");
101
+
102
+ const [project, agent] = await Promise.all([
103
+ req.db.project.findUnique({ where: { id: parseInt(req.params.projectId) } }),
104
+ req.db.projectAgent.findUnique({ where: { id: parseInt(req.params.agentId) } }),
105
+ ]);
106
+ if (!project || !agent) return res.status(404).json({ error: "Not found" });
107
+
108
+ const llmConfig = await getLLMConfig();
109
+ const oeConfig = (() => { try { return JSON.parse(project.oeConfig || "{}"); } catch { return {}; } })();
110
+ // Mirror CLI prepareConnectors: extract creds and wrap in authConfig/config JSON strings
111
+ // so every adapter can read them via JSON.parse(connector.config / connector.authConfig)
112
+ const connectors = (oeConfig.connectors || []).map((c, i) => {
113
+ const { connection_name, connection_type, ...creds } = c;
114
+ return {
115
+ id: i + 1,
116
+ name: connection_name,
117
+ type: connection_type,
118
+ status: "active",
119
+ authConfig: JSON.stringify(creds),
120
+ config: JSON.stringify(creds),
121
+ };
122
+ });
123
+ const doc = (() => { try { return yaml.load(agent.yamlContent) || {}; } catch { return {}; } })();
124
+ const agentSpec = {
125
+ systemPrompt: doc.instructions || doc.system_prompt || doc.systemPrompt || "",
126
+ workflow: (doc.steps || []).map(s => ({ name: s.name || "", content: s.content || "" })),
127
+ params: [], maxRounds: 25, input,
128
+ };
129
+ const refNames = (doc.connectors || []).map(c => c.connection_name);
130
+ const filteredConns = refNames.length ? connectors.filter(c => refNames.includes(c.name)) : connectors;
131
+
132
+ res.setHeader("Content-Type", "text/event-stream");
133
+ res.setHeader("Cache-Control", "no-cache");
134
+ res.setHeader("Connection", "keep-alive");
135
+
136
+ const run = await req.db.projectRun.create({
137
+ data: { projectId: project.id, agentId: agent.id, triggeredByUserId: req.user?.id || null, status: "running", input: input || null },
138
+ });
139
+
140
+ const { executeTool } = require("../utils/tools/registry");
141
+ await engine.run(agentSpec, llmConfig, filteredConns, {
142
+ toolExecutor: (name, args, conns) => executeTool(name, args, conns, req.db),
143
+ onToolCall: (name) => res.write(`data: ${JSON.stringify({ tool_call: name })}\n\n`),
144
+ checkCancel: async () => {
145
+ const [r] = await req.db.$queryRaw`SELECT cancelRequested FROM ProjectRun WHERE id = ${run.id}`;
146
+ return r?.cancelRequested || false;
147
+ },
148
+ onDone: async (output) => {
149
+ await req.db.projectRun.update({ where: { id: run.id }, data: { status: "success", output, completedAt: new Date() } });
150
+ res.write(`data: ${JSON.stringify({ done: true, output, runId: run.id })}\n\n`);
151
+ res.end();
152
+ },
153
+ onError: async (err) => {
154
+ await req.db.projectRun.update({ where: { id: run.id }, data: { status: "error", error: err.message, completedAt: new Date() } }).catch(() => {});
155
+ res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
156
+ res.end();
157
+ },
158
+ });
159
+ } catch (err) {
160
+ if (!res.headersSent) return res.status(500).json({ error: err.message });
161
+ res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
162
+ res.end();
163
+ }
164
+ });
165
+
166
+ // Cancel run (workspace member or manager/admin)
167
+ router.post("/:slug/projects/:projectId/runs/:runId/cancel", authenticate, async (req, res) => {
168
+ try {
169
+ const ws = await wsMember(req, req.params.slug);
170
+ if (!ws) return res.status(403).json({ error: "Access denied" });
171
+ await req.db.$executeRaw`UPDATE ProjectRun SET cancelRequested = 1 WHERE id = ${parseInt(req.params.runId)}`;
172
+ res.json({ ok: true });
173
+ } catch (err) { res.status(500).json({ error: err.message }); }
174
+ });
175
+
176
+ // List project runs (workspace member or manager/admin)
177
+ router.get("/:slug/project-runs", authenticate, async (req, res) => {
178
+ try {
179
+ const ws = await wsMember(req, req.params.slug);
180
+ if (!ws) return res.status(ws === null ? 404 : 403).json({ error: "Not found" });
181
+ const { period = "all" } = req.query;
182
+ const days = { "7d": 7, "30d": 30 }[period];
183
+ const startFilter = days ? { gte: new Date(Date.now() - days * 86400000) } : undefined;
184
+ const runs = await req.db.projectRun.findMany({
185
+ where: {
186
+ project: { workspaceId: ws.id },
187
+ ...(startFilter ? { startedAt: startFilter } : {}),
188
+ },
189
+ orderBy: { startedAt: "desc" },
190
+ take: 300,
191
+ include: {
192
+ project: { select: { id: true, name: true } },
193
+ agent: { select: { id: true, name: true, fileName: true } },
194
+ triggeredBy: { select: { id: true, name: true, email: true } },
195
+ },
196
+ });
197
+ res.json({ runs });
198
+ } catch (err) { res.status(500).json({ error: err.message }); }
199
+ });
200
+
201
+ // Clear all project runs (manager/admin only — destructive)
202
+ router.delete("/:slug/project-runs", authenticate, requireManagerOrAdmin, async (req, res) => {
203
+ try {
204
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
205
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
206
+ await req.db.projectRun.deleteMany({ where: { project: { workspaceId: workspace.id } } });
207
+ res.json({ ok: true });
208
+ } catch (err) { res.status(500).json({ error: err.message }); }
209
+ });
210
+
211
+ // Get single workspace
212
+ router.get("/:slug", authenticate, async (req, res) => {
213
+ const workspace = await req.db.workspace.findUnique({
214
+ where: { slug: req.params.slug },
215
+ include: { documents: { orderBy: { createdAt: "desc" } }, _count: { select: { chats: true } } }
216
+ });
217
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
218
+ res.json({ workspace });
219
+ });
220
+
221
+ // Create workspace (manager or admin)
222
+ router.post("/", authenticate, requireManagerOrAdmin, async (req, res) => {
223
+ const { name, systemPrompt } = req.body;
224
+ if (!name) return res.status(400).json({ error: "Name required" });
225
+
226
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") + "-" + uuidv4().slice(0, 6);
227
+ const workspace = await req.db.workspace.create({
228
+ data: { name, slug, systemPrompt: systemPrompt || null, createdByUserId: req.user.id || null },
229
+ include: { createdBy: { select: { id: true, name: true, email: true } } }
230
+ });
231
+
232
+ // Auto-add creator as member (skip superadmin who has no DB row)
233
+ if (req.user.id !== 0) {
234
+ await req.db.workspaceUser.create({ data: { userId: req.user.id, workspaceId: workspace.id } });
235
+ }
236
+
237
+ res.json({ workspace });
238
+ });
239
+
240
+ // Update workspace (manager or admin)
241
+ router.put("/:slug", authenticate, requireManagerOrAdmin, async (req, res) => {
242
+ const { name, systemPrompt, embedEnabled, temperature, chatHistory, queryRefusalResponse, starterPrompts } = req.body;
243
+ const workspace = await req.db.workspace.update({
244
+ where: { slug: req.params.slug },
245
+ data: {
246
+ ...(name && { name }),
247
+ ...(systemPrompt !== undefined && { systemPrompt: systemPrompt || null }),
248
+ ...(embedEnabled !== undefined && { embedEnabled: Boolean(embedEnabled) }),
249
+ ...(temperature !== undefined && { temperature: parseFloat(temperature) }),
250
+ ...(chatHistory !== undefined && { chatHistory: parseInt(chatHistory) }),
251
+ ...(queryRefusalResponse !== undefined && { queryRefusalResponse: queryRefusalResponse || null }),
252
+ ...(starterPrompts !== undefined && { starterPrompts: JSON.stringify(starterPrompts) }),
253
+ }
254
+ });
255
+ res.json({ workspace });
256
+ });
257
+
258
+ // Delete workspace (manager or admin)
259
+ router.delete("/:slug", authenticate, requireManagerOrAdmin, async (req, res) => {
260
+ await req.db.workspace.delete({ where: { slug: req.params.slug } });
261
+ res.json({ success: true });
262
+ });
263
+
264
+ // Add user to workspace (manager or admin)
265
+ router.post("/:slug/users", authenticate, requireManagerOrAdmin, async (req, res) => {
266
+ const { userId } = req.body;
267
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
268
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
269
+
270
+ await req.db.workspaceUser.upsert({
271
+ where: { userId_workspaceId: { userId: parseInt(userId), workspaceId: workspace.id } },
272
+ create: { userId: parseInt(userId), workspaceId: workspace.id },
273
+ update: {}
274
+ });
275
+ res.json({ success: true });
276
+ });
277
+
278
+ // Remove user from workspace (manager or admin)
279
+ router.delete("/:slug/users/:userId", authenticate, requireManagerOrAdmin, async (req, res) => {
280
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
281
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
282
+
283
+ await req.db.workspaceUser.deleteMany({
284
+ where: { userId: parseInt(req.params.userId), workspaceId: workspace.id }
285
+ });
286
+ res.json({ success: true });
287
+ });
288
+
289
+ // ── Manager: all agent runs across managed workspaces ────────────────────────
290
+ router.get("/agent-runs", authenticate, requireManagerOrAdmin, async (req, res) => {
291
+ try {
292
+ const { period = "all" } = req.query;
293
+ const days = { "7d": 7, "30d": 30 }[period];
294
+ const startedAt = days ? { gte: new Date(Date.now() - days * 86400000) } : undefined;
295
+
296
+ const workspaces = await req.db.workspace.findMany({
297
+ where: req.user.role === "admin" ? undefined : { users: { some: { userId: req.user.id } } },
298
+ select: { id: true },
299
+ });
300
+ const workspaceIds = workspaces.map(w => w.id);
301
+
302
+ const agents = await req.db.agent.findMany({
303
+ where: { workspaceId: { in: workspaceIds } },
304
+ select: { id: true },
305
+ });
306
+ const agentIds = agents.map(a => a.id);
307
+
308
+ const runs = await req.db.agentRun.findMany({
309
+ where: { agentId: { in: agentIds }, ...(startedAt ? { startedAt } : {}) },
310
+ orderBy: { startedAt: "desc" },
311
+ take: 200,
312
+ include: {
313
+ agent: { select: { id: true, name: true, workspaceId: true, workspace: { select: { id: true, name: true } } } },
314
+ triggeredBy: { select: { id: true, name: true, email: true } },
315
+ },
316
+ });
317
+ res.json({ runs });
318
+ } catch (err) { res.status(500).json({ error: err.message }); }
319
+ });
320
+
321
+ // ── User-facing agent routes ──────────────────────────────────────────────────
322
+ // Helpers
323
+ function isManagerOrAdmin(user) { return user.role === "admin" || user.role === "manager"; }
324
+
325
+ async function assertWorkspaceMember(db, user, workspaceId) {
326
+ if (user.role === "admin") return true;
327
+ const member = await db.workspaceUser.findFirst({ where: { userId: user.id, workspaceId } });
328
+ return !!member;
329
+ }
330
+ // ── Knowledge Base Sharing ────────────────────────────────────────────────────
331
+
332
+ router.get("/:slug/kb-shares", authenticate, requireCommercial, async (req, res) => {
333
+ try {
334
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
335
+ if (!workspace) return res.status(404).json({ error: "Not found" });
336
+ const [outgoing, incoming] = await Promise.all([
337
+ req.db.workspaceKBShare.findMany({
338
+ where: { sourceWorkspaceId: workspace.id },
339
+ include: { targetWorkspace: { select: { id: true, slug: true, name: true } } },
340
+ orderBy: { createdAt: "asc" },
341
+ }),
342
+ req.db.workspaceKBShare.findMany({
343
+ where: { targetWorkspaceId: workspace.id },
344
+ include: { sourceWorkspace: { select: { id: true, slug: true, name: true } } },
345
+ orderBy: { createdAt: "asc" },
346
+ }),
347
+ ]);
348
+ res.json({ outgoing, incoming });
349
+ } catch (err) { res.status(500).json({ error: err.message }); }
350
+ });
351
+
352
+ router.post("/:slug/kb-shares", authenticate, requireCommercial, async (req, res) => {
353
+ try {
354
+ const featureSetting = await req.db.setting.findUnique({ where: { key: "feature.kbSharing" } });
355
+ if (featureSetting?.value !== "true") return res.status(403).json({ error: "Knowledge Base Sharing is disabled on this installation." });
356
+
357
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
358
+ if (!workspace) return res.status(404).json({ error: "Not found" });
359
+ if (!await assertWorkspaceMember(req.db, req.user, workspace.id)) return res.status(403).json({ error: "Access denied" });
360
+ const target = await req.db.workspace.findUnique({ where: { slug: req.body.targetWorkspaceSlug } });
361
+ if (!target) return res.status(404).json({ error: "Target workspace not found" });
362
+ if (target.id === workspace.id) return res.status(400).json({ error: "Cannot share with yourself" });
363
+ const share = await req.db.workspaceKBShare.create({
364
+ data: { sourceWorkspaceId: workspace.id, targetWorkspaceId: target.id },
365
+ include: { targetWorkspace: { select: { id: true, slug: true, name: true } } },
366
+ });
367
+ res.json({ share });
368
+ } catch (err) {
369
+ if (err.code === "P2002") return res.status(409).json({ error: "Already shared with this workspace" });
370
+ res.status(500).json({ error: err.message });
371
+ }
372
+ });
373
+
374
+ router.delete("/:slug/kb-shares/:id", authenticate, requireCommercial, async (req, res) => {
375
+ try {
376
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
377
+ if (!workspace) return res.status(404).json({ error: "Not found" });
378
+ if (!await assertWorkspaceMember(req.db, req.user, workspace.id)) return res.status(403).json({ error: "Access denied" });
379
+ await req.db.workspaceKBShare.delete({ where: { id: parseInt(req.params.id), sourceWorkspaceId: workspace.id } });
380
+ res.json({ ok: true });
381
+ } catch (err) { res.status(500).json({ error: err.message }); }
382
+ });
383
+
384
+ router.get("/:slug/peer-workspaces", authenticate, async (req, res) => {
385
+ try {
386
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
387
+ if (!workspace) return res.status(404).json({ error: "Not found" });
388
+ const workspaces = await req.db.workspace.findMany({
389
+ where: { id: { not: workspace.id } },
390
+ select: { id: true, slug: true, name: true },
391
+ orderBy: { name: "asc" },
392
+ });
393
+ res.json({ workspaces });
394
+ } catch (err) { res.status(500).json({ error: err.message }); }
395
+ });
396
+
397
+ // ── Workspace DLP Policies ────────────────────────────────────────────────────
398
+
399
+ router.get("/:slug/dlp-policies", authenticate, requireManagerOrAdmin, async (req, res) => {
400
+ try {
401
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
402
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
403
+ const links = await req.db.workspaceDlpPolicy.findMany({
404
+ where: { workspaceId: workspace.id },
405
+ include: { policy: true },
406
+ orderBy: { createdAt: "asc" },
407
+ });
408
+ res.json({ policies: links.map(l => ({ linkId: l.id, ...l.policy })) });
409
+ } catch (err) { res.status(500).json({ error: err.message }); }
410
+ });
411
+
412
+ router.post("/:slug/dlp-policies", authenticate, requireManagerOrAdmin, async (req, res) => {
413
+ try {
414
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
415
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
416
+ const policy = await req.db.dlpPolicy.findUnique({ where: { id: parseInt(req.body.policyId) } });
417
+ if (!policy) return res.status(404).json({ error: "Policy not found" });
418
+ const link = await req.db.workspaceDlpPolicy.create({
419
+ data: { workspaceId: workspace.id, policyId: policy.id },
420
+ include: { policy: true },
421
+ });
422
+ res.json({ policy: { linkId: link.id, ...link.policy } });
423
+ } catch (err) {
424
+ if (err.code === "P2002") return res.status(409).json({ error: "Policy already assigned to this workspace" });
425
+ res.status(500).json({ error: err.message });
426
+ }
427
+ });
428
+
429
+ router.delete("/:slug/dlp-policies/:linkId", authenticate, requireManagerOrAdmin, async (req, res) => {
430
+ try {
431
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
432
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
433
+ await req.db.workspaceDlpPolicy.delete({
434
+ where: { id: parseInt(req.params.linkId), workspaceId: workspace.id },
435
+ });
436
+ res.json({ ok: true });
437
+ } catch (err) { res.status(500).json({ error: err.message }); }
438
+ });
439
+
440
+ // ── Agent config (inline connector credentials) ───────────────────────────────
441
+
442
+ router.get("/:slug/agent-config", authenticate, requireManagerOrAdmin, async (req, res) => {
443
+ try {
444
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
445
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
446
+ const config = (() => { try { return JSON.parse(workspace.agentConfig || "{}"); } catch { return {}; } })();
447
+ res.json({ config });
448
+ } catch (err) { res.status(500).json({ error: err.message }); }
449
+ });
450
+
451
+ router.put("/:slug/agent-config", authenticate, requireManagerOrAdmin, async (req, res) => {
452
+ try {
453
+ const workspace = await req.db.workspace.findUnique({ where: { slug: req.params.slug } });
454
+ if (!workspace) return res.status(404).json({ error: "Workspace not found" });
455
+ const { config } = req.body;
456
+ await req.db.workspace.update({
457
+ where: { slug: req.params.slug },
458
+ data: { agentConfig: JSON.stringify(config) },
459
+ });
460
+ res.json({ ok: true });
461
+ } catch (err) { res.status(500).json({ error: err.message }); }
462
+ });
463
+
464
+ module.exports = router;
@@ -0,0 +1,39 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { v4: uuidv4 } = require("uuid");
4
+ const { sendRegistration } = require("./registration");
5
+
6
+ const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../../package.json"), "utf8"));
7
+
8
+ module.exports = async function bootstrap(prisma) {
9
+ try {
10
+ if (process.env.LICENSE_TYPE === "enterprise") return;
11
+
12
+ const upsert = (key, value) => prisma.setting.upsert({
13
+ where: { key },
14
+ create: { key, value },
15
+ update: { value },
16
+ });
17
+
18
+ const getSetting = (key) => prisma.setting.findUnique({ where: { key } }).then(r => r?.value ?? null);
19
+
20
+ // Generate instance ID once
21
+ let instanceId = await getSetting("telemetry.instanceId");
22
+ if (!instanceId) {
23
+ instanceId = uuidv4();
24
+ await upsert("telemetry.instanceId", instanceId);
25
+ }
26
+
27
+ const lastVersion = await getSetting("telemetry.lastVersion");
28
+
29
+ // Send on first boot or version upgrade
30
+ if (lastVersion !== version) {
31
+ const email = process.env.SUPER_ADMIN_EMAIL || "";
32
+ await sendRegistration({ email, company: "", instanceId, version, event: lastVersion ? "upgrade" : "first_boot" });
33
+ await upsert("telemetry.lastVersion", version);
34
+ console.log(`[Telemetry] Registered instance ${instanceId} v${version}`);
35
+ }
36
+ } catch (err) {
37
+ // never crash the server
38
+ }
39
+ };
@@ -0,0 +1,16 @@
1
+ const TELEMETRY_URL = "https://script.google.com/macros/s/AKfycbxzUHUVoXFQW9Hh2cUMKeIhmNWNLPHU2nSKS_yDO3CLYNpfmvGMuyeH8Y8jypvbdAQ/exec";
2
+ const TELEMETRY_TOKEN = "oe-reg-2026-openenthrium";
3
+
4
+ async function sendRegistration({ email, company, instanceId, version, event = "first_boot" }) {
5
+ try {
6
+ await fetch(TELEMETRY_URL, {
7
+ method: "POST",
8
+ headers: { "Content-Type": "application/json" },
9
+ body: JSON.stringify({ token: TELEMETRY_TOKEN, email, company, instanceId, version, event }),
10
+ });
11
+ } catch {
12
+ // silent fail — never block the user
13
+ }
14
+ }
15
+
16
+ module.exports = { sendRegistration };
@@ -0,0 +1,16 @@
1
+ async function logActivity(db, user, action, details = {}) {
2
+ try {
3
+ await db.activityLog.create({
4
+ data: {
5
+ userId: user?.id || null,
6
+ userEmail: user?.email || null,
7
+ action,
8
+ details: Object.keys(details).length > 0 ? JSON.stringify(details) : null
9
+ }
10
+ });
11
+ } catch (e) {
12
+ console.error("Activity log failed:", e.message);
13
+ }
14
+ }
15
+
16
+ module.exports = { logActivity };