hedgequantx 1.1.1 → 1.2.32
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/README.md +128 -136
- package/bin/cli.js +28 -2076
- package/package.json +3 -3
- package/src/app.js +550 -0
- package/src/config/index.js +16 -2
- package/src/config/propfirms.js +324 -12
- package/src/pages/accounts.js +115 -0
- package/src/pages/algo.js +538 -0
- package/src/pages/index.js +13 -2
- package/src/pages/orders.js +114 -0
- package/src/pages/positions.js +115 -0
- package/src/pages/stats.js +212 -3
- package/src/pages/user.js +92 -0
- package/src/security/encryption.js +168 -0
- package/src/security/index.js +61 -0
- package/src/security/rateLimit.js +155 -0
- package/src/security/validation.js +253 -0
- package/src/services/hqx-server.js +34 -17
- package/src/services/index.js +2 -1
- package/src/services/projectx.js +383 -35
- package/src/services/session.js +150 -38
- package/src/ui/index.js +4 -1
- package/src/ui/menu.js +154 -0
- package/src/services/local-storage.js +0 -309
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Security module exports
|
|
3
|
+
* @module security
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
encrypt,
|
|
8
|
+
decrypt,
|
|
9
|
+
hashPassword,
|
|
10
|
+
verifyPassword,
|
|
11
|
+
generateToken,
|
|
12
|
+
maskSensitive
|
|
13
|
+
} = require('./encryption');
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
ValidationError,
|
|
17
|
+
validateUsername,
|
|
18
|
+
validatePassword,
|
|
19
|
+
validateApiKey,
|
|
20
|
+
validateAccountId,
|
|
21
|
+
validateQuantity,
|
|
22
|
+
validatePrice,
|
|
23
|
+
validateSymbol,
|
|
24
|
+
sanitizeString,
|
|
25
|
+
validateObject
|
|
26
|
+
} = require('./validation');
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
RateLimiter,
|
|
30
|
+
rateLimiters,
|
|
31
|
+
getLimiter,
|
|
32
|
+
withRateLimit
|
|
33
|
+
} = require('./rateLimit');
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
// Encryption
|
|
37
|
+
encrypt,
|
|
38
|
+
decrypt,
|
|
39
|
+
hashPassword,
|
|
40
|
+
verifyPassword,
|
|
41
|
+
generateToken,
|
|
42
|
+
maskSensitive,
|
|
43
|
+
|
|
44
|
+
// Validation
|
|
45
|
+
ValidationError,
|
|
46
|
+
validateUsername,
|
|
47
|
+
validatePassword,
|
|
48
|
+
validateApiKey,
|
|
49
|
+
validateAccountId,
|
|
50
|
+
validateQuantity,
|
|
51
|
+
validatePrice,
|
|
52
|
+
validateSymbol,
|
|
53
|
+
sanitizeString,
|
|
54
|
+
validateObject,
|
|
55
|
+
|
|
56
|
+
// Rate Limiting
|
|
57
|
+
RateLimiter,
|
|
58
|
+
rateLimiters,
|
|
59
|
+
getLimiter,
|
|
60
|
+
withRateLimit
|
|
61
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate limiting utilities for API protection
|
|
3
|
+
* @module security/rateLimit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rate limiter class for controlling request frequency
|
|
8
|
+
*/
|
|
9
|
+
class RateLimiter {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new rate limiter
|
|
12
|
+
* @param {Object} options - Rate limiter options
|
|
13
|
+
* @param {number} [options.maxRequests=60] - Maximum requests per window
|
|
14
|
+
* @param {number} [options.windowMs=60000] - Time window in milliseconds
|
|
15
|
+
* @param {number} [options.minInterval=100] - Minimum interval between requests in ms
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.maxRequests = options.maxRequests || 60;
|
|
19
|
+
this.windowMs = options.windowMs || 60000;
|
|
20
|
+
this.minInterval = options.minInterval || 100;
|
|
21
|
+
this.requests = [];
|
|
22
|
+
this.lastRequest = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cleans up old requests outside the current window
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
_cleanup() {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const windowStart = now - this.windowMs;
|
|
32
|
+
this.requests = this.requests.filter(time => time > windowStart);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a request is allowed
|
|
37
|
+
* @returns {boolean} True if request is allowed
|
|
38
|
+
*/
|
|
39
|
+
canRequest() {
|
|
40
|
+
this._cleanup();
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
|
|
43
|
+
// Check minimum interval
|
|
44
|
+
if (now - this.lastRequest < this.minInterval) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check max requests in window
|
|
49
|
+
return this.requests.length < this.maxRequests;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Records a request
|
|
54
|
+
*/
|
|
55
|
+
recordRequest() {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
this.requests.push(now);
|
|
58
|
+
this.lastRequest = now;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets remaining requests in current window
|
|
63
|
+
* @returns {number} Remaining requests
|
|
64
|
+
*/
|
|
65
|
+
getRemainingRequests() {
|
|
66
|
+
this._cleanup();
|
|
67
|
+
return Math.max(0, this.maxRequests - this.requests.length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets time until rate limit resets
|
|
72
|
+
* @returns {number} Milliseconds until reset
|
|
73
|
+
*/
|
|
74
|
+
getResetTime() {
|
|
75
|
+
if (this.requests.length === 0) return 0;
|
|
76
|
+
const oldestRequest = Math.min(...this.requests);
|
|
77
|
+
return Math.max(0, (oldestRequest + this.windowMs) - Date.now());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Waits until a request is allowed
|
|
82
|
+
* @returns {Promise<void>} Resolves when request is allowed
|
|
83
|
+
*/
|
|
84
|
+
async waitForSlot() {
|
|
85
|
+
while (!this.canRequest()) {
|
|
86
|
+
const waitTime = Math.max(this.minInterval, this.getResetTime());
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(waitTime, 1000)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Executes a function with rate limiting
|
|
93
|
+
* @param {Function} fn - Function to execute
|
|
94
|
+
* @returns {Promise<any>} Result of the function
|
|
95
|
+
*/
|
|
96
|
+
async execute(fn) {
|
|
97
|
+
await this.waitForSlot();
|
|
98
|
+
this.recordRequest();
|
|
99
|
+
return fn();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resets the rate limiter
|
|
104
|
+
*/
|
|
105
|
+
reset() {
|
|
106
|
+
this.requests = [];
|
|
107
|
+
this.lastRequest = 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates rate limiters for different API endpoints
|
|
113
|
+
*/
|
|
114
|
+
const rateLimiters = {
|
|
115
|
+
// General API calls - 60 per minute
|
|
116
|
+
api: new RateLimiter({ maxRequests: 60, windowMs: 60000, minInterval: 100 }),
|
|
117
|
+
|
|
118
|
+
// Login attempts - 5 per minute (stricter for security)
|
|
119
|
+
login: new RateLimiter({ maxRequests: 5, windowMs: 60000, minInterval: 2000 }),
|
|
120
|
+
|
|
121
|
+
// Order placement - 30 per minute
|
|
122
|
+
orders: new RateLimiter({ maxRequests: 30, windowMs: 60000, minInterval: 200 }),
|
|
123
|
+
|
|
124
|
+
// Data fetching - 120 per minute
|
|
125
|
+
data: new RateLimiter({ maxRequests: 120, windowMs: 60000, minInterval: 50 })
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gets a rate limiter by name
|
|
130
|
+
* @param {string} name - Rate limiter name
|
|
131
|
+
* @returns {RateLimiter} Rate limiter instance
|
|
132
|
+
*/
|
|
133
|
+
const getLimiter = (name) => {
|
|
134
|
+
return rateLimiters[name] || rateLimiters.api;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Wraps an async function with rate limiting
|
|
139
|
+
* @param {Function} fn - Function to wrap
|
|
140
|
+
* @param {string} [limiterName='api'] - Rate limiter to use
|
|
141
|
+
* @returns {Function} Rate-limited function
|
|
142
|
+
*/
|
|
143
|
+
const withRateLimit = (fn, limiterName = 'api') => {
|
|
144
|
+
const limiter = getLimiter(limiterName);
|
|
145
|
+
return async (...args) => {
|
|
146
|
+
return limiter.execute(() => fn(...args));
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
RateLimiter,
|
|
152
|
+
rateLimiters,
|
|
153
|
+
getLimiter,
|
|
154
|
+
withRateLimit
|
|
155
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Input validation and sanitization utilities
|
|
3
|
+
* @module security/validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validation error class
|
|
8
|
+
*/
|
|
9
|
+
class ValidationError extends Error {
|
|
10
|
+
constructor(message, field = null) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ValidationError';
|
|
13
|
+
this.field = field;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates username format
|
|
19
|
+
* @param {string} username - Username to validate
|
|
20
|
+
* @returns {boolean} True if valid
|
|
21
|
+
* @throws {ValidationError} If invalid
|
|
22
|
+
*/
|
|
23
|
+
const validateUsername = (username) => {
|
|
24
|
+
if (!username || typeof username !== 'string') {
|
|
25
|
+
throw new ValidationError('Username is required', 'username');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const trimmed = username.trim();
|
|
29
|
+
|
|
30
|
+
if (trimmed.length < 3) {
|
|
31
|
+
throw new ValidationError('Username must be at least 3 characters', 'username');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (trimmed.length > 50) {
|
|
35
|
+
throw new ValidationError('Username must be less than 50 characters', 'username');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Allow alphanumeric, dots, underscores, hyphens, and @ for emails
|
|
39
|
+
if (!/^[a-zA-Z0-9._@-]+$/.test(trimmed)) {
|
|
40
|
+
throw new ValidationError('Username contains invalid characters', 'username');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates password strength
|
|
48
|
+
* @param {string} password - Password to validate
|
|
49
|
+
* @param {Object} [options] - Validation options
|
|
50
|
+
* @param {number} [options.minLength=6] - Minimum length
|
|
51
|
+
* @param {boolean} [options.requireSpecial=false] - Require special character
|
|
52
|
+
* @returns {boolean} True if valid
|
|
53
|
+
* @throws {ValidationError} If invalid
|
|
54
|
+
*/
|
|
55
|
+
const validatePassword = (password, options = {}) => {
|
|
56
|
+
const { minLength = 6, requireSpecial = false } = options;
|
|
57
|
+
|
|
58
|
+
if (!password || typeof password !== 'string') {
|
|
59
|
+
throw new ValidationError('Password is required', 'password');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (password.length < minLength) {
|
|
63
|
+
throw new ValidationError(`Password must be at least ${minLength} characters`, 'password');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (password.length > 128) {
|
|
67
|
+
throw new ValidationError('Password must be less than 128 characters', 'password');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (requireSpecial && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
71
|
+
throw new ValidationError('Password must contain a special character', 'password');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates API key format
|
|
79
|
+
* @param {string} apiKey - API key to validate
|
|
80
|
+
* @returns {boolean} True if valid
|
|
81
|
+
* @throws {ValidationError} If invalid
|
|
82
|
+
*/
|
|
83
|
+
const validateApiKey = (apiKey) => {
|
|
84
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
85
|
+
throw new ValidationError('API key is required', 'apiKey');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const trimmed = apiKey.trim();
|
|
89
|
+
|
|
90
|
+
if (trimmed.length < 10) {
|
|
91
|
+
throw new ValidationError('API key is too short', 'apiKey');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (trimmed.length > 256) {
|
|
95
|
+
throw new ValidationError('API key is too long', 'apiKey');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Allow alphanumeric and common API key characters
|
|
99
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
100
|
+
throw new ValidationError('API key contains invalid characters', 'apiKey');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validates account ID
|
|
108
|
+
* @param {number|string} accountId - Account ID to validate
|
|
109
|
+
* @returns {number} Validated account ID as integer
|
|
110
|
+
* @throws {ValidationError} If invalid
|
|
111
|
+
*/
|
|
112
|
+
const validateAccountId = (accountId) => {
|
|
113
|
+
const id = parseInt(accountId, 10);
|
|
114
|
+
|
|
115
|
+
if (isNaN(id) || id <= 0) {
|
|
116
|
+
throw new ValidationError('Invalid account ID', 'accountId');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (id > Number.MAX_SAFE_INTEGER) {
|
|
120
|
+
throw new ValidationError('Account ID is too large', 'accountId');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return id;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates order quantity
|
|
128
|
+
* @param {number|string} quantity - Quantity to validate
|
|
129
|
+
* @param {Object} [options] - Validation options
|
|
130
|
+
* @param {number} [options.min=1] - Minimum quantity
|
|
131
|
+
* @param {number} [options.max=1000] - Maximum quantity
|
|
132
|
+
* @returns {number} Validated quantity as integer
|
|
133
|
+
* @throws {ValidationError} If invalid
|
|
134
|
+
*/
|
|
135
|
+
const validateQuantity = (quantity, options = {}) => {
|
|
136
|
+
const { min = 1, max = 1000 } = options;
|
|
137
|
+
const qty = parseInt(quantity, 10);
|
|
138
|
+
|
|
139
|
+
if (isNaN(qty)) {
|
|
140
|
+
throw new ValidationError('Quantity must be a number', 'quantity');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (qty < min) {
|
|
144
|
+
throw new ValidationError(`Quantity must be at least ${min}`, 'quantity');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (qty > max) {
|
|
148
|
+
throw new ValidationError(`Quantity must be at most ${max}`, 'quantity');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return qty;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validates price
|
|
156
|
+
* @param {number|string} price - Price to validate
|
|
157
|
+
* @returns {number} Validated price as float
|
|
158
|
+
* @throws {ValidationError} If invalid
|
|
159
|
+
*/
|
|
160
|
+
const validatePrice = (price) => {
|
|
161
|
+
const p = parseFloat(price);
|
|
162
|
+
|
|
163
|
+
if (isNaN(p)) {
|
|
164
|
+
throw new ValidationError('Price must be a number', 'price');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (p < 0) {
|
|
168
|
+
throw new ValidationError('Price cannot be negative', 'price');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (p > 1000000) {
|
|
172
|
+
throw new ValidationError('Price is too large', 'price');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return p;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validates symbol format
|
|
180
|
+
* @param {string} symbol - Symbol to validate
|
|
181
|
+
* @returns {string} Validated symbol (uppercase, trimmed)
|
|
182
|
+
* @throws {ValidationError} If invalid
|
|
183
|
+
*/
|
|
184
|
+
const validateSymbol = (symbol) => {
|
|
185
|
+
if (!symbol || typeof symbol !== 'string') {
|
|
186
|
+
throw new ValidationError('Symbol is required', 'symbol');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const trimmed = symbol.trim().toUpperCase();
|
|
190
|
+
|
|
191
|
+
if (trimmed.length < 1 || trimmed.length > 20) {
|
|
192
|
+
throw new ValidationError('Invalid symbol length', 'symbol');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!/^[A-Z0-9]+$/.test(trimmed)) {
|
|
196
|
+
throw new ValidationError('Symbol contains invalid characters', 'symbol');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return trimmed;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sanitizes a string by removing potentially dangerous characters
|
|
204
|
+
* @param {string} input - Input to sanitize
|
|
205
|
+
* @returns {string} Sanitized string
|
|
206
|
+
*/
|
|
207
|
+
const sanitizeString = (input) => {
|
|
208
|
+
if (!input || typeof input !== 'string') return '';
|
|
209
|
+
|
|
210
|
+
return input
|
|
211
|
+
.trim()
|
|
212
|
+
.replace(/[<>]/g, '') // Remove HTML brackets
|
|
213
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
|
|
214
|
+
.substring(0, 1000); // Limit length
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validates and sanitizes all fields in an object
|
|
219
|
+
* @param {Object} data - Data object to validate
|
|
220
|
+
* @param {Object} schema - Validation schema
|
|
221
|
+
* @returns {Object} Validated data
|
|
222
|
+
* @throws {ValidationError} If any field is invalid
|
|
223
|
+
*/
|
|
224
|
+
const validateObject = (data, schema) => {
|
|
225
|
+
const result = {};
|
|
226
|
+
|
|
227
|
+
for (const [field, validator] of Object.entries(schema)) {
|
|
228
|
+
const value = data[field];
|
|
229
|
+
|
|
230
|
+
if (typeof validator === 'function') {
|
|
231
|
+
result[field] = validator(value);
|
|
232
|
+
} else if (validator.required && (value === undefined || value === null)) {
|
|
233
|
+
throw new ValidationError(`${field} is required`, field);
|
|
234
|
+
} else if (value !== undefined && value !== null) {
|
|
235
|
+
result[field] = validator.validate ? validator.validate(value) : value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
ValidationError,
|
|
244
|
+
validateUsername,
|
|
245
|
+
validatePassword,
|
|
246
|
+
validateApiKey,
|
|
247
|
+
validateAccountId,
|
|
248
|
+
validateQuantity,
|
|
249
|
+
validatePrice,
|
|
250
|
+
validateSymbol,
|
|
251
|
+
sanitizeString,
|
|
252
|
+
validateObject
|
|
253
|
+
};
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
const WebSocket = require('ws');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
-
const
|
|
9
|
+
const http = require('http');
|
|
10
10
|
|
|
11
11
|
// HQX Server Configuration - Contabo Dedicated Server
|
|
12
12
|
const HQX_CONFIG = {
|
|
13
|
-
|
|
13
|
+
host: process.env.HQX_HOST || '173.212.223.75',
|
|
14
|
+
port: process.env.HQX_PORT || 3500,
|
|
14
15
|
wsUrl: process.env.HQX_WS_URL || 'ws://173.212.223.75:3500/ws',
|
|
15
16
|
version: 'v1'
|
|
16
17
|
};
|
|
@@ -19,8 +20,10 @@ class HQXServerService {
|
|
|
19
20
|
constructor() {
|
|
20
21
|
this.ws = null;
|
|
21
22
|
this.token = null;
|
|
23
|
+
this.refreshToken = null;
|
|
22
24
|
this.apiKey = null;
|
|
23
25
|
this.sessionId = null;
|
|
26
|
+
this.userId = null;
|
|
24
27
|
this.connected = false;
|
|
25
28
|
this.reconnectAttempts = 0;
|
|
26
29
|
this.maxReconnectAttempts = 5;
|
|
@@ -39,16 +42,16 @@ class HQXServerService {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
45
|
+
* HTTP request helper
|
|
43
46
|
*/
|
|
44
47
|
_request(endpoint, method = 'GET', data = null) {
|
|
45
48
|
return new Promise((resolve, reject) => {
|
|
46
|
-
const
|
|
49
|
+
const postData = data ? JSON.stringify(data) : null;
|
|
47
50
|
|
|
48
51
|
const options = {
|
|
49
|
-
hostname:
|
|
50
|
-
port:
|
|
51
|
-
path:
|
|
52
|
+
hostname: HQX_CONFIG.host,
|
|
53
|
+
port: HQX_CONFIG.port,
|
|
54
|
+
path: `/${HQX_CONFIG.version}${endpoint}`,
|
|
52
55
|
method: method,
|
|
53
56
|
headers: {
|
|
54
57
|
'Content-Type': 'application/json',
|
|
@@ -57,6 +60,10 @@ class HQXServerService {
|
|
|
57
60
|
}
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
if (postData) {
|
|
64
|
+
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
if (this.token) {
|
|
61
68
|
options.headers['Authorization'] = `Bearer ${this.token}`;
|
|
62
69
|
}
|
|
@@ -64,7 +71,7 @@ class HQXServerService {
|
|
|
64
71
|
options.headers['X-API-Key'] = this.apiKey;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
const req =
|
|
74
|
+
const req = http.request(options, (res) => {
|
|
68
75
|
let body = '';
|
|
69
76
|
res.on('data', chunk => body += chunk);
|
|
70
77
|
res.on('end', () => {
|
|
@@ -83,8 +90,8 @@ class HQXServerService {
|
|
|
83
90
|
reject(new Error('Request timeout'));
|
|
84
91
|
});
|
|
85
92
|
|
|
86
|
-
if (
|
|
87
|
-
req.write(
|
|
93
|
+
if (postData) {
|
|
94
|
+
req.write(postData);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
req.end();
|
|
@@ -93,20 +100,30 @@ class HQXServerService {
|
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
102
|
* Authenticate with HQX Server
|
|
103
|
+
* @param {string} userId - User identifier (can be device ID or API key)
|
|
104
|
+
* @param {string} propfirm - PropFirm name (optional)
|
|
96
105
|
*/
|
|
97
|
-
async authenticate(
|
|
106
|
+
async authenticate(userId, propfirm = 'unknown') {
|
|
98
107
|
try {
|
|
108
|
+
const deviceId = this._generateDeviceId();
|
|
109
|
+
|
|
99
110
|
const response = await this._request('/auth/token', 'POST', {
|
|
100
|
-
|
|
101
|
-
deviceId:
|
|
111
|
+
userId: userId || deviceId,
|
|
112
|
+
deviceId: deviceId,
|
|
113
|
+
propfirm: propfirm,
|
|
102
114
|
timestamp: Date.now()
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
if (response.statusCode === 200 && response.data.success) {
|
|
106
|
-
this.token = response.data.token;
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
|
|
118
|
+
this.token = response.data.data.token;
|
|
119
|
+
this.refreshToken = response.data.data.refreshToken;
|
|
120
|
+
this.apiKey = response.data.data.apiKey;
|
|
121
|
+
this.sessionId = response.data.data.sessionId;
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
sessionId: this.sessionId,
|
|
125
|
+
apiKey: this.apiKey
|
|
126
|
+
};
|
|
110
127
|
} else {
|
|
111
128
|
return {
|
|
112
129
|
success: false,
|