3xui-api-client 2.0.0 → 2.1.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.
@@ -1,493 +1,493 @@
1
- /**
2
- * Session Management System for 3xui-api-client
3
- * Provides both built-in caching and user-managed database integration
4
- */
5
-
6
- const crypto = require('crypto');
7
-
8
- /**
9
- * Abstract base class for session storage
10
- */
11
- class SessionStore {
12
- async get(_key) {
13
- throw new Error('get method must be implemented');
14
- }
15
-
16
- async set(_key, _value, _ttl) {
17
- throw new Error('set method must be implemented');
18
- }
19
-
20
- async delete(_key) {
21
- throw new Error('delete method must be implemented');
22
- }
23
-
24
- async clear() {
25
- throw new Error('clear method must be implemented');
26
- }
27
-
28
- async exists(key) {
29
- const value = await this.get(key);
30
- return value !== null;
31
- }
32
- }
33
-
34
- /**
35
- * Built-in memory session store (default)
36
- * Recommended for development and single-instance deployments
37
- */
38
- class MemorySessionStore extends SessionStore {
39
- constructor() {
40
- super();
41
- this.cache = new Map();
42
- this.timers = new Map();
43
- }
44
-
45
- async get(key) {
46
- const item = this.cache.get(key);
47
- if (!item) {
48
- return null;
49
- }
50
-
51
- if (item.expires && item.expires <= Date.now()) {
52
- this.delete(key);
53
- return null;
54
- }
55
-
56
- return item.value;
57
- }
58
-
59
- async set(key, value, ttl = 3600) {
60
- // Clear existing timer if it exists
61
- const existingTimer = this.timers.get(key);
62
- if (existingTimer) {
63
- clearTimeout(existingTimer);
64
- }
65
-
66
- const expires = ttl > 0 ? Date.now() + (ttl * 1000) : null;
67
- this.cache.set(key, { value, expires });
68
-
69
- // Set expiration timer
70
- if (ttl > 0) {
71
- const timer = setTimeout(() => {
72
- this.delete(key);
73
- }, ttl * 1000);
74
- this.timers.set(key, timer);
75
- }
76
- }
77
-
78
- async delete(key) {
79
- this.cache.delete(key);
80
- const timer = this.timers.get(key);
81
- if (timer) {
82
- clearTimeout(timer);
83
- this.timers.delete(key);
84
- }
85
- }
86
-
87
- async clear() {
88
- // Clear all timers
89
- for (const timer of this.timers.values()) {
90
- clearTimeout(timer);
91
- }
92
- this.cache.clear();
93
- this.timers.clear();
94
- }
95
-
96
- // Get cache statistics
97
- getStats() {
98
- return {
99
- size: this.cache.size,
100
- keys: Array.from(this.cache.keys())
101
- };
102
- }
103
- }
104
-
105
- /**
106
- * Redis session store adapter
107
- * Recommended for production and multi-instance deployments
108
- */
109
- class RedisSessionStore extends SessionStore {
110
- constructor(redisClient, options = {}) {
111
- super();
112
- this.redis = redisClient;
113
- this.keyPrefix = options.keyPrefix || '3xui:session:';
114
- this.defaultTTL = options.defaultTTL || 3600;
115
- }
116
-
117
- _getKey(key) {
118
- return `${this.keyPrefix}${key}`;
119
- }
120
-
121
- async get(key) {
122
- try {
123
- const value = await this.redis.get(this._getKey(key));
124
- return value ? JSON.parse(value) : null;
125
- } catch (error) {
126
- console.error('Redis get error:', error);
127
- return null;
128
- }
129
- }
130
-
131
- async set(key, value, ttl = this.defaultTTL) {
132
- try {
133
- const serialized = JSON.stringify(value);
134
- if (ttl > 0) {
135
- await this.redis.setex(this._getKey(key), ttl, serialized);
136
- } else {
137
- await this.redis.set(this._getKey(key), serialized);
138
- }
139
- } catch (error) {
140
- console.error('Redis set error:', error);
141
- throw error;
142
- }
143
- }
144
-
145
- async delete(key) {
146
- try {
147
- await this.redis.del(this._getKey(key));
148
- } catch (error) {
149
- console.error('Redis delete error:', error);
150
- }
151
- }
152
-
153
- async clear() {
154
- try {
155
- const keys = await this.redis.keys(`${this.keyPrefix}*`);
156
- if (keys.length > 0) {
157
- await this.redis.del(keys);
158
- }
159
- } catch (error) {
160
- console.error('Redis clear error:', error);
161
- }
162
- }
163
-
164
- async exists(key) {
165
- try {
166
- const result = await this.redis.exists(this._getKey(key));
167
- return result === 1;
168
- } catch (error) {
169
- console.error('Redis exists error:', error);
170
- return false;
171
- }
172
- }
173
- }
174
-
175
- /**
176
- * Database session store adapter
177
- * Works with any SQL database through a provided database client
178
- */
179
- class DatabaseSessionStore extends SessionStore {
180
- constructor(database, options = {}) {
181
- super();
182
- this.db = database;
183
- this.tableName = options.tableName || 'sessions';
184
- this.keyColumn = options.keyColumn || 'session_key';
185
- this.valueColumn = options.valueColumn || 'session_data';
186
- this.expiresColumn = options.expiresColumn || 'expires_at';
187
- this.defaultTTL = options.defaultTTL || 3600;
188
- }
189
-
190
- async get(key) {
191
- try {
192
- const query = `
193
- SELECT ${this.valueColumn}
194
- FROM ${this.tableName}
195
- WHERE ${this.keyColumn} = ?
196
- AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
197
- `;
198
- const result = await this.db.query(query, [key, new Date()]);
199
-
200
- if (result.length === 0) {
201
- return null;
202
- }
203
-
204
- return JSON.parse(result[0][this.valueColumn]);
205
- } catch (error) {
206
- console.error('Database get error:', error);
207
- return null;
208
- }
209
- }
210
-
211
- async set(key, value, ttl = this.defaultTTL) {
212
- try {
213
- const serialized = JSON.stringify(value);
214
- const expiresAt = ttl > 0 ? new Date(Date.now() + ttl * 1000) : null;
215
-
216
- // Use UPSERT operation
217
- const query = `
218
- INSERT INTO ${this.tableName} (${this.keyColumn}, ${this.valueColumn}, ${this.expiresColumn})
219
- VALUES (?, ?, ?)
220
- ON DUPLICATE KEY UPDATE
221
- ${this.valueColumn} = VALUES(${this.valueColumn}),
222
- ${this.expiresColumn} = VALUES(${this.expiresColumn})
223
- `;
224
-
225
- await this.db.query(query, [key, serialized, expiresAt]);
226
- } catch (error) {
227
- console.error('Database set error:', error);
228
- throw error;
229
- }
230
- }
231
-
232
- async delete(key) {
233
- try {
234
- const query = `DELETE FROM ${this.tableName} WHERE ${this.keyColumn} = ?`;
235
- await this.db.query(query, [key]);
236
- } catch (error) {
237
- console.error('Database delete error:', error);
238
- }
239
- }
240
-
241
- async clear() {
242
- try {
243
- const query = `DELETE FROM ${this.tableName}`;
244
- await this.db.query(query);
245
- } catch (error) {
246
- console.error('Database clear error:', error);
247
- }
248
- }
249
-
250
- async exists(key) {
251
- try {
252
- const query = `
253
- SELECT 1 FROM ${this.tableName}
254
- WHERE ${this.keyColumn} = ?
255
- AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
256
- LIMIT 1
257
- `;
258
- const result = await this.db.query(query, [key, new Date()]);
259
- return result.length > 0;
260
- } catch (error) {
261
- console.error('Database exists error:', error);
262
- return false;
263
- }
264
- }
265
-
266
- // Cleanup expired sessions
267
- async cleanupExpired() {
268
- try {
269
- const query = `DELETE FROM ${this.tableName} WHERE ${this.expiresColumn} <= ?`;
270
- const result = await this.db.query(query, [new Date()]);
271
- return result.affectedRows || 0;
272
- } catch (error) {
273
- console.error('Database cleanup error:', error);
274
- return 0;
275
- }
276
- }
277
-
278
- // Get database schema for creating sessions table
279
- static getCreateTableSQL(tableName = 'sessions') {
280
- return `
281
- CREATE TABLE IF NOT EXISTS ${tableName} (
282
- session_key VARCHAR(255) PRIMARY KEY,
283
- session_data TEXT NOT NULL,
284
- expires_at TIMESTAMP NULL,
285
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
286
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
287
- INDEX idx_expires (expires_at)
288
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
289
- `;
290
- }
291
- }
292
-
293
- /**
294
- * User-managed session handler
295
- * Allows users to provide their own session management functions
296
- */
297
- class CustomSessionHandler {
298
- constructor(handlers) {
299
- this.getSession = handlers.getSession;
300
- this.setSession = handlers.setSession;
301
- this.deleteSession = handlers.deleteSession;
302
- this.validateSession = handlers.validateSession;
303
- }
304
-
305
- async get(key) {
306
- try {
307
- return await this.getSession(key);
308
- } catch (error) {
309
- console.error('Custom session get error:', error);
310
- return null;
311
- }
312
- }
313
-
314
- async set(key, value, ttl) {
315
- try {
316
- await this.setSession(key, value, ttl);
317
- } catch (error) {
318
- console.error('Custom session set error:', error);
319
- throw error;
320
- }
321
- }
322
-
323
- async delete(key) {
324
- try {
325
- await this.deleteSession(key);
326
- } catch (error) {
327
- console.error('Custom session delete error:', error);
328
- }
329
- }
330
-
331
- async validate(key) {
332
- try {
333
- if (this.validateSession) {
334
- return await this.validateSession(key);
335
- }
336
- return await this.get(key) !== null;
337
- } catch (error) {
338
- console.error('Custom session validate error:', error);
339
- return false;
340
- }
341
- }
342
- }
343
-
344
- /**
345
- * Main session manager class
346
- */
347
- class SessionManager {
348
- constructor(options = {}) {
349
- this.sessionTTL = options.sessionTTL || 3600; // 1 hour default
350
- this.autoRefresh = options.autoRefresh !== false;
351
- this.refreshThreshold = options.refreshThreshold || 0.8; // Refresh when 80% expired
352
-
353
- // Initialize session store
354
- if (options.store) {
355
- this.store = options.store;
356
- } else if (options.redis) {
357
- this.store = new RedisSessionStore(options.redis, options.redisOptions);
358
- } else if (options.database) {
359
- this.store = new DatabaseSessionStore(options.database, options.databaseOptions);
360
- } else if (options.customHandler) {
361
- this.store = new CustomSessionHandler(options.customHandler);
362
- } else {
363
- this.store = new MemorySessionStore();
364
- }
365
- }
366
-
367
- /**
368
- * Generate session key for a server
369
- */
370
- generateSessionKey(baseURL, username) {
371
- const hash = crypto
372
- .createHash('sha256')
373
- .update(`${baseURL}:${username}`)
374
- .digest('hex');
375
- return `session_${hash}`;
376
- }
377
-
378
- /**
379
- * Store session data
380
- */
381
- async storeSession(baseURL, username, sessionData) {
382
- const key = this.generateSessionKey(baseURL, username);
383
- const data = {
384
- ...sessionData,
385
- createdAt: Date.now(),
386
- baseURL,
387
- username
388
- };
389
-
390
- await this.store.set(key, data, this.sessionTTL);
391
- return key;
392
- }
393
-
394
- /**
395
- * Retrieve session data
396
- */
397
- async getSession(baseURL, username) {
398
- const key = this.generateSessionKey(baseURL, username);
399
- return await this.store.get(key);
400
- }
401
-
402
- /**
403
- * Check if session exists and is valid
404
- */
405
- async hasValidSession(baseURL, username) {
406
- const session = await this.getSession(baseURL, username);
407
- if (!session) {
408
- return false;
409
- }
410
-
411
- // Check if session needs refresh
412
- if (this.autoRefresh && this.shouldRefreshSession(session)) {
413
- return false; // Trigger re-authentication
414
- }
415
-
416
- return true;
417
- }
418
-
419
- /**
420
- * Check if session should be refreshed
421
- */
422
- shouldRefreshSession(session) {
423
- if (!session.createdAt) {
424
- return true;
425
- }
426
-
427
- const age = Date.now() - session.createdAt;
428
- const maxAge = this.sessionTTL * 1000;
429
- const threshold = maxAge * this.refreshThreshold;
430
-
431
- return age >= threshold;
432
- }
433
-
434
- /**
435
- * Delete session
436
- */
437
- async deleteSession(baseURL, username) {
438
- const key = this.generateSessionKey(baseURL, username);
439
- await this.store.delete(key);
440
- }
441
-
442
- /**
443
- * Clear all sessions
444
- */
445
- async clearAllSessions() {
446
- await this.store.clear();
447
- }
448
-
449
- /**
450
- * Get session statistics
451
- */
452
- async getStats() {
453
- if (this.store.getStats) {
454
- return await this.store.getStats();
455
- }
456
- return { message: 'Statistics not available for this store type' };
457
- }
458
- }
459
-
460
- // Export helper factory functions
461
- const createSessionManager = (options = {}) => {
462
- return new SessionManager(options);
463
- };
464
-
465
- const createMemoryStore = () => {
466
- return new MemorySessionStore();
467
- };
468
-
469
- const createRedisStore = (redisClient, options = {}) => {
470
- return new RedisSessionStore(redisClient, options);
471
- };
472
-
473
- const createDatabaseStore = (database, options = {}) => {
474
- return new DatabaseSessionStore(database, options);
475
- };
476
-
477
- const createCustomHandler = (handlers) => {
478
- return new CustomSessionHandler(handlers);
479
- };
480
-
481
- module.exports = {
482
- SessionManager,
483
- SessionStore,
484
- MemorySessionStore,
485
- RedisSessionStore,
486
- DatabaseSessionStore,
487
- CustomSessionHandler,
488
- createSessionManager,
489
- createMemoryStore,
490
- createRedisStore,
491
- createDatabaseStore,
492
- createCustomHandler
1
+ /**
2
+ * Session Management System for 3xui-api-client
3
+ * Provides both built-in caching and user-managed database integration
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+
8
+ /**
9
+ * Abstract base class for session storage
10
+ */
11
+ class SessionStore {
12
+ async get(_key) {
13
+ throw new Error('get method must be implemented');
14
+ }
15
+
16
+ async set(_key, _value, _ttl) {
17
+ throw new Error('set method must be implemented');
18
+ }
19
+
20
+ async delete(_key) {
21
+ throw new Error('delete method must be implemented');
22
+ }
23
+
24
+ async clear() {
25
+ throw new Error('clear method must be implemented');
26
+ }
27
+
28
+ async exists(key) {
29
+ const value = await this.get(key);
30
+ return value !== null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Built-in memory session store (default)
36
+ * Recommended for development and single-instance deployments
37
+ */
38
+ class MemorySessionStore extends SessionStore {
39
+ constructor() {
40
+ super();
41
+ this.cache = new Map();
42
+ this.timers = new Map();
43
+ }
44
+
45
+ async get(key) {
46
+ const item = this.cache.get(key);
47
+ if (!item) {
48
+ return null;
49
+ }
50
+
51
+ if (item.expires && item.expires <= Date.now()) {
52
+ this.delete(key);
53
+ return null;
54
+ }
55
+
56
+ return item.value;
57
+ }
58
+
59
+ async set(key, value, ttl = 3600) {
60
+ // Clear existing timer if it exists
61
+ const existingTimer = this.timers.get(key);
62
+ if (existingTimer) {
63
+ clearTimeout(existingTimer);
64
+ }
65
+
66
+ const expires = ttl > 0 ? Date.now() + (ttl * 1000) : null;
67
+ this.cache.set(key, { value, expires });
68
+
69
+ // Set expiration timer
70
+ if (ttl > 0) {
71
+ const timer = setTimeout(() => {
72
+ this.delete(key);
73
+ }, ttl * 1000);
74
+ this.timers.set(key, timer);
75
+ }
76
+ }
77
+
78
+ async delete(key) {
79
+ this.cache.delete(key);
80
+ const timer = this.timers.get(key);
81
+ if (timer) {
82
+ clearTimeout(timer);
83
+ this.timers.delete(key);
84
+ }
85
+ }
86
+
87
+ async clear() {
88
+ // Clear all timers
89
+ for (const timer of this.timers.values()) {
90
+ clearTimeout(timer);
91
+ }
92
+ this.cache.clear();
93
+ this.timers.clear();
94
+ }
95
+
96
+ // Get cache statistics
97
+ getStats() {
98
+ return {
99
+ size: this.cache.size,
100
+ keys: Array.from(this.cache.keys())
101
+ };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Redis session store adapter
107
+ * Recommended for production and multi-instance deployments
108
+ */
109
+ class RedisSessionStore extends SessionStore {
110
+ constructor(redisClient, options = {}) {
111
+ super();
112
+ this.redis = redisClient;
113
+ this.keyPrefix = options.keyPrefix || '3xui:session:';
114
+ this.defaultTTL = options.defaultTTL || 3600;
115
+ }
116
+
117
+ _getKey(key) {
118
+ return `${this.keyPrefix}${key}`;
119
+ }
120
+
121
+ async get(key) {
122
+ try {
123
+ const value = await this.redis.get(this._getKey(key));
124
+ return value ? JSON.parse(value) : null;
125
+ } catch (error) {
126
+ console.error('Redis get error:', error);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ async set(key, value, ttl = this.defaultTTL) {
132
+ try {
133
+ const serialized = JSON.stringify(value);
134
+ if (ttl > 0) {
135
+ await this.redis.setex(this._getKey(key), ttl, serialized);
136
+ } else {
137
+ await this.redis.set(this._getKey(key), serialized);
138
+ }
139
+ } catch (error) {
140
+ console.error('Redis set error:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ async delete(key) {
146
+ try {
147
+ await this.redis.del(this._getKey(key));
148
+ } catch (error) {
149
+ console.error('Redis delete error:', error);
150
+ }
151
+ }
152
+
153
+ async clear() {
154
+ try {
155
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
156
+ if (keys.length > 0) {
157
+ await this.redis.del(keys);
158
+ }
159
+ } catch (error) {
160
+ console.error('Redis clear error:', error);
161
+ }
162
+ }
163
+
164
+ async exists(key) {
165
+ try {
166
+ const result = await this.redis.exists(this._getKey(key));
167
+ return result === 1;
168
+ } catch (error) {
169
+ console.error('Redis exists error:', error);
170
+ return false;
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Database session store adapter
177
+ * Works with any SQL database through a provided database client
178
+ */
179
+ class DatabaseSessionStore extends SessionStore {
180
+ constructor(database, options = {}) {
181
+ super();
182
+ this.db = database;
183
+ this.tableName = options.tableName || 'sessions';
184
+ this.keyColumn = options.keyColumn || 'session_key';
185
+ this.valueColumn = options.valueColumn || 'session_data';
186
+ this.expiresColumn = options.expiresColumn || 'expires_at';
187
+ this.defaultTTL = options.defaultTTL || 3600;
188
+ }
189
+
190
+ async get(key) {
191
+ try {
192
+ const query = `
193
+ SELECT ${this.valueColumn}
194
+ FROM ${this.tableName}
195
+ WHERE ${this.keyColumn} = ?
196
+ AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
197
+ `;
198
+ const result = await this.db.query(query, [key, new Date()]);
199
+
200
+ if (result.length === 0) {
201
+ return null;
202
+ }
203
+
204
+ return JSON.parse(result[0][this.valueColumn]);
205
+ } catch (error) {
206
+ console.error('Database get error:', error);
207
+ return null;
208
+ }
209
+ }
210
+
211
+ async set(key, value, ttl = this.defaultTTL) {
212
+ try {
213
+ const serialized = JSON.stringify(value);
214
+ const expiresAt = ttl > 0 ? new Date(Date.now() + ttl * 1000) : null;
215
+
216
+ // Use UPSERT operation
217
+ const query = `
218
+ INSERT INTO ${this.tableName} (${this.keyColumn}, ${this.valueColumn}, ${this.expiresColumn})
219
+ VALUES (?, ?, ?)
220
+ ON DUPLICATE KEY UPDATE
221
+ ${this.valueColumn} = VALUES(${this.valueColumn}),
222
+ ${this.expiresColumn} = VALUES(${this.expiresColumn})
223
+ `;
224
+
225
+ await this.db.query(query, [key, serialized, expiresAt]);
226
+ } catch (error) {
227
+ console.error('Database set error:', error);
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ async delete(key) {
233
+ try {
234
+ const query = `DELETE FROM ${this.tableName} WHERE ${this.keyColumn} = ?`;
235
+ await this.db.query(query, [key]);
236
+ } catch (error) {
237
+ console.error('Database delete error:', error);
238
+ }
239
+ }
240
+
241
+ async clear() {
242
+ try {
243
+ const query = `DELETE FROM ${this.tableName}`;
244
+ await this.db.query(query);
245
+ } catch (error) {
246
+ console.error('Database clear error:', error);
247
+ }
248
+ }
249
+
250
+ async exists(key) {
251
+ try {
252
+ const query = `
253
+ SELECT 1 FROM ${this.tableName}
254
+ WHERE ${this.keyColumn} = ?
255
+ AND (${this.expiresColumn} IS NULL OR ${this.expiresColumn} > ?)
256
+ LIMIT 1
257
+ `;
258
+ const result = await this.db.query(query, [key, new Date()]);
259
+ return result.length > 0;
260
+ } catch (error) {
261
+ console.error('Database exists error:', error);
262
+ return false;
263
+ }
264
+ }
265
+
266
+ // Cleanup expired sessions
267
+ async cleanupExpired() {
268
+ try {
269
+ const query = `DELETE FROM ${this.tableName} WHERE ${this.expiresColumn} <= ?`;
270
+ const result = await this.db.query(query, [new Date()]);
271
+ return result.affectedRows || 0;
272
+ } catch (error) {
273
+ console.error('Database cleanup error:', error);
274
+ return 0;
275
+ }
276
+ }
277
+
278
+ // Get database schema for creating sessions table
279
+ static getCreateTableSQL(tableName = 'sessions') {
280
+ return `
281
+ CREATE TABLE IF NOT EXISTS ${tableName} (
282
+ session_key VARCHAR(255) PRIMARY KEY,
283
+ session_data TEXT NOT NULL,
284
+ expires_at TIMESTAMP NULL,
285
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
286
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
287
+ INDEX idx_expires (expires_at)
288
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
289
+ `;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * User-managed session handler
295
+ * Allows users to provide their own session management functions
296
+ */
297
+ class CustomSessionHandler {
298
+ constructor(handlers) {
299
+ this.getSession = handlers.getSession;
300
+ this.setSession = handlers.setSession;
301
+ this.deleteSession = handlers.deleteSession;
302
+ this.validateSession = handlers.validateSession;
303
+ }
304
+
305
+ async get(key) {
306
+ try {
307
+ return await this.getSession(key);
308
+ } catch (error) {
309
+ console.error('Custom session get error:', error);
310
+ return null;
311
+ }
312
+ }
313
+
314
+ async set(key, value, ttl) {
315
+ try {
316
+ await this.setSession(key, value, ttl);
317
+ } catch (error) {
318
+ console.error('Custom session set error:', error);
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ async delete(key) {
324
+ try {
325
+ await this.deleteSession(key);
326
+ } catch (error) {
327
+ console.error('Custom session delete error:', error);
328
+ }
329
+ }
330
+
331
+ async validate(key) {
332
+ try {
333
+ if (this.validateSession) {
334
+ return await this.validateSession(key);
335
+ }
336
+ return await this.get(key) !== null;
337
+ } catch (error) {
338
+ console.error('Custom session validate error:', error);
339
+ return false;
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Main session manager class
346
+ */
347
+ class SessionManager {
348
+ constructor(options = {}) {
349
+ this.sessionTTL = options.sessionTTL || 3600; // 1 hour default
350
+ this.autoRefresh = options.autoRefresh !== false;
351
+ this.refreshThreshold = options.refreshThreshold || 0.8; // Refresh when 80% expired
352
+
353
+ // Initialize session store
354
+ if (options.store) {
355
+ this.store = options.store;
356
+ } else if (options.redis) {
357
+ this.store = new RedisSessionStore(options.redis, options.redisOptions);
358
+ } else if (options.database) {
359
+ this.store = new DatabaseSessionStore(options.database, options.databaseOptions);
360
+ } else if (options.customHandler) {
361
+ this.store = new CustomSessionHandler(options.customHandler);
362
+ } else {
363
+ this.store = new MemorySessionStore();
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Generate session key for a server
369
+ */
370
+ generateSessionKey(baseURL, username) {
371
+ const hash = crypto
372
+ .createHash('sha256')
373
+ .update(`${baseURL}:${username}`)
374
+ .digest('hex');
375
+ return `session_${hash}`;
376
+ }
377
+
378
+ /**
379
+ * Store session data
380
+ */
381
+ async storeSession(baseURL, username, sessionData) {
382
+ const key = this.generateSessionKey(baseURL, username);
383
+ const data = {
384
+ ...sessionData,
385
+ createdAt: Date.now(),
386
+ baseURL,
387
+ username
388
+ };
389
+
390
+ await this.store.set(key, data, this.sessionTTL);
391
+ return key;
392
+ }
393
+
394
+ /**
395
+ * Retrieve session data
396
+ */
397
+ async getSession(baseURL, username) {
398
+ const key = this.generateSessionKey(baseURL, username);
399
+ return await this.store.get(key);
400
+ }
401
+
402
+ /**
403
+ * Check if session exists and is valid
404
+ */
405
+ async hasValidSession(baseURL, username) {
406
+ const session = await this.getSession(baseURL, username);
407
+ if (!session) {
408
+ return false;
409
+ }
410
+
411
+ // Check if session needs refresh
412
+ if (this.autoRefresh && this.shouldRefreshSession(session)) {
413
+ return false; // Trigger re-authentication
414
+ }
415
+
416
+ return true;
417
+ }
418
+
419
+ /**
420
+ * Check if session should be refreshed
421
+ */
422
+ shouldRefreshSession(session) {
423
+ if (!session.createdAt) {
424
+ return true;
425
+ }
426
+
427
+ const age = Date.now() - session.createdAt;
428
+ const maxAge = this.sessionTTL * 1000;
429
+ const threshold = maxAge * this.refreshThreshold;
430
+
431
+ return age >= threshold;
432
+ }
433
+
434
+ /**
435
+ * Delete session
436
+ */
437
+ async deleteSession(baseURL, username) {
438
+ const key = this.generateSessionKey(baseURL, username);
439
+ await this.store.delete(key);
440
+ }
441
+
442
+ /**
443
+ * Clear all sessions
444
+ */
445
+ async clearAllSessions() {
446
+ await this.store.clear();
447
+ }
448
+
449
+ /**
450
+ * Get session statistics
451
+ */
452
+ async getStats() {
453
+ if (this.store.getStats) {
454
+ return await this.store.getStats();
455
+ }
456
+ return { message: 'Statistics not available for this store type' };
457
+ }
458
+ }
459
+
460
+ // Export helper factory functions
461
+ const createSessionManager = (options = {}) => {
462
+ return new SessionManager(options);
463
+ };
464
+
465
+ const createMemoryStore = () => {
466
+ return new MemorySessionStore();
467
+ };
468
+
469
+ const createRedisStore = (redisClient, options = {}) => {
470
+ return new RedisSessionStore(redisClient, options);
471
+ };
472
+
473
+ const createDatabaseStore = (database, options = {}) => {
474
+ return new DatabaseSessionStore(database, options);
475
+ };
476
+
477
+ const createCustomHandler = (handlers) => {
478
+ return new CustomSessionHandler(handlers);
479
+ };
480
+
481
+ module.exports = {
482
+ SessionManager,
483
+ SessionStore,
484
+ MemorySessionStore,
485
+ RedisSessionStore,
486
+ DatabaseSessionStore,
487
+ CustomSessionHandler,
488
+ createSessionManager,
489
+ createMemoryStore,
490
+ createRedisStore,
491
+ createDatabaseStore,
492
+ createCustomHandler
493
493
  };