hedgequantx 1.1.1 → 1.2.31

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.
@@ -1,60 +1,111 @@
1
1
  /**
2
- * Session Management
3
- * Handles multi-connection state and persistence
2
+ * @fileoverview Secure session management with encryption
3
+ * @module services/session
4
4
  */
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
+ const { encrypt, decrypt, maskSensitive } = require('../security');
9
10
  const { ProjectXService } = require('./projectx');
10
11
 
11
- const SESSION_FILE = path.join(os.homedir(), '.hedgequantx', 'session.json');
12
+ const SESSION_DIR = path.join(os.homedir(), '.hedgequantx');
13
+ const SESSION_FILE = path.join(SESSION_DIR, 'session.enc');
12
14
 
13
- // Session Storage
15
+ /**
16
+ * Secure session storage with AES-256 encryption
17
+ */
14
18
  const storage = {
19
+ /**
20
+ * Ensures the session directory exists with proper permissions
21
+ * @private
22
+ */
23
+ _ensureDir() {
24
+ if (!fs.existsSync(SESSION_DIR)) {
25
+ fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
26
+ }
27
+ },
28
+
29
+ /**
30
+ * Saves sessions with encryption
31
+ * @param {Array} sessions - Sessions to save
32
+ */
15
33
  save(sessions) {
16
34
  try {
17
- const dir = path.dirname(SESSION_FILE);
18
- if (!fs.existsSync(dir)) {
19
- fs.mkdirSync(dir, { recursive: true });
20
- }
21
- fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2));
22
- } catch (e) { /* ignore */ }
35
+ this._ensureDir();
36
+ const data = JSON.stringify(sessions);
37
+ const encrypted = encrypt(data);
38
+ fs.writeFileSync(SESSION_FILE, encrypted, { mode: 0o600 });
39
+ } catch (e) {
40
+ // Silently fail - don't expose errors
41
+ }
23
42
  },
24
-
43
+
44
+ /**
45
+ * Loads and decrypts sessions
46
+ * @returns {Array} Decrypted sessions or empty array
47
+ */
25
48
  load() {
26
49
  try {
27
50
  if (fs.existsSync(SESSION_FILE)) {
28
- return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
51
+ const encrypted = fs.readFileSync(SESSION_FILE, 'utf8');
52
+ const decrypted = decrypt(encrypted);
53
+ if (decrypted) {
54
+ return JSON.parse(decrypted);
55
+ }
29
56
  }
30
- } catch (e) { /* ignore */ }
57
+ } catch (e) {
58
+ // Session corrupted or from different machine - clear it
59
+ this.clear();
60
+ }
31
61
  return [];
32
62
  },
33
-
63
+
64
+ /**
65
+ * Securely clears session data
66
+ */
34
67
  clear() {
35
68
  try {
36
69
  if (fs.existsSync(SESSION_FILE)) {
70
+ // Overwrite with random data before deleting
71
+ const size = fs.statSync(SESSION_FILE).size;
72
+ fs.writeFileSync(SESSION_FILE, require('crypto').randomBytes(size));
37
73
  fs.unlinkSync(SESSION_FILE);
38
74
  }
39
- } catch (e) { /* ignore */ }
75
+ } catch (e) {
76
+ // Ignore errors
77
+ }
40
78
  }
41
79
  };
42
80
 
