nyxora 1.6.2 → 1.6.4

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 (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -12
  3. package/SECURITY.md +25 -21
  4. package/assets/raw-diagram.png +0 -0
  5. package/assets/security-flow.png +0 -0
  6. package/bin/nyxora.mjs +236 -0
  7. package/launcher.js +8 -3
  8. package/launcher.ts +28 -1
  9. package/package.json +11 -8
  10. package/packages/core/package.json +4 -4
  11. package/packages/core/src/agent/reasoning.ts +10 -8
  12. package/packages/core/src/config/parser.ts +2 -1
  13. package/packages/core/src/gateway/cli.ts +2 -64
  14. package/packages/core/src/gateway/server.ts +89 -8
  15. package/packages/core/src/gateway/setup-cli.ts +7 -0
  16. package/packages/core/src/gateway/setup.ts +52 -28
  17. package/packages/core/src/gateway/telegram.ts +147 -89
  18. package/packages/core/src/memory/logger.ts +83 -20
  19. package/packages/core/src/system/pluginManager.ts +48 -34
  20. package/packages/core/src/utils/state.ts +15 -2
  21. package/packages/core/src/web3/config.ts +18 -3
  22. package/packages/core/src/web3/skills/marketAnalysis.ts +43 -17
  23. package/packages/core/src/web3/skills/swapToken.ts +9 -1
  24. package/packages/dashboard/dist/assets/index-CfIids2e.js +170 -0
  25. package/packages/dashboard/dist/assets/index-POJM-7Fd.css +1 -0
  26. package/packages/dashboard/dist/favicon.svg +1 -1
  27. package/packages/dashboard/dist/index.html +2 -2
  28. package/packages/dashboard/package.json +7 -7
  29. package/packages/dashboard/public/favicon.svg +1 -1
  30. package/packages/dashboard/src/App.tsx +224 -167
  31. package/packages/dashboard/src/Settings.tsx +55 -0
  32. package/packages/dashboard/src/Skills.tsx +8 -1
  33. package/packages/dashboard/src/index.css +146 -35
  34. package/packages/policy/package.json +1 -1
  35. package/packages/policy/src/server.ts +21 -28
  36. package/packages/signer/package.json +1 -1
  37. package/packages/signer/src/server.ts +40 -13
  38. package/test-db.ts +3 -0
  39. package/bin/nyxora.js +0 -13
  40. package/packages/dashboard/dist/assets/index-BK4qmIy6.js +0 -200
  41. package/packages/dashboard/dist/assets/index-C1m4ohce.css +0 -1
  42. package/packages/dashboard/package-lock.json +0 -2748
  43. package/packages/dashboard/src/Memory.tsx +0 -110
@@ -59,72 +59,10 @@ console.log(`================================`);
59
59
  await runSetupWizard();
60
60
  }
61
61
 
