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.
@@ -140,16 +140,24 @@ const authenticateWebSocket = async (request) => {
140
140
  } catch { /* ignore */ }
141
141
  }
142
142
 
143
- if (!token) return null;
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
- } catch {}
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
 
@@ -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
- const dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'fork bomb', '> /dev/sd'];
239
- if (dangerous.some(d => cmd.includes(d))) throw new Error('Command blocked for safety');
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 || os.homedir(), timeout: 60000 });
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
- const blocked = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.env'];
252
- if (blocked.some(b => normalizedPath.includes(b))) throw new Error('Access denied');
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
- const blockedDirs = ['/etc/', '/usr/bin/', '/usr/sbin/', 'System32', '.ssh/'];
265
- if (blockedDirs.some(d => normalizedFp.includes(d))) throw new Error('Access denied');
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
- const tree = await buildFileTree(dirPath || os.homedir(), depth);
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 result = await execCommand('git', [gitCommand], { cwd: gitCwd });
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
- detected[agent.name] = {
350
- installed: false,
351
- label: agent.label,
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.thinqmesh.com';
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
  });