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.
@@ -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 https = require('https');
9
+ const http = require('http');
10
10
 
11
11
  // HQX Server Configuration - Contabo Dedicated Server
12
12
  const HQX_CONFIG = {
13
- apiUrl: process.env.HQX_API_URL || 'http://173.212.223.75:3500',
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
- * HTTPS request helper
45
+ * HTTP request helper
43
46
  */
44
47
  _request(endpoint, method = 'GET', data = null) {
45
48
  return new Promise((resolve, reject) => {
46
- const url = new URL(`${HQX_CONFIG.apiUrl}/${HQX_CONFIG.version}${endpoint}`);
49
+ const postData = data ? JSON.stringify(data) : null;
47
50
 
48
51
  const options = {
49
- hostname: url.hostname,
50
- port: 443,
51
- path: url.pathname,
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 = https.request(options, (res) => {
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 (data) {
87
- req.write(JSON.stringify(data));
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(apiKey) {
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
- apiKey: apiKey,
101
- deviceId: this._generateDeviceId(),
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.apiKey = apiKey;
108
- this.sessionId = response.data.sessionId;
109
- return { success: true, sessionId: this.sessionId };
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,
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Services Module Exports
2
+ * @fileoverview Services module exports
3
+ * @module services
3
4
  */
4
5
 
5
6
  const { ProjectXService } = require('./projectx');