62
- // 3. Load Private Key into Memory
63
- const keystorePath = path.join(appDir, 'keystore.json');
64
- if (fs.existsSync(keystorePath)) {
65
- const masterPassword = await password({
66
- message: '🔒 Vault locked! Enter Master Password to access Nyxora:',
67
- });
68
-
69
- if (isCancel(masterPassword) || !masterPassword) {
70
- console.log(pc.red('Access denied. Exiting Nyxora.'));
71
- return process.exit(0);
72
- }
73
-
74
- try {
75
- const keystore = JSON.parse(fs.readFileSync(keystorePath, 'utf8'));
76
- const internalToken = process.env.INTERNAL_AUTH_TOKEN;
77
- let unlocked = false;
78
- for (let i = 0; i < 5; i++) {
79
- try {
80
- const res = await fetch('http://127.0.0.1:3001/unlock', {
81
- method: 'POST',
82
- headers: {
83
- 'Content-Type': 'application/json',
84
- 'Authorization': `Bearer ${internalToken}`
85
- },
86
- body: JSON.stringify({ keystore, password: masterPassword })
87
- });
88
-
89
- const data = await res.json();
90
- if (res.ok && data.success) {
91
- console.log(pc.green(`✅ Vault successfully unlocked. Agent Address: ${data.address}`));
92
- unlocked = true;
93
- break;
94
- } else {
95
- console.log(pc.red(`❌ Failed to unlock Vault: ${data.error || 'Unknown error'}`));
96
- break; // Stop retrying on auth error
97
- }
98
- } catch (e: any) {
99
- if (i === 4) {
100
- console.log(pc.red(`❌ IPC Connection to Policy failed: ${e.message}`));
101
- } else {
102
- await new Promise(r => setTimeout(r, 1000));
103
- }
104
- }
105
- }
106
-
107
- if (!unlocked) {
108
- console.log(pc.yellow('⚠️ Proceeding anyway. You can retry unlock via Dashboard later.'));
109
- }
110
- } catch (err: any) {
111
- console.log(pc.red(`❌ Failed to read keystore or connect: ${err.message}`));
112
- }
113
- } else {
114
- console.log(pc.yellow('⚠️ Keystore not found. Web3 features will be disabled unless you run `nyxora setup`.'));
115
- }
116
-
117
62
  // 4. Start the Express API Server (which also serves the static dashboard and Telegram bot)
118
63
  startServer();
119
-
120
- // 5. Open the Dashboard in the default browser
121
- const PORT = process.env.PORT || 3000;
122
- const token = getSessionToken();
123
- setTimeout(() => {
124
- const url = `http://localhost:${PORT}?token=${token}`;
125
- console.log(`🌐 Opening Dashboard at ${url}`);
126
- open(url);
127
- }, 1500);
64
+ getSessionToken(); // Initialize token file
65
+ console.log(`🌐 Nyxora API Server running on port ${process.env.PORT || 3000}`);
128
66
  }
129
67
 
130
68
  main().catch(console.error);
@@ -5,6 +5,7 @@ import helmet from 'helmet';
5
5
  import rateLimit from 'express-rate-limit';
6
6
  import http from 'http';
7
7
  import jwt from 'jsonwebtoken';
8
+ import crypto from 'crypto';
8
9
  import { getPath } from '../config/paths';
9
10
  import { getSessionToken } from '../utils/state';
10
11
 
@@ -31,17 +32,21 @@ import { executeCustomTx, customTxToolDefinition } from '../web3/skills/customTx
31
32
  import { startTelegramBot } from './telegram';
32
33
  import { formatTransactionSuccess, formatTransactionError } from '../utils/formatter';
33
34
 
35
+ import util from 'util';
36
+
34
37
  // Intercept console.log and console.error
35
38
  const originalLog = console.log;
36
39
  const originalError = console.error;
37
40
 
41
+ const safeFormat = (a: any) => typeof a === 'object' ? util.inspect(a, { depth: 2 }) : String(a);
42
+
38
43
  console.log = function (...args) {
39
- Tracker.addGatewayLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '));
44
+ Tracker.addGatewayLog(args.map(safeFormat).join(' '));
40
45
  originalLog.apply(console, args);
41
46
  };
42
47
 
43
48
  console.error = function (...args) {
44
- Tracker.addGatewayLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), { level: 'error' });
49
+ Tracker.addGatewayLog(args.map(safeFormat).join(' '), { level: 'error' });
45
50
  originalError.apply(console, args);
46
51
  };
47
52
 
@@ -52,7 +57,7 @@ app.use(express.json());
52
57
 
53
58
  const apiLimiter = rateLimit({
54
59
  windowMs: 15 * 60 * 1000,
55
- max: 100,
60
+ max: 10000, // Increased from 100 to 10000 to prevent breaking dashboard polling (which polls every 2s)
56
61
  message: 'Too many requests from this IP, please try again after 15 minutes'
57
62
  });
58
63
  app.use('/api/', apiLimiter);
