@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,238 @@
|
|
|
1
|
+
const router = require("express").Router({ mergeParams: true });
|
|
2
|
+
const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
|
|
3
|
+
const { getLLMConfig } = require("../providers/llm");
|
|
4
|
+
const engine = require("../engine");
|
|
5
|
+
const yaml = require("js-yaml");
|
|
6
|
+
|
|
7
|
+
router.use(authenticate, requireManagerOrAdmin);
|
|
8
|
+
|
|
9
|
+
// ── List projects ─────────────────────────────────────────────────────────────
|
|
10
|
+
router.get("/", async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const projects = await req.db.project.findMany({
|
|
13
|
+
where: { workspaceId: parseInt(req.params.workspaceId) },
|
|
14
|
+
include: { _count: { select: { agents: true, runs: true } } },
|
|
15
|
+
orderBy: { createdAt: "desc" },
|
|
16
|
+
});
|
|
17
|
+
res.json({ projects });
|
|
18
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ── Create project ────────────────────────────────────────────────────────────
|
|
22
|
+
router.post("/", async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { name, description, manifest, oeConfig, agents = [] } = req.body;
|
|
25
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
26
|
+
|
|
27
|
+
const project = await req.db.project.create({
|
|
28
|
+
data: {
|
|
29
|
+
workspaceId: parseInt(req.params.workspaceId),
|
|
30
|
+
name,
|
|
31
|
+
description: description || null,
|
|
32
|
+
manifest: manifest ? JSON.stringify(manifest) : null,
|
|
33
|
+
oeConfig: oeConfig ? JSON.stringify(oeConfig) : null,
|
|
34
|
+
createdByUserId: req.user?.id || null,
|
|
35
|
+
agents: {
|
|
36
|
+
create: agents.map(a => ({
|
|
37
|
+
name: a.name,
|
|
38
|
+
description: a.description || null,
|
|
39
|
+
fileName: a.fileName || "agent.yaml",
|
|
40
|
+
yamlContent: a.yamlContent || "",
|
|
41
|
+
isDefault: a.isDefault || false,
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
include: { agents: { orderBy: { createdAt: "asc" } }, _count: { select: { agents: true, runs: true } } },
|
|
46
|
+
});
|
|
47
|
+
res.json({ project });
|
|
48
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── Get project ───────────────────────────────────────────────────────────────
|
|
52
|
+
router.get("/:projectId", async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const project = await req.db.project.findFirst({
|
|
55
|
+
where: { id: parseInt(req.params.projectId), workspaceId: parseInt(req.params.workspaceId) },
|
|
56
|
+
include: { agents: { orderBy: { createdAt: "asc" } } },
|
|
57
|
+
});
|
|
58
|
+
if (!project) return res.status(404).json({ error: "Project not found" });
|
|
59
|
+
res.json({ project });
|
|
60
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── Update project ────────────────────────────────────────────────────────────
|
|
64
|
+
router.put("/:projectId", async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const { name, description, oeConfig, manifest } = req.body;
|
|
67
|
+
const project = await req.db.project.update({
|
|
68
|
+
where: { id: parseInt(req.params.projectId) },
|
|
69
|
+
data: {
|
|
70
|
+
...(name !== undefined && { name }),
|
|
71
|
+
...(description !== undefined && { description: description || null }),
|
|
72
|
+
...(oeConfig !== undefined && { oeConfig: JSON.stringify(oeConfig) }),
|
|
73
|
+
...(manifest !== undefined && {
|
|
74
|
+
manifest: JSON.stringify(manifest),
|
|
75
|
+
...(manifest.name && { name: manifest.name }),
|
|
76
|
+
...(manifest.description !== undefined && { description: manifest.description || null }),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
include: { agents: { orderBy: { createdAt: "asc" } } },
|
|
80
|
+
});
|
|
81
|
+
res.json({ project });
|
|
82
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── Delete project ────────────────────────────────────────────────────────────
|
|
86
|
+
router.delete("/:projectId", async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
await req.db.project.delete({ where: { id: parseInt(req.params.projectId) } });
|
|
89
|
+
res.json({ ok: true });
|
|
90
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Agents ────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
router.get("/:projectId/agents", async (req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
const agents = await req.db.projectAgent.findMany({
|
|
98
|
+
where: { projectId: parseInt(req.params.projectId) },
|
|
99
|
+
orderBy: { createdAt: "asc" },
|
|
100
|
+
});
|
|
101
|
+
res.json({ agents });
|
|
102
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
router.post("/:projectId/agents", async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const { name, description, fileName, yamlContent, isDefault } = req.body;
|
|
108
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
109
|
+
const agent = await req.db.projectAgent.create({
|
|
110
|
+
data: {
|
|
111
|
+
projectId: parseInt(req.params.projectId),
|
|
112
|
+
name,
|
|
113
|
+
description: description || null,
|
|
114
|
+
fileName: fileName || "agent.yaml",
|
|
115
|
+
yamlContent: yamlContent || "",
|
|
116
|
+
isDefault: !!isDefault,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
res.json({ agent });
|
|
120
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
router.put("/:projectId/agents/:agentId", async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const { name, description, fileName, yamlContent, isDefault } = req.body;
|
|
126
|
+
const agent = await req.db.projectAgent.update({
|
|
127
|
+
where: { id: parseInt(req.params.agentId) },
|
|
128
|
+
data: {
|
|
129
|
+
...(name !== undefined && { name }),
|
|
130
|
+
...(description !== undefined && { description: description || null }),
|
|
131
|
+
...(fileName !== undefined && { fileName }),
|
|
132
|
+
...(yamlContent !== undefined && { yamlContent }),
|
|
133
|
+
...(isDefault !== undefined && { isDefault: !!isDefault }),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
res.json({ agent });
|
|
137
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
router.delete("/:projectId/agents/:agentId", async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
await req.db.projectAgent.delete({ where: { id: parseInt(req.params.agentId) } });
|
|
143
|
+
res.json({ ok: true });
|
|
144
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── Run agent (SSE) ───────────────────────────────────────────────────────────
|
|
148
|
+
router.post("/:projectId/agents/:agentId/run", async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const { input = "" } = req.body;
|
|
151
|
+
const [project, agent] = await Promise.all([
|
|
152
|
+
req.db.project.findUnique({ where: { id: parseInt(req.params.projectId) } }),
|
|
153
|
+
req.db.projectAgent.findUnique({ where: { id: parseInt(req.params.agentId) } }),
|
|
154
|
+
]);
|
|
155
|
+
if (!project || !agent) return res.status(404).json({ error: "Not found" });
|
|
156
|
+
|
|
157
|
+
// LLM: platform config (workspace LLM settings)
|
|
158
|
+
const llmConfig = await getLLMConfig();
|
|
159
|
+
|
|
160
|
+
// Connectors: from project's oe-config.json
|
|
161
|
+
const oeConfig = (() => { try { return JSON.parse(project.oeConfig || "{}"); } catch { return {}; } })();
|
|
162
|
+
const connectors = (oeConfig.connectors || []).map((c, i) => ({
|
|
163
|
+
id: i + 1,
|
|
164
|
+
name: c.connection_name,
|
|
165
|
+
type: c.connection_type,
|
|
166
|
+
...c,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// Parse YAML → agentSpec
|
|
170
|
+
const doc = (() => { try { return yaml.load(agent.yamlContent) || {}; } catch { return {}; } })();
|
|
171
|
+
const agentSpec = {
|
|
172
|
+
systemPrompt: doc.instructions || doc.system_prompt || doc.systemPrompt || "",
|
|
173
|
+
workflow: (doc.steps || []).map(s => ({ name: s.name || "", content: s.content || "" })),
|
|
174
|
+
params: [],
|
|
175
|
+
maxRounds: 25,
|
|
176
|
+
input,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Filter connectors by what YAML declares (if any)
|
|
180
|
+
const refNames = (doc.connectors || []).map(c => c.connection_name);
|
|
181
|
+
const filteredConns = refNames.length ? connectors.filter(c => refNames.includes(c.name)) : connectors;
|
|
182
|
+
|
|
183
|
+
// SSE
|
|
184
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
185
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
186
|
+
res.setHeader("Connection", "keep-alive");
|
|
187
|
+
|
|
188
|
+
const run = await req.db.projectRun.create({
|
|
189
|
+
data: {
|
|
190
|
+
projectId: project.id,
|
|
191
|
+
agentId: agent.id,
|
|
192
|
+
triggeredByUserId: req.user?.id || null,
|
|
193
|
+
status: "running",
|
|
194
|
+
input: input || null,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const { executeTool } = require("../utils/tools/registry");
|
|
199
|
+
|
|
200
|
+
await engine.run(
|
|
201
|
+
agentSpec,
|
|
202
|
+
llmConfig,
|
|
203
|
+
filteredConns,
|
|
204
|
+
{
|
|
205
|
+
toolExecutor: (name, args, conns) => executeTool(name, args, conns, req.db),
|
|
206
|
+
onToolCall: (name) => res.write(`data: ${JSON.stringify({ tool_call: name })}\n\n`),
|
|
207
|
+
checkCancel: async () => {
|
|
208
|
+
const [r] = await req.db.$queryRaw`SELECT cancelRequested FROM ProjectRun WHERE id = ${run.id}`;
|
|
209
|
+
return r?.cancelRequested || false;
|
|
210
|
+
},
|
|
211
|
+
onDone: async (output) => {
|
|
212
|
+
await req.db.projectRun.update({ where: { id: run.id }, data: { status: "success", output, completedAt: new Date() } });
|
|
213
|
+
res.write(`data: ${JSON.stringify({ done: true, output, runId: run.id })}\n\n`);
|
|
214
|
+
res.end();
|
|
215
|
+
},
|
|
216
|
+
onError: async (err) => {
|
|
217
|
+
await req.db.projectRun.update({ where: { id: run.id }, data: { status: "error", error: err.message, completedAt: new Date() } }).catch(() => {});
|
|
218
|
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
219
|
+
res.end();
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (!res.headersSent) return res.status(500).json({ error: err.message });
|
|
225
|
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
226
|
+
res.end();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── Cancel run ────────────────────────────────────────────────────────────────
|
|
231
|
+
router.post("/:projectId/runs/:runId/cancel", async (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
await req.db.$executeRaw`UPDATE ProjectRun SET cancelRequested = 1 WHERE id = ${parseInt(req.params.runId)}`;
|
|
234
|
+
res.json({ ok: true });
|
|
235
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
module.exports = router;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireAdmin } = require("../middleware/auth");
|
|
3
|
+
const { logActivity } = require("../utils/activityLog");
|
|
4
|
+
|
|
5
|
+
router.use(authenticate, requireAdmin);
|
|
6
|
+
|
|
7
|
+
router.get("/", async (req, res) => {
|
|
8
|
+
const settings = await req.db.setting.findMany();
|
|
9
|
+
const safe = settings.reduce((acc, s) => {
|
|
10
|
+
const isSensitive = s.key.includes("api_key") || s.key.toLowerCase().includes("secret");
|
|
11
|
+
const isEmpty = !s.value || s.value === "null" || s.value === "undefined";
|
|
12
|
+
acc[s.key] = isSensitive ? (isEmpty ? null : "********") : s.value;
|
|
13
|
+
return acc;
|
|
14
|
+
}, {});
|
|
15
|
+
res.json({ settings: safe });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
router.put("/", async (req, res) => {
|
|
19
|
+
const { settings } = req.body;
|
|
20
|
+
const changedKeys = [];
|
|
21
|
+
|
|
22
|
+
for (const [key, value] of Object.entries(settings || {})) {
|
|
23
|
+
if (value === "********") continue;
|
|
24
|
+
const isSensitive = key.includes("api_key") || key.toLowerCase().includes("secret");
|
|
25
|
+
const isEmpty = value === null || value === undefined || value === "" || value === "null";
|
|
26
|
+
if (isEmpty && isSensitive) {
|
|
27
|
+
await req.db.setting.deleteMany({ where: { key } });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
await req.db.setting.upsert({
|
|
31
|
+
where: { key },
|
|
32
|
+
create: { key, value: String(value) },
|
|
33
|
+
update: { value: String(value) }
|
|
34
|
+
});
|
|
35
|
+
changedKeys.push(isSensitive ? `${key} (updated, value hidden)` : `${key}=${value}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (changedKeys.length > 0) {
|
|
39
|
+
await logActivity(req.db, req.user, "settings.updated", { changed: changedKeys });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.json({ success: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
module.exports = router;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const bcrypt = require("bcryptjs");
|
|
3
|
+
const jwt = require("jsonwebtoken");
|
|
4
|
+
|
|
5
|
+
router.get("/status", async (req, res) => {
|
|
6
|
+
const adminCount = await req.db.user.count({ where: { role: "admin" } });
|
|
7
|
+
res.json({ setupComplete: adminCount > 0 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
router.post("/complete", async (req, res) => {
|
|
11
|
+
const adminCount = await req.db.user.count({ where: { role: "admin" } });
|
|
12
|
+
if (adminCount > 0) return res.status(400).json({ error: "Setup already completed" });
|
|
13
|
+
|
|
14
|
+
const { name, email, password, settings = {} } = req.body;
|
|
15
|
+
if (!email || !password) return res.status(400).json({ error: "Email and password required" });
|
|
16
|
+
if (password.length < 8) return res.status(400).json({ error: "Password must be at least 8 characters" });
|
|
17
|
+
|
|
18
|
+
const hash = await bcrypt.hash(password, 12);
|
|
19
|
+
const user = await req.db.user.create({
|
|
20
|
+
data: { email: email.toLowerCase(), password: hash, name, role: "admin" }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Save all LLM/embedding settings generically
|
|
24
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
25
|
+
if (value === undefined || value === null || value === "") continue;
|
|
26
|
+
await req.db.setting.upsert({
|
|
27
|
+
where: { key },
|
|
28
|
+
create: { key, value: String(value) },
|
|
29
|
+
update: { value: String(value) }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = jwt.sign(
|
|
34
|
+
{ id: user.id, email: user.email, role: user.role, name: user.name },
|
|
35
|
+
process.env.JWT_SECRET,
|
|
36
|
+
{ expiresIn: "7d" }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
module.exports = router;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const jwt = require("jsonwebtoken");
|
|
3
|
+
const axios = require("axios");
|
|
4
|
+
const { authenticate, requireAdmin } = require("../middleware/auth");
|
|
5
|
+
|
|
6
|
+
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000";
|
|
7
|
+
const CALLBACK_BASE = process.env.OAUTH_CALLBACK_BASE || "http://localhost:3001";
|
|
8
|
+
const CALLBACK_URL = `${CALLBACK_BASE}/api/sso/callback`;
|
|
9
|
+
|
|
10
|
+
const PROVIDERS = {
|
|
11
|
+
google: {
|
|
12
|
+
name: "Google",
|
|
13
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
14
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
15
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
16
|
+
scope: "openid email profile",
|
|
17
|
+
getEmail: d => d.email,
|
|
18
|
+
},
|
|
19
|
+
microsoft: {
|
|
20
|
+
name: "Microsoft",
|
|
21
|
+
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
22
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
23
|
+
userInfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
24
|
+
scope: "openid email User.Read",
|
|
25
|
+
getEmail: d => d.mail || d.userPrincipalName,
|
|
26
|
+
},
|
|
27
|
+
github: {
|
|
28
|
+
name: "GitHub",
|
|
29
|
+
authUrl: "https://github.com/login/oauth/authorize",
|
|
30
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
31
|
+
userInfoUrl: "https://api.github.com/user/emails",
|
|
32
|
+
scope: "user:email",
|
|
33
|
+
getEmail: d => {
|
|
34
|
+
if (Array.isArray(d)) {
|
|
35
|
+
const primary = d.find(e => e.primary && e.verified);
|
|
36
|
+
return primary?.email || d[0]?.email;
|
|
37
|
+
}
|
|
38
|
+
return d.email;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
facebook: {
|
|
42
|
+
name: "Facebook",
|
|
43
|
+
authUrl: "https://www.facebook.com/v18.0/dialog/oauth",
|
|
44
|
+
tokenUrl: "https://graph.facebook.com/v18.0/oauth/access_token",
|
|
45
|
+
userInfoUrl: "https://graph.facebook.com/me?fields=email",
|
|
46
|
+
scope: "email",
|
|
47
|
+
getEmail: d => d.email,
|
|
48
|
+
},
|
|
49
|
+
apple: {
|
|
50
|
+
name: "Apple",
|
|
51
|
+
authUrl: "https://appleid.apple.com/auth/authorize",
|
|
52
|
+
tokenUrl: "https://appleid.apple.com/auth/token",
|
|
53
|
+
userInfoUrl: null,
|
|
54
|
+
scope: "name email",
|
|
55
|
+
getEmail: d => d.email,
|
|
56
|
+
note: "Apple requires a private key (p8) to generate the client secret. Contact support to enable.",
|
|
57
|
+
},
|
|
58
|
+
zoho: {
|
|
59
|
+
name: "Zoho",
|
|
60
|
+
authUrl: "https://accounts.zoho.com/oauth/v2/auth",
|
|
61
|
+
tokenUrl: "https://accounts.zoho.com/oauth/v2/token",
|
|
62
|
+
userInfoUrl: "https://accounts.zoho.com/oauth/v2/userinfo",
|
|
63
|
+
scope: "openid email",
|
|
64
|
+
getEmail: d => d.email,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const SSO_ERROR_MESSAGES = {
|
|
69
|
+
not_configured: "SSO is not configured",
|
|
70
|
+
unknown_provider: "Unknown SSO provider",
|
|
71
|
+
no_email: "SSO provider did not return an email address",
|
|
72
|
+
user_not_found: "No account found for this email. Contact your administrator.",
|
|
73
|
+
account_suspended: "Your account has been suspended",
|
|
74
|
+
use_password_login: "This account must use password login",
|
|
75
|
+
apple_not_supported: "Apple SSO requires additional server-side setup",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function getSetting(db, key) {
|
|
79
|
+
return db.setting.findUnique({ where: { key } }).then(r => r?.value ?? null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Public: login page checks this on load ────────────────────────────────────
|
|
83
|
+
router.get("/config", async (req, res) => {
|
|
84
|
+
res.set("Cache-Control", "no-store");
|
|
85
|
+
try {
|
|
86
|
+
const [enabled, provider, clientId] = await Promise.all([
|
|
87
|
+
getSetting(req.db, "sso.enabled"),
|
|
88
|
+
getSetting(req.db, "sso.provider"),
|
|
89
|
+
getSetting(req.db, "sso.clientId"),
|
|
90
|
+
]);
|
|
91
|
+
const active = enabled === "true" && !!provider && !!clientId;
|
|
92
|
+
res.json({ enabled: active, provider: active ? provider : null, callbackUrl: CALLBACK_URL });
|
|
93
|
+
} catch {
|
|
94
|
+
res.json({ enabled: false, provider: null, callbackUrl: CALLBACK_URL });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Admin: save SSO config ────────────────────────────────────────────────────
|
|
99
|
+
router.put("/config", authenticate, requireAdmin, async (req, res) => {
|
|
100
|
+
const { provider, clientId, clientSecret, enabled } = req.body;
|
|
101
|
+
const upsert = (key, value) => req.db.setting.upsert({
|
|
102
|
+
where: { key },
|
|
103
|
+
create: { key, value: String(value) },
|
|
104
|
+
update: { value: String(value) },
|
|
105
|
+
});
|
|
106
|
+
if (provider !== undefined) await upsert("sso.provider", provider);
|
|
107
|
+
if (clientId !== undefined) await upsert("sso.clientId", clientId);
|
|
108
|
+
if (clientSecret && clientSecret !== "********") await upsert("sso.clientSecret", clientSecret);
|
|
109
|
+
if (enabled !== undefined) await upsert("sso.enabled", String(enabled));
|
|
110
|
+
res.json({ success: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── Public: start OAuth flow ──────────────────────────────────────────────────
|
|
114
|
+
router.get("/start", async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const [enabled, provider, clientId] = await Promise.all([
|
|
117
|
+
getSetting(req.db, "sso.enabled"),
|
|
118
|
+
getSetting(req.db, "sso.provider"),
|
|
119
|
+
getSetting(req.db, "sso.clientId"),
|
|
120
|
+
]);
|
|
121
|
+
if (enabled !== "true" || !provider || !clientId)
|
|
122
|
+
return res.redirect(`${FRONTEND_URL}/login?sso_error=not_configured`);
|
|
123
|
+
|
|
124
|
+
if (provider === "apple")
|
|
125
|
+
return res.redirect(`${FRONTEND_URL}/login?sso_error=apple_not_supported`);
|
|
126
|
+
|
|
127
|
+
const p = PROVIDERS[provider];
|
|
128
|
+
if (!p) return res.redirect(`${FRONTEND_URL}/login?sso_error=unknown_provider`);
|
|
129
|
+
|
|
130
|
+
const state = Buffer.from(JSON.stringify({ ts: Date.now() })).toString("base64url");
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
client_id: clientId,
|
|
133
|
+
redirect_uri: CALLBACK_URL,
|
|
134
|
+
response_type: "code",
|
|
135
|
+
scope: p.scope,
|
|
136
|
+
state,
|
|
137
|
+
});
|
|
138
|
+
res.redirect(`${p.authUrl}?${params}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error("[SSO] start error:", err.message);
|
|
141
|
+
res.redirect(`${FRONTEND_URL}/login?sso_error=${encodeURIComponent(err.message)}`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── Public: OAuth callback ────────────────────────────────────────────────────
|
|
146
|
+
router.get("/callback", async (req, res) => {
|
|
147
|
+
const { code, error } = req.query;
|
|
148
|
+
if (error) return res.redirect(`${FRONTEND_URL}/login?sso_error=${encodeURIComponent(error)}`);
|
|
149
|
+
if (!code) return res.redirect(`${FRONTEND_URL}/login?sso_error=no_code`);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const [provider, clientId, clientSecret] = await Promise.all([
|
|
153
|
+
getSetting(req.db, "sso.provider"),
|
|
154
|
+
getSetting(req.db, "sso.clientId"),
|
|
155
|
+
getSetting(req.db, "sso.clientSecret"),
|
|
156
|
+
]);
|
|
157
|
+
if (!provider || !clientId || !clientSecret)
|
|
158
|
+
return res.redirect(`${FRONTEND_URL}/login?sso_error=not_configured`);
|
|
159
|
+
|
|
160
|
+
const p = PROVIDERS[provider];
|
|
161
|
+
|
|
162
|
+
// Exchange code for access token
|
|
163
|
+
const tokenHeaders = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
164
|
+
if (provider === "github") tokenHeaders["Accept"] = "application/json";
|
|
165
|
+
|
|
166
|
+
const { data: tokens } = await axios.post(
|
|
167
|
+
p.tokenUrl,
|
|
168
|
+
new URLSearchParams({
|
|
169
|
+
grant_type: "authorization_code",
|
|
170
|
+
code,
|
|
171
|
+
client_id: clientId,
|
|
172
|
+
client_secret: clientSecret,
|
|
173
|
+
redirect_uri: CALLBACK_URL,
|
|
174
|
+
}),
|
|
175
|
+
{ headers: tokenHeaders }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const accessToken = tokens.access_token;
|
|
179
|
+
if (!accessToken) throw new Error("No access token returned");
|
|
180
|
+
|
|
181
|
+
// Get user info / email
|
|
182
|
+
const { data: userInfo } = await axios.get(p.userInfoUrl, {
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${accessToken}`,
|
|
185
|
+
Accept: "application/json",
|
|
186
|
+
...(provider === "github" ? { "User-Agent": "OpenEnthrium-SSO" } : {}),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const email = p.getEmail(userInfo)?.toLowerCase();
|
|
191
|
+
if (!email) return res.redirect(`${FRONTEND_URL}/login?sso_error=no_email`);
|
|
192
|
+
|
|
193
|
+
// Block super admin from OAuth
|
|
194
|
+
const superEmail = process.env.SUPER_ADMIN_EMAIL?.toLowerCase();
|
|
195
|
+
if (superEmail && email === superEmail)
|
|
196
|
+
return res.redirect(`${FRONTEND_URL}/login?sso_error=use_password_login`);
|
|
197
|
+
|
|
198
|
+
// Look up user in DB
|
|
199
|
+
const user = await req.db.user.findUnique({ where: { email } });
|
|
200
|
+
if (!user) return res.redirect(`${FRONTEND_URL}/login?sso_error=user_not_found`);
|
|
201
|
+
if (user.suspended) return res.redirect(`${FRONTEND_URL}/login?sso_error=account_suspended`);
|
|
202
|
+
|
|
203
|
+
// Issue JWT — same structure as password login, with sso flag
|
|
204
|
+
const token = jwt.sign(
|
|
205
|
+
{ id: user.id, email: user.email, role: user.role, name: user.name, sso: true },
|
|
206
|
+
process.env.JWT_SECRET,
|
|
207
|
+
{ expiresIn: "7d" }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
res.redirect(`${FRONTEND_URL}/login?sso_token=${token}`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("[SSO] callback error:", err.message);
|
|
213
|
+
const msg = err.response?.data?.error_description || err.response?.data?.error || err.message;
|
|
214
|
+
res.redirect(`${FRONTEND_URL}/login?sso_error=${encodeURIComponent(msg)}`);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
module.exports = { router, SSO_ERROR_MESSAGES };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate } = require("../middleware/auth");
|
|
3
|
+
|
|
4
|
+
function requireSuperAdmin(req, res, next) {
|
|
5
|
+
if (req.user?.id !== 0) return res.status(403).json({ error: "Super admin only" });
|
|
6
|
+
next();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
router.use(authenticate, requireSuperAdmin);
|
|
10
|
+
|
|
11
|
+
const CONFIG_KEYS = [
|
|
12
|
+
"tier.maxWorkspaces",
|
|
13
|
+
"tier.maxUsers",
|
|
14
|
+
"tier.maxConnectors",
|
|
15
|
+
"tier.maxAgentRunsPerMonth",
|
|
16
|
+
"tier.ingestionSpaceGb",
|
|
17
|
+
"storage.uploadPath",
|
|
18
|
+
"storage.maxFileSizeMb",
|
|
19
|
+
"feature.kbSharing",
|
|
20
|
+
"feature.agentSharing",
|
|
21
|
+
"feature.connectorSharing",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// GET /api/superadmin/config
|
|
25
|
+
router.get("/config", async (req, res) => {
|
|
26
|
+
const rows = await req.db.setting.findMany({
|
|
27
|
+
where: { key: { in: CONFIG_KEYS } },
|
|
28
|
+
});
|
|
29
|
+
const config = {};
|
|
30
|
+
for (const r of rows) config[r.key] = r.value;
|
|
31
|
+
res.json({ config });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// PUT /api/superadmin/config
|
|
35
|
+
router.put("/config", async (req, res) => {
|
|
36
|
+
const { config } = req.body;
|
|
37
|
+
if (!config || typeof config !== "object") return res.status(400).json({ error: "config object required" });
|
|
38
|
+
|
|
39
|
+
for (const [key, value] of Object.entries(config)) {
|
|
40
|
+
if (!CONFIG_KEYS.includes(key)) continue;
|
|
41
|
+
await req.db.setting.upsert({
|
|
42
|
+
where: { key },
|
|
43
|
+
create: { key, value: String(value) },
|
|
44
|
+
update: { value: String(value) },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
res.json({ success: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
module.exports = router;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const yaml = require("js-yaml");
|
|
6
|
+
|
|
7
|
+
router.use(authenticate, requireManagerOrAdmin);
|
|
8
|
+
|
|
9
|
+
const SAMPLES_DIR = path.resolve(__dirname, "../../cli/samples");
|
|
10
|
+
|
|
11
|
+
// ── List templates ─────────────────────────────────────────────────────────────
|
|
12
|
+
router.get("/", (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const entries = fs.readdirSync(SAMPLES_DIR, { withFileTypes: true })
|
|
15
|
+
.filter(e => e.isDirectory())
|
|
16
|
+
.map(e => {
|
|
17
|
+
const dir = path.join(SAMPLES_DIR, e.name);
|
|
18
|
+
let name = e.name;
|
|
19
|
+
let version = "1.0.0";
|
|
20
|
+
let description = "";
|
|
21
|
+
let tags = [];
|
|
22
|
+
let agentCount = 0;
|
|
23
|
+
|
|
24
|
+
// Prefer oe-project.json metadata
|
|
25
|
+
const projFile = path.join(dir, "oe-project.json");
|
|
26
|
+
if (fs.existsSync(projFile)) {
|
|
27
|
+
try {
|
|
28
|
+
const m = JSON.parse(fs.readFileSync(projFile, "utf8"));
|
|
29
|
+
if (m.name) name = m.name;
|
|
30
|
+
if (m.version) version = m.version;
|
|
31
|
+
if (m.description) description = m.description;
|
|
32
|
+
if (Array.isArray(m.tags)) tags = m.tags;
|
|
33
|
+
agentCount = (m.agents || []).length;
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback: read name from the first YAML file
|
|
38
|
+
if (agentCount === 0) {
|
|
39
|
+
const yamls = fs.readdirSync(dir).filter(f => /\.ya?ml$/i.test(f));
|
|
40
|
+
agentCount = yamls.length;
|
|
41
|
+
if (!fs.existsSync(projFile) && yamls[0]) {
|
|
42
|
+
try {
|
|
43
|
+
const doc = yaml.load(fs.readFileSync(path.join(dir, yamls[0]), "utf8")) || {};
|
|
44
|
+
if (doc.name) name = doc.name;
|
|
45
|
+
if (doc.description) description = doc.description;
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { id: e.name, name, version, description, tags, agentCount };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
res.json({ templates: entries });
|
|
54
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── Get template files ─────────────────────────────────────────────────────────
|
|
58
|
+
router.get("/:templateId", (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.join(SAMPLES_DIR, path.basename(req.params.templateId));
|
|
61
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
62
|
+
return res.status(404).json({ error: "Template not found" });
|
|
63
|
+
|
|
64
|
+
const fileMap = {};
|
|
65
|
+
for (const fname of fs.readdirSync(dir)) {
|
|
66
|
+
const fpath = path.join(dir, fname);
|
|
67
|
+
if (!fs.statSync(fpath).isFile()) continue;
|
|
68
|
+
if (fname.startsWith(".")) continue;
|
|
69
|
+
fileMap[fname] = fs.readFileSync(fpath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
res.json({ fileMap });
|
|
72
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
module.exports = router;
|