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.
- package/CHANGELOG.md +23 -0
- package/README.md +22 -12
- package/SECURITY.md +25 -21
- package/assets/raw-diagram.png +0 -0
- package/assets/security-flow.png +0 -0
- package/bin/nyxora.mjs +236 -0
- package/launcher.js +8 -3
- package/launcher.ts +28 -1
- package/package.json +11 -8
- package/packages/core/package.json +4 -4
- package/packages/core/src/agent/reasoning.ts +10 -8
- package/packages/core/src/config/parser.ts +2 -1
- package/packages/core/src/gateway/cli.ts +2 -64
- package/packages/core/src/gateway/server.ts +89 -8
- package/packages/core/src/gateway/setup-cli.ts +7 -0
- package/packages/core/src/gateway/setup.ts +52 -28
- package/packages/core/src/gateway/telegram.ts +147 -89
- package/packages/core/src/memory/logger.ts +83 -20
- package/packages/core/src/system/pluginManager.ts +48 -34
- package/packages/core/src/utils/state.ts +15 -2
- package/packages/core/src/web3/config.ts +18 -3
- package/packages/core/src/web3/skills/marketAnalysis.ts +43 -17
- package/packages/core/src/web3/skills/swapToken.ts +9 -1
- package/packages/dashboard/dist/assets/index-CfIids2e.js +170 -0
- package/packages/dashboard/dist/assets/index-POJM-7Fd.css +1 -0
- package/packages/dashboard/dist/favicon.svg +1 -1
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package.json +7 -7
- package/packages/dashboard/public/favicon.svg +1 -1
- package/packages/dashboard/src/App.tsx +224 -167
- package/packages/dashboard/src/Settings.tsx +55 -0
- package/packages/dashboard/src/Skills.tsx +8 -1
- package/packages/dashboard/src/index.css +146 -35
- package/packages/policy/package.json +1 -1
- package/packages/policy/src/server.ts +21 -28
- package/packages/signer/package.json +1 -1
- package/packages/signer/src/server.ts +40 -13
- package/test-db.ts +3 -0
- package/bin/nyxora.js +0 -13
- package/packages/dashboard/dist/assets/index-BK4qmIy6.js +0 -200
- package/packages/dashboard/dist/assets/index-C1m4ohce.css +0 -1
- package/packages/dashboard/package-lock.json +0 -2748
- 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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
-
//
|
|
258
|
-
if (privateKey
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.
|
|
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
|
|