@@ -61,7 +66,7 @@ app.use('/api/', apiLimiter);
61
66
  app.use('/api', (req, res, next) => {
62
67
  const token = req.headers['x-nyxora-token'];
63
68
  if (token !== getSessionToken()) {
64
- return res.status(401).json({ error: 'Unauthorized: Invalid or missing token' });
69
+ return res.status(401).json({ error: `Unauthorized: Invalid or missing token. Expected: ${getSessionToken()}, Received: ${token}` });
65
70
  }
66
71
  next();
67
72
  });
@@ -76,7 +81,8 @@ app.get('/', (req, res) => {
76
81
 
77
82
  app.get('/api/history', (req, res) => {
78
83
  try {
79
- const history = logger.getHistory();
84
+ const sessionId = req.query.session_id as string | undefined;
85
+ const history = logger.getHistory(sessionId);
80
86
  // Filter out internal system prompt for the frontend
81
87
  const cleanHistory = history.filter((msg: any) => msg.role !== 'system');
82
88
  res.json(cleanHistory);
@@ -87,7 +93,8 @@ app.get('/api/history', (req, res) => {
87
93
 
88
94
  app.delete('/api/history', (req, res) => {
89
95
  try {
90
- logger.clear();
96
+ const sessionId = req.query.session_id as string | undefined;
97
+ logger.clear(sessionId);
91
98
  Tracker.addEvent('memory.cleared');
92
99
  res.json({ success: true });
93
100
  } catch (error: any) {
@@ -95,6 +102,45 @@ app.delete('/api/history', (req, res) => {
95
102
  }
96
103
  });
97
104
 
105
+ app.get('/api/sessions', (req, res) => {
106
+ try {
107
+ res.json(logger.getSessions());
108
+ } catch (error: any) {
109
+ res.status(500).json({ error: error.message });
110
+ }
111
+ });
112
+
113
+ app.post('/api/sessions', (req, res) => {
114
+ try {
115
+ const { title } = req.body;
116
+ const id = logger.createSession(title || 'New Chat');
117
+ res.json({ id });
118
+ } catch (error: any) {
119
+ res.status(500).json({ error: error.message });
120
+ }
121
+ });
122
+
123
+ app.delete('/api/sessions/:id', (req, res) => {
124
+ try {
125
+ logger.deleteSession(req.params.id);
126
+ res.json({ success: true });
127
+ } catch (error: any) {
128
+ res.status(500).json({ error: error.message });
129
+ }
130
+ });
131
+
132
+ app.put('/api/sessions/:id', (req, res) => {
133
+ try {
134
+ const { title } = req.body;
135
+ if (title) {
136
+ logger.renameSession(req.params.id, title);
137
+ }
138
+ res.json({ success: true });
139
+ } catch (error: any) {
140
+ res.status(500).json({ error: error.message });
141
+ }
142
+ });
143
+
98
144
  app.get('/api/config', (req, res) => {
99
145
  try {
100
146
  const config = loadConfig();
@@ -155,6 +201,10 @@ app.post('/api/transactions/:id/approve', (req, res) => {
155
201
 
156
202
  const jwtToken = jwt.sign({ service: 'core' }, token, { expiresIn: '1m' });
157
203
 
204
+ // Generate Challenge Nonce
205
+ const nonce = crypto.randomBytes(16).toString('hex');
206
+ const approvalHash = crypto.createHash('sha256').update(id + nonce + token).digest('hex');
207
+
158
208
  const options = {
159
209
  hostname: '127.0.0.1',
160
210
  port: 3001,
@@ -166,6 +216,9 @@ app.post('/api/transactions/:id/approve', (req, res) => {
166
216
  }
167
217
  };
168
218
 
219
+ const requestPayload = JSON.stringify({ nonce, approvalHash });
220
+ options.headers['Content-Length'] = Buffer.byteLength(requestPayload);
221
+
169
222
  const proxyReq = http.request(options, (proxyRes) => {
170
223
  res.status(proxyRes.statusCode || 200);
171
224
  proxyRes.pipe(res);
@@ -175,6 +228,7 @@ app.post('/api/transactions/:id/approve', (req, res) => {
175
228
  res.status(500).json({ error: 'Policy Engine unreachable: ' + e.message });
176
229
  });
177
230
 
231
+ proxyReq.write(requestPayload);
178
232
  proxyReq.end();
179
233
  });
180
234
 
@@ -188,15 +242,42 @@ app.post('/api/transactions/:id/reject', (req, res) => {
188
242
  res.json({ success: true });
189
243
  });
190
244
 
245
+ let cachedTrending: string[] | null = null;
246
+ let lastTrendingFetch = 0;
247
+
248
+ app.get('/api/trending', async (req, res) => {
249
+ const now = Date.now();
250
+ if (cachedTrending && now - lastTrendingFetch < 5 * 60 * 1000) {
251
+ return res.json(cachedTrending);
252
+ }
253
+ try {
254
+ const response = await fetch('https://api.coingecko.com/api/v3/search/trending');
255
+ if (response.ok) {
256
+ const data = await response.json();
257
+ const top5 = data.coins.slice(0, 5).map((c: any) => '$' + c.item.symbol.toUpperCase());
258
+ cachedTrending = top5;
259
+ lastTrendingFetch = now;
260
+ res.json(top5);
261
+ } else {
262
+ // Fallback if coingecko rate limits
263
+ if (cachedTrending) return res.json(cachedTrending);
264
+ res.status(response.status).json({ error: 'Failed to fetch trending' });
265
+ }
266
+ } catch (err: any) {
267
+ if (cachedTrending) return res.json(cachedTrending);
268
+ res.status(500).json({ error: err.message });
269
+ }
270
+ });
271
+
191
272
  app.post('/api/chat', async (req, res) => {
192
273
  try {
193
- const { message } = req.body;
274
+ const { message, session_id } = req.body;
194
275
  if (!message) {
195
276
  return res.status(400).json({ error: 'Message is required' });
196
277
  }
197
278
 
198
279
  // Process input (this will automatically add to memory)
199
- const response = await processUserInput(message);
280
+ const response = await processUserInput(message, 'user', undefined, session_id);
200
281
 
201
282
  res.json({ response });
202
283
  } catch (error: any) {
@@ -0,0 +1,7 @@
1
+ import { runSetupWizard } from './setup';
2
+ import * as process from 'process';
3
+
4
+ runSetupWizard().catch((err) => {
5
+ console.error("Setup failed:", err);
6
+ process.exit(1);
7
+ });
@@ -1,4 +1,4 @@
1
- import { intro, outro, confirm, select, text, isCancel, cancel, note, password } from '@clack/prompts';
1
+ import { intro, outro, confirm, select, text, isCancel, cancel, note, password, spinner } from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -184,11 +184,48 @@ Provider: ${config.llm.provider}`;
184
184
  if (isCancel(setupTelegram)) return process.exit(0);
185
185
 
186
186
  let telegramToken = '';
187
+ let authorizedChatId = config.integrations?.telegram?.authorized_chat_id;
187
188
  if (setupTelegram) {
188
189
  telegramToken = (await password({
189
190
  message: 'Enter Telegram Bot Token from @BotFather (Leave empty if already set):',
190
191
  })) as string;
191
192
  if (isCancel(telegramToken)) return process.exit(0);
193
+
194
+ if (telegramToken && !authorizedChatId) {
195
+ const s = spinner();
196
+ const pin = Math.floor(100000 + Math.random() * 900000).toString();
197
+
198
+ note(pc.cyan(`1. Open Telegram and search for your Bot.\n2. Send this exact message to your bot:\n\n /auth ${pin}\n\nWaiting for your message...`), 'Telegram Pairing Required');
199
+ s.start(`Waiting for /auth ${pin} on Telegram...`);
200
+
201
+ try {
202
+ const { Telegraf } = require('telegraf');
203
+ const bot = new Telegraf(telegramToken);
204
+ let paired = false;
205
+
206
+ bot.command('auth', (ctx: any) => {
207
+ const text = ctx.message.text.split(' ');
208
+ if (text[1] === pin) {
209
+ authorizedChatId = ctx.chat.id;
210
+ paired = true;
211
+ ctx.reply('✅ Bot successfully paired with Nyxora!');
212
+ bot.stop();
213
+ } else {
214
+ ctx.reply('❌ Invalid PIN.');
215
+ }
216
+ });
217
+
218
+ bot.launch();
219
+
220
+ // Wait until paired
221
+ while (!paired) {
222
+ await new Promise(r => setTimeout(r, 1000));
223
+ }
224
+ s.stop(`Bot successfully paired with Chat ID: ${authorizedChatId}`);
225
+ } catch (err: any) {
226
+ s.stop(`Failed to start bot listener: ${err.message}. You can pair it later.`);
227
+ }
228
+ }
192
229
  }
193
230
 
194
231
  // 6. Wallet Setup
@@ -214,21 +251,7 @@ Provider: ${config.llm.provider}`;
214
251
  note(`New Wallet Generated!\n\nAddress: ${account.address}\nPrivate Key: ${privateKey}\n\nIMPORTANT: Backup this Private Key NOW! It is securely injected into your local vault, but you will need it to import your wallet elsewhere.`, 'Wallet Created');
215
252
  }
216
253
 
217
- let masterPassword = '';
218
- if (privateKey) {
219
- masterPassword = (await password({
220
- message: 'Enter a strong MASTER PASSWORD to encrypt your key vault:',
221
- })) as string;
222
- if (isCancel(masterPassword) || !masterPassword) return process.exit(0);
223
254
 
224
- const masterPasswordConfirm = (await password({
225
- message: 'Confirm MASTER PASSWORD:',
226
- })) as string;
227
- if (isCancel(masterPasswordConfirm) || masterPassword !== masterPasswordConfirm) {
228
- console.log(pc.red('❌ Passwords do not match. Setup cancelled.'));
229
- return process.exit(1);
230
- }
231
- }
232
255
 
233
256
  // --- SAVING ---
234
257
 
@@ -251,24 +274,25 @@ Provider: ${config.llm.provider}`;
251
274
  if (setupTelegram && telegramToken) {
252
275
  config.integrations.telegram.bot_token = telegramToken as string;
253
276
  }
277
+
278
+ if (authorizedChatId) {
279
+ config.integrations.telegram.authorized_chat_id = authorizedChatId;
280
+ }
254
281
 
255
282
  saveConfig(config);
256
283
 
257
- // Update keystore.json exclusively for Private Key
258
- if (privateKey && masterPassword) {
259
- const keystorePath = path.join(appDir, 'keystore.json');
284
+ // Save Private Key to OS Keyring or fallback to .env
285
+ if (privateKey) {
260
286
  try {
261
- const encryptedData = encryptKey(privateKey as string, masterPassword);
262
- fs.writeFileSync(keystorePath, JSON.stringify(encryptedData, null, 2), 'utf8');
263
-
264
- // Cleanup old .env if it existed
265
- const envPath = path.join(appDir, '.env');
266
- if (fs.existsSync(envPath)) {
267
- fs.unlinkSync(envPath);
268
- console.log(pc.yellow('Legacy .env file has been deleted for security.'));
269
- }
287
+ const { Entry } = require('@napi-rs/keyring');
288
+ const entry = new Entry('nyxora', 'wallet');
289
+ await entry.setPassword(privateKey as string);
290
+ console.log(pc.green('Private key saved securely to OS Keyring.'));
270
291
  } catch (error) {
271
- console.error('Failed to save keystore.json:', error);
292
+ console.warn(pc.yellow('Failed to save to OS Keyring (Module mismatch or headless server). Falling back to local vault.key'));
293
+ const vaultPath = path.join(appDir, 'vault.key');
294
+ fs.writeFileSync(vaultPath, `PRIVATE_KEY=${privateKey}\n`, { mode: 0o600 });
295
+ console.log(pc.green('Private key saved to ~/.nyxora/vault.key with 0600 permissions.'));
272
296
  }
273
297
  }
274
298