43
- // Connection Manager
81
+ /**
82
+ * Multi-connection manager with secure token handling
83
+ */
44
84
  const connections = {
85
+ /** @type {Array} Active connections */
45
86
  services: [],
46
-
87
+
88
+ /**
89
+ * Adds a new connection
90
+ * @param {string} type - Connection type (projectx, rithmic, etc.)
91
+ * @param {Object} service - Service instance
92
+ * @param {string} [propfirm] - PropFirm name
93
+ * @param {string} [token] - Auth token
94
+ */
47
95
  add(type, service, propfirm = null, token = null) {
48
- this.services.push({
49
- type,
50
- service,
51
- propfirm,
52
- token,
53
- connectedAt: new Date()
96
+ this.services.push({
97
+ type,
98
+ service,
99
+ propfirm,
100
+ token: token || service.token,
101
+ connectedAt: new Date()
54
102
  });
55
103
  this.saveToStorage();
56
104
  },
57
-
105
+
106
+ /**
107
+ * Saves all sessions to encrypted storage
108
+ */
58
109
  saveToStorage() {
59
110
  const sessions = this.services.map(conn => ({
60
111
  type: conn.type,
@@ -63,15 +114,22 @@ const connections = {
63
114
  }));
64
115
  storage.save(sessions);
65
116
  },
66
-
117
+
118
+ /**
119
+ * Restores sessions from encrypted storage
120
+ * @returns {Promise<boolean>} True if sessions were restored
121
+ */
67
122
  async restoreFromStorage() {
68
123
  const sessions = storage.load();
124
+
69
125
  for (const session of sessions) {
70
126
  try {
71
127
  if (session.type === 'projectx' && session.token) {
72
- const service = new ProjectXService(session.propfirm.toLowerCase().replace(/ /g, '_'));
128
+ const propfirmKey = session.propfirm.toLowerCase().replace(/ /g, '_');
129
+ const service = new ProjectXService(propfirmKey);
73
130
  service.token = session.token;
74
-
131
+
132
+ // Validate token is still valid
75
133
  const userResult = await service.getUser();
76
134
  if (userResult.success) {
77
135
  this.services.push({
@@ -83,30 +141,61 @@ const connections = {
83
141
  });
84
142
  }
85
143
  }
86
- } catch (e) { /* invalid session */ }
144
+ } catch (e) {
145
+ // Invalid session - skip
146
+ }
87
147
  }
148
+
88
149
  return this.services.length > 0;
89
150
  },
90
-
151
+
152
+ /**
153
+ * Removes a connection by index
154
+ * @param {number} index - Connection index
155
+ */
91
156
  remove(index) {
92
- this.services.splice(index, 1);
93
- this.saveToStorage();
157
+ if (index >= 0 && index < this.services.length) {
158
+ const conn = this.services[index];
159
+ if (conn.service && conn.service.logout) {
160
+ conn.service.logout();
161
+ }
162
+ this.services.splice(index, 1);
163
+ this.saveToStorage();
164
+ }
94
165
  },
95
-
166
+
167
+ /**
168
+ * Gets all connections
169
+ * @returns {Array} All connections
170
+ */
96
171
  getAll() {
97
172
  return this.services;
98
173
  },
99
-
174
+
175
+ /**
176
+ * Gets connections by type
177
+ * @param {string} type - Connection type
178
+ * @returns {Array} Filtered connections
179
+ */
100
180
  getByType(type) {
101
181
  return this.services.filter(c => c.type === type);
102
182
  },
103
-
183
+
184
+ /**
185
+ * Gets connection count
186
+ * @returns {number} Number of connections
187
+ */
104
188
  count() {
105
189
  return this.services.length;
106
190
  },
107
-
191
+
192
+ /**
193
+ * Gets all accounts from all connections
194
+ * @returns {Promise<Array>} All accounts
195
+ */
108
196
  async getAllAccounts() {
109
197
  const allAccounts = [];
198
+
110
199
  for (const conn of this.services) {
111
200
  try {
112
201
  const result = await conn.service.getTradingAccounts();
@@ -120,15 +209,25 @@ const connections = {
120
209
  });
121
210
  });
122
211
  }
123
- } catch (e) { /* ignore */ }
212
+ } catch (e) {
213
+ // Skip failed connections
214
+ }
124
215
  }
216
+
125
217
  return allAccounts;
126
218
  },
