@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,81 @@
1
+ const router = require("express").Router();
2
+ const { authenticate } = require("../middleware/auth");
3
+ const { PrismaClient } = require("@prisma/client");
4
+ const db = new PrismaClient();
5
+
6
+ const STATIC_MODELS = {
7
+ openai: ["gpt-4o","gpt-4o-mini","gpt-4-turbo","gpt-4-turbo-preview","gpt-4","gpt-3.5-turbo","o1","o1-mini","o1-preview","o3-mini"],
8
+ anthropic: ["claude-opus-4-8","claude-sonnet-4-6","claude-3-5-sonnet-20241022","claude-3-5-haiku-20241022","claude-3-opus-20240229","claude-3-sonnet-20240229","claude-3-haiku-20240307"],
9
+ gemini: ["gemini-2.0-flash","gemini-2.0-flash-lite","gemini-2.0-pro-exp","gemini-1.5-pro","gemini-1.5-flash","gemini-1.5-flash-8b"],
10
+ groq: ["llama-3.3-70b-versatile","llama-3.1-70b-versatile","llama-3.1-8b-instant","llama3-70b-8192","llama3-8b-8192","mixtral-8x7b-32768","gemma2-9b-it","gemma-7b-it"],
11
+ mistral: ["mistral-large-latest","mistral-medium-latest","mistral-small-latest","open-mistral-nemo","open-mixtral-8x22b","open-mixtral-8x7b","codestral-latest","ministral-8b-latest","ministral-3b-latest"],
12
+ deepseek: ["deepseek-chat","deepseek-reasoner"],
13
+ perplexity: ["llama-3.1-sonar-huge-128k-online","llama-3.1-sonar-large-128k-online","llama-3.1-sonar-small-128k-online","llama-3.1-8b-instruct","llama-3.1-70b-instruct"],
14
+ xai: ["grok-2-latest","grok-2-vision-latest","grok-beta","grok-vision-beta"],
15
+ togetherai: ["meta-llama/Llama-3.3-70B-Instruct-Turbo","meta-llama/Llama-3.1-405B-Instruct-Turbo","meta-llama/Llama-3.1-70B-Instruct-Turbo","meta-llama/Llama-3.1-8B-Instruct-Turbo","meta-llama/Llama-3-70b-chat-hf","mistralai/Mixtral-8x7B-Instruct-v0.1","mistralai/Mistral-7B-Instruct-v0.3","Qwen/Qwen2.5-72B-Instruct-Turbo","deepseek-ai/DeepSeek-R1","deepseek-ai/DeepSeek-V3"],
16
+ fireworks: ["accounts/fireworks/models/llama-v3p3-70b-instruct","accounts/fireworks/models/llama-v3p1-405b-instruct","accounts/fireworks/models/llama-v3p1-70b-instruct","accounts/fireworks/models/llama-v3p1-8b-instruct","accounts/fireworks/models/mixtral-8x7b-instruct","accounts/fireworks/models/deepseek-r1","accounts/fireworks/models/deepseek-v3","accounts/fireworks/models/qwen2p5-72b-instruct"],
17
+ openrouter: ["openai/gpt-4o","openai/gpt-4o-mini","openai/o1","anthropic/claude-3.5-sonnet","anthropic/claude-3-opus","google/gemini-2.0-flash-exp","google/gemini-pro-1.5","meta-llama/llama-3.3-70b-instruct","deepseek/deepseek-r1","deepseek/deepseek-chat","mistralai/mistral-large-2411","x-ai/grok-2-1212","x-ai/grok-beta"],
18
+ nvidiaNim: ["meta/llama-3.3-70b-instruct","meta/llama-3.1-405b-instruct","meta/llama-3.1-70b-instruct","meta/llama-3.1-8b-instruct","microsoft/phi-3-mini-128k-instruct","mistralai/mistral-large","mistralai/mixtral-8x22b-instruct-v0.1","google/gemma-2-27b-it"],
19
+ sambanova: ["Meta-Llama-3.3-70B-Instruct","Meta-Llama-3.1-405B-Instruct","Meta-Llama-3.1-70B-Instruct","Meta-Llama-3.1-8B-Instruct","Qwen2.5-72B-Instruct","Qwen2.5-Coder-32B-Instruct","DeepSeek-R1","DeepSeek-V3"],
20
+ ollama: ["llama3.2","llama3.1","llama3","mistral","mixtral","phi3","phi4","gemma2","qwen2.5","deepseek-r1","codellama","nomic-embed-text"],
21
+ lmstudio: [],
22
+ "generic-openai": [],
23
+ };
24
+
25
+ async function getStoredApiKey() {
26
+ const s = await db.setting.findUnique({ where: { key: "llm_api_key" } });
27
+ return s?.value || null;
28
+ }
29
+
30
+ async function getStoredBaseUrl() {
31
+ const s = await db.setting.findUnique({ where: { key: "llm_base_url" } });
32
+ return s?.value || null;
33
+ }
34
+
35
+ router.get("/:provider", authenticate, async (req, res) => {
36
+ const { provider } = req.params;
37
+ let { apiKey, baseUrl } = req.query;
38
+
39
+ // Masked value from admin settings — use the DB key instead
40
+ if (apiKey === "********" || !apiKey) apiKey = null;
41
+ if (!baseUrl) baseUrl = null;
42
+
43
+ try {
44
+ // OpenAI — try live fetch with provided or stored key
45
+ if (provider === "openai") {
46
+ const key = apiKey || await getStoredApiKey();
47
+ if (key) {
48
+ try {
49
+ const { default: OpenAI } = await import("openai");
50
+ const client = new OpenAI({ apiKey: key });
51
+ const list = await client.models.list();
52
+ const models = list.data
53
+ .filter(m => m.id.startsWith("gpt-") || m.id.startsWith("o1") || m.id.startsWith("o3"))
54
+ .sort((a, b) => b.created - a.created)
55
+ .map(m => m.id);
56
+ if (models.length) return res.json({ models });
57
+ } catch { /* fall through to static */ }
58
+ }
59
+ return res.json({ models: STATIC_MODELS.openai });
60
+ }
61
+
62
+ // Ollama — try live fetch from local server
63
+ if (provider === "ollama") {
64
+ const url = (baseUrl || await getStoredBaseUrl() || "http://localhost:11434").replace(/\/v1\/?$/, "");
65
+ try {
66
+ const r = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(3000) });
67
+ const json = await r.json();
68
+ const models = (json.models || []).map(m => m.name);
69
+ if (models.length) return res.json({ models });
70
+ } catch { /* fall through to static */ }
71
+ return res.json({ models: STATIC_MODELS.ollama });
72
+ }
73
+
74
+ // All other providers — return static list
75
+ res.json({ models: STATIC_MODELS[provider] || [] });
76
+ } catch (err) {
77
+ res.json({ models: STATIC_MODELS[provider] || [], error: err.message });
78
+ }
79
+ });
80
+
81
+ module.exports = router;
@@ -0,0 +1,423 @@
1
+ const router = require("express").Router();
2
+ const { authenticate, requireManagerOrAdmin } = require("../middleware/auth");
3
+ const { makeOAuth2Client, getGoogleCredentials } = require("../utils/tools/adapters/gmail");
4
+
5
+ const GMAIL_SCOPES = [
6
+ "https://www.googleapis.com/auth/gmail.send",
7
+ "https://www.googleapis.com/auth/gmail.readonly",
8
+ "https://www.googleapis.com/auth/userinfo.email",
9
+ ];
10
+
11
+ const GDRIVE_SCOPES = [
12
+ "https://www.googleapis.com/auth/drive",
13
+ "https://www.googleapis.com/auth/userinfo.email",
14
+ ];
15
+
16
+ const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000";
17
+ const CALLBACK_BASE = process.env.OAUTH_CALLBACK_BASE || "http://localhost:3001";
18
+
19
+ function generateSlug(name) {
20
+ return (name || "").toLowerCase().replace(/[^a-z0-9]/g, "") || "connector";
21
+ }
22
+
23
+ async function uniqueSlug(db, base) {
24
+ if (!base) base = "connector";
25
+ let slug = base, suffix = 2;
26
+ while (await db.connector.findUnique({ where: { slug } })) slug = `${base}${suffix++}`;
27
+ return slug;
28
+ }
29
+
30
+ async function uniqueName(db, base) {
31
+ if (!base) base = "connector";
32
+ let name = base, suffix = 1;
33
+ while (await db.connector.findFirst({ where: { name } })) name = `${base}-${suffix++}`;
34
+ return name;
35
+ }
36
+
37
+ async function workspaceRedirect(db, workspaceId, type, success) {
38
+ try {
39
+ const ws = await db.workspace.findUnique({ where: { id: workspaceId }, select: { slug: true } });
40
+ if (ws?.slug) return `${FRONTEND_URL}/workspace/${ws.slug}?oauth_${success ? "success" : "error"}=${encodeURIComponent(type)}&ws=${workspaceId}`;
41
+ } catch {}
42
+ return `${FRONTEND_URL}?oauth_${success ? "success" : "error"}=${encodeURIComponent(type)}&ws=${workspaceId}`;
43
+ }
44
+
45
+ // Check if Google OAuth is configured for a workspace
46
+ router.get("/gmail/status", authenticate, async (req, res) => {
47
+ const workspaceId = parseInt(req.query.workspaceId);
48
+ const { clientId } = await getGoogleCredentials(req.db, workspaceId || undefined);
49
+ res.json({ configured: !!clientId });
50
+ });
51
+
52
+ // Save per-workspace Google OAuth credentials
53
+ router.post("/gmail/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
54
+ const { clientId, clientSecret, workspaceId } = req.body;
55
+ if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) {
56
+ return res.status(400).json({ error: "Client ID, Client Secret and workspaceId are required" });
57
+ }
58
+ try {
59
+ const wsId = parseInt(workspaceId);
60
+ await req.db.setting.upsert({
61
+ where: { key: `oauth.google.clientId.ws.${wsId}` },
62
+ create: { key: `oauth.google.clientId.ws.${wsId}`, value: clientId.trim() },
63
+ update: { value: clientId.trim() },
64
+ });
65
+ await req.db.setting.upsert({
66
+ where: { key: `oauth.google.clientSecret.ws.${wsId}` },
67
+ create: { key: `oauth.google.clientSecret.ws.${wsId}`, value: clientSecret.trim() },
68
+ update: { value: clientSecret.trim() },
69
+ });
70
+ res.json({ success: true });
71
+ } catch (err) {
72
+ res.status(500).json({ error: err.message });
73
+ }
74
+ });
75
+
76
+ // Delete per-workspace Google OAuth credentials
77
+ router.delete("/gmail/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
78
+ const workspaceId = parseInt(req.query.workspaceId);
79
+ if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
80
+ try {
81
+ await req.db.setting.deleteMany({
82
+ where: { key: { in: [
83
+ `oauth.google.clientId.ws.${workspaceId}`,
84
+ `oauth.google.clientSecret.ws.${workspaceId}`,
85
+ ]}}
86
+ });
87
+ res.json({ success: true });
88
+ } catch (err) {
89
+ res.status(500).json({ error: err.message });
90
+ }
91
+ });
92
+
93
+ // Returns the Google OAuth URL — frontend calls this via axios then does window.location.href
94
+ router.get("/gmail/start", authenticate, requireManagerOrAdmin, async (req, res) => {
95
+ const { workspaceId, connectionName, connectionSlug } = req.query;
96
+ if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
97
+
98
+ const wsId = parseInt(workspaceId);
99
+ const { clientId } = await getGoogleCredentials(req.db, wsId);
100
+ if (!clientId) return res.status(400).json({ error: "Google Client ID not configured. Add it in the Integrations tab." });
101
+
102
+ const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName: connectionName || "", connectionSlug: connectionSlug || "" })).toString("base64url");
103
+ const oauth2 = await makeOAuth2Client(req.db, wsId);
104
+ const url = oauth2.generateAuthUrl({
105
+ access_type: "offline",
106
+ prompt: "consent",
107
+ scope: GMAIL_SCOPES,
108
+ state,
109
+ });
110
+ res.json({ url });
111
+ });
112
+
113
+ // Gmail OAuth callback — exchange code, store connector, redirect to frontend
114
+ router.get("/gmail/callback", async (req, res) => {
115
+ const { code, state, error } = req.query;
116
+
117
+ if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
118
+ if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
119
+
120
+ let workspaceId, connectionName, connectionSlug;
121
+ try {
122
+ ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString()));
123
+ } catch {
124
+ return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`);
125
+ }
126
+
127
+ try {
128
+ const oauth2 = await makeOAuth2Client(req.db, workspaceId);
129
+ const { tokens } = await oauth2.getToken(code);
130
+ oauth2.setCredentials(tokens);
131
+
132
+ // Get the user's Gmail address
133
+ const { google } = require("googleapis");
134
+ const oauth2Api = google.oauth2({ version: "v2", auth: oauth2 });
135
+ const { data: userInfo } = await oauth2Api.userinfo.get();
136
+ const email = userInfo.email;
137
+
138
+ const db = req.db || require("../utils/prisma");
139
+
140
+ // Upsert: one Gmail connector per workspace per email
141
+ const existing = await db.connector.findFirst({
142
+ where: { workspaceId, type: "gmail", name: email }
143
+ });
144
+
145
+ const authConfig = JSON.stringify({
146
+ accessToken: tokens.access_token,
147
+ refreshToken: tokens.refresh_token,
148
+ expiresAt: tokens.expiry_date,
149
+ });
150
+
151
+ const connName = connectionName?.trim() || email;
152
+ if (existing) {
153
+ await db.connector.update({
154
+ where: { id: existing.id },
155
+ data: { authConfig, status: "active", lastTestedAt: new Date() }
156
+ });
157
+ } else {
158
+ const finalName = await uniqueName(db, connName);
159
+ await db.connector.create({
160
+ data: {
161
+ workspaceId,
162
+ name: finalName,
163
+ type: "gmail",
164
+ slug: finalName,
165
+ config: JSON.stringify({ email }),
166
+ authConfig,
167
+ status: "active",
168
+ }
169
+ });
170
+ }
171
+
172
+ res.redirect(await workspaceRedirect(req.db, workspaceId, "gmail", true));
173
+ } catch (err) {
174
+ console.error("[oauth/gmail] callback error:", err.message);
175
+ res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`);
176
+ }
177
+ });
178
+
179
+ // ── Google Drive OAuth ──────────────────────────────────────────────────────
180
+
181
+ router.get("/gdrive/start", authenticate, requireManagerOrAdmin, async (req, res) => {
182
+ const { workspaceId, connectionName, connectionSlug } = req.query;
183
+ if (!workspaceId) return res.status(400).json({ error: "workspaceId required" });
184
+
185
+ const wsId = parseInt(workspaceId);
186
+ const { clientId } = await getGoogleCredentials(req.db, wsId);
187
+ if (!clientId) return res.status(400).json({ error: "Google Client ID not configured. Add it in the Gmail integration tab first." });
188
+
189
+ const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName: connectionName || "", connectionSlug: connectionSlug || "" })).toString("base64url");
190
+ const oauth2 = await makeOAuth2Client(req.db, wsId, "/api/oauth/gdrive/callback");
191
+ const url = oauth2.generateAuthUrl({
192
+ access_type: "offline",
193
+ prompt: "consent",
194
+ scope: GDRIVE_SCOPES,
195
+ state,
196
+ });
197
+ res.json({ url });
198
+ });
199
+
200
+ router.get("/gdrive/callback", async (req, res) => {
201
+ const { code, state, error } = req.query;
202
+
203
+ if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
204
+ if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
205
+
206
+ let workspaceId, connectionName, connectionSlug;
207
+ try {
208
+ ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString()));
209
+ } catch {
210
+ return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`);
211
+ }
212
+
213
+ try {
214
+ const oauth2 = await makeOAuth2Client(req.db, workspaceId, "/api/oauth/gdrive/callback");
215
+ const { tokens } = await oauth2.getToken(code);
216
+ oauth2.setCredentials(tokens);
217
+
218
+ const { google } = require("googleapis");
219
+ const oauth2Api = google.oauth2({ version: "v2", auth: oauth2 });
220
+ const { data: userInfo } = await oauth2Api.userinfo.get();
221
+ const email = userInfo.email;
222
+
223
+ const db = req.db || require("../utils/prisma");
224
+
225
+ const existing = await db.connector.findFirst({
226
+ where: { workspaceId, type: "gdrive", name: email }
227
+ });
228
+
229
+ const authConfig = JSON.stringify({
230
+ accessToken: tokens.access_token,
231
+ refreshToken: tokens.refresh_token,
232
+ expiresAt: tokens.expiry_date,
233
+ });
234
+
235
+ const connName = connectionName?.trim() || email;
236
+ if (existing) {
237
+ await db.connector.update({
238
+ where: { id: existing.id },
239
+ data: { authConfig, status: "active", lastTestedAt: new Date() }
240
+ });
241
+ } else {
242
+ const finalName = await uniqueName(db, connName);
243
+ await db.connector.create({
244
+ data: {
245
+ workspaceId,
246
+ name: finalName,
247
+ type: "gdrive",
248
+ slug: finalName,
249
+ slug,
250
+ config: JSON.stringify({ email }),
251
+ authConfig,
252
+ status: "active",
253
+ }
254
+ });
255
+ }
256
+
257
+ res.redirect(await workspaceRedirect(req.db, workspaceId, "gdrive", true));
258
+ } catch (err) {
259
+ console.error("[oauth/gdrive] callback error:", err.message);
260
+ res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`);
261
+ }
262
+ });
263
+
264
+ // ── OneDrive OAuth ───────────────────────────────────────────────────────────
265
+
266
+ router.post("/onedrive/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
267
+ const { clientId, clientSecret, tenantId, workspaceId } = req.body;
268
+ if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) return res.status(400).json({ error: "clientId, clientSecret, workspaceId required" });
269
+ const wsId = parseInt(workspaceId);
270
+ await req.db.setting.upsert({ where: { key: `oauth.onedrive.clientId.ws.${wsId}` }, create: { key: `oauth.onedrive.clientId.ws.${wsId}`, value: clientId.trim() }, update: { value: clientId.trim() } });
271
+ await req.db.setting.upsert({ where: { key: `oauth.onedrive.clientSecret.ws.${wsId}` }, create: { key: `oauth.onedrive.clientSecret.ws.${wsId}`, value: clientSecret.trim() }, update: { value: clientSecret.trim() } });
272
+ if (tenantId?.trim()) await req.db.setting.upsert({ where: { key: `oauth.onedrive.tenantId.ws.${wsId}` }, create: { key: `oauth.onedrive.tenantId.ws.${wsId}`, value: tenantId.trim() }, update: { value: tenantId.trim() } });
273
+ res.json({ success: true });
274
+ });
275
+
276
+ router.get("/onedrive/start", authenticate, requireManagerOrAdmin, async (req, res) => {
277
+ const wsId = parseInt(req.query.workspaceId);
278
+ if (!wsId) return res.status(400).json({ error: "workspaceId required" });
279
+ const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
280
+ const clientId = await get(`oauth.onedrive.clientId.ws.${wsId}`);
281
+ const tenantId = (await get(`oauth.onedrive.tenantId.ws.${wsId}`)) || "common";
282
+ if (!clientId) return res.status(400).json({ error: "OneDrive not configured. Add credentials in Integrations." });
283
+ const { connectionName = "", connectionSlug = "" } = req.query;
284
+ const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
285
+ const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/onedrive/callback`, scope: "Files.Read.All offline_access User.Read", state });
286
+ res.json({ url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params}` });
287
+ });
288
+
289
+ router.get("/onedrive/callback", async (req, res) => {
290
+ const { code, state, error } = req.query;
291
+ if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
292
+ if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
293
+ let workspaceId, connectionName, connectionSlug;
294
+ try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
295
+ try {
296
+ const db = req.db;
297
+ const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
298
+ const clientId = await get(`oauth.onedrive.clientId.ws.${workspaceId}`);
299
+ const clientSecret = await get(`oauth.onedrive.clientSecret.ws.${workspaceId}`);
300
+ const tenantId = (await get(`oauth.onedrive.tenantId.ws.${workspaceId}`)) || "common";
301
+ const axios = require("axios");
302
+ const { data: tokens } = await axios.post(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
303
+ new URLSearchParams({ grant_type: "authorization_code", code, client_id: clientId, client_secret: clientSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/onedrive/callback`, scope: "Files.Read.All offline_access User.Read" }),
304
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
305
+ const { data: me } = await axios.get("https://graph.microsoft.com/v1.0/me", { headers: { Authorization: `Bearer ${tokens.access_token}` } });
306
+ const email = me.mail || me.userPrincipalName;
307
+ const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000 });
308
+ const config = JSON.stringify({ clientId, clientSecret, tenantId, email });
309
+ const existing = await db.connector.findFirst({ where: { workspaceId, type: "onedrive", name: email } });
310
+ if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
311
+ else {
312
+ const finalName = await uniqueName(db, connectionName?.trim() || email);
313
+ await db.connector.create({ data: { workspaceId, name: finalName, type: "onedrive", slug: finalName, config, authConfig, status: "active" } });
314
+ }
315
+ res.redirect(await workspaceRedirect(req.db, workspaceId, "onedrive", true));
316
+ } catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
317
+ });
318
+
319
+ // ── Dropbox OAuth ─────────────────────────────────────────────────────────────
320
+
321
+ router.post("/dropbox/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
322
+ const { appKey, appSecret, workspaceId } = req.body;
323
+ if (!appKey?.trim() || !appSecret?.trim() || !workspaceId) return res.status(400).json({ error: "appKey, appSecret, workspaceId required" });
324
+ const wsId = parseInt(workspaceId);
325
+ await req.db.setting.upsert({ where: { key: `oauth.dropbox.appKey.ws.${wsId}` }, create: { key: `oauth.dropbox.appKey.ws.${wsId}`, value: appKey.trim() }, update: { value: appKey.trim() } });
326
+ await req.db.setting.upsert({ where: { key: `oauth.dropbox.appSecret.ws.${wsId}` }, create: { key: `oauth.dropbox.appSecret.ws.${wsId}`, value: appSecret.trim() }, update: { value: appSecret.trim() } });
327
+ res.json({ success: true });
328
+ });
329
+
330
+ router.get("/dropbox/start", authenticate, requireManagerOrAdmin, async (req, res) => {
331
+ const wsId = parseInt(req.query.workspaceId);
332
+ if (!wsId) return res.status(400).json({ error: "workspaceId required" });
333
+ const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
334
+ const appKey = await get(`oauth.dropbox.appKey.ws.${wsId}`);
335
+ if (!appKey) return res.status(400).json({ error: "Dropbox not configured. Add credentials in Integrations." });
336
+ const { connectionName = "", connectionSlug = "" } = req.query;
337
+ const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
338
+ const params = new URLSearchParams({ client_id: appKey, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/dropbox/callback`, token_access_type: "offline", state });
339
+ res.json({ url: `https://www.dropbox.com/oauth2/authorize?${params}` });
340
+ });
341
+
342
+ router.get("/dropbox/callback", async (req, res) => {
343
+ const { code, state, error } = req.query;
344
+ if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
345
+ if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
346
+ let workspaceId, connectionName, connectionSlug;
347
+ try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
348
+ try {
349
+ const db = req.db;
350
+ const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
351
+ const appKey = await get(`oauth.dropbox.appKey.ws.${workspaceId}`);
352
+ const appSecret = await get(`oauth.dropbox.appSecret.ws.${workspaceId}`);
353
+ const axios = require("axios");
354
+ const { data: tokens } = await axios.post("https://api.dropboxapi.com/oauth2/token",
355
+ new URLSearchParams({ grant_type: "authorization_code", code, client_id: appKey, client_secret: appSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/dropbox/callback` }),
356
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
357
+ const { data: me } = await axios.post("https://api.dropboxapi.com/2/users/get_current_account", null, { headers: { Authorization: `Bearer ${tokens.access_token}` } });
358
+ const email = me.email;
359
+ const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null });
360
+ const config = JSON.stringify({ appKey, appSecret, email });
361
+ const existing = await db.connector.findFirst({ where: { workspaceId, type: "dropbox", name: email } });
362
+ if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
363
+ else {
364
+ const finalName = await uniqueName(db, connectionName?.trim() || email);
365
+ await db.connector.create({ data: { workspaceId, name: finalName, type: "dropbox", slug: finalName, config, authConfig, status: "active" } });
366
+ }
367
+ res.redirect(await workspaceRedirect(req.db, workspaceId, "dropbox", true));
368
+ } catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
369
+ });
370
+
371
+ // ── Box OAuth ─────────────────────────────────────────────────────────────────
372
+
373
+ router.post("/box/configure", authenticate, requireManagerOrAdmin, async (req, res) => {
374
+ const { clientId, clientSecret, workspaceId } = req.body;
375
+ if (!clientId?.trim() || !clientSecret?.trim() || !workspaceId) return res.status(400).json({ error: "clientId, clientSecret, workspaceId required" });
376
+ const wsId = parseInt(workspaceId);
377
+ await req.db.setting.upsert({ where: { key: `oauth.box.clientId.ws.${wsId}` }, create: { key: `oauth.box.clientId.ws.${wsId}`, value: clientId.trim() }, update: { value: clientId.trim() } });
378
+ await req.db.setting.upsert({ where: { key: `oauth.box.clientSecret.ws.${wsId}` }, create: { key: `oauth.box.clientSecret.ws.${wsId}`, value: clientSecret.trim() }, update: { value: clientSecret.trim() } });
379
+ res.json({ success: true });
380
+ });
381
+
382
+ router.get("/box/start", authenticate, requireManagerOrAdmin, async (req, res) => {
383
+ const wsId = parseInt(req.query.workspaceId);
384
+ if (!wsId) return res.status(400).json({ error: "workspaceId required" });
385
+ const get = async key => (await req.db.setting.findUnique({ where: { key } }))?.value;
386
+ const clientId = await get(`oauth.box.clientId.ws.${wsId}`);
387
+ if (!clientId) return res.status(400).json({ error: "Box not configured. Add credentials in Integrations." });
388
+ const { connectionName = "", connectionSlug = "" } = req.query;
389
+ const state = Buffer.from(JSON.stringify({ workspaceId: wsId, connectionName, connectionSlug })).toString("base64url");
390
+ const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: `${CALLBACK_BASE}/api/oauth/box/callback`, state });
391
+ res.json({ url: `https://account.box.com/api/oauth2/authorize?${params}` });
392
+ });
393
+
394
+ router.get("/box/callback", async (req, res) => {
395
+ const { code, state, error } = req.query;
396
+ if (error) return res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(error)}`);
397
+ if (!code || !state) return res.redirect(`${FRONTEND_URL}?oauth_error=missing_params`);
398
+ let workspaceId, connectionName, connectionSlug;
399
+ try { ({ workspaceId, connectionName, connectionSlug } = JSON.parse(Buffer.from(state, "base64url").toString())); } catch { return res.redirect(`${FRONTEND_URL}?oauth_error=invalid_state`); }
400
+ try {
401
+ const db = req.db;
402
+ const get = async key => (await db.setting.findUnique({ where: { key } }))?.value;
403
+ const clientId = await get(`oauth.box.clientId.ws.${workspaceId}`);
404
+ const clientSecret = await get(`oauth.box.clientSecret.ws.${workspaceId}`);
405
+ const axios = require("axios");
406
+ const { data: tokens } = await axios.post("https://api.box.com/oauth2/token",
407
+ new URLSearchParams({ grant_type: "authorization_code", code, client_id: clientId, client_secret: clientSecret, redirect_uri: `${CALLBACK_BASE}/api/oauth/box/callback` }),
408
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
409
+ const { data: me } = await axios.get("https://api.box.com/2.0/users/me", { headers: { Authorization: `Bearer ${tokens.access_token}` } });
410
+ const email = me.login;
411
+ const authConfig = JSON.stringify({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000 });
412
+ const config = JSON.stringify({ clientId, clientSecret, email });
413
+ const existing = await db.connector.findFirst({ where: { workspaceId, type: "box", name: email } });
414
+ if (existing) await db.connector.update({ where: { id: existing.id }, data: { authConfig, config, status: "active", lastTestedAt: new Date() } });
415
+ else {
416
+ const finalName = await uniqueName(db, connectionName?.trim() || email);
417
+ await db.connector.create({ data: { workspaceId, name: finalName, type: "box", slug: finalName, config, authConfig, status: "active" } });
418
+ }
419
+ res.redirect(await workspaceRedirect(req.db, workspaceId, "box", true));
420
+ } catch (err) { res.redirect(`${FRONTEND_URL}?oauth_error=${encodeURIComponent(err.message)}`); }
421
+ });
422
+
423
+ module.exports = router;