3xui-api-client 2.1.1 → 3.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "3xui-api-client",
3
- "version": "2.1.1",
3
+ "version": "3.1.0",
4
4
  "description": "A Node.js client library for 3x-ui panel API with built-in credential generation, session management, and web integration support",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -12,13 +12,8 @@
12
12
  }
13
13
  },
14
14
  "scripts": {
15
- "test": "node test/main-test.js",
15
+ "test": "echo \"No automated tests configured. Tests require a live panel environment.\"",
16
16
  "test:jest": "jest",
17
- "test:manual": "node test/main-test.js",
18
- "test:main": "node test/main-test.js",
19
- "test:login": "node test/login-test.js",
20
- "test:inbounds": "node test/inbounds-test.js",
21
- "test:create": "node test/create-inbound-test.js",
22
17
  "lint": "eslint index.js src/",
23
18
  "prepublishOnly": "echo 'Skipping tests for publishing'"
24
19
  },
@@ -93,7 +88,8 @@
93
88
  "dotenv": "^16.5.0",
94
89
  "jest": "^29.7.0",
95
90
  "eslint": "^9.0.0",
96
- "@eslint/js": "^9.0.0"
91
+ "@eslint/js": "^9.0.0",
92
+ "chai": "^4.3.4"
97
93
  },