127
-
219
+
220
+ /**
221
+ * Checks if any connection is active
222
+ * @returns {boolean} True if connected
223
+ */
128
224
  isConnected() {
129
225
  return this.services.length > 0;
130
226
  },
131
-
227
+
228
+ /**
229
+ * Disconnects all connections and clears sessions
230
+ */
132
231
  disconnectAll() {
133
232
  this.services.forEach(conn => {
134
233
  if (conn.service && conn.service.logout) {
@@ -137,6 +236,19 @@ const connections = {
137
236
  });
138
237
  this.services = [];
139
238
  storage.clear();
239
+ },
240
+
241
+ /**
242
+ * Gets masked connection info for logging
243
+ * @returns {Array} Masked connection info
244
+ */
245
+ getInfo() {
246
+ return this.services.map(conn => ({
247
+ type: conn.type,
248
+ propfirm: conn.propfirm,
249
+ token: maskSensitive(conn.token),
250
+ connectedAt: conn.connectedAt
251
+ }));
140
252
  }
141
253
  };
142
254
 
package/src/ui/index.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  draw2ColSeparator,
23
23
  fmtRow
24
24
  } = require('./table');
25
+ const { createBoxMenu } = require('./menu');
25
26
 
26
27
  module.exports = {
27
28
  // Device
@@ -44,5 +45,7 @@ module.exports = {
44
45
  draw2ColRow,
45
46
  draw2ColRowRaw,
46
47
  draw2ColSeparator,
47
- fmtRow
48
+ fmtRow,
49
+ // Menu
50
+ createBoxMenu
48
51
  };
package/src/ui/menu.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Custom Box Menu with keyboard navigation
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const readline = require('readline');
7
+ const { getLogoWidth, centerText } = require('./box');
8
+
9
+ /**
10
+ * Creates a custom menu inside a box
11
+ * @param {string} title - Menu title
12
+ * @param {Array} items - Menu items [{label, value, color, disabled, separator}]
13
+ * @param {Object} options - Options {headerLines: [], footerText: ''}
14
+ * @returns {Promise<string>} Selected value
15
+ */
16
+ const createBoxMenu = async (title, items, options = {}) => {
17
+ const boxWidth = getLogoWidth();
18
+ const innerWidth = boxWidth - 2;
19
+
20
+ let selectedIndex = 0;
21
+
22
+ // Find first non-separator, non-disabled item
23
+ while (selectedIndex < items.length && (items[selectedIndex].separator || items[selectedIndex].disabled)) {
24
+ selectedIndex++;
25
+ }
26
+
27
+ const renderMenu = () => {
28
+ // Clear screen and move cursor to top
29
+ process.stdout.write('\x1b[2J\x1b[H');
30
+
31
+ const version = require('../../package.json').version;
32
+
33
+ // Full ASCII logo
34
+ const logo = [
35
+ '██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗██╗ ██╗',
36
+ '██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝╚██╗██╔╝',
37
+ '███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ╚███╔╝ ',
38
+ '██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ██╔██╗ ',
39
+ '██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ██╔╝ ██╗',
40
+ '╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝'
41
+ ];
42
+
43
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
44
+
45
+ logo.forEach(line => {
46
+ const padded = centerText(line, innerWidth);
47
+ console.log(chalk.cyan('║') + chalk.cyan(padded) + chalk.cyan('║'));
48
+ });
49
+
50
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
51
+ console.log(chalk.cyan('║') + chalk.white(centerText(`Prop Futures Algo Trading v${version}`, innerWidth)) + chalk.cyan('║'));
52
+
53
+ // Stats bar if provided
54
+ if (options.statsLine) {
55
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
56
+ const statsLen = options.statsLine.replace(/\x1b\[[0-9;]*m/g, '').length;
57
+ const statsPad = innerWidth - statsLen;
58
+ const leftPad = Math.floor(statsPad / 2);
59
+ const rightPad = statsPad - leftPad;
60
+ console.log(chalk.cyan('║') + ' '.repeat(leftPad) + options.statsLine + ' '.repeat(rightPad) + chalk.cyan('║'));
61
+ }
62
+
63
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
64
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText(title, innerWidth)) + chalk.cyan('║'));
65
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
66
+
67
+ // Header lines (connection info, etc.)
68
+ if (options.headerLines && options.headerLines.length > 0) {
69
+ options.headerLines.forEach(line => {
70
+ const text = ' ' + line;
71
+ console.log(chalk.cyan('║') + text.padEnd(innerWidth) + chalk.cyan('║'));
72
+ });
73
+ console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
74
+ }
75
+
76
+ // Menu items
77
+ items.forEach((item, index) => {
78
+ if (item.separator) {
79
+ console.log(chalk.cyan('║') + chalk.gray(' ' + '─'.repeat(innerWidth - 4) + ' ') + chalk.cyan('║'));
80
+ } else {
81
+ const isSelected = index === selectedIndex;
82
+ const prefix = isSelected ? chalk.white('▸ ') : ' ';
83
+ const color = item.disabled ? chalk.gray : (item.color || chalk.cyan);
84
+ const label = item.label + (item.disabled ? ' (Coming Soon)' : '');
85
+ const text = prefix + color(label);
86
+ const visLen = text.replace(/\x1b\[[0-9;]*m/g, '').length;
87
+ const padding = innerWidth - visLen;
88
+
89
+ if (isSelected && !item.disabled) {
90
+ console.log(chalk.cyan('║') + chalk.bgGray.white(text + ' '.repeat(padding)) + chalk.cyan('║'));
91
+ } else {
92
+ console.log(chalk.cyan('║') + text + ' '.repeat(padding) + chalk.cyan('║'));
93
+ }
94
+ }
95
+ });
96
+
97
+ // Footer
98
+ console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
99
+ const footerText = options.footerText || 'Use ↑↓ arrows to navigate, Enter to select';
100
+ console.log(chalk.cyan('║') + chalk.gray(centerText(footerText, innerWidth)) + chalk.cyan('║'));
101
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
102
+ };
103
+
104
+ return new Promise((resolve) => {
105
+ renderMenu();
106
+
107
+ readline.emitKeypressEvents(process.stdin);
108
+ if (process.stdin.isTTY) {
109
+ process.stdin.setRawMode(true);
110
+ }
111
+
112
+ const onKeyPress = (str, key) => {
113
+ if (key.name === 'up') {
114
+ // Move up, skip separators and disabled
115
+ let newIndex = selectedIndex - 1;
116
+ while (newIndex >= 0 && (items[newIndex].separator || items[newIndex].disabled)) {
117
+ newIndex--;
118
+ }
119
+ if (newIndex >= 0) {
120
+ selectedIndex = newIndex;
121
+ renderMenu();
122
+ }
123
+ } else if (key.name === 'down') {
124
+ // Move down, skip separators and disabled
125
+ let newIndex = selectedIndex + 1;
126
+ while (newIndex < items.length && (items[newIndex].separator || items[newIndex].disabled)) {
127
+ newIndex++;
128
+ }
129
+ if (newIndex < items.length) {
130
+ selectedIndex = newIndex;
131
+ renderMenu();
132
+ }
133
+ } else if (key.name === 'return') {
134
+ // Select current item
135
+ cleanup();
136
+ resolve(items[selectedIndex].value);
137
+ } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
138
+ cleanup();
139
+ process.exit(0);
140
+ }
141
+ };
142
+
143
+ const cleanup = () => {
144
+ process.stdin.removeListener('keypress', onKeyPress);
145
+ if (process.stdin.isTTY) {
146
+ process.stdin.setRawMode(false);
147
+ }
148
+ };
149
+
150
+ process.stdin.on('keypress', onKeyPress);
151
+ });
152
+ };
153
+
154
+ module.exports = { createBoxMenu };