upfynai-code 2.5.1 → 2.6.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/package.json +2 -8
- package/server/cli.js +1 -1
- package/server/database/db.js +16 -2
- package/server/index.js +2738 -2621
- package/server/middleware/auth.js +10 -2
- package/server/relay-client.js +73 -20
- package/server/routes/agent.js +1226 -1266
- package/server/routes/auth.js +32 -29
- package/server/routes/commands.js +598 -601
- package/server/routes/cursor.js +806 -807
- package/server/routes/dashboard.js +154 -1
- package/server/routes/git.js +1151 -1165
- package/server/routes/mcp.js +534 -551
- package/server/routes/settings.js +261 -269
- package/server/routes/taskmaster.js +1927 -1963
- package/server/routes/vapi-chat.js +94 -0
- package/server/routes/voice.js +0 -4
- package/server/sandbox.js +120 -0
|
@@ -140,16 +140,24 @@ const authenticateWebSocket = async (request) => {
|
|
|
140
140
|
} catch { /* ignore */ }
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
if (!token)
|
|
143
|
+
if (!token) {
|
|
144
|
+
console.log('[WS-Auth] No token found in cookies or query params');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
144
147
|
|
|
145
148
|
// Relay token (upfyn_ prefix) — validate against DB, not JWT
|
|
146
149
|
if (token.startsWith('upfyn_') || token.startsWith('rt_')) {
|
|
150
|
+
console.log(`[WS-Auth] Relay token detected: ${token.slice(0, 12)}...`);
|
|
147
151
|
try {
|
|
148
152
|
const tokenData = await relayTokensDb.validateToken(token);
|
|
149
153
|
if (tokenData) {
|
|
154
|
+
console.log(`[WS-Auth] Relay token VALID: userId=${tokenData.user_id} username=${tokenData.username}`);
|
|
150
155
|
return { userId: Number(tokenData.user_id), username: tokenData.username };
|
|
151
156
|
}
|
|
152
|
-
|
|
157
|
+
console.log('[WS-Auth] Relay token NOT FOUND in database');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('[WS-Auth] Relay token validation error:', err.message);
|
|
160
|
+
}
|
|
153
161
|
return null;
|
|
154
162
|
}
|
|
155
163
|
|
package/server/relay-client.js
CHANGED
|
@@ -89,6 +89,21 @@ function execCommand(cmd, args, options = {}) {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a binary name to its full path, checking PATH first then local node_modules/.bin
|
|
94
|
+
*/
|
|
95
|
+
function resolveBinary(name) {
|
|
96
|
+
const isWindows = process.platform === 'win32';
|
|
97
|
+
try {
|
|
98
|
+
const result = execSync(`${isWindows ? 'where' : 'which'} ${name}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
99
|
+
return result.split('\n')[0].trim();
|
|
100
|
+
} catch {
|
|
101
|
+
const localPath = path.join(__dirname_rc, '../node_modules/.bin', name + (isWindows ? '.cmd' : ''));
|
|
102
|
+
if (fs.existsSync(localPath)) return localPath;
|
|
103
|
+
return name; // fallback to bare name
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
/**
|
|
93
108
|
* Handle incoming relay commands from the server
|
|
94
109
|
*/
|
|
@@ -105,7 +120,7 @@ async function handleRelayCommand(data, ws) {
|
|
|
105
120
|
if (options?.projectPath) args.push('--cwd', options.projectPath);
|
|
106
121
|
if (options?.sessionId) args.push('--continue', options.sessionId);
|
|
107
122
|
|
|
108
|
-
const proc = spawn('claude', [...args, command || ''], {
|
|
123
|
+
const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
|
|
109
124
|
shell: true,
|
|
110
125
|
cwd: options?.projectPath || os.homedir(),
|
|
111
126
|
env: process.env,
|
|
@@ -147,7 +162,7 @@ async function handleRelayCommand(data, ws) {
|
|
|
147
162
|
}
|
|
148
163
|
if (options?.model) codexArgs.push('--model', options.model);
|
|
149
164
|
|
|
150
|
-
const codexProc = spawn('codex', [...codexArgs, command || ''], {
|
|
165
|
+
const codexProc = spawn(resolveBinary('codex'), [...codexArgs, command || ''], {
|
|
151
166
|
shell: true,
|
|
152
167
|
cwd: options?.projectPath || options?.cwd || os.homedir(),
|
|
153
168
|
env: process.env,
|
|
@@ -189,7 +204,7 @@ async function handleRelayCommand(data, ws) {
|
|
|
189
204
|
}
|
|
190
205
|
if (options?.model) cursorArgs.push('--model', options.model);
|
|
191
206
|
|
|
192
|
-
const cursorProc = spawn('cursor-agent', [...cursorArgs, command || ''], {
|
|
207
|
+
const cursorProc = spawn(resolveBinary('cursor-agent'), [...cursorArgs, command || ''], {
|
|
193
208
|
shell: true,
|
|
194
209
|
cwd: options?.projectPath || options?.cwd || os.homedir(),
|
|
195
210
|
env: process.env,
|
|
@@ -233,12 +248,17 @@ async function handleRelayCommand(data, ws) {
|
|
|
233
248
|
|
|
234
249
|
case 'shell-command': {
|
|
235
250
|
const { command: cmd, cwd } = data;
|
|
236
|
-
// Block dangerous shell patterns
|
|
237
251
|
if (!cmd || typeof cmd !== 'string') throw new Error('Invalid command');
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
// Block dangerous shell patterns (cross-platform)
|
|
253
|
+
const cmdLower = cmd.toLowerCase();
|
|
254
|
+
const dangerous = [
|
|
255
|
+
'rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd', // Unix
|
|
256
|
+
'format c:', 'format d:', 'format e:', 'del /s /q c:\\', // Windows
|
|
257
|
+
'rd /s /q c:\\', 'reg delete', 'bcdedit', // Windows
|
|
258
|
+
];
|
|
259
|
+
if (dangerous.some(d => cmdLower.includes(d.toLowerCase()))) throw new Error('Command blocked for safety');
|
|
240
260
|
logRelayEvent('$', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
|
|
241
|
-
const result = await execCommand(cmd, [], { cwd: cwd ||
|
|
261
|
+
const result = await execCommand(cmd, [], { cwd: cwd || process.cwd(), timeout: 60000 });
|
|
242
262
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
|
|
243
263
|
break;
|
|
244
264
|
}
|
|
@@ -246,10 +266,11 @@ async function handleRelayCommand(data, ws) {
|
|
|
246
266
|
case 'file-read': {
|
|
247
267
|
const { filePath } = data;
|
|
248
268
|
if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
|
|
249
|
-
// Block reading sensitive system files
|
|
250
269
|
const normalizedPath = path.resolve(filePath);
|
|
251
|
-
|
|
252
|
-
|
|
270
|
+
// Block reading sensitive files (cross-platform, case-insensitive)
|
|
271
|
+
const normLower = normalizedPath.toLowerCase().replace(/\\/g, '/');
|
|
272
|
+
const blockedRead = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.ssh/id_ed25519', '/.env'];
|
|
273
|
+
if (blockedRead.some(b => normLower.includes(b))) throw new Error('Access denied');
|
|
253
274
|
logRelayEvent('R', `Read: ${filePath}`, 'dim');
|
|
254
275
|
const content = await fsPromises.readFile(normalizedPath, 'utf8');
|
|
255
276
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
|
|
@@ -259,10 +280,18 @@ async function handleRelayCommand(data, ws) {
|
|
|
259
280
|
case 'file-write': {
|
|
260
281
|
const { filePath: fp, content: fileContent } = data;
|
|
261
282
|
if (!fp || typeof fp !== 'string') throw new Error('Invalid file path');
|
|
262
|
-
// Block writing to sensitive locations
|
|
263
283
|
const normalizedFp = path.resolve(fp);
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
// Block writing to sensitive locations (cross-platform, case-insensitive)
|
|
285
|
+
const fpLower = normalizedFp.toLowerCase().replace(/\\/g, '/');
|
|
286
|
+
const blockedWrite = [
|
|
287
|
+
'/etc/', '/usr/bin/', '/usr/sbin/', // Unix
|
|
288
|
+
'/windows/system32', '/windows/syswow64', '/program files', // Windows
|
|
289
|
+
'.ssh/', '/.env',
|
|
290
|
+
];
|
|
291
|
+
if (blockedWrite.some(d => fpLower.includes(d))) throw new Error('Access denied');
|
|
292
|
+
// Ensure parent directory exists (works across drives)
|
|
293
|
+
const parentDir = path.dirname(normalizedFp);
|
|
294
|
+
await fsPromises.mkdir(parentDir, { recursive: true }).catch(() => {});
|
|
266
295
|
logRelayEvent('W', `Write: ${fp}`, 'dim');
|
|
267
296
|
await fsPromises.writeFile(normalizedFp, fileContent, 'utf8');
|
|
268
297
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
|
|
@@ -271,7 +300,9 @@ async function handleRelayCommand(data, ws) {
|
|
|
271
300
|
|
|
272
301
|
case 'file-tree': {
|
|
273
302
|
const { dirPath, depth = 3 } = data;
|
|
274
|
-
|
|
303
|
+
// Use provided path (any drive), fall back to cwd, then home
|
|
304
|
+
const resolvedDir = dirPath ? path.resolve(dirPath) : process.cwd();
|
|
305
|
+
const tree = await buildFileTree(resolvedDir, depth);
|
|
275
306
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { tree } }));
|
|
276
307
|
break;
|
|
277
308
|
}
|
|
@@ -279,7 +310,8 @@ async function handleRelayCommand(data, ws) {
|
|
|
279
310
|
case 'git-operation': {
|
|
280
311
|
const { gitCommand, cwd: gitCwd } = data;
|
|
281
312
|
logRelayEvent('G', `Git: ${gitCommand}`, 'dim');
|
|
282
|
-
const
|
|
313
|
+
const resolvedGitCwd = gitCwd ? path.resolve(gitCwd) : process.cwd();
|
|
314
|
+
const result = await execCommand('git', [gitCommand], { cwd: resolvedGitCwd });
|
|
283
315
|
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
|
|
284
316
|
break;
|
|
285
317
|
}
|
|
@@ -330,6 +362,9 @@ function detectInstalledAgents() {
|
|
|
330
362
|
const isWindows = process.platform === 'win32';
|
|
331
363
|
const whichCmd = isWindows ? 'where' : 'which';
|
|
332
364
|
|
|
365
|
+
// Also check our own node_modules/.bin for bundled agents
|
|
366
|
+
const localBinDir = path.join(__dirname_rc, '../node_modules/.bin');
|
|
367
|
+
|
|
333
368
|
const agents = [
|
|
334
369
|
{ name: 'claude', binary: 'claude', label: 'Claude Code' },
|
|
335
370
|
{ name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
|
|
@@ -346,10 +381,20 @@ function detectInstalledAgents() {
|
|
|
346
381
|
label: agent.label,
|
|
347
382
|
};
|
|
348
383
|
} catch {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
384
|
+
// Fallback: check bundled node_modules/.bin
|
|
385
|
+
const localPath = path.join(localBinDir, agent.binary + (isWindows ? '.cmd' : ''));
|
|
386
|
+
if (fs.existsSync(localPath)) {
|
|
387
|
+
detected[agent.name] = {
|
|
388
|
+
installed: true,
|
|
389
|
+
path: localPath,
|
|
390
|
+
label: agent.label,
|
|
391
|
+
};
|
|
392
|
+
} else {
|
|
393
|
+
detected[agent.name] = {
|
|
394
|
+
installed: false,
|
|
395
|
+
label: agent.label,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
353
398
|
}
|
|
354
399
|
}
|
|
355
400
|
return detected;
|
|
@@ -377,7 +422,7 @@ function createRelayConnection(wsUrl, config = {}) {
|
|
|
377
422
|
*/
|
|
378
423
|
export async function connectToServer(options = {}) {
|
|
379
424
|
const config = loadConfig();
|
|
380
|
-
const serverUrl = options.server || config.server || 'https://upfynai.
|
|
425
|
+
const serverUrl = options.server || config.server || 'https://upfynai-code-production.up.railway.app';
|
|
381
426
|
const relayKey = options.key || config.relayKey;
|
|
382
427
|
|
|
383
428
|
if (!relayKey) {
|
|
@@ -510,6 +555,14 @@ export async function connectToServer(options = {}) {
|
|
|
510
555
|
ws.on('error', (err) => {
|
|
511
556
|
if (err.code === 'ECONNREFUSED') {
|
|
512
557
|
spinner.fail(`Cannot reach ${serverUrl}. Is the server running?`);
|
|
558
|
+
} else if (err.message?.includes('401')) {
|
|
559
|
+
spinner.fail('Authentication failed (401). Your relay token may be invalid or expired.');
|
|
560
|
+
logRelayEvent('!', 'Get a new token from the dashboard: https://cli.upfyn.com/dashboard', 'yellow');
|
|
561
|
+
logRelayEvent('!', 'Then run: uc connect --server <url> --key <new_token>', 'yellow');
|
|
562
|
+
} else if (err.message?.includes('403') || err.message?.includes('Forbidden')) {
|
|
563
|
+
spinner.fail('Access forbidden (403). Your account may be inactive.');
|
|
564
|
+
} else {
|
|
565
|
+
logRelayEvent('!', `WebSocket error: ${err.message || err.code || 'unknown'}`, 'red');
|
|
513
566
|
}
|
|
514
567
|
// close handler will trigger reconnect
|
|
515
568
|
});
|