98
94
  "jest": {
99
95
  "testEnvironment": "node",
@@ -101,25 +101,30 @@ class CredentialGenerator {
101
101
  return keyBytes.toString('base64');
102
102
  }
103
103
 
104
+ /**
105
+ * Generate a Curve25519 (X25519) key pair using Node's crypto module
106
+ * @returns {{privateKeyBytes: Buffer, publicKeyBytes: Buffer}} Raw 32-byte key pair
107
+ */
108
+ static _generateX25519KeyPair() {
109
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519');
110
+ const privateJwk = privateKey.export({ format: 'jwk' });
111
+ const publicJwk = publicKey.export({ format: 'jwk' });
112
+
113
+ return {
114
+ privateKeyBytes: Buffer.from(privateJwk.d, 'base64url'),
115
+ publicKeyBytes: Buffer.from(publicJwk.x, 'base64url')
116
+ };
117
+ }
118
+
104
119
  /**
105
120
  * Generate WireGuard key pair
106
121
  * @returns {Object} Object containing private and public keys
107
122
  */
108
123
  static generateWireGuardKeys() {
109
- // Generate 32 random bytes for private key
110
- const privateKeyBytes = crypto.randomBytes(32);
111
-
112
- // Clamp the private key (standard WireGuard key clamping)
113
- privateKeyBytes[0] &= 248;
114
- privateKeyBytes[31] &= 127;
115
- privateKeyBytes[31] |= 64;
124
+ // WireGuard uses Curve25519 (X25519) key pairs encoded as standard base64
125
+ const { privateKeyBytes, publicKeyBytes } = this._generateX25519KeyPair();
116
126
 
117
127
  const privateKey = privateKeyBytes.toString('base64');
118
-
119
- // For the public key, we would normally use Curve25519 scalar multiplication
120
- // This is a simplified implementation - in production, use a proper crypto library
121
- // like noble-curves or libsodium for accurate public key derivation
122
- const publicKeyBytes = crypto.randomBytes(32); // Placeholder
123
128
  const publicKey = publicKeyBytes.toString('base64');
124
129
 
125
130
  return {
@@ -140,13 +145,11 @@ class CredentialGenerator {
140
145
  * @returns {Object} Object containing private and public keys
141
146
  */
142
147
  static generateRealityKeys() {
143
- // Reality uses X25519 key exchange
144
- const privateKeyBytes = crypto.randomBytes(32);
145
- const privateKey = privateKeyBytes.toString('base64').replace(/\//g, '_').replace(/\+/g, '-');
148
+ // Reality uses X25519 key exchange, keys encoded as base64url without padding
149
+ const { privateKeyBytes, publicKeyBytes } = this._generateX25519KeyPair();
146
150
 
147
- // Public key derivation (simplified - use proper X25519 in production)
148
- const publicKeyBytes = crypto.randomBytes(32);
149
- const publicKey = publicKeyBytes.toString('base64').replace(/\//g, '_').replace(/\+/g, '-');
151
+ const privateKey = privateKeyBytes.toString('base64url');
152
+ const publicKey = publicKeyBytes.toString('base64url');
150
153
 
151
154
  return {
152
155
  privateKey,
@@ -34,6 +34,13 @@ class SessionStore {
34
34
  /**
35
35
  * Built-in memory session store (default)
36
36
  * Recommended for development and single-instance deployments
37
+ *
38
+ * Security note: session cookies are stored as plain JavaScript objects in
39
+ * process memory — unencrypted. The cookie grants full admin access to the
40
+ * 3x-ui panel and should be treated as a high-value secret. In shared or
41
+ * multi-tenant Node.js processes a heap dump or memory inspection can expose
42
+ * a live session. For production workloads consider a Redis or Database store
43
+ * protected by appropriate infrastructure-level access controls.
37
44
  */
38
45
  class MemorySessionStore extends SessionStore {
39
46
  constructor() {
@@ -175,6 +182,14 @@ class RedisSessionStore extends SessionStore {
175
182
  /**
176
183
  * Database session store adapter
177
184
  * Works with any SQL database through a provided database client
185
+ *
186
+ * Security note: session data (including the 3x-ui admin cookie) is
187
+ * persisted as `session_data TEXT` — unencrypted. Anyone with read access
188
+ * to the session table can extract a live admin-equivalent credential.
189
+ * Restrict table access with the least-privilege DB user possible, and
190
+ * consider enabling encryption at rest for the database. The session key
191
+ * column (sha256 hash) also allows correlation of rows to specific
192
+ * panels/users — see generateSessionKey() for details.
178
193
  */
179
194
  class DatabaseSessionStore extends SessionStore {
180
195
  constructor(database, options = {}) {
@@ -365,7 +380,15 @@ class SessionManager {
365
380
  }
366
381
 
367
382
  /**
368
- * Generate session key for a server
383
+ * Generate a deterministic cache key for a given panel + username pair.
384
+ *
385
+ * The key is sha256(baseURL:username) — intentionally predictable so that
386
+ * the same panel session can be looked up across restarts. It is NOT a
387
+ * security secret: anyone with read access to the session store can
388
+ * correlate rows back to a specific panel/user. Treat the *value* stored
389
+ * under this key (the session cookie) as the sensitive material that must
390
+ * be protected at the infrastructure level (access control, encryption at
391
+ * rest, etc.), not the key itself.
369
392
  */
370
393
  generateSessionKey(baseURL, username) {
371
394
  const hash = crypto
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Byte Conversion Utilities
3
+ *
4
+ * Converts bandwidth/storage values to bytes for consistent handling
5
+ * in the 3x-ui API which expects all data limits in bytes internally.
6
+ */
7
+
8
+ const UNITS = {
9
+ B: 1,
10
+ KB: 1024,
11
+ MB: 1024 ** 2,
12
+ GB: 1024 ** 3,
13
+ TB: 1024 ** 4
14
+ };
15
+
16
+ /**
17
+ * Convert gigabytes to bytes
18
+ * @param {number} gb - Gigabytes value
19
+ * @returns {number} Value in bytes
20
+ */
21
+ function gbToBytes(gb) {
22
+ if (typeof gb !== 'number' || gb < 0) {
23
+ throw new Error(`Invalid GB value: ${gb}. Must be a non-negative number.`);
24
+ }
25
+ return gb * UNITS.GB;
26
+ }
27
+
28
+ /**
29
+ * Convert bytes to gigabytes
30
+ * @param {number} bytes - Bytes value
31
+ * @returns {number} Value in gigabytes
32
+ */
33
+ function bytesToGb(bytes) {
34
+ if (typeof bytes !== 'number' || bytes < 0) {
35
+ throw new Error(`Invalid byte value: ${bytes}. Must be a non-negative number.`);
36
+ }
37
+ return bytes / UNITS.GB;
38
+ }
39
+
40
+ /**
41
+ * Format bytes to human-readable format
42
+ * @param {number} bytes - Bytes value
43
+ * @param {number} decimals - Decimal places (default: 2)
44
+ * @returns {string} Formatted string like "1.5 GB"
45
+ */
46
+ function formatBytes(bytes, decimals = 2) {
47
+ if (typeof bytes !== 'number' || bytes < 0) {
48
+ return '0 B';
49
+ }
50
+
51
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
52
+ let size = bytes;
53
+ let unitIndex = 0;
54
+
55
+ while (size >= 1024 && unitIndex < units.length - 1) {
56
+ size /= 1024;
57
+ unitIndex++;
58
+ }
59
+
60
+ return `${size.toFixed(decimals)} ${units[unitIndex]}`;
61
+ }
62
+
63
+ /**
64
+ * Safely convert totalGB option to bytes with warning for suspiciously small values
65
+ * @param {number|undefined} totalGbValue - The totalGB value from options
66
+ * @returns {number} Value in bytes (0 if undefined/null)
67
+ */
68
+ function sanitizeTotalGb(totalGbValue) {
69
+ if (totalGbValue === undefined || totalGbValue === null) {
70
+ return 0;
71
+ }
72
+
73
+ if (typeof totalGbValue !== 'number') {
74
+ throw new Error(`Invalid totalGB value: ${totalGbValue}. Must be a number.`);
75
+ }
76
+
77
+ // Warn if the value looks suspiciously small (likely a user error)
78
+ if (totalGbValue > 0 && totalGbValue < 1) {
79
+ console.warn(
80
+ '⚠️ WARNING: totalGB is set to ' + totalGbValue + '. ' +
81
+ 'This will be converted to ' + gbToBytes(totalGbValue) + ' bytes. ' +
82
+ 'If you meant ' + totalGbValue + ' gigabytes, this is correct. ' +
83
+ 'If you meant something else, please check your value.'
84
+ );
85
+ }
86
+
87
+ return gbToBytes(totalGbValue);
88
+ }
89
+
90
+ /**
91
+ * Process options object to convert all bandwidth-related fields to bytes
92
+ * Converts: totalGB → totalGB (in bytes)
93
+ * @param {Object} options - Options object
94
+ * @returns {Object} New object with converted values
95
+ */
96
+ function convertBandwidthFields(options) {
97
+ if (!options || typeof options !== 'object') {
98
+ return options;
99
+ }
100
+
101
+ const converted = { ...options };
102
+
103
+ // Convert totalGB if present
104
+ if (converted.totalGB !== undefined && converted.totalGB !== null) {
105
+ converted.totalGB = sanitizeTotalGb(converted.totalGB);
106
+ }
107
+
108
+ return converted;
109
+ }
110
+
111
+ /**
112
+ * Process array of options objects for bulk operations
113
+ * @param {Array<Object>} optionsArray - Array of option objects
114
+ * @returns {Array<Object>} New array with converted values
115
+ */
116
+ function convertBandwidthFieldsBulk(optionsArray) {
117
+ if (!Array.isArray(optionsArray)) {
118
+ return optionsArray;
119
+ }
120
+
121
+ return optionsArray.map(convertBandwidthFields);
122
+ }
123
+
124
+ module.exports = {
125
+ gbToBytes,
126
+ bytesToGb,
127
+ formatBytes,
128
+ sanitizeTotalGb,
129
+ convertBandwidthFields,
130
+ convertBandwidthFieldsBulk,
131
+ UNITS
132
+ };