3xui-api-client 2.1.0 → 3.0.1

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/index.js CHANGED
@@ -1,715 +1,1360 @@
1
- const axios = require('axios');
2
- const CredentialGenerator = require('./src/generators/CredentialGenerator');
3
- const { SessionManager, createSessionManager } = require('./src/session/SessionManager');
4
- const {
5
- InputValidator,
6
- SecurityMonitor,
7
- SecureHeaders,
8
- CredentialSecurity,
9
- ErrorSecurity
10
- } = require('./src/security/SecurityEnhancer');
11
-
12
- /**
13
- * 3X-UI API Client Library
14
- *
15
- * A Node.js client for managing 3x-ui panel APIs with automatic session management.
16
- * Now includes credential generation and advanced session management for web applications.
17
- *
18
- * @class ThreeXUI
19
- * @version 2.0.0
20
- * @author Helitha Guruge
21
- */
22
- class ThreeXUI {
23
- /**
24
- * Creates a new ThreeXUI client instance
25
- *
26
- * @param {string} baseURL - The base URL of your 3x-ui server (e.g., 'https://your-server.com')
27
- * @param {string} username - Admin username for authentication
28
- * @param {string} password - Admin password for authentication
29
- * @param {Object} options - Configuration options
30
- * @param {Object} options.sessionManager - Session management configuration
31
- * @param {boolean} options.autoGenerateCredentials - Enable automatic credential generation
32
- * @param {number} options.timeout - Request timeout in milliseconds (default: 30000)
33
- * @throws {Error} If baseURL, username, or password is missing
34
- */
35
- constructor(baseURL, username, password, options = {}) {
36
- if (!baseURL) {
37
- throw new Error('baseURL is required');
38
- }
39
- if (!username) {
40
- throw new Error('username is required');
41
- }
42
- if (!password) {
43
- throw new Error('password is required');
44
- }
45
-
46
- // Apply security validations
47
- this.baseURL = InputValidator.validateURL(baseURL);
48
- this.username = InputValidator.validateUsername(username);
49
- this.password = InputValidator.validatePassword(password);
50
- this.cookie = null;
51
- this.options = options;
52
- this.loginMutex = false; // Add mutex to prevent concurrent logins
53
- this.loginRetryCount = 0; // Add retry counter
54
- this.maxLoginRetries = 3; // Maximum login attempts
55
-
56
- // Initialize security monitoring
57
- this.securityMonitor = new SecurityMonitor({
58
- maxRequestsPerMinute: options.maxRequestsPerMinute || 60,
59
- maxLoginAttemptsPerHour: options.maxLoginAttemptsPerHour || 10
60
- });
61
-
62
- // Security configuration
63
- this.isDevelopment = options.isDevelopment || process.env.NODE_ENV === 'development';
64
-
65
- // Initialize session manager if provided
66
- if (options.sessionManager) {
67
- this.sessionManager = options.sessionManager instanceof SessionManager
68
- ? options.sessionManager
69
- : createSessionManager(options.sessionManager);
70
- } else {
71
- // Default to memory-based session management
72
- this.sessionManager = createSessionManager();
73
- }
74
-
75
- // Create axios instance with security best practices
76
- this.api = axios.create({
77
- baseURL: this.baseURL,
78
- timeout: options.timeout || 30000, // 30 second timeout
79
- maxRedirects: 5,
80
- validateStatus: (status) => status >= 200 && status < 300,
81
- headers: SecureHeaders.getSecureHeaders({
82
- userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
83
- enableCSP: options.enableCSP || false
84
- })
85
- });
86
-
87
- // Add request interceptor for security headers
88
- this.api.interceptors.request.use((config) => {
89
- // Add security headers
90
- config.headers['X-Requested-With'] = 'XMLHttpRequest';
91
- return config;
92
- });
93
-
94
- // Add response interceptor for error handling
95
- this.api.interceptors.response.use(
96
- (response) => response,
97
- (error) => {
98
- if (error.code === 'ECONNABORTED') {
99
- throw new Error('Request timeout - server took too long to respond');
100
- }
101
- if (error.code === 'ENOTFOUND') {
102
- throw new Error(`Cannot connect to server: ${this.baseURL}`);
103
- }
104
- throw error;
105
- }
106
- );
107
- }
108
-
109
- /**
110
- * Login with session management support
111
- * @param {boolean} forceRefresh - Force a new login even if session exists
112
- * @returns {Object} Login response
113
- */
114
- async login(forceRefresh = false) {
115
- // Check rate limiting first
116
- const identifier = CredentialSecurity.hashForLogging(this.username);
117
- if (!this.securityMonitor.checkRateLimit(identifier, 'login')) {
118
- const error = new Error('Rate limit exceeded for login attempts');
119
- ErrorSecurity.logError(error, { username: this.username, baseURL: this.baseURL });
120
- throw error;
121
- }
122
-
123
- // Increment retry count
124
- this.loginRetryCount++;
125
- // Check for existing valid session first
126
- if (!forceRefresh && this.sessionManager) {
127
- const existingSession = await this.sessionManager.getSession(this.baseURL, this.username);
128
- if (existingSession && existingSession.cookie) {
129
- this.cookie = existingSession.cookie;
130
- this.api.defaults.headers.Cookie = this.cookie;
131
- // Reset retry count on successful session restore
132
- this.loginRetryCount = 0;
133
- return {
134
- success: true,
135
- fromCache: true,
136
- data: { msg: 'Session restored from cache' }
137
- };
138
- }
139
- }
140
-
141
- try {
142
- const params = new URLSearchParams();
143
- params.append('username', this.username);
144
- params.append('password', this.password);
145
-
146
- const response = await this.api.post('/login', params, {
147
- headers: {
148
- 'Content-Type': 'application/x-www-form-urlencoded'
149
- }
150
- });
151
-
152
- if (response.data.success) {
153
- const cookies = response.headers['set-cookie'];
154
- if (cookies && cookies.length > 0) {
155
- this.cookie = cookies[0].split(';')[0];
156
- this.api.defaults.headers.Cookie = this.cookie;
157
-
158
- // Store session if session manager is available
159
- if (this.sessionManager) {
160
- try {
161
- await this.sessionManager.storeSession(this.baseURL, this.username, {
162
- cookie: this.cookie,
163
- loginTime: new Date().toISOString()
164
- });
165
- } catch (sessionError) {
166
- console.warn('Failed to store session, continuing without cache:', sessionError.message);
167
- }
168
- }
169
- // Reset retry count on successful login
170
- this.loginRetryCount = 0;
171
- } else {
172
- throw new Error('Login failed: No session cookie received.');
173
- }
174
- return {
175
- success: true,
176
- fromCache: false,
177
- headers: response.headers,
178
- data: response.data
179
- };
180
- } else {
181
- throw new Error(`Login failed: ${response.data.msg}`);
182
- }
183
- } catch (error) {
184
- // Log the error securely
185
- ErrorSecurity.logError(error, {
186
- username: this.username,
187
- baseURL: this.baseURL,
188
- action: 'login'
189
- });
190
-
191
- // Log suspicious activity for multiple failed logins
192
- if (this.loginRetryCount >= this.maxLoginRetries) {
193
- this.securityMonitor.logSuspiciousActivity('multiple_failed_logins', {
194
- username: CredentialSecurity.hashForLogging(this.username),
195
- attempts: this.loginRetryCount
196
- });
197
- }
198
-
199
- // Return sanitized error
200
- const sanitizedError = ErrorSecurity.sanitizeError(error, this.isDevelopment);
201
- throw sanitizedError;
202
- }
203
- }
204
-
205
- /**
206
- * Logout and clear session
207
- */
208
- async logout() {
209
- this.cookie = null;
210
- delete this.api.defaults.headers.Cookie;
211
-
212
- if (this.sessionManager) {
213
- await this.sessionManager.deleteSession(this.baseURL, this.username);
214
- }
215
- }
216
-
217
- async _request(method, path, data = {}) {
218
- // Check session validity first with mutex protection
219
- if (!this.loginMutex && this.sessionManager && !await this.sessionManager.hasValidSession(this.baseURL, this.username)) {
220
- await this._ensureAuthenticated();
221
- } else if (!this.loginMutex && !this.cookie) {
222
- await this._ensureAuthenticated();
223
- }
224
-
225
- try {
226
- const response = await this.api.request({
227
- method,
228
- url: path,
229
- data,
230
- ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
231
- });
232
- // Reset retry counter on successful request
233
- this.loginRetryCount = 0;
234
- return response.data;
235
- } catch (error) {
236
- if (error.response && error.response.status === 401) {
237
- // Cookie might have expired, try to login again with retry limit
238
- if (this.loginRetryCount < this.maxLoginRetries) {
239
- await this._ensureAuthenticated(true); // Force refresh
240
- const response = await this.api.request({
241
- method,
242
- url: path,
243
- data,
244
- ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
245
- });
246
- return response.data;
247
- } else {
248
- throw new Error('Maximum login retry attempts exceeded. Check your credentials.');
249
- }
250
- }
251
- throw error;
252
- }
253
- }
254
-
255
- /**
256
- * Ensure authentication with mutex protection
257
- * @param {boolean} forceRefresh - Force a new login
258
- */
259
- async _ensureAuthenticated(forceRefresh = false) {
260
- // Prevent concurrent login attempts
261
- if (this.loginMutex) {
262
- // Wait for ongoing login to complete
263
- while (this.loginMutex) {
264
- await new Promise(resolve => setTimeout(resolve, 100));
265
- }
266
- return;
267
- }
268
-
269
- this.loginMutex = true;
270
- try {
271
- await this.login(forceRefresh);
272
- } finally {
273
- this.loginMutex = false;
274
- }
275
- }
276
-
277
- // ===========================================
278
- // CREDENTIAL GENERATION METHODS
279
- // ===========================================
280
-
281
- /**
282
- * Generate credentials for a specific protocol
283
- * @param {string} protocol - Protocol name (vless, vmess, trojan, etc.)
284
- * @param {Object} options - Generation options
285
- * @returns {Object} Generated credentials
286
- */
287
- generateCredentials(protocol, options = {}) {
288
- return CredentialGenerator.generateForProtocol(protocol, options);
289
- }
290
-
291
- /**
292
- * Generate UUID for VLESS/VMess protocols
293
- * @param {boolean} secure - Use cryptographically secure generation
294
- * @returns {string} UUID
295
- */
296
- generateUUID(secure = true) {
297
- return secure ? CredentialGenerator.generateSecureUUID() : CredentialGenerator.generateUUID();
298
- }
299
-
300
- /**
301
- * Generate password for Trojan/Shadowsocks protocols
302
- * @param {number} length - Password length
303
- * @param {Object} options - Password options
304
- * @returns {string} Generated password
305
- */
306
- generatePassword(length = 16, options = {}) {
307
- return CredentialGenerator.generatePassword(length, options);
308
- }
309
-
310
- /**
311
- * Generate bulk credentials for multiple clients
312
- * @param {string} protocol - Protocol name
313
- * @param {number} count - Number of credentials to generate
314
- * @param {Object} options - Generation options
315
- * @returns {Array} Array of credentials
316
- */
317
- generateBulkCredentials(protocol, count, options = {}) {
318
- return CredentialGenerator.generateBulk(protocol, count, options);
319
- }
320
-
321
- /**
322
- * Get available cipher methods for Shadowsocks
323
- * @returns {Array} Array of cipher methods
324
- */
325
- getShadowsocksCiphers() {
326
- return CredentialGenerator.getShadowsocksCipherMethods();
327
- }
328
-
329
- /**
330
- * Get recommended cipher for Shadowsocks
331
- * @returns {string} Recommended cipher
332
- */
333
- getRecommendedShadowsocksCipher() {
334
- return CredentialGenerator.getRecommendedShadowsocksCipher();
335
- }
336
-
337
- /**
338
- * Generate WireGuard key pair
339
- * @returns {Object} Key pair with helper methods
340
- */
341
- generateWireGuardKeys() {
342
- return CredentialGenerator.generateWireGuardKeys();
343
- }
344
-
345
- /**
346
- * Generate Reality key pair for anti-censorship
347
- * @returns {Object} Reality key pair with helper methods
348
- */
349
- generateRealityKeys() {
350
- return CredentialGenerator.generateRealityKeys();
351
- }
352
-
353
- /**
354
- * Generate random port number
355
- * @param {number} min - Minimum port
356
- * @param {number} max - Maximum port
357
- * @returns {number} Random port
358
- */
359
- generatePort(min = 10000, max = 65535) {
360
- return CredentialGenerator.generatePort(min, max);
361
- }
362
-
363
- /**
364
- * Validate generated credentials
365
- * @param {Object} credentials - Credentials to validate
366
- * @param {string} protocol - Protocol name
367
- * @returns {Object} Validation result
368
- */
369
- validateCredentials(credentials, protocol) {
370
- return CredentialGenerator.validateCredentials(credentials, protocol);
371
- }
372
-
373
- // ===========================================
374
- // ENHANCED CLIENT MANAGEMENT WITH AUTO-GENERATION
375
- // ===========================================
376
-
377
- /**
378
- * Add client with automatic credential generation
379
- * @param {number} inboundId - Inbound ID
380
- * @param {string} protocol - Protocol type
381
- * @param {Object} options - Client options
382
- * @returns {Object} Created client with credentials
383
- */
384
- async addClientWithCredentials(inboundId, protocol, options = {}) {
385
- const credentials = this.generateCredentials(protocol, options);
386
-
387
- const clientConfig = {
388
- id: inboundId,
389
- settings: JSON.stringify({
390
- clients: [{
391
- ...credentials,
392
- enable: true,
393
- expiryTime: options.expiryTime || 0,
394
- limitIp: options.limitIp || 0,
395
- totalGB: options.totalGB || 0,
396
- subId: options.subId || this.generateUUID()
397
- }]
398
- })
399
- };
400
-
401
- const result = await this.addClient(clientConfig);
402
- return {
403
- ...result,
404
- credentials,
405
- protocol
406
- };
407
- }
408
-
409
- /**
410
- * Update client with automatic credential management
411
- * @param {string} clientId - Client UUID/ID
412
- * @param {number} inboundId - Inbound ID
413
- * @param {Object} options - Update options
414
- * @returns {Object} Update result
415
- */
416
- async updateClientWithCredentials(clientId, inboundId, options = {}) {
417
- try {
418
- // First, get the current inbound to obtain all existing clients
419
- const inboundData = await this.getInbound(inboundId);
420
-
421
- if (!inboundData.success || !inboundData.obj) {
422
- throw new Error(`Failed to get inbound ${inboundId} for client update`);
423
- }
424
-
425
- // Parse existing settings to get all clients
426
- const currentSettings = JSON.parse(inboundData.obj.settings);
427
- const existingClients = currentSettings.clients || [];
428
-
429
- // Find the client to update
430
- const clientIndex = existingClients.findIndex(client => client.id === clientId);
431
- if (clientIndex === -1) {
432
- throw new Error(`Client with ID ${clientId} not found in inbound ${inboundId}`);
433
- }
434
-
435
- // Convert user-friendly options to API format
436
- const processedOptions = {
437
- email: options.email || existingClients[clientIndex].email,
438
- limitIp: options.limitIp !== undefined ? options.limitIp : existingClients[clientIndex].limitIp,
439
- // totalGB is specified in gigabytes in 3x-ui config; do not convert to bytes
440
- totalGB: options.totalGB !== undefined ? options.totalGB : existingClients[clientIndex].totalGB,
441
- expiryTime: options.expiryDays ? Date.now() + (options.expiryDays * 24 * 60 * 60 * 1000) : existingClients[clientIndex].expiryTime,
442
- enable: options.enable !== undefined ? options.enable : existingClients[clientIndex].enable,
443
- flow: options.flow || existingClients[clientIndex].flow,
444
- encryption: options.encryption || existingClients[clientIndex].encryption || 'none',
445
- subId: options.subId || existingClients[clientIndex].subId
446
- };
447
-
448
- // Update the specific client while preserving others
449
- existingClients[clientIndex] = {
450
- ...existingClients[clientIndex],
451
- ...processedOptions
452
- };
453
-
454
- // Prepare the complete settings with all clients
455
- const updatedSettings = {
456
- ...currentSettings,
457
- clients: existingClients
458
- };
459
-
460
- const clientConfig = {
461
- id: inboundId,
462
- settings: JSON.stringify(updatedSettings)
463
- };
464
-
465
- const result = await this.updateClient(clientId, clientConfig);
466
- return {
467
- ...result,
468
- updatedOptions: processedOptions,
469
- conversions: {
470
- totalGB: options.totalGB !== undefined ? `${options.totalGB}GB` : 'unchanged',
471
- expiryDays: options.expiryDays ? `${options.expiryDays} days ${new Date(processedOptions.expiryTime).toISOString()}` : 'unchanged'
472
- }
473
- };
474
- } catch (error) {
475
- return {
476
- success: false,
477
- message: error.message,
478
- error: error.message,
479
- details: 'updateClientWithCredentials failed - check client ID and inbound ID'
480
- };
481
- }
482
- }
483
-
484
- // ===========================================
485
- // SESSION MANAGEMENT METHODS
486
- // ===========================================
487
-
488
- /**
489
- * Get session statistics
490
- * @returns {Object} Session statistics
491
- */
492
- async getSessionStats() {
493
- if (this.sessionManager) {
494
- return await this.sessionManager.getStats();
495
- }
496
- return { message: 'Session manager not initialized' };
497
- }
498
-
499
- /**
500
- * Clear all cached sessions
501
- */
502
- async clearAllSessions() {
503
- if (this.sessionManager) {
504
- await this.sessionManager.clearAllSessions();
505
- }
506
- }
507
-
508
- /**
509
- * Check if current session is valid
510
- * @returns {boolean} Session validity
511
- */
512
- async isSessionValid() {
513
- if (this.sessionManager) {
514
- return await this.sessionManager.hasValidSession(this.baseURL, this.username);
515
- }
516
- return !!this.cookie;
517
- }
518
-
519
- // ===========================================
520
- // ORIGINAL API METHODS (UNCHANGED)
521
- // ===========================================
522
-
523
- // Inbounds
524
- getInbounds() {
525
- return this._request('get', '/panel/api/inbounds/list');
526
- }
527
-
528
- getInbound(id) {
529
- return this._request('get', `/panel/api/inbounds/get/${id}`);
530
- }
531
-
532
- addInbound(inboundConfig) {
533
- // Validate inbound configuration for security
534
- const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
535
- return this._request('post', '/panel/api/inbounds/add', validatedConfig);
536
- }
537
-
538
- deleteInbound(id) {
539
- return this._request('post', `/panel/api/inbounds/del/${id}`);
540
- }
541
-
542
- updateInbound(id, inboundConfig) {
543
- // Validate inbound configuration for security
544
- const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
545
- return this._request('post', `/panel/api/inbounds/update/${id}`, validatedConfig);
546
- }
547
-
548
- // Clients
549
- addClient(clientConfig) {
550
- // Validate client configuration for security
551
- const validatedConfig = InputValidator.validateClientConfig(clientConfig);
552
- return this._request('post', '/panel/api/inbounds/addClient', validatedConfig);
553
- }
554
-
555
- deleteClient(inboundId, clientId) {
556
- return this._request('post', `/panel/api/inbounds/${inboundId}/delClient/${clientId}`);
557
- }
558
-
559
- updateClient(clientId, clientConfig) {
560
- // Validate client configuration for security
561
- const validatedConfig = InputValidator.validateClientConfig(clientConfig);
562
- return this._request('post', `/panel/api/inbounds/updateClient/${clientId}`, validatedConfig);
563
- }
564
-
565
- getClientTrafficsByEmail(email) {
566
- return this._request('get', `/panel/api/inbounds/getClientTraffics/${email}`);
567
- }
568
-
569
- getClientTrafficsById(id) {
570
- return this._request('get', `/panel/api/inbounds/getClientTrafficsById/${id}`);
571
- }
572
-
573
- getClientIps(email) {
574
- return this._request('post', `/panel/api/inbounds/clientIps/${email}`);
575
- }
576
-
577
- clearClientIps(email) {
578
- return this._request('post', `/panel/api/inbounds/clearClientIps/${email}`);
579
- }
580
-
581
- // Traffic
582
- resetClientTraffic(inboundId, email) {
583
- return this._request('post', `/panel/api/inbounds/${inboundId}/resetClientTraffic/${email}`);
584
- }
585
-
586
- resetAllTraffics() {
587
- return this._request('post', '/panel/api/inbounds/resetAllTraffics');
588
- }
589
-
590
- resetAllClientTraffics(inboundId) {
591
- return this._request('post', `/panel/api/inbounds/resetAllClientTraffics/${inboundId}`);
592
- }
593
-
594
- deleteDepletedClients(inboundId) {
595
- return this._request('post', `/panel/api/inbounds/delDepletedClients/${inboundId}`);
596
- }
597
-
598
- // System
599
- getOnlineClients() {
600
- return this._request('post', '/panel/api/inbounds/onlines');
601
- }
602
-
603
- createBackup() {
604
- return this._request('get', '/panel/api/inbounds/createbackup');
605
- }
606
-
607
- // Security Methods
608
-
609
- /**
610
- * Get security statistics and monitoring data
611
- * @returns {Object} Security statistics
612
- */
613
- getSecurityStats() {
614
- return this.securityMonitor.getStats();
615
- }
616
-
617
- /**
618
- * Clear blocked IPs (admin function)
619
- */
620
- clearBlockedIPs() {
621
- this.securityMonitor.clearBlockedIPs();
622
- }
623
-
624
- /**
625
- * Validate credential strength
626
- * @param {string} credential - Credential to validate
627
- * @param {string} type - Type of credential
628
- * @returns {Object} Validation result
629
- */
630
- validateCredentialStrength(credential, type) {
631
- return CredentialSecurity.validateCredentialStrength(credential, type);
632
- }
633
-
634
- /**
635
- * Generate secure session token
636
- * @returns {string} Secure session token
637
- */
638
- generateSecureToken() {
639
- return CredentialSecurity.generateSessionToken();
640
- }
641
-
642
- /**
643
- * Enable development mode for detailed error messages
644
- * @param {boolean} enabled - Whether to enable development mode
645
- */
646
- setDevelopmentMode(enabled) {
647
- this.isDevelopment = enabled;
648
- }
649
- }
650
-
651
- // Export static methods for standalone use
652
- ThreeXUI.CredentialGenerator = CredentialGenerator;
653
- ThreeXUI.SessionManager = SessionManager;
654
- ThreeXUI.createSessionManager = createSessionManager;
655
-
656
- module.exports = ThreeXUI;
657
-
658
- // Define lazy getters to avoid circular dependencies
659
- Object.defineProperties(module.exports, {
660
- // Web middleware helpers
661
- createExpressMiddleware: {
662
- enumerable: true,
663
- get: () => require('./src/middleware/WebMiddleware').createExpressMiddleware
664
- },
665
- withThreeXUI: {
666
- enumerable: true,
667
- get: () => require('./src/middleware/WebMiddleware').withThreeXUI
668
- },
669
- createReactHook: {
670
- enumerable: true,
671
- get: () => require('./src/middleware/WebMiddleware').createReactHook
672
- },
673
- createNextjsRoutes: {
674
- enumerable: true,
675
- get: () => require('./src/middleware/WebMiddleware').createNextjsRoutes
676
- },
677
- SessionConfig: {
678
- enumerable: true,
679
- get: () => require('./src/middleware/WebMiddleware').SessionConfig
680
- },
681
- // Protocol builders
682
- ProtocolBuilder: {
683
- enumerable: true,
684
- get: () => require('./src/builders/ProtocolBuilders').ProtocolBuilder
685
- },
686
- VLESSBuilder: {
687
- enumerable: true,
688
- get: () => require('./src/builders/ProtocolBuilders').VLESSBuilder
689
- },
690
- VMESSBuilder: {
691
- enumerable: true,
692
- get: () => require('./src/builders/ProtocolBuilders').VMESSBuilder
693
- },
694
- TrojanBuilder: {
695
- enumerable: true,
696
- get: () => require('./src/builders/ProtocolBuilders').TrojanBuilder
697
- },
698
- ShadowsocksBuilder: {
699
- enumerable: true,
700
- get: () => require('./src/builders/ProtocolBuilders').ShadowsocksBuilder
701
- },
702
- WireGuardBuilder: {
703
- enumerable: true,
704
- get: () => require('./src/builders/ProtocolBuilders').WireGuardBuilder
705
- },
706
- BaseBuilder: {
707
- enumerable: true,
708
- get: () => require('./src/builders/ProtocolBuilders').BaseBuilder
709
- },
710
- // Security helpers
711
- SecurityEnhancer: {
712
- enumerable: true,
713
- get: () => require('./src/security/SecurityEnhancer')
714
- }
1
+ const axios = require('axios');
2
+ const CredentialGenerator = require('./src/generators/CredentialGenerator');
3
+ const { SessionManager, createSessionManager } = require('./src/session/SessionManager');
4
+ const {
5
+ InputValidator,
6
+ SecurityMonitor,
7
+ SecureHeaders,
8
+ CredentialSecurity,
9
+ ErrorSecurity
10
+ } = require('./src/security/SecurityEnhancer');
11
+
12
+ /**
13
+ * 3X-UI API Client Library
14
+ *
15
+ * A Node.js client for managing 3x-ui panel APIs with automatic session management.
16
+ * Now includes credential generation and advanced session management for web applications.
17
+ *
18
+ * @class ThreeXUI
19
+ * @version 2.0.0
20
+ * @author Helitha Guruge
21
+ */
22
+ class ThreeXUI {
23
+ /**
24
+ * Creates a new ThreeXUI client instance
25
+ *
26
+ * @param {string} baseURL - The base URL of your 3x-ui server (e.g., 'https://your-server.com')
27
+ * @param {string} username - Admin username for authentication
28
+ * @param {string} password - Admin password for authentication
29
+ * @param {Object} options - Configuration options
30
+ * @param {Object} options.sessionManager - Session management configuration
31
+ * @param {boolean} options.autoGenerateCredentials - Enable automatic credential generation
32
+ * @param {number} options.timeout - Request timeout in milliseconds (default: 30000)
33
+ * @throws {Error} If baseURL, username, or password is missing
34
+ */
35
+ constructor(baseURL, usernameOrOptions, password, options = {}) {
36
+ if (!baseURL) {
37
+ throw new Error('baseURL is required');
38
+ }
39
+
40
+ // Apply security validations
41
+ this.baseURL = InputValidator.validateURL(baseURL);
42
+
43
+ // Warning for common configuration error
44
+ if (this.baseURL.endsWith('/panel') || this.baseURL.endsWith('/panel/')) {
45
+ console.warn('WARNING: baseURL should NOT end with "/panel". The library appends this automatically. Please remove it from your configuration.');
46
+ // Auto-fix for better user experience
47
+ this.baseURL = this.baseURL.replace(/\/panel\/?$/, '');
48
+ }
49
+
50
+ // Object containing configs as 2nd parameter
51
+ if (typeof usernameOrOptions === 'object' && usernameOrOptions !== null) {
52
+ options = usernameOrOptions;
53
+ this.username = options.username;
54
+ this.password = options.password;
55
+ } else {
56
+ this.username = usernameOrOptions;
57
+ this.password = password;
58
+ }
59
+
60
+ this.token = options.token || options.apiToken || null;
61
+
62
+ if (this.token) {
63
+ this.username = this.username || 'token-auth'; // prevent missing args error
64
+ this.password = this.password || 'token-auth';
65
+ } else {
66
+ if (!this.username) {
67
+ throw new Error('username is required');
68
+ }
69
+ if (!this.password) {
70
+ throw new Error('password is required');
71
+ }
72
+ this.username = InputValidator.validateUsername(this.username);
73
+ this.password = InputValidator.validatePassword(this.password);
74
+ }
75
+
76
+ this.cookie = null;
77
+ this.options = options;
78
+ this.loginMutex = false; // Add mutex to prevent concurrent logins
79
+ this.loginRetryCount = 0; // Add retry counter
80
+ this.maxLoginRetries = 3; // Maximum login attempts
81
+
82
+ // Initialize security monitoring
83
+ this.securityMonitor = new SecurityMonitor({
84
+ maxRequestsPerMinute: options.maxRequestsPerMinute || 60,
85
+ maxLoginAttemptsPerHour: options.maxLoginAttemptsPerHour || 10
86
+ });
87
+
88
+ // Security configuration
89
+ this.isDevelopment = options.isDevelopment || process.env.NODE_ENV === 'development';
90
+
91
+ // Initialize session manager if provided
92
+ if (options.sessionManager) {
93
+ this.sessionManager = options.sessionManager instanceof SessionManager
94
+ ? options.sessionManager
95
+ : createSessionManager(options.sessionManager);
96
+ } else {
97
+ // Default to memory-based session management
98
+ this.sessionManager = createSessionManager();
99
+ }
100
+
101
+ // Create axios instance with security best practices
102
+ const secureHeaders = SecureHeaders.getSecureHeaders({
103
+ userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
104
+ enableCSP: options.enableCSP || false
105
+ });
106
+
107
+ if (this.token) {
108
+ secureHeaders['Authorization'] = `Bearer ${this.token}`;
109
+ }
110
+
111
+ this.api = axios.create({
112
+ baseURL: this.baseURL,
113
+ timeout: options.timeout || 30000, // 30 second timeout
114
+ maxRedirects: 5,
115
+ validateStatus: (status) => status >= 200 && status < 300,
116
+ headers: secureHeaders
117
+ });
118
+
119
+ // Add request interceptor for security headers
120
+ this.api.interceptors.request.use((config) => {
121
+ // Add security headers
122
+ config.headers['X-Requested-With'] = 'XMLHttpRequest';
123
+ return config;
124
+ });
125
+
126
+ // Add response interceptor for error handling
127
+ this.api.interceptors.response.use(
128
+ (response) => response,
129
+ (error) => {
130
+ if (error.code === 'ECONNABORTED') {
131
+ throw new Error('Request timeout - server took too long to respond');
132
+ }
133
+ if (error.code === 'ENOTFOUND') {
134
+ throw new Error(`Cannot connect to server: ${this.baseURL}`);
135
+ }
136
+ throw error;
137
+ }
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Login with session management support
143
+ * @param {boolean} forceRefresh - Force a new login even if session exists
144
+ * @returns {Object} Login response
145
+ */
146
+ async login(forceRefresh = false) {
147
+ // If API token is configured, skip cookie auth
148
+ if (this.token) {
149
+ return {
150
+ success: true,
151
+ message: 'Authenticated successfully using API Token'
152
+ };
153
+ }
154
+
155
+ // Check rate limiting first
156
+ const identifier = CredentialSecurity.hashForLogging(this.username);
157
+ if (!this.securityMonitor.checkRateLimit(identifier, 'login')) {
158
+ const error = new Error('Rate limit exceeded for login attempts');
159
+ ErrorSecurity.logError(error, { username: this.username, baseURL: this.baseURL });
160
+ throw error;
161
+ }
162
+
163
+ // Increment retry count
164
+ this.loginRetryCount++;
165
+ // Check for existing valid session first
166
+ if (!forceRefresh && this.sessionManager) {
167
+ const existingSession = await this.sessionManager.getSession(this.baseURL, this.username);
168
+ if (existingSession && existingSession.cookie) {
169
+ this.cookie = existingSession.cookie;
170
+ this.api.defaults.headers.Cookie = this.cookie;
171
+ // Reset retry count on successful session restore
172
+ this.loginRetryCount = 0;
173
+ return {
174
+ success: true,
175
+ fromCache: true,
176
+ data: { msg: 'Session restored from cache' }
177
+ };
178
+ }
179
+ }
180
+
181
+ try {
182
+ const params = new URLSearchParams();
183
+ params.append('username', this.username);
184
+ params.append('password', this.password);
185
+
186
+ const response = await this.api.post('/login', params, {
187
+ headers: {
188
+ 'Content-Type': 'application/x-www-form-urlencoded'
189
+ }
190
+ });
191
+
192
+ if (response.data.success) {
193
+ const cookies = response.headers['set-cookie'];
194
+ if (cookies && cookies.length > 0) {
195
+ this.cookie = cookies[0].split(';')[0];
196
+ this.api.defaults.headers.Cookie = this.cookie;
197
+
198
+ // Store session if session manager is available
199
+ if (this.sessionManager) {
200
+ try {
201
+ await this.sessionManager.storeSession(this.baseURL, this.username, {
202
+ cookie: this.cookie,
203
+ loginTime: new Date().toISOString()
204
+ });
205
+ } catch (sessionError) {
206
+ console.warn('Failed to store session, continuing without cache:', sessionError.message);
207
+ }
208
+ }
209
+ // Reset retry count on successful login
210
+ this.loginRetryCount = 0;
211
+ } else {
212
+ throw new Error('Login failed: No session cookie received.');
213
+ }
214
+ return {
215
+ success: true,
216
+ fromCache: false,
217
+ cookie: this.cookie, // Explicitly return the cookie
218
+ headers: response.headers,
219
+ data: response.data
220
+ };
221
+ } else {
222
+ throw new Error(`Login failed: ${response.data.msg}`);
223
+ }
224
+ } catch (error) {
225
+ // Log the error securely
226
+ ErrorSecurity.logError(error, {
227
+ username: this.username,
228
+ baseURL: this.baseURL,
229
+ action: 'login'
230
+ });
231
+
232
+ // Log suspicious activity for multiple failed logins
233
+ if (this.loginRetryCount >= this.maxLoginRetries) {
234
+ this.securityMonitor.logSuspiciousActivity('multiple_failed_logins', {
235
+ username: CredentialSecurity.hashForLogging(this.username),
236
+ attempts: this.loginRetryCount
237
+ });
238
+ }
239
+
240
+ // Return sanitized error
241
+ const sanitizedError = ErrorSecurity.sanitizeError(error, this.isDevelopment);
242
+ throw sanitizedError;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Logout and clear session
248
+ */
249
+ async logout() {
250
+ try {
251
+ await this._request('get', '/logout');
252
+ } catch {
253
+ // Ignore server-side logout errors, proceed to clear local session
254
+ }
255
+
256
+ this.cookie = null;
257
+ delete this.api.defaults.headers.Cookie;
258
+
259
+ if (this.sessionManager) {
260
+ await this.sessionManager.deleteSession(this.baseURL, this.username);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Check if Two-Factor Authentication is enabled
266
+ * @returns {Promise<Object>} 2FA status
267
+ */
268
+ getTwoFactorEnable() {
269
+ return this._request('post', '/getTwoFactorEnable');
270
+ }
271
+
272
+ async _request(method, path, data = {}) {
273
+ // Check session validity first with mutex protection if token is not provided
274
+ if (!this.token) {
275
+ if (!this.loginMutex && this.sessionManager && !await this.sessionManager.hasValidSession(this.baseURL, this.username)) {
276
+ await this._ensureAuthenticated();
277
+ } else if (!this.loginMutex && !this.cookie) {
278
+ await this._ensureAuthenticated();
279
+ }
280
+ }
281
+
282
+ try {
283
+ const response = await this.api.request({
284
+ method,
285
+ url: path,
286
+ data,
287
+ ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
288
+ });
289
+ // Reset retry counter on successful request
290
+ this.loginRetryCount = 0;
291
+ return response.data;
292
+ } catch (error) {
293
+ if (error.response && error.response.status === 401) {
294
+ if (this.token) {
295
+ throw new Error('API Token is invalid or expired. Please check your credentials.');
296
+ }
297
+ // Cookie might have expired, try to login again with retry limit
298
+ if (this.loginRetryCount < this.maxLoginRetries) {
299
+ await this._ensureAuthenticated(true); // Force refresh
300
+ const response = await this.api.request({
301
+ method,
302
+ url: path,
303
+ data,
304
+ ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
305
+ });
306
+ return response.data;
307
+ } else {
308
+ throw new Error('Maximum login retry attempts exceeded. Check your credentials.');
309
+ }
310
+ }
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Ensure authentication with mutex protection
317
+ * @param {boolean} forceRefresh - Force a new login
318
+ */
319
+ async _ensureAuthenticated(forceRefresh = false) {
320
+ // Prevent concurrent login attempts
321
+ if (this.loginMutex) {
322
+ // Wait for ongoing login to complete
323
+ while (this.loginMutex) {
324
+ await new Promise(resolve => setTimeout(resolve, 100));
325
+ }
326
+ return;
327
+ }
328
+
329
+ this.loginMutex = true;
330
+ try {
331
+ await this.login(forceRefresh);
332
+ } finally {
333
+ this.loginMutex = false;
334
+ }
335
+ }
336
+
337
+ // ===========================================
338
+ // CREDENTIAL GENERATION METHODS
339
+ // ===========================================
340
+
341
+ /**
342
+ * Generate credentials for a specific protocol
343
+ * @param {string} protocol - Protocol name (vless, vmess, trojan, etc.)
344
+ * @param {Object} options - Generation options
345
+ * @returns {Object} Generated credentials
346
+ */
347
+ generateCredentials(protocol, options = {}) {
348
+ return CredentialGenerator.generateForProtocol(protocol, options);
349
+ }
350
+
351
+ /**
352
+ * Generate UUID for VLESS/VMess protocols
353
+ * @param {boolean} secure - Use cryptographically secure generation
354
+ * @returns {string} UUID
355
+ */
356
+ generateUUID(secure = true) {
357
+ return secure ? CredentialGenerator.generateSecureUUID() : CredentialGenerator.generateUUID();
358
+ }
359
+
360
+ /**
361
+ * Generate password for Trojan/Shadowsocks protocols
362
+ * @param {number} length - Password length
363
+ * @param {Object} options - Password options
364
+ * @returns {string} Generated password
365
+ */
366
+ generatePassword(length = 16, options = {}) {
367
+ return CredentialGenerator.generatePassword(length, options);
368
+ }
369
+
370
+ /**
371
+ * Generate bulk credentials for multiple clients
372
+ * @param {string} protocol - Protocol name
373
+ * @param {number} count - Number of credentials to generate
374
+ * @param {Object} options - Generation options
375
+ * @returns {Array} Array of credentials
376
+ */
377
+ generateBulkCredentials(protocol, count, options = {}) {
378
+ return CredentialGenerator.generateBulk(protocol, count, options);
379
+ }
380
+
381
+ /**
382
+ * Get available cipher methods for Shadowsocks
383
+ * @returns {Array} Array of cipher methods
384
+ */
385
+ getShadowsocksCiphers() {
386
+ return CredentialGenerator.getShadowsocksCipherMethods();
387
+ }
388
+
389
+ /**
390
+ * Get recommended cipher for Shadowsocks
391
+ * @returns {string} Recommended cipher
392
+ */
393
+ getRecommendedShadowsocksCipher() {
394
+ return CredentialGenerator.getRecommendedShadowsocksCipher();
395
+ }
396
+
397
+ /**
398
+ * Generate WireGuard key pair
399
+ * @returns {Object} Key pair with helper methods
400
+ */
401
+ generateWireGuardKeys() {
402
+ return CredentialGenerator.generateWireGuardKeys();
403
+ }
404
+
405
+ /**
406
+ * Generate Reality key pair for anti-censorship
407
+ * @returns {Object} Reality key pair with helper methods
408
+ */
409
+ generateRealityKeys() {
410
+ return CredentialGenerator.generateRealityKeys();
411
+ }
412
+
413
+ /**
414
+ * Generate random port number
415
+ * @param {number} min - Minimum port
416
+ * @param {number} max - Maximum port
417
+ * @returns {number} Random port
418
+ */
419
+ generatePort(min = 10000, max = 65535) {
420
+ return CredentialGenerator.generatePort(min, max);
421
+ }
422
+
423
+ /**
424
+ * Validate generated credentials
425
+ * @param {Object} credentials - Credentials to validate
426
+ * @param {string} protocol - Protocol name
427
+ * @returns {Object} Validation result
428
+ */
429
+ validateCredentials(credentials, protocol) {
430
+ return CredentialGenerator.validateCredentials(credentials, protocol);
431
+ }
432
+
433
+ // ===========================================
434
+ // ENHANCED CLIENT MANAGEMENT WITH AUTO-GENERATION
435
+ // ===========================================
436
+
437
+ /**
438
+ * Add client with automatic credential generation
439
+ * @param {number} inboundId - Inbound ID
440
+ * @param {string} protocol - Protocol type
441
+ * @param {Object} options - Client options
442
+ * @returns {Object} Created client with credentials
443
+ */
444
+ async addClientWithCredentials(inboundId, protocol, options = {}) {
445
+ const credentials = this.generateCredentials(protocol, options);
446
+
447
+ const clientConfig = {
448
+ id: inboundId,
449
+ settings: JSON.stringify({
450
+ clients: [{
451
+ ...credentials,
452
+ enable: true,
453
+ expiryTime: options.expiryTime || 0,
454
+ limitIp: options.limitIp || 0,
455
+ totalGB: options.totalGB || 0,
456
+ subId: options.subId || this.generateUUID()
457
+ }]
458
+ })
459
+ };
460
+
461
+ const result = await this.addClient(clientConfig);
462
+ return {
463
+ ...result,
464
+ credentials,
465
+ protocol
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Update client with automatic credential management
471
+ * @param {string} clientId - Client UUID/ID
472
+ * @param {number} inboundId - Inbound ID
473
+ * @param {Object} options - Update options
474
+ * @returns {Object} Update result
475
+ */
476
+ async updateClientWithCredentials(clientId, inboundId, options = {}) {
477
+ try {
478
+ // First, get the current inbound to obtain all existing clients
479
+ const inboundData = await this.getInbound(inboundId);
480
+
481
+ if (!inboundData.success || !inboundData.obj) {
482
+ throw new Error(`Failed to get inbound ${inboundId} for client update`);
483
+ }
484
+
485
+ // Parse existing settings to get all clients
486
+ const currentSettings = JSON.parse(inboundData.obj.settings);
487
+ const existingClients = currentSettings.clients || [];
488
+
489
+ // Find the client to update
490
+ const clientIndex = existingClients.findIndex(client => client.id === clientId);
491
+ if (clientIndex === -1) {
492
+ throw new Error(`Client with ID ${clientId} not found in inbound ${inboundId}`);
493
+ }
494
+
495
+ // Convert user-friendly options to API format
496
+ const processedOptions = {
497
+ email: options.email || existingClients[clientIndex].email,
498
+ limitIp: options.limitIp !== undefined ? options.limitIp : existingClients[clientIndex].limitIp,
499
+ // totalGB is specified in gigabytes in 3x-ui config; do not convert to bytes
500
+ totalGB: options.totalGB !== undefined ? options.totalGB : existingClients[clientIndex].totalGB,
501
+ expiryTime: options.expiryDays ? Date.now() + (options.expiryDays * 24 * 60 * 60 * 1000) : existingClients[clientIndex].expiryTime,
502
+ enable: options.enable !== undefined ? options.enable : existingClients[clientIndex].enable,
503
+ flow: options.flow || existingClients[clientIndex].flow,
504
+ encryption: options.encryption || existingClients[clientIndex].encryption || 'none',
505
+ subId: options.subId || existingClients[clientIndex].subId
506
+ };
507
+
508
+ // Update the specific client while preserving others
509
+ existingClients[clientIndex] = {
510
+ ...existingClients[clientIndex],
511
+ ...processedOptions
512
+ };
513
+
514
+ // Prepare the complete settings with all clients
515
+ const updatedSettings = {
516
+ ...currentSettings,
517
+ clients: existingClients
518
+ };
519
+
520
+ const clientConfig = {
521
+ id: inboundId,
522
+ settings: JSON.stringify(updatedSettings)
523
+ };
524
+
525
+ const result = await this.updateClient(clientId, clientConfig);
526
+ return {
527
+ ...result,
528
+ updatedOptions: processedOptions,
529
+ conversions: {
530
+ totalGB: options.totalGB !== undefined ? `${options.totalGB}GB` : 'unchanged',
531
+ expiryDays: options.expiryDays ? `${options.expiryDays} days → ${new Date(processedOptions.expiryTime).toISOString()}` : 'unchanged'
532
+ }
533
+ };
534
+ } catch (error) {
535
+ return {
536
+ success: false,
537
+ message: error.message,
538
+ error: error.message,
539
+ details: 'updateClientWithCredentials failed - check client ID and inbound ID'
540
+ };
541
+ }
542
+ }
543
+
544
+ // ===========================================
545
+ // SESSION MANAGEMENT METHODS
546
+ // ===========================================
547
+
548
+ /**
549
+ * Get session statistics
550
+ * @returns {Object} Session statistics
551
+ */
552
+ async getSessionStats() {
553
+ if (this.sessionManager) {
554
+ return await this.sessionManager.getStats();
555
+ }
556
+ return { message: 'Session manager not initialized' };
557
+ }
558
+
559
+ /**
560
+ * Clear all cached sessions
561
+ */
562
+ async clearAllSessions() {
563
+ if (this.sessionManager) {
564
+ await this.sessionManager.clearAllSessions();
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Check if current session is valid
570
+ * @returns {boolean} Session validity
571
+ */
572
+ async isSessionValid() {
573
+ if (this.token) {
574
+ return true;
575
+ }
576
+ if (this.sessionManager) {
577
+ return await this.sessionManager.hasValidSession(this.baseURL, this.username);
578
+ }
579
+ return !!this.cookie;
580
+ }
581
+
582
+ // ===========================================
583
+ // MODERN API METHODS (3X-UI >= 2.x)
584
+ // ===========================================
585
+
586
+ // --- Clients ---
587
+
588
+ /**
589
+ * Get list of all clients
590
+ * @returns {Promise<Object>} Formatted list of all clients
591
+ */
592
+ getClients() {
593
+ return this._request('get', '/panel/api/clients/list');
594
+ }
595
+
596
+ /**
597
+ * Get paginated list of clients
598
+ * @param {Object} params - Pagination parameters
599
+ * @param {number} params.page - Page number (default: 1)
600
+ * @param {number} params.size - Items per page (default: 10)
601
+ * @param {string} params.sort - Sort field (e.g., 'email', 'expireTime')
602
+ * @param {string} params.order - Sort order ('asc' or 'desc')
603
+ * @param {string} params.email - Filter by email
604
+ * @returns {Promise<Object>} Paginated clients
605
+ */
606
+ getPagedClients(params = {}) {
607
+ const queryParams = new URLSearchParams();
608
+ if (params.page !== undefined) {
609
+ queryParams.append('page', params.page);
610
+ }
611
+ if (params.size !== undefined) {
612
+ queryParams.append('size', params.size);
613
+ }
614
+ if (params.sort !== undefined) {
615
+ queryParams.append('sort', params.sort);
616
+ }
617
+ if (params.order !== undefined) {
618
+ queryParams.append('order', params.order);
619
+ }
620
+ if (params.email !== undefined) {
621
+ queryParams.append('email', params.email);
622
+ }
623
+
624
+ const queryString = queryParams.toString();
625
+ const url = queryString ? `/panel/api/clients/list/paged?${queryString}` : '/panel/api/clients/list/paged';
626
+
627
+ return this._request('get', url);
628
+ }
629
+
630
+ /**
631
+ * Get client by email
632
+ * @param {string} email - Exact client email address
633
+ * @returns {Promise<Object>} Client metadata
634
+ */
635
+ getClient(email) {
636
+ return this._request('get', `/panel/api/clients/get/${encodeURIComponent(email)}`);
637
+ }
638
+
639
+ /**
640
+ * Get client traffic by email
641
+ * @param {string} email - Exact client email
642
+ * @returns {Promise<Object>} Client traffic details
643
+ */
644
+ getClientTraffic(email) {
645
+ return this._request('get', `/panel/api/clients/traffic/${encodeURIComponent(email)}`);
646
+ }
647
+
648
+ /**
649
+ * Get subscription links for a client by subscription ID
650
+ * @param {string} subId - Subscription ID (UUID)
651
+ * @returns {Promise<Object>} Subscription details and links
652
+ */
653
+ getSubLinks(subId) {
654
+ return this._request('get', `/panel/api/clients/subLinks/${encodeURIComponent(subId)}`);
655
+ }
656
+
657
+ /**
658
+ * Get generic client links by email
659
+ * @param {string} email - Exact client email
660
+ * @returns {Promise<Object>} Link strings
661
+ */
662
+ getClientLinks(email) {
663
+ return this._request('get', `/panel/api/clients/links/${encodeURIComponent(email)}`);
664
+ }
665
+
666
+ /**
667
+ * Add a new client via Modern API
668
+ * @param {Object} data - Client payload
669
+ * @returns {Promise<Object>} Addition response
670
+ */
671
+ addModernClient(data) {
672
+ return this._request('post', '/panel/api/clients/add', data);
673
+ }
674
+
675
+ /**
676
+ * Update client by email via Modern API
677
+ * @param {string} email - Exact client email
678
+ * @param {Object} data - Update payload
679
+ * @returns {Promise<Object>} Update response
680
+ */
681
+ updateModernClient(email, data) {
682
+ return this._request('post', `/panel/api/clients/update/${encodeURIComponent(email)}`, data);
683
+ }
684
+
685
+ /**
686
+ * Delete client by email via Modern API
687
+ * @param {string} email - Exact client email
688
+ * @returns {Promise<Object>} Delete response
689
+ */
690
+ deleteModernClient(email) {
691
+ return this._request('post', `/panel/api/clients/del/${encodeURIComponent(email)}`);
692
+ }
693
+
694
+ attachClientToInbounds(email, data) {
695
+ return this._request('post', `/panel/api/clients/${encodeURIComponent(email)}/attach`, data);
696
+ }
697
+
698
+ detachClientFromInbounds(email, data) {
699
+ return this._request('post', `/panel/api/clients/${encodeURIComponent(email)}/detach`, data);
700
+ }
701
+
702
+ resetAllModernClientTraffics() {
703
+ return this._request('post', '/panel/api/clients/resetAllTraffics');
704
+ }
705
+
706
+ deleteDepletedModernClients() {
707
+ return this._request('post', '/panel/api/clients/delDepleted');
708
+ }
709
+
710
+ bulkAdjustModernClients(data) {
711
+ return this._request('post', '/panel/api/clients/bulkAdjust', data);
712
+ }
713
+
714
+ bulkDeleteModernClients(data) {
715
+ return this._request('post', '/panel/api/clients/bulkDel', data);
716
+ }
717
+
718
+ bulkCreateModernClients(data) {
719
+ return this._request('post', '/panel/api/clients/bulkCreate', data);
720
+ }
721
+
722
+ bulkAttachModernClients(data) {
723
+ return this._request('post', '/panel/api/clients/bulkAttach', data);
724
+ }
725
+
726
+ bulkDetachModernClients(data) {
727
+ return this._request('post', '/panel/api/clients/bulkDetach', data);
728
+ }
729
+
730
+ bulkResetTrafficModernClients(data) {
731
+ return this._request('post', '/panel/api/clients/bulkResetTraffic', data);
732
+ }
733
+
734
+ resetModernClientTrafficByEmail(email) {
735
+ return this._request('post', `/panel/api/clients/resetTraffic/${encodeURIComponent(email)}`);
736
+ }
737
+
738
+ updateModernClientTrafficByEmail(email, data) {
739
+ return this._request('post', `/panel/api/clients/updateTraffic/${encodeURIComponent(email)}`, data);
740
+ }
741
+
742
+ getModernClientIps(email) {
743
+ return this._request('post', `/panel/api/clients/ips/${encodeURIComponent(email)}`);
744
+ }
745
+
746
+ clearModernClientIps(email) {
747
+ return this._request('post', `/panel/api/clients/clearIps/${encodeURIComponent(email)}`);
748
+ }
749
+
750
+ getOnlines() {
751
+ return this._request('post', '/panel/api/clients/onlines');
752
+ }
753
+
754
+ getModernLastOnline() {
755
+ return this._request('post', '/panel/api/clients/lastOnline');
756
+ }
757
+
758
+ // --- Client Groups ---
759
+
760
+ /**
761
+ * Get list of all client groups
762
+ * @returns {Promise<Object>} List of client groups
763
+ */
764
+ getGroups() {
765
+ return this._request('get', '/panel/api/clients/groups');
766
+ }
767
+
768
+ /**
769
+ * Get list of emails belonging to a specific group
770
+ * @param {string} groupName - The name of the group
771
+ * @returns {Promise<Object>} List of emails in the group
772
+ */
773
+ getGroupEmails(groupName) {
774
+ return this._request('get', `/panel/api/clients/groups/${encodeURIComponent(groupName)}/emails`);
775
+ }
776
+
777
+ createGroup(data) {
778
+ return this._request('post', '/panel/api/clients/groups/create', data);
779
+ }
780
+
781
+ renameGroup(data) {
782
+ return this._request('post', '/panel/api/clients/groups/rename', data);
783
+ }
784
+
785
+ deleteGroup(data) {
786
+ return this._request('post', '/panel/api/clients/groups/delete', data);
787
+ }
788
+
789
+ bulkAddGroups(data) {
790
+ return this._request('post', '/panel/api/clients/groups/bulkAdd', data);
791
+ }
792
+
793
+ bulkRemoveGroups(data) {
794
+ return this._request('post', '/panel/api/clients/groups/bulkRemove', data);
795
+ }
796
+
797
+ // --- Nodes ---
798
+
799
+ /**
800
+ * Get list of all nodes
801
+ * @returns {Promise<Object>} List of nodes
802
+ */
803
+ getNodes() {
804
+ return this._request('get', '/panel/api/nodes/list');
805
+ }
806
+
807
+ /**
808
+ * Get specific node by ID
809
+ * @param {number|string} id - Node ID
810
+ * @returns {Promise<Object>} Node details
811
+ */
812
+ getNode(id) {
813
+ return this._request('get', `/panel/api/nodes/get/${encodeURIComponent(id)}`);
814
+ }
815
+
816
+ /**
817
+ * Get history metrics for a node
818
+ * @param {number|string} id - Node ID
819
+ * @param {string} metric - Metric name (e.g., 'cpu', 'memory')
820
+ * @param {string} bucket - Time bucket size
821
+ * @returns {Promise<Object>} Node history data
822
+ */
823
+ getNodeHistory(id, metric, bucket) {
824
+ return this._request('get', `/panel/api/nodes/history/${encodeURIComponent(id)}/${encodeURIComponent(metric)}/${encodeURIComponent(bucket)}`);
825
+ }
826
+
827
+ addNode(data) {
828
+ return this._request('post', '/panel/api/nodes/add', data);
829
+ }
830
+
831
+ updateNode(id, data) {
832
+ return this._request('post', `/panel/api/nodes/update/${encodeURIComponent(id)}`, data);
833
+ }
834
+
835
+ deleteNode(id) {
836
+ return this._request('post', `/panel/api/nodes/del/${encodeURIComponent(id)}`);
837
+ }
838
+
839
+ setNodeEnable(id) {
840
+ return this._request('post', `/panel/api/nodes/setEnable/${encodeURIComponent(id)}`);
841
+ }
842
+
843
+ testNode(data) {
844
+ return this._request('post', '/panel/api/nodes/test', data);
845
+ }
846
+
847
+ probeNode(id) {
848
+ return this._request('post', `/panel/api/nodes/probe/${encodeURIComponent(id)}`);
849
+ }
850
+
851
+ // --- Custom Geo ---
852
+
853
+ /**
854
+ * Get list of custom geo sites/ips
855
+ * @returns {Promise<Object>} List of custom geos
856
+ */
857
+ getCustomGeos() {
858
+ return this._request('get', '/panel/api/custom-geo/list');
859
+ }
860
+
861
+ /**
862
+ * Get aliases for custom geos
863
+ * @returns {Promise<Object>} Custom geo aliases
864
+ */
865
+ getGeoAliases() {
866
+ return this._request('get', '/panel/api/custom-geo/aliases');
867
+ }
868
+
869
+ addCustomGeo(data) {
870
+ return this._request('post', '/panel/api/custom-geo/add', data);
871
+ }
872
+
873
+ updateCustomGeo(id, data) {
874
+ return this._request('post', `/panel/api/custom-geo/update/${encodeURIComponent(id)}`, data);
875
+ }
876
+
877
+ deleteCustomGeo(id) {
878
+ return this._request('post', `/panel/api/custom-geo/delete/${encodeURIComponent(id)}`);
879
+ }
880
+
881
+ downloadCustomGeo(id) {
882
+ return this._request('post', `/panel/api/custom-geo/download/${encodeURIComponent(id)}`);
883
+ }
884
+
885
+ updateAllCustomGeo() {
886
+ return this._request('post', '/panel/api/custom-geo/update-all');
887
+ }
888
+
889
+ // ===========================================
890
+ // ORIGINAL API METHODS (UNCHANGED)
891
+ // ===========================================
892
+
893
+ // Inbounds
894
+ getInbounds() {
895
+ return this._request('get', '/panel/api/inbounds/list');
896
+ }
897
+
898
+ getInbound(id) {
899
+ return this._request('get', `/panel/api/inbounds/get/${id}`);
900
+ }
901
+
902
+ addInbound(inboundConfig) {
903
+ // Validate inbound configuration for security
904
+ const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
905
+ return this._request('post', '/panel/api/inbounds/add', validatedConfig);
906
+ }
907
+
908
+ deleteInbound(id) {
909
+ return this._request('post', `/panel/api/inbounds/del/${id}`);
910
+ }
911
+
912
+ updateInbound(id, inboundConfig) {
913
+ // Validate inbound configuration for security
914
+ const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
915
+ return this._request('post', `/panel/api/inbounds/update/${id}`, validatedConfig);
916
+ }
917
+
918
+ /**
919
+ * Import inbounds
920
+ * @param {Array} inbounds - Array of inbound configurations
921
+ */
922
+ importInbounds(inbounds) {
923
+ return this._request('post', '/panel/api/inbounds/import', { inbounds });
924
+ }
925
+
926
+ /**
927
+ * Get last online time for clients
928
+ */
929
+ getLastOnline() {
930
+ return this._request('post', '/panel/api/inbounds/lastOnline');
931
+ }
932
+
933
+ // Clients
934
+ addClient(clientConfig) {
935
+ // Validate client configuration for security
936
+ const validatedConfig = InputValidator.validateClientConfig(clientConfig);
937
+ return this._request('post', '/panel/api/inbounds/addClient', validatedConfig);
938
+ }
939
+
940
+ deleteClient(inboundId, clientId) {
941
+ return this._request('post', `/panel/api/inbounds/${inboundId}/delClient/${clientId}`);
942
+ }
943
+
944
+ updateClient(clientId, clientConfig) {
945
+ // Validate client configuration for security
946
+ const validatedConfig = InputValidator.validateClientConfig(clientConfig);
947
+ return this._request('post', `/panel/api/inbounds/updateClient/${clientId}`, validatedConfig);
948
+ }
949
+
950
+ /**
951
+ * Update client traffic limit and expiry by email
952
+ * @param {string} email - Client email
953
+ * @param {Object} trafficConfig - Traffic configuration (totalGB, expiryTime)
954
+ */
955
+ updateClientTraffic(email, trafficConfig) {
956
+ return this._request('post', `/panel/api/inbounds/updateClientTraffic/${email}`, trafficConfig);
957
+ }
958
+
959
+ /**
960
+ * Delete client by email
961
+ * @param {number} inboundId - Inbound ID
962
+ * @param {string} email - Client email
963
+ */
964
+ deleteClientByEmail(inboundId, email) {
965
+ return this._request('post', `/panel/api/inbounds/${inboundId}/delClientByEmail/${email}`);
966
+ }
967
+
968
+ getClientTrafficsByEmail(email) {
969
+ return this._request('get', `/panel/api/inbounds/getClientTraffics/${email}`);
970
+ }
971
+
972
+ getClientTrafficsById(id) {
973
+ return this._request('get', `/panel/api/inbounds/getClientTrafficsById/${id}`);
974
+ }
975
+
976
+ getClientIps(email) {
977
+ return this._request('post', `/panel/api/inbounds/clientIps/${email}`);
978
+ }
979
+
980
+ clearClientIps(email) {
981
+ return this._request('post', `/panel/api/inbounds/clearClientIps/${email}`);
982
+ }
983
+
984
+ // Traffic
985
+ resetClientTraffic(inboundId, email) {
986
+ return this._request('post', `/panel/api/inbounds/${inboundId}/resetClientTraffic/${email}`);
987
+ }
988
+
989
+ resetAllTraffics() {
990
+ return this._request('post', '/panel/api/inbounds/resetAllTraffics');
991
+ }
992
+
993
+ resetAllClientTraffics(inboundId) {
994
+ return this._request('post', `/panel/api/inbounds/resetAllClientTraffics/${inboundId}`);
995
+ }
996
+
997
+ deleteDepletedClients(inboundId) {
998
+ return this._request('post', `/panel/api/inbounds/delDepletedClients/${inboundId}`);
999
+ }
1000
+
1001
+ // System
1002
+ getOnlineClients() {
1003
+ return this._request('post', '/panel/api/inbounds/onlines');
1004
+ }
1005
+
1006
+ createBackup() {
1007
+ return this._request('get', '/panel/api/inbounds/createbackup');
1008
+ }
1009
+
1010
+ /**
1011
+ * Trigger sending a backup to Telegram bot admins
1012
+ */
1013
+ backupToTgBot() {
1014
+ return this._request('post', '/panel/api/backuptotgbot');
1015
+ }
1016
+
1017
+ // ===========================================
1018
+ // SERVER MANAGEMENT
1019
+ // ===========================================
1020
+
1021
+ /**
1022
+ * Get server status (CPU, RAM, etc.)
1023
+ */
1024
+ getServerStatus() {
1025
+ return this._request('get', '/panel/api/server/status');
1026
+ }
1027
+
1028
+ /**
1029
+ * Get CPU usage history
1030
+ * @param {string} bucket - Time bucket (e.g., 'min', 'hour')
1031
+ */
1032
+ getCPUHistory(bucket = 'min') {
1033
+ return this._request('get', `/panel/api/server/cpuHistory/${bucket}`);
1034
+ }
1035
+
1036
+ /**
1037
+ * Get current Xray version
1038
+ */
1039
+ getXrayVersion() {
1040
+ return this._request('get', '/panel/api/server/getXrayVersion');
1041
+ }
1042
+
1043
+ /**
1044
+ * Get Xray config as JSON
1045
+ */
1046
+ getConfigJson() {
1047
+ return this._request('get', '/panel/api/server/getConfigJson');
1048
+ }
1049
+
1050
+ /**
1051
+ * Download database
1052
+ */
1053
+ getDb() {
1054
+ return this._request('get', '/panel/api/server/getDb');
1055
+ }
1056
+
1057
+ /**
1058
+ * Stop Xray core service
1059
+ */
1060
+ stopXrayService() {
1061
+ return this._request('post', '/panel/api/server/stopXrayService');
1062
+ }
1063
+
1064
+ /**
1065
+ * Restart Xray core service
1066
+ */
1067
+ restartXrayService() {
1068
+ return this._request('post', '/panel/api/server/restartXrayService');
1069
+ }
1070
+
1071
+ /**
1072
+ * Install specific Xray version
1073
+ * @param {string} version - Version to install
1074
+ */
1075
+ installXray(version) {
1076
+ return this._request('post', `/panel/api/server/installXray/${version}`);
1077
+ }
1078
+
1079
+ /**
1080
+ * Get panel logs
1081
+ * @param {number} count - Number of logs to retrieve
1082
+ */
1083
+ getPanelLogs(count = 100) {
1084
+ return this._request('post', `/panel/api/server/logs/${count}`);
1085
+ }
1086
+
1087
+ /**
1088
+ * Get Xray logs
1089
+ * @param {number} count - Number of logs to retrieve
1090
+ */
1091
+ getXrayLogs(count = 100) {
1092
+ return this._request('post', `/panel/api/server/xraylogs/${count}`);
1093
+ }
1094
+
1095
+ /**
1096
+ * Update GeoIP/GeoSite files
1097
+ * @param {string} [fileName] - Specific file to update (optional)
1098
+ */
1099
+ updateGeofile(fileName) {
1100
+ const url = fileName
1101
+ ? `/panel/api/server/updateGeofile/${fileName}`
1102
+ : '/panel/api/server/updateGeofile';
1103
+ return this._request('post', url);
1104
+ }
1105
+
1106
+ /**
1107
+ * Import database
1108
+ * @param {FormData} formData - FormData containing the database file
1109
+ */
1110
+ async importDB(formData) {
1111
+ // Ensure authenticated before direct API call
1112
+ if (!this.cookie) {
1113
+ await this._ensureAuthenticated();
1114
+ }
1115
+ // Use direct api call to handle multipart/form-data correctly
1116
+ return this.api.post('/panel/api/server/importDB', formData);
1117
+ }
1118
+
1119
+ // ===========================================
1120
+ // SERVER-SIDE GENERATORS
1121
+ // ===========================================
1122
+
1123
+ getNewUUID() {
1124
+ return this._request('get', '/panel/api/server/getNewUUID');
1125
+ }
1126
+
1127
+ getNewX25519Cert() {
1128
+ return this._request('get', '/panel/api/server/getNewX25519Cert');
1129
+ }
1130
+
1131
+ getNewmldsa65() {
1132
+ return this._request('get', '/panel/api/server/getNewmldsa65');
1133
+ }
1134
+
1135
+ getNewmlkem768() {
1136
+ return this._request('get', '/panel/api/server/getNewmlkem768');
1137
+ }
1138
+
1139
+ getNewVlessEnc() {
1140
+ return this._request('get', '/panel/api/server/getNewVlessEnc');
1141
+ }
1142
+
1143
+ getNewEchCert() {
1144
+ return this._request('post', '/panel/api/server/getNewEchCert');
1145
+ }
1146
+
1147
+ // ===========================================
1148
+ // PANEL SETTINGS
1149
+ // ===========================================
1150
+
1151
+ /**
1152
+ * Get all panel settings
1153
+ */
1154
+ getAllSettings() {
1155
+ return this._request('post', '/panel/setting/all');
1156
+ }
1157
+
1158
+ /**
1159
+ * Update panel settings
1160
+ * @param {Object} settings - Settings to update
1161
+ */
1162
+ updateSetting(settings) {
1163
+ return this._request('post', '/panel/setting/update', settings);
1164
+ }
1165
+
1166
+ /**
1167
+ * Update admin username and password
1168
+ * @param {string} oldUsername - Current username
1169
+ * @param {string} oldPassword - Current password
1170
+ * @param {string} newUsername - New username
1171
+ * @param {string} newPassword - New password
1172
+ */
1173
+ updateUser(oldUsername, oldPassword, newUsername, newPassword) {
1174
+ return this._request('post', '/panel/setting/updateUser', {
1175
+ oldUsername,
1176
+ oldPassword,
1177
+ newUsername,
1178
+ newPassword
1179
+ });
1180
+ }
1181
+
1182
+ /**
1183
+ * Restart the panel
1184
+ */
1185
+ restartPanel() {
1186
+ return this._request('post', '/panel/setting/restartPanel');
1187
+ }
1188
+
1189
+ /**
1190
+ * Get default settings
1191
+ */
1192
+ getDefaultSettings() {
1193
+ return this._request('post', '/panel/setting/defaultSettings');
1194
+ }
1195
+
1196
+ /**
1197
+ * Get default Xray JSON config
1198
+ */
1199
+ getDefaultJsonConfig() {
1200
+ return this._request('get', '/panel/setting/getDefaultJsonConfig');
1201
+ }
1202
+
1203
+ // ===========================================
1204
+ // XRAY CONFIGURATION
1205
+ // ===========================================
1206
+
1207
+ /**
1208
+ * Get Xray configuration
1209
+ */
1210
+ getXrayConfig() {
1211
+ return this._request('post', '/panel/xray/');
1212
+ }
1213
+
1214
+ /**
1215
+ * Update Xray configuration
1216
+ * @param {string} config - Xray configuration content
1217
+ */
1218
+ updateXrayConfig(config) {
1219
+ return this._request('post', '/panel/xray/update', { content: config });
1220
+ }
1221
+
1222
+ /**
1223
+ * Manage WARP
1224
+ * @param {string} action - Action to perform (data, del, config, reg, license)
1225
+ * @param {Object} [data] - Additional data for the action
1226
+ */
1227
+ manageWarp(action, data = {}) {
1228
+ return this._request('post', `/panel/xray/warp/${action}`, data);
1229
+ }
1230
+
1231
+ /**
1232
+ * Get outbound traffic statistics
1233
+ */
1234
+ getOutboundsTraffic() {
1235
+ return this._request('get', '/panel/xray/getOutboundsTraffic');
1236
+ }
1237
+
1238
+ /**
1239
+ * Reset outbound traffic statistics
1240
+ */
1241
+ resetOutboundsTraffic() {
1242
+ return this._request('post', '/panel/xray/resetOutboundsTraffic');
1243
+ }
1244
+
1245
+ /**
1246
+ * Get Xray execution result
1247
+ */
1248
+ getXrayResult() {
1249
+ return this._request('get', '/panel/xray/getXrayResult');
1250
+ }
1251
+
1252
+ // Security Methods
1253
+
1254
+ /**
1255
+ * Get security statistics and monitoring data
1256
+ * @returns {Object} Security statistics
1257
+ */
1258
+ getSecurityStats() {
1259
+ return this.securityMonitor.getStats();
1260
+ }
1261
+
1262
+ /**
1263
+ * Clear blocked IPs (admin function)
1264
+ */
1265
+ clearBlockedIPs() {
1266
+ this.securityMonitor.clearBlockedIPs();
1267
+ }
1268
+
1269
+ /**
1270
+ * Validate credential strength
1271
+ * @param {string} credential - Credential to validate
1272
+ * @param {string} type - Type of credential
1273
+ * @returns {Object} Validation result
1274
+ */
1275
+ validateCredentialStrength(credential, type) {
1276
+ return CredentialSecurity.validateCredentialStrength(credential, type);
1277
+ }
1278
+
1279
+ /**
1280
+ * Generate secure session token
1281
+ * @returns {string} Secure session token
1282
+ */
1283
+ generateSecureToken() {
1284
+ return CredentialSecurity.generateSessionToken();
1285
+ }
1286
+
1287
+ /**
1288
+ * Enable development mode for detailed error messages
1289
+ * @param {boolean} enabled - Whether to enable development mode
1290
+ */
1291
+ setDevelopmentMode(enabled) {
1292
+ this.isDevelopment = enabled;
1293
+ }
1294
+ }
1295
+
1296
+ // Export static methods for standalone use
1297
+ ThreeXUI.CredentialGenerator = CredentialGenerator;
1298
+ ThreeXUI.SessionManager = SessionManager;
1299
+ ThreeXUI.createSessionManager = createSessionManager;
1300
+
1301
+ module.exports = ThreeXUI;
1302
+
1303
+ // Define lazy getters to avoid circular dependencies
1304
+ Object.defineProperties(module.exports, {
1305
+ // Web middleware helpers
1306
+ createExpressMiddleware: {
1307
+ enumerable: true,
1308
+ get: () => require('./src/middleware/WebMiddleware').createExpressMiddleware
1309
+ },
1310
+ withThreeXUI: {
1311
+ enumerable: true,
1312
+ get: () => require('./src/middleware/WebMiddleware').withThreeXUI
1313
+ },
1314
+ createReactHook: {
1315
+ enumerable: true,
1316
+ get: () => require('./src/middleware/WebMiddleware').createReactHook
1317
+ },
1318
+ createNextjsRoutes: {
1319
+ enumerable: true,
1320
+ get: () => require('./src/middleware/WebMiddleware').createNextjsRoutes
1321
+ },
1322
+ SessionConfig: {
1323
+ enumerable: true,
1324
+ get: () => require('./src/middleware/WebMiddleware').SessionConfig
1325
+ },
1326
+ // Protocol builders
1327
+ ProtocolBuilder: {
1328
+ enumerable: true,
1329
+ get: () => require('./src/builders/ProtocolBuilders').ProtocolBuilder
1330
+ },
1331
+ VLESSBuilder: {
1332
+ enumerable: true,
1333
+ get: () => require('./src/builders/ProtocolBuilders').VLESSBuilder
1334
+ },
1335
+ VMESSBuilder: {
1336
+ enumerable: true,
1337
+ get: () => require('./src/builders/ProtocolBuilders').VMESSBuilder
1338
+ },
1339
+ TrojanBuilder: {
1340
+ enumerable: true,
1341
+ get: () => require('./src/builders/ProtocolBuilders').TrojanBuilder
1342
+ },
1343
+ ShadowsocksBuilder: {
1344
+ enumerable: true,
1345
+ get: () => require('./src/builders/ProtocolBuilders').ShadowsocksBuilder
1346
+ },
1347
+ WireGuardBuilder: {
1348
+ enumerable: true,
1349
+ get: () => require('./src/builders/ProtocolBuilders').WireGuardBuilder
1350
+ },
1351
+ BaseBuilder: {
1352
+ enumerable: true,
1353
+ get: () => require('./src/builders/ProtocolBuilders').BaseBuilder
1354
+ },
1355
+ // Security helpers
1356
+ SecurityEnhancer: {
1357
+ enumerable: true,
1358
+ get: () => require('./src/security/SecurityEnhancer')
1359
+ }
715
1360
  });