x3ui-api 1.0.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # x3ui-api
2
+
3
+ A Node.js client for interacting with the x3ui panel API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install x3ui-api
9
+ ```
10
+
11
+ ## License
12
+
13
+ MIT
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "x3ui-api",
3
+ "version": "1.0.0",
4
+ "description": "API client for x3ui panel",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "scripts": {
8
+ "test": "jest"
9
+ },
10
+ "keywords": ["x3ui", "api", "client"],
11
+ "author": "",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "axios": "^1.6.5",
15
+ "form-data": "^4.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.11.5"
19
+ },
20
+ "repository": {
21
+ "url": "https://github.com/RedGuys/x3ui-api"
22
+ }
23
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,293 @@
1
+ import {AxiosInstance} from 'axios';
2
+
3
+ export interface X3UIConfig {
4
+ baseURL: string;
5
+ token?: string;
6
+ parseJSONSettings?: boolean;
7
+ }
8
+
9
+ export interface LoginResponse {
10
+ success: boolean;
11
+ msg: string;
12
+ obj: any;
13
+ }
14
+
15
+ export interface InboundConfig {
16
+ id?: number;
17
+ up: number;
18
+ down: number;
19
+ total: number;
20
+ remark: string;
21
+ enable: boolean;
22
+ expiryTime: number | null;
23
+ clientStats: ClientStats[];
24
+ listen: string;
25
+ port: number;
26
+ protocol: string;
27
+ settings: string | InboundSettings;
28
+ streamSettings: string | StreamSettings;
29
+ tag: string;
30
+ sniffing: string | SniffingSettings;
31
+ allocate: string | AllocateSettings;
32
+ }
33
+
34
+ export interface AllocateSettings {
35
+ strategy: string;
36
+ refresh: number;
37
+ concurrency: number;
38
+ }
39
+
40
+ export interface SniffingSettings {
41
+ enabled: boolean;
42
+ destOverride: string[];
43
+ metadataOnly: boolean;
44
+ routeOnly: boolean;
45
+ }
46
+
47
+ export interface StreamSettings {
48
+ network: string;
49
+ security: string;
50
+ externalProxy: any[];
51
+ realitySettings: RealitySettings;
52
+ tcpSettings?: TCPSettings;
53
+ }
54
+
55
+ export interface TCPSettings {
56
+ acceptProxyProtocol: boolean;
57
+ header: {
58
+ type: string;
59
+ }
60
+ }
61
+
62
+ export interface RealitySettings {
63
+ show: boolean;
64
+ xver: number;
65
+ dest: string;
66
+ serverNames: string[];
67
+ privateKey: string;
68
+ minClient: string;
69
+ maxClient: string;
70
+ maxTimediff: number;
71
+ shortIds: string[];
72
+ settings: {
73
+ publicKey: string;
74
+ fingerprint: string;
75
+ serverName: string;
76
+ spiderX: string;
77
+ }
78
+ }
79
+
80
+ export interface InboundSettings {
81
+ clients: ClientSettings[];
82
+ decryption: string;
83
+ fallbacks: any[];
84
+ }
85
+
86
+ export interface ClientSettings {
87
+ id: string;
88
+ flow: string;
89
+ email: string;
90
+ limitIp: number;
91
+ totalGB: number;
92
+ expiryTime: number | null;
93
+ enable: boolean;
94
+ tgId: string;
95
+ subId: string;
96
+ reset: number;
97
+ }
98
+
99
+ export interface ClientStats {
100
+ id: number;
101
+ inboundId: number;
102
+ enable: boolean;
103
+ email: string;
104
+ up: number;
105
+ down: number;
106
+ expiryTime: number | null;
107
+ total: number;
108
+ reset: number;
109
+ }
110
+
111
+ export interface SystemStats {
112
+ cpu: number;
113
+ cpuCores: number;
114
+ logicalPro: number;
115
+ cpuSpeedMhz: number;
116
+ mem: {
117
+ current: number;
118
+ total: number;
119
+ };
120
+ swap: {
121
+ current: number;
122
+ total: number;
123
+ }
124
+ disk: {
125
+ current: number;
126
+ total: number;
127
+ };
128
+ xray: {
129
+ state: boolean;
130
+ errorMsg: string;
131
+ version: string;
132
+ };
133
+ uptime: number;
134
+ loads: number[];
135
+ tcpCount: number;
136
+ udpCount: number;
137
+ netIO: {
138
+ up: number;
139
+ down: number;
140
+ };
141
+ netTraffic: {
142
+ up: number;
143
+ down: number;
144
+ };
145
+ publicIP: {
146
+ ipv4: string;
147
+ ipv6: string;
148
+ }
149
+ appStats: {
150
+ threads: number;
151
+ mem: number;
152
+ uptime: number;
153
+ }
154
+ }
155
+
156
+ export interface RealityInboundOptions {
157
+ remark: string;
158
+ port?: number;
159
+ dest?: string;
160
+ serverNames?: string[];
161
+ privateKey?: string;
162
+ publicKey?: string;
163
+ shortIds?: string[];
164
+ fingerprint?: string;
165
+ listenIP?: string;
166
+ expiryTime?: number;
167
+ }
168
+
169
+ export interface ClientOptions {
170
+ id?: string;
171
+ email?: string;
172
+ totalGB?: number;
173
+ expiryTime?: number;
174
+ tgId?: string;
175
+ }
176
+
177
+ export interface ClientBuilder {
178
+ /**
179
+ * Set client UUID. If not provided, will be auto-generated
180
+ */
181
+ setId(id: string): this;
182
+
183
+ /**
184
+ * Set client email
185
+ */
186
+ setEmail(email: string): this;
187
+
188
+ /**
189
+ * Set total traffic limit in GB
190
+ */
191
+ setTotalGB(gb: number): this;
192
+
193
+ /**
194
+ * Set expiry time in unix timestamp
195
+ */
196
+ setExpiryTime(timestamp: number): this;
197
+
198
+ /**
199
+ * Set Telegram ID
200
+ */
201
+ setTgId(id: string): this;
202
+
203
+ /**
204
+ * Build client configuration
205
+ */
206
+ build(): Promise<ClientSettings>;
207
+ }
208
+
209
+ export interface RealityBuilder {
210
+ /**
211
+ * Set the port for the inbound. If not provided, will auto-generate unused port
212
+ */
213
+ setPort(port: number): this;
214
+
215
+ /**
216
+ * Set the remark/name for the inbound
217
+ */
218
+ setRemark(remark: string): this;
219
+
220
+ /**
221
+ * Set the destination address (e.g. "yahoo.com:443")
222
+ */
223
+ setDest(dest: string): this;
224
+
225
+ /**
226
+ * Set server names for SNI
227
+ */
228
+ setServerNames(names: string[]): this;
229
+
230
+ /**
231
+ * Set Reality keypair. If not provided, will be auto-generated
232
+ */
233
+ setKeyPair(privateKey: string, publicKey: string): this;
234
+
235
+ /**
236
+ * Set short IDs for Reality. If not provided, random ones will be generated
237
+ */
238
+ setShortIds(ids: string[]): this;
239
+
240
+ /**
241
+ * Set browser fingerprint. Defaults to "chrome"
242
+ */
243
+ setFingerprint(fingerprint: string): this;
244
+
245
+ /**
246
+ * Set listen IP address. Defaults to empty string
247
+ */
248
+ setListenIP(ip: string): this;
249
+
250
+ /**
251
+ * Set inbound expiry time. Defaults to 0 (no expiry)
252
+ */
253
+ setExpiryTime(timestamp: number): this;
254
+
255
+ /**
256
+ * Add a new client to the inbound
257
+ */
258
+ addClient(options?: Partial<ClientSettings>): ClientBuilder;
259
+
260
+ /**
261
+ * Build the final inbound config
262
+ */
263
+ build(): Promise<InboundConfig>;
264
+ }
265
+
266
+ export default class X3UIClient {
267
+ private client: AxiosInstance;
268
+
269
+ constructor(config: X3UIConfig);
270
+
271
+ login(username: string, password: string): Promise<LoginResponse>;
272
+
273
+ getSystemStats(): Promise<SystemStats>;
274
+
275
+ getInbounds(): Promise<InboundConfig[]>;
276
+
277
+ addInbound(config: Omit<InboundConfig, 'id'>): Promise<InboundConfig>;
278
+
279
+ updateInbound(id: number, config: Partial<InboundConfig>): Promise<void>;
280
+
281
+ deleteInbound(id: number): Promise<void>;
282
+
283
+ getInboundTraffic(id: number): Promise<{ up: number; down: number }>;
284
+
285
+ resetInboundTraffic(id: number): Promise<void>;
286
+
287
+ getNewX25519Cert(): Promise<{ privateKey: string; publicKey: string }>;
288
+
289
+ /**
290
+ * Create a new Reality inbound builder
291
+ */
292
+ createRealityBuilder(options?: Partial<InboundConfig>): RealityBuilder;
293
+ }
package/src/index.js ADDED
@@ -0,0 +1,415 @@
1
+ const axios = require('axios');
2
+ const crypto = require('crypto');
3
+
4
+ module.exports = class X3UIClient {
5
+ constructor(config) {
6
+ this.parseJSONSettings = config.parseJSONSettings || true;
7
+ this.client = axios.create({
8
+ baseURL: config.baseURL,
9
+ headers: config.token ? {
10
+ 'Cookie': `lang=en-US; 3x-ui=${config.token}`,
11
+ } : {
12
+ 'Cookie': 'lang=en-US'
13
+ }
14
+ });
15
+ }
16
+
17
+ /**
18
+ * Login to the x3ui panel
19
+ * @param {string} username Panel username
20
+ * @param {string} password Panel password
21
+ * @returns {Promise<{success: boolean, msg: string, obj: any}>} Login response with success status and token
22
+ */
23
+ async login(username, password) {
24
+ const formData = new URLSearchParams();
25
+ formData.append('username', username);
26
+ formData.append('password', password);
27
+
28
+ const response = await this.client.post('/login', formData, {
29
+ headers: {
30
+ 'Content-Type': 'application/x-www-form-urlencoded'
31
+ }
32
+ });
33
+
34
+ if (response.data.success) {
35
+ this.client.defaults.headers.Cookie = `lang=en-US; ${response.headers.get("set-cookie")[0].split(";")[0]}`;
36
+ }
37
+
38
+ return response.data;
39
+ }
40
+
41
+ async getSystemStats() {
42
+ const response = await this.client.post('/server/status');
43
+ return response.data.obj;
44
+ }
45
+
46
+ /**
47
+ * Get all inbounds
48
+ * @returns {Promise<Array<{
49
+ * id?: number,
50
+ * up: number,
51
+ * down: number,
52
+ * total: number,
53
+ * remark: string,
54
+ * enable: boolean,
55
+ * expiryTime: number | null,
56
+ * listen: string,
57
+ * port: number,
58
+ * protocol: string,
59
+ * settings: any,
60
+ * streamSettings: any,
61
+ * sniffing: any
62
+ * }>>}
63
+ */
64
+ async getInbounds() {
65
+ const response = await this.client.get('/panel/api/inbounds/list');
66
+ let inbounds = response.data.obj;
67
+ if (this.parseJSONSettings) {
68
+ inbounds = inbounds.map(inbound => this.parseInbound(inbound));
69
+ }
70
+ return inbounds;
71
+ }
72
+
73
+ parseInbound(inbound) {
74
+ if (this.parseJSONSettings) {
75
+ inbound.settings = JSON.parse(inbound.settings);
76
+ inbound.streamSettings = JSON.parse(inbound.streamSettings);
77
+ inbound.sniffing = JSON.parse(inbound.sniffing);
78
+ inbound.allocate = JSON.parse(inbound.allocate);
79
+ }
80
+ return inbound;
81
+ }
82
+
83
+ stringifyInbound(inbound) {
84
+ inbound.settings = JSON.stringify(inbound.settings);
85
+ inbound.streamSettings = JSON.stringify(inbound.streamSettings);
86
+ inbound.sniffing = JSON.stringify(inbound.sniffing);
87
+ inbound.allocate = JSON.stringify(inbound.allocate);
88
+ return inbound;
89
+ }
90
+
91
+ /**
92
+ * Add new inbound
93
+ * @param {Object} config Inbound configuration
94
+ * @param {number} config.up Upload traffic in bytes
95
+ * @param {number} config.down Download traffic in bytes
96
+ * @param {number} config.total Total traffic limit in bytes
97
+ * @param {string} config.remark Inbound remark/name
98
+ * @param {boolean} config.enable Enable status
99
+ * @param {number|null} config.expiryTime Expiry timestamp
100
+ * @param {string} config.listen Listen address
101
+ * @param {number} config.port Port number
102
+ * @param {string} config.protocol Protocol type
103
+ * @param {Object} config.settings Protocol settings
104
+ * @param {Object} config.streamSettings Stream settings
105
+ * @param {Object} config.sniffing Sniffing settings
106
+ * @param {Object} config.allocate Allocation settings
107
+ * @returns {Promise<void>}
108
+ */
109
+ async addInbound(config) {
110
+ config = this.stringifyInbound(config);
111
+ const response = await this.client.post('/panel/api/inbounds/add', config);
112
+ if(!response.data.success)
113
+ throw new Error(response.data.msg);
114
+ return this.parseInbound(response.data.obj);
115
+ }
116
+
117
+ /**
118
+ * Update existing inbound
119
+ * @param {number} id Inbound ID
120
+ * @param {Object} config Partial inbound configuration
121
+ * @returns {Promise<void>}
122
+ */
123
+ async updateInbound(id, config) {
124
+ config = this.stringifyInbound(config);
125
+ const response = await this.client.post(`/panel/api/inbounds/update/${id}`, config);
126
+ if(!response.data.success)
127
+ throw new Error(response.data.msg);
128
+ return this.parseInbound(response.data.obj);
129
+ }
130
+
131
+ /**
132
+ * Delete inbound
133
+ * @param {number} id Inbound ID
134
+ * @returns {Promise<void>}
135
+ */
136
+ async deleteInbound(id) {
137
+ await this.client.post(`/panel/api/inbounds/del/${id}`);
138
+ }
139
+
140
+ /**
141
+ * Get new X25519 certificate
142
+ * @returns {Promise<{privateKey: string, publicKey: string}>} New X25519 key pair
143
+ */
144
+ async getNewX25519Cert() {
145
+ const response = await this.client.post('/server/getNewX25519Cert');
146
+ return response.data.obj;
147
+ }
148
+
149
+ /**
150
+ * Create a new Reality inbound builder
151
+ * @param {Partial<import('./index').RealityInboundOptions>} options Initial options
152
+ * @returns {import('./index').RealityBuilder} Reality builder instance
153
+ */
154
+ createRealityBuilder(options = {}) {
155
+ return new RealityBuilder(this, options);
156
+ }
157
+ }
158
+
159
+ class ClientBuilder {
160
+ constructor(parent) {
161
+ this.parent = parent;
162
+ this.id = '';
163
+ this.flow = '';
164
+ this.email = '';
165
+ this.limitIp = 0;
166
+ this.totalGB = 0;
167
+ this.expiryTime = 0;
168
+ this.enable = true;
169
+ this.tgId = '';
170
+ this.subId = '';
171
+ this.reset = 0;
172
+ }
173
+
174
+ setId(id) {
175
+ this.id = id;
176
+ return this;
177
+ }
178
+
179
+ setEmail(email) {
180
+ this.email = email;
181
+ return this;
182
+ }
183
+
184
+ setTotalGB(gb) {
185
+ this.totalGB = gb;
186
+ return this;
187
+ }
188
+
189
+ setExpiryTime(timestamp) {
190
+ this.expiryTime = timestamp;
191
+ return this;
192
+ }
193
+
194
+ setTgId(id) {
195
+ this.tgId = id;
196
+ return this;
197
+ }
198
+
199
+ async build() {
200
+ return {
201
+ id: this.id || crypto.randomUUID(),
202
+ flow: this.flow,
203
+ email: this.email || Math.random().toString(36).substring(2, 10),
204
+ limitIp: this.limitIp,
205
+ totalGB: this.totalGB,
206
+ expiryTime: this.expiryTime,
207
+ enable: this.enable,
208
+ tgId: this.tgId,
209
+ subId: this.subId || Math.random().toString(36).substring(2, 18),
210
+ reset: this.reset
211
+ };
212
+ }
213
+ }
214
+
215
+ class RealityBuilder {
216
+ constructor(client, options = {}) {
217
+ this.client = client;
218
+ // Initialize from InboundConfig
219
+ this.id = options.id || undefined;
220
+ this.port = options.port || 0;
221
+ this.remark = options.remark || '';
222
+ this.listenIP = options.listen || '';
223
+ this.expiryTime = options.expiryTime || 0;
224
+ this.enable = true;
225
+
226
+ // Initialize from StreamSettings and RealitySettings
227
+ const streamSettings = typeof options.streamSettings === 'string'
228
+ ? JSON.parse(options.streamSettings)
229
+ : options.streamSettings || {};
230
+
231
+ const realitySettings = streamSettings?.realitySettings || {};
232
+
233
+ this.dest = realitySettings.dest || 'yahoo.com:443';
234
+ this.serverNames = realitySettings.serverNames || ['yahoo.com', 'www.yahoo.com'];
235
+ this.privateKey = realitySettings.privateKey || '';
236
+ this.publicKey = realitySettings.settings?.publicKey || '';
237
+ this.shortIds = realitySettings.shortIds;
238
+ this.fingerprint = realitySettings.settings?.fingerprint || 'chrome';
239
+
240
+ // Initialize clients
241
+ this.clients = [];
242
+ const settings = typeof options.settings === 'string'
243
+ ? JSON.parse(options.settings)
244
+ : options.settings;
245
+
246
+ if (settings?.clients) {
247
+ settings.clients.forEach(client => {
248
+ this.addClient(client);
249
+ });
250
+ }
251
+ }
252
+
253
+ setPort(port) {
254
+ this.port = port;
255
+ return this;
256
+ }
257
+
258
+ setRemark(remark) {
259
+ this.remark = remark;
260
+ return this;
261
+ }
262
+
263
+ setDest(dest) {
264
+ this.dest = dest;
265
+ return this;
266
+ }
267
+
268
+ setServerNames(names) {
269
+ this.serverNames = names;
270
+ return this;
271
+ }
272
+
273
+ setKeyPair(privateKey, publicKey) {
274
+ this.privateKey = privateKey;
275
+ this.publicKey = publicKey;
276
+ return this;
277
+ }
278
+
279
+ setShortIds(ids) {
280
+ this.shortIds = ids;
281
+ return this;
282
+ }
283
+
284
+ setFingerprint(fingerprint) {
285
+ this.fingerprint = fingerprint;
286
+ return this;
287
+ }
288
+
289
+ setListenIP(ip) {
290
+ this.listenIP = ip;
291
+ return this;
292
+ }
293
+
294
+ setExpiryTime(timestamp) {
295
+ this.expiryTime = timestamp;
296
+ return this;
297
+ }
298
+
299
+ addClient(options = {}) {
300
+ const builder = new ClientBuilder(this);
301
+ if (options.id) builder.setId(options.id);
302
+ if (options.email) builder.setEmail(options.email);
303
+ if (options.totalGB) builder.setTotalGB(options.totalGB);
304
+ if (options.expiryTime) builder.setExpiryTime(options.expiryTime);
305
+ if (options.tgId) builder.setTgId(options.tgId);
306
+ this.clients.push(builder);
307
+ return builder;
308
+ }
309
+
310
+ generateRandomPort() {
311
+ return Math.floor(Math.random() * (65535 - 1024) + 1024);
312
+ }
313
+
314
+ generateShortId() {
315
+ const length = Math.floor(Math.random() * 7) * 2 + 2; // Random length between 2 and 16
316
+ return crypto.randomBytes(Math.ceil(length / 2))
317
+ .toString('hex')
318
+ .slice(0, length);
319
+ }
320
+
321
+ async build() {
322
+ if (!this.remark) {
323
+ throw new Error('Remark is required');
324
+ }
325
+
326
+ // If no port specified, find unused one
327
+ if (!this.port) {
328
+ const inbounds = await this.client.getInbounds();
329
+ const usedPorts = new Set(inbounds.map(i => i.port));
330
+ let port;
331
+ do {
332
+ port = this.generateRandomPort();
333
+ } while (usedPorts.has(port));
334
+ this.port = port;
335
+ }
336
+
337
+ // If no keypair provided, generate one
338
+ if (!this.privateKey || !this.publicKey) {
339
+ const cert = await this.client.getNewX25519Cert();
340
+ this.privateKey = cert.privateKey;
341
+ this.publicKey = cert.publicKey;
342
+ }
343
+
344
+ // If no shortIds provided, generate random ones
345
+ if (!this.shortIds) {
346
+ this.shortIds = Array.from({ length: 8 }, () => this.generateShortId());
347
+ }
348
+
349
+ // If no clients added, create one default client
350
+ if (this.clients.length === 0) {
351
+ this.addClient();
352
+ }
353
+
354
+ // Build all clients
355
+ const clientConfigs = await Promise.all(this.clients.map(builder => builder.build()));
356
+
357
+ return {
358
+ id: this.id,
359
+ up: 0,
360
+ down: 0,
361
+ total: 0,
362
+ remark: this.remark,
363
+ enable: true,
364
+ expiryTime: this.expiryTime,
365
+ listen: this.listenIP,
366
+ port: this.port,
367
+ protocol: 'vless',
368
+ settings: {
369
+ clients: clientConfigs,
370
+ decryption: 'none',
371
+ fallbacks: []
372
+ },
373
+ streamSettings: {
374
+ network: 'tcp',
375
+ security: 'reality',
376
+ externalProxy: [],
377
+ realitySettings: {
378
+ show: false,
379
+ xver: 0,
380
+ dest: this.dest,
381
+ serverNames: this.serverNames,
382
+ privateKey: this.privateKey,
383
+ minClient: '',
384
+ maxClient: '',
385
+ maxTimediff: 0,
386
+ shortIds: this.shortIds,
387
+ settings: {
388
+ publicKey: this.publicKey,
389
+ fingerprint: this.fingerprint,
390
+ serverName: '',
391
+ spiderX: '/'
392
+ }
393
+ },
394
+ tcpSettings: {
395
+ acceptProxyProtocol: false,
396
+ header: {
397
+ type: 'none'
398
+ }
399
+ }
400
+ },
401
+ tag: `inbound-${this.port}`,
402
+ sniffing: {
403
+ enabled: false,
404
+ destOverride: ['http', 'tls', 'quic', 'fakedns'],
405
+ metadataOnly: false,
406
+ routeOnly: false
407
+ },
408
+ allocate: {
409
+ strategy: 'always',
410
+ refresh: 5,
411
+ concurrency: 3
412
+ }
413
+ };
414
+ }
415
+ }