@realtimex/email-automator 2.10.9 → 2.11.0
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/api/server.ts +195 -6
- package/api/src/middleware/validation.ts +2 -4
- package/api/src/routes/actions.ts +1 -10
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/migrate.ts +27 -21
- package/api/src/routes/sdk.ts +55 -0
- package/api/src/routes/settings.ts +22 -19
- package/api/src/services/SDKService.ts +256 -0
- package/api/src/services/intelligence.ts +88 -321
- package/api/src/services/processor.ts +2 -18
- package/api/src/services/supabase.ts +2 -3
- package/bin/email-automator.js +29 -10
- package/dist/api/server.js +163 -0
- package/dist/api/src/middleware/validation.js +2 -4
- package/dist/api/src/routes/actions.js +1 -7
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/migrate.js +24 -18
- package/dist/api/src/routes/sdk.js +40 -0
- package/dist/api/src/routes/settings.js +13 -10
- package/dist/api/src/services/SDKService.js +224 -0
- package/dist/api/src/services/intelligence.js +84 -303
- package/dist/api/src/services/processor.js +2 -14
- package/dist/assets/index-Bb7BPcc9.js +106 -0
- package/dist/assets/index-D6Bd2h8f.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +5 -2
- package/scripts/migrate.sh +52 -141
- package/supabase/functions/api-v1-settings/index.ts +72 -62
- package/supabase/migrations/20260130000000_remove_legacy_llm_config.sql +18 -0
- package/supabase/migrations/20260130000001_update_default_llm.sql +14 -0
- package/dist/assets/index-DRfeBOC2.js +0 -97
- package/dist/assets/index-SRvHiNrG.css +0 -1
package/api/server.ts
CHANGED
|
@@ -4,6 +4,9 @@ import path, { join } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import { SDKService } from './src/services/SDKService.js';
|
|
7
10
|
import { config, validateConfig } from './src/config/index.js';
|
|
8
11
|
import { errorHandler } from './src/middleware/errorHandler.js';
|
|
9
12
|
import { apiRateLimit } from './src/middleware/rateLimit.js';
|
|
@@ -21,6 +24,9 @@ if (!configValidation.valid) {
|
|
|
21
24
|
logger.warn('Configuration warnings', { errors: configValidation.errors });
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
// Initialize RealTimeX SDK
|
|
28
|
+
SDKService.initialize();
|
|
29
|
+
|
|
24
30
|
const app = express();
|
|
25
31
|
|
|
26
32
|
// Security headers
|
|
@@ -36,8 +42,8 @@ app.use((req, res, next) => {
|
|
|
36
42
|
|
|
37
43
|
// CORS configuration
|
|
38
44
|
app.use(cors({
|
|
39
|
-
origin: config.isProduction
|
|
40
|
-
? config.security.corsOrigins
|
|
45
|
+
origin: config.isProduction
|
|
46
|
+
? config.security.corsOrigins
|
|
41
47
|
: true, // Allow all in development
|
|
42
48
|
credentials: true,
|
|
43
49
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
@@ -67,6 +73,189 @@ app.use('/api', apiRateLimit);
|
|
|
67
73
|
// API routes
|
|
68
74
|
app.use('/api', routes);
|
|
69
75
|
|
|
76
|
+
// --- SETUP & PROVISIONING (Golden Standard) ---
|
|
77
|
+
|
|
78
|
+
// GET /api/setup/organizations - List Supabase organizations using access token
|
|
79
|
+
app.get('/api/setup/organizations', async (req, res) => {
|
|
80
|
+
const authHeader = req.headers['authorization'];
|
|
81
|
+
if (!authHeader) {
|
|
82
|
+
return res.status(401).json({ error: 'Missing Authorization header' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const response = await axios.get('https://api.supabase.com/v1/organizations', {
|
|
87
|
+
headers: { 'Authorization': authHeader }
|
|
88
|
+
});
|
|
89
|
+
res.json(response.data);
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
logger.error('Failed to fetch organizations', error);
|
|
92
|
+
res.status(error.response?.status || 500).json({
|
|
93
|
+
error: error.response?.data?.message || 'Failed to fetch organizations'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// POST /api/setup/auto-provision - Create project and poll for readiness (SSE)
|
|
99
|
+
app.post('/api/setup/auto-provision', async (req, res) => {
|
|
100
|
+
const { orgId, projectName: customProjectName, region: customRegion } = req.body;
|
|
101
|
+
const authHeader = req.headers['authorization'];
|
|
102
|
+
|
|
103
|
+
if (!orgId) {
|
|
104
|
+
return res.status(400).json({ error: 'Missing required parameter (orgId)' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!authHeader) {
|
|
108
|
+
return res.status(401).json({ error: 'Missing Authorization header' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
112
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
113
|
+
res.setHeader('Connection', 'keep-alive');
|
|
114
|
+
|
|
115
|
+
const sendEvent = (type: string, data: any) => {
|
|
116
|
+
res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const projectName = customProjectName || `Email-Automator-${crypto.randomBytes(2).toString('hex')}`;
|
|
121
|
+
const region = customRegion || 'us-east-1';
|
|
122
|
+
|
|
123
|
+
// Generate a secure DB password server-side
|
|
124
|
+
const dbPass = crypto.randomBytes(16).toString('base64')
|
|
125
|
+
.replace(/\+/g, 'a')
|
|
126
|
+
.replace(/\//g, 'b')
|
|
127
|
+
.replace(/=/g, 'c') + '1!Aa';
|
|
128
|
+
|
|
129
|
+
sendEvent('info', `🚀 Creating Supabase project: ${projectName} in ${region}...`);
|
|
130
|
+
|
|
131
|
+
// 1. Create Project
|
|
132
|
+
const createResponse = await axios.post('https://api.supabase.com/v1/projects', {
|
|
133
|
+
name: projectName,
|
|
134
|
+
organization_id: orgId,
|
|
135
|
+
region: region,
|
|
136
|
+
db_pass: dbPass
|
|
137
|
+
}, {
|
|
138
|
+
headers: { 'Authorization': authHeader },
|
|
139
|
+
timeout: 15000
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const project = createResponse.data;
|
|
143
|
+
const projectRef = project.id;
|
|
144
|
+
|
|
145
|
+
sendEvent('info', `📦 Project created! ID: ${projectRef}. Waiting for it to go live...`);
|
|
146
|
+
sendEvent('project_id', projectRef);
|
|
147
|
+
|
|
148
|
+
// 2. Poll for Readiness
|
|
149
|
+
let isReady = false;
|
|
150
|
+
let attempts = 0;
|
|
151
|
+
const maxAttempts = 60; // 5 minutes
|
|
152
|
+
|
|
153
|
+
while (!isReady && attempts < maxAttempts) {
|
|
154
|
+
attempts++;
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
|
|
159
|
+
headers: { 'Authorization': authHeader },
|
|
160
|
+
timeout: 10000
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const status = statusResponse.data.status;
|
|
164
|
+
sendEvent('info', `⏳ Status: ${status} (Attempt ${attempts}/${maxAttempts})`);
|
|
165
|
+
|
|
166
|
+
if (status === 'ACTIVE_HEALTHY' || status === 'ACTIVE') {
|
|
167
|
+
isReady = true;
|
|
168
|
+
}
|
|
169
|
+
} catch (pollError: any) {
|
|
170
|
+
logger.warn('Polling error during provision', { error: pollError.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!isReady) {
|
|
175
|
+
throw new Error('Project provision timed out after 5 minutes.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Get API Keys
|
|
179
|
+
sendEvent('info', '🔑 Retrieving API keys...');
|
|
180
|
+
|
|
181
|
+
let anonKey = '';
|
|
182
|
+
let keyAttempts = 0;
|
|
183
|
+
const maxKeyAttempts = 10;
|
|
184
|
+
|
|
185
|
+
while (!anonKey && keyAttempts < maxKeyAttempts) {
|
|
186
|
+
keyAttempts++;
|
|
187
|
+
if (keyAttempts > 1) {
|
|
188
|
+
sendEvent('info', `⏳ API keys not ready yet. Retrying (Attempt ${keyAttempts}/${maxKeyAttempts})...`);
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const keysResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}/api-keys`, {
|
|
194
|
+
headers: { 'Authorization': authHeader },
|
|
195
|
+
timeout: 10000
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const keys = keysResponse.data;
|
|
199
|
+
if (Array.isArray(keys)) {
|
|
200
|
+
anonKey = keys.find((k: any) => k.name === 'anon')?.api_key;
|
|
201
|
+
if (anonKey) {
|
|
202
|
+
sendEvent('info', '✅ API keys retrieved successfully.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (err: any) {
|
|
206
|
+
logger.warn('Key retrieval attempt failed', { error: err.message });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!anonKey) {
|
|
211
|
+
throw new Error('Could not find anonymous API key for the new project.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const supabaseUrl = `https://${projectRef}.supabase.co`;
|
|
215
|
+
|
|
216
|
+
// 4. DNS Verification
|
|
217
|
+
sendEvent('info', '🌐 Waiting for DNS propagation...');
|
|
218
|
+
let dnsReady = false;
|
|
219
|
+
let dnsAttempts = 0;
|
|
220
|
+
const maxDnsAttempts = 20;
|
|
221
|
+
|
|
222
|
+
while (!dnsReady && dnsAttempts < maxDnsAttempts) {
|
|
223
|
+
dnsAttempts++;
|
|
224
|
+
try {
|
|
225
|
+
const pingResponse = await axios.get(`${supabaseUrl}/rest/v1/`, {
|
|
226
|
+
timeout: 5000,
|
|
227
|
+
validateStatus: () => true
|
|
228
|
+
});
|
|
229
|
+
if (pingResponse.status < 500) {
|
|
230
|
+
dnsReady = true;
|
|
231
|
+
sendEvent('info', '✨ DNS resolved! Project is fully accessible.');
|
|
232
|
+
}
|
|
233
|
+
} catch (pingError: any) {
|
|
234
|
+
if (dnsAttempts % 5 === 0) {
|
|
235
|
+
sendEvent('info', '⏳ DNS still propagating... standby.');
|
|
236
|
+
}
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
sendEvent('success', {
|
|
242
|
+
url: supabaseUrl,
|
|
243
|
+
anonKey: anonKey,
|
|
244
|
+
projectId: projectRef,
|
|
245
|
+
dbPass: dbPass
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
sendEvent('done', 'success');
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
const errorMsg = error.response?.data?.message || error.message || 'Auto-provisioning failed';
|
|
251
|
+
logger.error('Auto-provision failed', { error: errorMsg });
|
|
252
|
+
sendEvent('error', errorMsg);
|
|
253
|
+
sendEvent('done', 'failed');
|
|
254
|
+
} finally {
|
|
255
|
+
res.end();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
70
259
|
// Robust resolution for static assets (dist folder)
|
|
71
260
|
function getDistPath() {
|
|
72
261
|
// 1. Check environment variable override (must contain index.html)
|
|
@@ -103,9 +292,9 @@ app.get(/.*/, (req, res, next) => {
|
|
|
103
292
|
res.sendFile(path.join(distPath, 'index.html'), (err) => {
|
|
104
293
|
if (err) {
|
|
105
294
|
// If dist doesn't exist, return 404 for non-API routes
|
|
106
|
-
res.status(404).json({
|
|
107
|
-
success: false,
|
|
108
|
-
error: { code: 'NOT_FOUND', message: 'Frontend not built or endpoint not found' }
|
|
295
|
+
res.status(404).json({
|
|
296
|
+
success: false,
|
|
297
|
+
error: { code: 'NOT_FOUND', message: 'Frontend not built or endpoint not found' }
|
|
109
298
|
});
|
|
110
299
|
}
|
|
111
300
|
});
|
|
@@ -131,7 +320,7 @@ const server = app.listen(config.port, () => {
|
|
|
131
320
|
environment: config.nodeEnv,
|
|
132
321
|
supabase: getServerSupabase() ? 'connected' : 'not configured',
|
|
133
322
|
});
|
|
134
|
-
|
|
323
|
+
|
|
135
324
|
// Start background scheduler
|
|
136
325
|
if (getServerSupabase()) {
|
|
137
326
|
startScheduler();
|
|
@@ -82,8 +82,7 @@ export const schemas = {
|
|
|
82
82
|
// Migration schemas
|
|
83
83
|
migrate: z.object({
|
|
84
84
|
projectRef: z.string().min(1, 'Project reference is required'),
|
|
85
|
-
|
|
86
|
-
accessToken: z.string().optional(),
|
|
85
|
+
accessToken: z.string().min(1, 'Access token is required for automatic migration'),
|
|
87
86
|
}),
|
|
88
87
|
|
|
89
88
|
// Rule schemas - supports both single action (legacy) and actions array
|
|
@@ -116,9 +115,8 @@ export const schemas = {
|
|
|
116
115
|
|
|
117
116
|
// Settings schemas
|
|
118
117
|
updateSettings: z.object({
|
|
118
|
+
llm_provider: z.string().optional(),
|
|
119
119
|
llm_model: z.string().optional(),
|
|
120
|
-
llm_base_url: z.string().url().optional().or(z.literal('')),
|
|
121
|
-
llm_api_key: z.string().optional(),
|
|
122
120
|
auto_trash_spam: z.boolean().optional(),
|
|
123
121
|
smart_drafts: z.boolean().optional(),
|
|
124
122
|
sync_interval_minutes: z.number().min(1).max(60).optional(),
|
|
@@ -152,16 +152,7 @@ router.post('/draft/:emailId',
|
|
|
152
152
|
throw new NotFoundError('Email');
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
const { data: settings } = await req.supabase!
|
|
157
|
-
.from('user_settings')
|
|
158
|
-
.select('llm_model, llm_base_url')
|
|
159
|
-
.eq('user_id', userId)
|
|
160
|
-
.single();
|
|
161
|
-
|
|
162
|
-
const intelligenceService = getIntelligenceService(
|
|
163
|
-
settings ? { model: settings.llm_model, baseUrl: settings.llm_base_url } : undefined
|
|
164
|
-
);
|
|
155
|
+
const intelligenceService = getIntelligenceService();
|
|
165
156
|
|
|
166
157
|
const draft = await intelligenceService.generateDraftReply(
|
|
167
158
|
{
|
package/api/src/routes/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import rulesRoutes from './rules.js';
|
|
|
7
7
|
import settingsRoutes from './settings.js';
|
|
8
8
|
import emailsRoutes from './emails.js';
|
|
9
9
|
import migrateRoutes from './migrate.js';
|
|
10
|
+
import sdkRoutes from './sdk.js';
|
|
10
11
|
|
|
11
12
|
const router = Router();
|
|
12
13
|
|
|
@@ -18,5 +19,6 @@ router.use('/rules', rulesRoutes);
|
|
|
18
19
|
router.use('/settings', settingsRoutes);
|
|
19
20
|
router.use('/emails', emailsRoutes);
|
|
20
21
|
router.use('/migrate', migrateRoutes);
|
|
22
|
+
router.use('/sdk', sdkRoutes);
|
|
21
23
|
|
|
22
24
|
export default router;
|
|
@@ -13,65 +13,71 @@ const logger = createLogger('MigrateRoutes');
|
|
|
13
13
|
router.post('/',
|
|
14
14
|
validateBody(schemas.migrate),
|
|
15
15
|
asyncHandler(async (req, res) => {
|
|
16
|
-
const { projectRef,
|
|
16
|
+
const { projectRef, accessToken } = req.body;
|
|
17
17
|
|
|
18
18
|
logger.info('Starting migration', { projectRef });
|
|
19
19
|
|
|
20
|
-
res.setHeader('Content-Type', 'text/
|
|
21
|
-
res.setHeader('
|
|
20
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
21
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
22
|
+
res.setHeader('Connection', 'keep-alive');
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
res.write(
|
|
24
|
+
const sendEvent = (type: string, data: string) => {
|
|
25
|
+
res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
sendEvent('info', '🔧 Starting migration...');
|
|
30
|
+
|
|
30
31
|
const scriptPath = join(config.scriptsDir, 'migrate.sh');
|
|
31
32
|
|
|
32
33
|
const env = {
|
|
33
34
|
...process.env,
|
|
34
35
|
SUPABASE_PROJECT_ID: projectRef,
|
|
35
|
-
SUPABASE_DB_PASSWORD: dbPassword || '',
|
|
36
36
|
SUPABASE_ACCESS_TOKEN: accessToken || '',
|
|
37
|
-
SKIP_FUNCTIONS: '
|
|
37
|
+
SKIP_FUNCTIONS: '0',
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const child = spawn('bash', [scriptPath], {
|
|
41
|
-
env,
|
|
40
|
+
const child = spawn('bash', [scriptPath], {
|
|
41
|
+
env,
|
|
42
42
|
cwd: config.rootDir,
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
child.stdout.on('data', (data) => {
|
|
46
|
-
|
|
46
|
+
const lines = data.toString().split('\n').filter((l: string) => l.trim());
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
sendEvent('stdout', line);
|
|
49
|
+
}
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
child.stderr.on('data', (data) => {
|
|
50
|
-
|
|
53
|
+
const lines = data.toString().split('\n').filter((l: string) => l.trim());
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
sendEvent('stderr', line);
|
|
56
|
+
}
|
|
51
57
|
});
|
|
52
58
|
|
|
53
59
|
child.on('close', (code) => {
|
|
54
60
|
if (code === 0) {
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
sendEvent('info', '✅ Migration completed successfully!');
|
|
62
|
+
sendEvent('done', 'success');
|
|
57
63
|
logger.info('Migration completed successfully', { projectRef });
|
|
58
64
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
sendEvent('error', `❌ Migration failed with code ${code}`);
|
|
66
|
+
sendEvent('done', 'failed');
|
|
61
67
|
logger.error('Migration failed', new Error(`Exit code: ${code}`), { projectRef });
|
|
62
68
|
}
|
|
63
69
|
res.end();
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
child.on('error', (error) => {
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
sendEvent('error', `❌ Failed to run migration: ${error.message}`);
|
|
74
|
+
sendEvent('done', 'failed');
|
|
69
75
|
logger.error('Migration spawn error', error);
|
|
70
76
|
res.end();
|
|
71
77
|
});
|
|
72
78
|
} catch (error: any) {
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
sendEvent('error', `❌ Server error: ${error.message}`);
|
|
80
|
+
sendEvent('done', 'failed');
|
|
75
81
|
logger.error('Migration error', error);
|
|
76
82
|
res.end();
|
|
77
83
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { SDKService } from '../services/SDKService.js';
|
|
3
|
+
import { ProvidersResponse } from '@realtimex/sdk';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/sdk/providers/chat
|
|
9
|
+
* Returns available chat providers and their models
|
|
10
|
+
*/
|
|
11
|
+
router.get('/providers/chat', async (req: Request, res: Response) => {
|
|
12
|
+
try {
|
|
13
|
+
const sdk = SDKService.getSDK();
|
|
14
|
+
if (!sdk) {
|
|
15
|
+
return res.json({ success: false, message: 'SDK not available', providers: [] });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { providers } = await SDKService.withTimeout<ProvidersResponse>(
|
|
19
|
+
sdk.llm.chatProviders(),
|
|
20
|
+
30000,
|
|
21
|
+
'Chat providers fetch timed out'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
res.json({ success: true, providers: providers || [] });
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
console.warn('[SDK API] Failed to fetch chat providers:', error.message);
|
|
27
|
+
res.json({ success: false, providers: [], message: error.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/sdk/providers/embed
|
|
33
|
+
* Returns available embedding providers and their models
|
|
34
|
+
*/
|
|
35
|
+
router.get('/providers/embed', async (req: Request, res: Response) => {
|
|
36
|
+
try {
|
|
37
|
+
const sdk = SDKService.getSDK();
|
|
38
|
+
if (!sdk) {
|
|
39
|
+
return res.json({ success: false, message: 'SDK not available', providers: [] });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { providers } = await SDKService.withTimeout<ProvidersResponse>(
|
|
43
|
+
sdk.llm.embedProviders(),
|
|
44
|
+
30000,
|
|
45
|
+
'Embed providers fetch timed out'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
res.json({ success: true, providers: providers || [] });
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
console.warn('[SDK API] Failed to fetch embed providers:', error.message);
|
|
51
|
+
res.json({ success: false, providers: [], message: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default router;
|
|
@@ -4,6 +4,7 @@ import { authMiddleware } from '../middleware/auth.js';
|
|
|
4
4
|
import { apiRateLimit } from '../middleware/rateLimit.js';
|
|
5
5
|
import { validateBody, schemas } from '../middleware/validation.js';
|
|
6
6
|
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { SDKService } from '../services/SDKService.js';
|
|
7
8
|
|
|
8
9
|
const router = Router();
|
|
9
10
|
const logger = createLogger('SettingsRoutes');
|
|
@@ -28,13 +29,21 @@ router.get('/',
|
|
|
28
29
|
const settingsData = settingsResult.data;
|
|
29
30
|
const integrationsData = integrationsResult.data || [];
|
|
30
31
|
|
|
31
|
-
// Return defaults if no settings exist
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Return defaults if no settings exist, or upgrade old defaults
|
|
33
|
+
let settings = settingsData || {
|
|
34
|
+
llm_provider: SDKService.DEFAULT_LLM_PROVIDER,
|
|
35
|
+
llm_model: 'gpt-4o-mini',
|
|
35
36
|
sync_interval_minutes: 5,
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
// Proactive upgrade for existing settings objects
|
|
40
|
+
if (settings.llm_model === 'gpt-4.1-mini' || !settings.llm_model) {
|
|
41
|
+
settings.llm_model = 'gpt-4o-mini';
|
|
42
|
+
}
|
|
43
|
+
if (!settings.llm_provider) {
|
|
44
|
+
settings.llm_provider = SDKService.DEFAULT_LLM_PROVIDER;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
// Merge integration credentials back into settings for frontend compatibility
|
|
39
48
|
const googleIntegration = integrationsData.find((i: any) => i.provider === 'google');
|
|
40
49
|
if (googleIntegration?.credentials) {
|
|
@@ -67,7 +76,7 @@ router.patch('/',
|
|
|
67
76
|
microsoft_tenant_id,
|
|
68
77
|
...userSettingsUpdates
|
|
69
78
|
} = req.body;
|
|
70
|
-
|
|
79
|
+
|
|
71
80
|
const userId = req.user!.id;
|
|
72
81
|
|
|
73
82
|
// 1. Update user_settings
|
|
@@ -91,7 +100,7 @@ router.patch('/',
|
|
|
91
100
|
.eq('user_id', userId)
|
|
92
101
|
.eq('provider', 'google')
|
|
93
102
|
.single();
|
|
94
|
-
|
|
103
|
+
|
|
95
104
|
const credentials: any = {};
|
|
96
105
|
if (google_client_id) credentials.client_id = google_client_id;
|
|
97
106
|
if (google_client_secret) credentials.client_secret = google_client_secret;
|
|
@@ -110,19 +119,19 @@ router.patch('/',
|
|
|
110
119
|
|
|
111
120
|
// 3. Handle Microsoft Integration
|
|
112
121
|
if (microsoft_client_id || microsoft_client_secret || microsoft_tenant_id) {
|
|
113
|
-
|
|
122
|
+
const { data: existing } = await req.supabase!
|
|
114
123
|
.from('integrations')
|
|
115
124
|
.select('credentials')
|
|
116
125
|
.eq('user_id', userId)
|
|
117
126
|
.eq('provider', 'microsoft')
|
|
118
127
|
.single();
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
const credentials: any = {};
|
|
130
|
+
if (microsoft_client_id) credentials.client_id = microsoft_client_id;
|
|
131
|
+
if (microsoft_client_secret) credentials.client_secret = microsoft_client_secret;
|
|
132
|
+
if (microsoft_tenant_id) credentials.tenant_id = microsoft_tenant_id;
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
const newCredentials = { ...(existing?.credentials || {}), ...credentials };
|
|
126
135
|
|
|
127
136
|
await req.supabase!
|
|
128
137
|
.from('integrations')
|
|
@@ -156,14 +165,8 @@ router.post('/test-llm',
|
|
|
156
165
|
apiRateLimit,
|
|
157
166
|
authMiddleware,
|
|
158
167
|
asyncHandler(async (req, res) => {
|
|
159
|
-
const { llm_model, llm_base_url, llm_api_key } = req.body;
|
|
160
|
-
|
|
161
168
|
const { getIntelligenceService } = await import('../services/intelligence.js');
|
|
162
|
-
const intelligence = getIntelligenceService(
|
|
163
|
-
model: llm_model,
|
|
164
|
-
baseUrl: llm_base_url,
|
|
165
|
-
apiKey: llm_api_key,
|
|
166
|
-
});
|
|
169
|
+
const intelligence = getIntelligenceService();
|
|
167
170
|
|
|
168
171
|
const result = await intelligence.testConnection();
|
|
169
172
|
res.json(result);
|