@joystick.js/db-canary 0.0.0-canary.2295 → 0.0.0-canary.2297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,745 @@
1
+ /**
2
+ * @fileoverview User-based authentication manager with username/password validation.
3
+ *
4
+ * Provides comprehensive user authentication services including user management,
5
+ * username/password validation, rate limiting with exponential backoff, and secure
6
+ * user storage in admin database. Replaces single-password authentication with
7
+ * proper user management system.
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import bcrypt from 'bcrypt';
12
+ import crypto from 'crypto';
13
+ import create_logger from './logger.js';
14
+
15
+ const { create_context_logger } = create_logger('user_auth_manager');
16
+ const log = create_context_logger();
17
+
18
+ import { load_settings as load_joystick_settings, has_settings } from './load_settings.js';
19
+
20
+ /** @type {number} BCrypt salt rounds for password hashing */
21
+ const SALT_ROUNDS = 12;
22
+
23
+ /** @type {number} Rate limiting window in milliseconds (1 minute) */
24
+ const RATE_LIMIT_WINDOW = 60 * 1000;
25
+
26
+ /** @type {number} Maximum failed attempts before rate limiting */
27
+ const MAX_FAILED_ATTEMPTS = 5;
28
+
29
+ /** @type {number} Base backoff duration in milliseconds */
30
+ const BACKOFF_BASE = 1000;
31
+
32
+ /** @type {number} Backoff multiplier for exponential backoff */
33
+ const BACKOFF_MULTIPLIER = 2;
34
+
35
+ /** @type {string} Admin database name for server-level data */
36
+ const ADMIN_DATABASE = '_admin';
37
+
38
+ /** @type {string} Users collection name */
39
+ const USERS_COLLECTION = '_users';
40
+
41
+ /** @type {Object|null} Cached settings data */
42
+ let settings_data = null;
43
+
44
+ /** @type {Map<string, Array<number>>} Map of IP addresses to failed attempt timestamps */
45
+ let failed_attempts = new Map();
46
+
47
+ /** @type {Map<string, Object>} Map of IP addresses to rate limit information */
48
+ let rate_limits = new Map();
49
+
50
+ /** @type {Object|null} Reference to storage engine for user data */
51
+ let storage_engine = null;
52
+
53
+ /**
54
+ * Sets the storage engine reference for user data operations.
55
+ * @param {Object} engine - Storage engine instance
56
+ */
57
+ const set_storage_engine = (engine) => {
58
+ storage_engine = engine;
59
+ };
60
+
61
+ /**
62
+ * Generates a cryptographically secure random password.
63
+ * @returns {string} 32-character hexadecimal password
64
+ */
65
+ const generate_secure_password = () => {
66
+ return crypto.randomBytes(16).toString('hex');
67
+ };
68
+
69
+ /**
70
+ * Loads settings data from environment variable.
71
+ * @returns {Object|null} Loaded settings data or null if not available
72
+ */
73
+ const load_settings_data = () => {
74
+ try {
75
+ if (!has_settings()) {
76
+ return null;
77
+ }
78
+
79
+ const parsed_data = load_joystick_settings();
80
+ settings_data = parsed_data;
81
+
82
+ if (parsed_data.authentication && parsed_data.authentication.failed_attempts) {
83
+ failed_attempts = new Map(Object.entries(parsed_data.authentication.failed_attempts));
84
+ }
85
+
86
+ if (parsed_data.authentication && parsed_data.authentication.rate_limits) {
87
+ rate_limits = new Map(Object.entries(parsed_data.authentication.rate_limits));
88
+ }
89
+
90
+ log.info('Settings data loaded successfully from environment variable');
91
+ return settings_data;
92
+ } catch (error) {
93
+ log.error('Failed to load settings data', { error: error.message });
94
+ throw new Error(`Failed to load settings data: ${error.message}`);
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Updates the JOYSTICK_DB_SETTINGS environment variable with current state.
100
+ */
101
+ const save_settings_data = () => {
102
+ try {
103
+ if (!settings_data) {
104
+ // NOTE: Create minimal settings data if none exists (for tests)
105
+ settings_data = {
106
+ port: 1983,
107
+ authentication: {}
108
+ };
109
+ }
110
+
111
+ if (!settings_data.authentication) {
112
+ settings_data.authentication = {};
113
+ }
114
+
115
+ settings_data.authentication.failed_attempts = Object.fromEntries(failed_attempts);
116
+ settings_data.authentication.rate_limits = Object.fromEntries(rate_limits);
117
+
118
+ process.env.JOYSTICK_DB_SETTINGS = JSON.stringify(settings_data);
119
+
120
+ log.info('Settings data saved successfully to environment variable');
121
+ } catch (error) {
122
+ log.error('Failed to save settings data', { error: error.message });
123
+ // NOTE: Don't throw in test environment to avoid breaking tests
124
+ if (process.env.NODE_ENV !== 'test') {
125
+ throw new Error(`Failed to save settings data: ${error.message}`);
126
+ }
127
+ }
128
+ };
129
+
130
+ /**
131
+ * Checks if an IP address is whitelisted (localhost addresses).
132
+ * @param {string} ip - IP address to check
133
+ * @returns {boolean} True if IP is whitelisted
134
+ */
135
+ const is_ip_whitelisted = (ip) => {
136
+ if (process.env.NODE_ENV === 'test') {
137
+ return false;
138
+ }
139
+
140
+ const whitelisted_ips = ['127.0.0.1', '::1', 'localhost'];
141
+ return whitelisted_ips.includes(ip);
142
+ };
143
+
144
+ /**
145
+ * Extracts client IP address from socket connection.
146
+ * @param {net.Socket} socket - Socket connection
147
+ * @returns {string} Client IP address or localhost fallback
148
+ */
149
+ const get_client_ip = (socket) => {
150
+ return socket.remoteAddress || '127.0.0.1';
151
+ };
152
+
153
+ /**
154
+ * Checks if an IP address is currently rate limited.
155
+ * @param {string} ip - IP address to check
156
+ * @returns {boolean} True if IP is rate limited
157
+ */
158
+ const is_rate_limited = (ip) => {
159
+ if (is_ip_whitelisted(ip)) {
160
+ return false;
161
+ }
162
+
163
+ const now = Date.now();
164
+ const attempts = failed_attempts.get(ip) || [];
165
+
166
+ const recent_attempts = attempts.filter(timestamp => now - timestamp < RATE_LIMIT_WINDOW);
167
+
168
+ if (recent_attempts.length >= MAX_FAILED_ATTEMPTS) {
169
+ const rate_limit_info = rate_limits.get(ip);
170
+ if (rate_limit_info && now < rate_limit_info.expires_at) {
171
+ return true;
172
+ }
173
+
174
+ const backoff_duration = Math.min(
175
+ BACKOFF_BASE * Math.pow(BACKOFF_MULTIPLIER, Math.floor(recent_attempts.length / MAX_FAILED_ATTEMPTS)),
176
+ 30 * 60 * 1000
177
+ );
178
+
179
+ rate_limits.set(ip, {
180
+ expires_at: now + backoff_duration,
181
+ attempts: recent_attempts.length
182
+ });
183
+
184
+ save_settings_data();
185
+
186
+ log.warn('IP rate limited', {
187
+ ip,
188
+ attempts: recent_attempts.length,
189
+ backoff_duration_ms: backoff_duration
190
+ });
191
+
192
+ return true;
193
+ }
194
+
195
+ return false;
196
+ };
197
+
198
+ /**
199
+ * Records a failed authentication attempt for an IP address.
200
+ * @param {string} ip - IP address that failed authentication
201
+ */
202
+ const record_failed_attempt = (ip) => {
203
+ if (is_ip_whitelisted(ip)) {
204
+ return;
205
+ }
206
+
207
+ const now = Date.now();
208
+ const attempts = failed_attempts.get(ip) || [];
209
+ attempts.push(now);
210
+
211
+ const recent_attempts = attempts.filter(timestamp => now - timestamp < RATE_LIMIT_WINDOW);
212
+ failed_attempts.set(ip, recent_attempts);
213
+
214
+ log.warn('Failed authentication attempt recorded', {
215
+ ip,
216
+ total_recent_attempts: recent_attempts.length
217
+ });
218
+
219
+ save_settings_data();
220
+ };
221
+
222
+ /**
223
+ * Clears failed attempts and rate limits for an IP address.
224
+ * @param {string} ip - IP address to clear
225
+ */
226
+ const clear_failed_attempts = (ip) => {
227
+ failed_attempts.delete(ip);
228
+ rate_limits.delete(ip);
229
+ save_settings_data();
230
+ };
231
+
232
+ /**
233
+ * Creates the user key for storage.
234
+ * @param {string} username - Username
235
+ * @returns {string} Storage key for user
236
+ */
237
+ const create_user_key = (username) => {
238
+ return `${ADMIN_DATABASE}:${USERS_COLLECTION}:${username}`;
239
+ };
240
+
241
+ /**
242
+ * Creates a user object with required fields.
243
+ * @param {string} username - Username
244
+ * @param {string} password - Plain text password
245
+ * @param {string} role - User role (default: 'user')
246
+ * @returns {Object} User object with hashed password
247
+ */
248
+ const create_user_object = async (username, password, role = 'user') => {
249
+ const password_hash = await bcrypt.hash(password, SALT_ROUNDS);
250
+ const now = new Date().toISOString();
251
+
252
+ return {
253
+ username,
254
+ password_hash,
255
+ role,
256
+ active: true,
257
+ created_at: now,
258
+ last_login: null
259
+ };
260
+ };
261
+
262
+ /**
263
+ * Creates a new user in the system.
264
+ * @param {string} username - Username (must be unique)
265
+ * @param {string} password - Plain text password
266
+ * @param {string} email - User email
267
+ * @param {string} role - User role (default: 'user')
268
+ * @returns {Promise<Object>} Created user object or error result
269
+ */
270
+ const create_user = async (username, password, email, role = 'user') => {
271
+ try {
272
+ if (!storage_engine) {
273
+ return { success: false, error: 'Storage engine not initialized' };
274
+ }
275
+
276
+ if (!username || !password) {
277
+ return { success: false, error: 'Username and password are required' };
278
+ }
279
+
280
+ if (typeof username !== 'string' || username.trim().length === 0) {
281
+ return { success: false, error: 'Username is required and must be a non-empty string' };
282
+ }
283
+
284
+ if (typeof password !== 'string' || password.length < 6) {
285
+ return { success: false, error: 'Password is required and must be at least 6 characters' };
286
+ }
287
+
288
+ if (role && !['admin', 'user'].includes(role)) {
289
+ return { success: false, error: 'Role must be either "admin" or "user"' };
290
+ }
291
+
292
+ const user_key = create_user_key(username.trim().toLowerCase());
293
+
294
+ // NOTE: Check if user already exists.
295
+ const existing_user = await storage_engine.get(user_key);
296
+ if (existing_user) {
297
+ return { success: false, error: 'User already exists' };
298
+ }
299
+
300
+ const user_object = await create_user_object(username.trim().toLowerCase(), password, role);
301
+
302
+ await storage_engine.put(user_key, user_object);
303
+
304
+ log.info('User created successfully', {
305
+ username: user_object.username,
306
+ role: user_object.role
307
+ });
308
+
309
+ const { password_hash, ...user_without_password } = user_object;
310
+ return {
311
+ success: true,
312
+ message: 'User created successfully',
313
+ ok: 1,
314
+ user: user_without_password
315
+ };
316
+ } catch (error) {
317
+ return { success: false, error: error.message };
318
+ }
319
+ };
320
+
321
+ /**
322
+ * Retrieves a user by username.
323
+ * @param {string} username - Username to retrieve
324
+ * @returns {Promise<Object>} User object or error result
325
+ */
326
+ const get_user = async (username) => {
327
+ try {
328
+ if (!storage_engine) {
329
+ return { success: false, error: 'Storage engine not initialized' };
330
+ }
331
+
332
+ if (!username) {
333
+ return { success: false, error: 'Username is required' };
334
+ }
335
+
336
+ const user_key = create_user_key(username.trim().toLowerCase());
337
+ const user = await storage_engine.get(user_key);
338
+
339
+ if (!user) {
340
+ return { success: false, error: 'User not found' };
341
+ }
342
+
343
+ const { password_hash, ...user_without_password } = user;
344
+ return { success: true, user: user_without_password };
345
+ } catch (error) {
346
+ return { success: false, error: error.message };
347
+ }
348
+ };
349
+
350
+ /**
351
+ * Updates a user's information.
352
+ * @param {string} username - Username to update
353
+ * @param {Object} updates - Fields to update
354
+ * @returns {Promise<Object>} Update result or error
355
+ */
356
+ const update_user = async (username, updates) => {
357
+ try {
358
+ if (!storage_engine) {
359
+ return { success: false, error: 'Storage engine not initialized' };
360
+ }
361
+
362
+ if (!username) {
363
+ return { success: false, error: 'Username is required' };
364
+ }
365
+
366
+ // NOTE: Get user directly from storage without success/error wrapper
367
+ const user_key = create_user_key(username.trim().toLowerCase());
368
+ const user = await storage_engine.get(user_key);
369
+
370
+ if (!user) {
371
+ return { success: false, error: `User '${username}' not found` };
372
+ }
373
+
374
+ // NOTE: Validate email format if provided
375
+ if (updates.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(updates.email)) {
376
+ return { success: false, error: 'Invalid email format' };
377
+ }
378
+
379
+ // NOTE: Validate role if provided
380
+ if (updates.role && !['admin', 'user'].includes(updates.role)) {
381
+ return { success: false, error: 'Role must be either "admin" or "user"' };
382
+ }
383
+
384
+ const allowed_updates = ['role', 'active', 'email'];
385
+ const filtered_updates = {};
386
+
387
+ for (const [key, value] of Object.entries(updates)) {
388
+ if (allowed_updates.includes(key)) {
389
+ filtered_updates[key] = value;
390
+ }
391
+ }
392
+
393
+ const updated_user = {
394
+ ...user,
395
+ ...filtered_updates
396
+ };
397
+
398
+ await storage_engine.put(user_key, updated_user);
399
+
400
+ log.info('User updated successfully', {
401
+ username,
402
+ updates: filtered_updates
403
+ });
404
+
405
+ return { success: true, message: 'User updated successfully' };
406
+ } catch (error) {
407
+ return { success: false, error: error.message };
408
+ }
409
+ };
410
+
411
+ /**
412
+ * Resets a user's password.
413
+ * @param {string} username - Username
414
+ * @param {string} new_password - New plain text password
415
+ * @returns {Promise<Object>} Reset result or error
416
+ */
417
+ const reset_user_password = async (username, new_password) => {
418
+ try {
419
+ if (!storage_engine) {
420
+ return { success: false, error: 'Storage engine not initialized' };
421
+ }
422
+
423
+ if (!new_password || typeof new_password !== 'string' || new_password.length < 6) {
424
+ return { success: false, error: 'Password must be at least 6 characters long' };
425
+ }
426
+
427
+ // NOTE: Get user directly from storage
428
+ const user_key = create_user_key(username.trim().toLowerCase());
429
+ const user = await storage_engine.get(user_key);
430
+
431
+ if (!user) {
432
+ return { success: false, error: `User '${username}' not found` };
433
+ }
434
+
435
+ const password_hash = await bcrypt.hash(new_password, SALT_ROUNDS);
436
+ const updated_user = {
437
+ ...user,
438
+ password_hash
439
+ };
440
+
441
+ await storage_engine.put(user_key, updated_user);
442
+
443
+ log.info('User password reset successfully', { username });
444
+ return { success: true, message: 'Password reset successfully' };
445
+ } catch (error) {
446
+ return { success: false, error: error.message };
447
+ }
448
+ };
449
+
450
+ /**
451
+ * Deletes a user from the system.
452
+ * @param {string} username - Username to delete
453
+ * @returns {Promise<Object>} Delete result or error
454
+ */
455
+ const delete_user = async (username) => {
456
+ try {
457
+ if (!storage_engine) {
458
+ return { success: false, error: 'Storage engine not initialized' };
459
+ }
460
+
461
+ // NOTE: Get user directly from storage
462
+ const user_key = create_user_key(username.trim().toLowerCase());
463
+ const user = await storage_engine.get(user_key);
464
+
465
+ if (!user) {
466
+ return { success: false, error: 'User not found' };
467
+ }
468
+
469
+ await storage_engine.del(user_key);
470
+
471
+ log.info('User deleted successfully', { username });
472
+ return { success: true, message: 'User deleted successfully' };
473
+ } catch (error) {
474
+ return { success: false, error: error.message };
475
+ }
476
+ };
477
+
478
+ /**
479
+ * Lists all users in the system.
480
+ * @returns {Promise<Object>} User list result or error
481
+ */
482
+ const list_users = async () => {
483
+ try {
484
+ if (!storage_engine) {
485
+ return { success: false, error: 'Storage engine not initialized' };
486
+ }
487
+
488
+ const users = [];
489
+ const user_prefix = `${ADMIN_DATABASE}:${USERS_COLLECTION}:`;
490
+
491
+ // NOTE: Use LMDB's getRange method to scan for user keys.
492
+ try {
493
+ for (const { key, value } of storage_engine.getRange({
494
+ start: user_prefix,
495
+ end: user_prefix + '\xFF'
496
+ })) {
497
+ if (value && typeof value === 'object') {
498
+ const { password_hash, ...user_without_password } = value;
499
+ users.push(user_without_password);
500
+ }
501
+ }
502
+ } catch (error) {
503
+ log.error('Failed to list users', { error: error.message });
504
+ return { success: false, error: error.message };
505
+ }
506
+
507
+ return { success: true, users: users.sort((a, b) => a.username.localeCompare(b.username)) };
508
+ } catch (error) {
509
+ return { success: false, error: error.message };
510
+ }
511
+ };
512
+
513
+ /**
514
+ * Verifies username and password credentials.
515
+ * @param {string} username - Username
516
+ * @param {string} password - Plain text password
517
+ * @param {string} ip - Client IP address for rate limiting
518
+ * @returns {Promise<Object>} Authentication result or error
519
+ */
520
+ const verify_credentials = async (username, password, ip) => {
521
+ try {
522
+ if (!storage_engine) {
523
+ return { success: false, error: 'Storage engine not initialized' };
524
+ }
525
+
526
+ if (!username || !password) {
527
+ return { success: false, error: 'Username and password are required' };
528
+ }
529
+
530
+ if (is_rate_limited(ip)) {
531
+ return { success: false, error: 'Too many failed attempts' };
532
+ }
533
+
534
+ const start_time = Date.now();
535
+
536
+ try {
537
+ // NOTE: Get user directly from storage
538
+ const user_key = create_user_key(username.trim().toLowerCase());
539
+ const user = await storage_engine.get(user_key);
540
+
541
+ if (!user) {
542
+ record_failed_attempt(ip);
543
+ log.warn('Authentication failed - user not found', { username, ip });
544
+ return { success: false, error: 'Invalid credentials' };
545
+ }
546
+
547
+ if (!user.active) {
548
+ record_failed_attempt(ip);
549
+ log.warn('Authentication failed - user inactive', { username, ip });
550
+ return { success: false, error: 'Account is disabled' };
551
+ }
552
+
553
+ const is_valid = await bcrypt.compare(password, user.password_hash);
554
+
555
+ const verification_time = Date.now() - start_time;
556
+ const min_time = 100;
557
+ if (verification_time < min_time) {
558
+ await new Promise(resolve => setTimeout(resolve, min_time - verification_time));
559
+ }
560
+
561
+ if (is_valid) {
562
+ clear_failed_attempts(ip);
563
+
564
+ // NOTE: Update last login timestamp.
565
+ const updated_user = {
566
+ ...user,
567
+ last_login: new Date().toISOString()
568
+ };
569
+
570
+ await storage_engine.put(user_key, updated_user);
571
+
572
+ log.info('Authentication successful', { username, ip });
573
+
574
+ const { password_hash, ...user_without_password } = updated_user;
575
+ return { success: true, user: user_without_password };
576
+ } else {
577
+ record_failed_attempt(ip);
578
+ log.warn('Authentication failed - invalid password', { username, ip });
579
+ return { success: false, error: 'Invalid credentials' };
580
+ }
581
+ } catch (error) {
582
+ record_failed_attempt(ip);
583
+ log.error('Authentication error', { username, ip, error: error.message });
584
+ return { success: false, error: error.message };
585
+ }
586
+ } catch (error) {
587
+ return { success: false, error: error.message };
588
+ }
589
+ };
590
+
591
+ /**
592
+ * Sets up initial admin user if no users exist.
593
+ * @param {string} username - Admin username
594
+ * @param {string} password - Admin password
595
+ * @param {string} email - Admin email (optional)
596
+ * @returns {Promise<Object>} Setup result with admin credentials
597
+ */
598
+ const setup_initial_admin = async (username, password, email = null) => {
599
+ try {
600
+ if (!storage_engine) {
601
+ return { success: false, error: 'Storage engine not initialized' };
602
+ }
603
+
604
+ // NOTE: Validate required fields.
605
+ if (!username || !password || (email !== null && !email)) {
606
+ return { success: false, error: 'Username, password, and email are required' };
607
+ }
608
+
609
+ if (password.length < 6) {
610
+ return { success: false, error: 'Password must be at least 6 characters long' };
611
+ }
612
+
613
+ if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
614
+ return { success: false, error: 'Invalid email format' };
615
+ }
616
+
617
+ // NOTE: Check if any users already exist.
618
+ const existing_users_result = await list_users();
619
+ if (existing_users_result.success && existing_users_result.users.length > 0) {
620
+ return { success: false, error: 'Initial admin user already exists' };
621
+ }
622
+
623
+ // NOTE: Create admin user with email parameter
624
+ const admin_user_result = await create_user(username, password, email || `${username}@localhost`, 'admin');
625
+
626
+ if (!admin_user_result.success) {
627
+ return admin_user_result;
628
+ }
629
+
630
+ const now = new Date().toISOString();
631
+
632
+ // NOTE: Update settings to mark authentication as configured.
633
+ if (!settings_data) {
634
+ load_settings_data();
635
+ }
636
+
637
+ if (!settings_data) {
638
+ settings_data = {
639
+ port: 1983,
640
+ cluster: true,
641
+ worker_count: 2,
642
+ authentication: {},
643
+ backup: { enabled: false },
644
+ replication: { enabled: false, role: "primary" },
645
+ auto_indexing: { enabled: true, threshold: 100 },
646
+ performance: {
647
+ monitoring_enabled: true,
648
+ log_slow_queries: true,
649
+ slow_query_threshold_ms: 1000
650
+ },
651
+ logging: { level: "info", structured: true }
652
+ };
653
+ }
654
+
655
+ settings_data.authentication = {
656
+ user_based: true,
657
+ created_at: now,
658
+ last_updated: now,
659
+ failed_attempts: {},
660
+ rate_limits: {}
661
+ };
662
+
663
+ save_settings_data();
664
+
665
+ log.info('Initial admin setup completed', {
666
+ admin_username: admin_user_result.username,
667
+ created_at: now
668
+ });
669
+
670
+ return {
671
+ success: true,
672
+ admin_user: admin_user_result,
673
+ message: 'Initial admin user created successfully',
674
+ ok: 1
675
+ };
676
+ } catch (error) {
677
+ return { success: false, error: error.message };
678
+ }
679
+ };
680
+
681
+ /**
682
+ * Gets authentication statistics and status information.
683
+ * @returns {Promise<Object>} Authentication statistics
684
+ */
685
+ const get_auth_stats = async () => {
686
+ const auth_configured = !!(settings_data && settings_data.authentication && settings_data.authentication.user_based);
687
+ let user_count = 0;
688
+
689
+ if (auth_configured) {
690
+ const users_result = await list_users();
691
+ user_count = users_result.success ? users_result.users.length : 0;
692
+ }
693
+
694
+ return {
695
+ configured: auth_configured,
696
+ user_based: true,
697
+ user_count,
698
+ failed_attempts_count: failed_attempts.size,
699
+ rate_limited_ips: rate_limits.size,
700
+ created_at: settings_data?.authentication?.created_at || null,
701
+ last_updated: settings_data?.authentication?.last_updated || null
702
+ };
703
+ };
704
+
705
+ /**
706
+ * Initializes the user authentication manager.
707
+ */
708
+ const initialize_user_auth_manager = () => {
709
+ try {
710
+ load_settings_data();
711
+ } catch (error) {
712
+ log.warn('Could not load settings data on startup', { error: error.message });
713
+ }
714
+ };
715
+
716
+ /**
717
+ * Resets authentication state (used for testing).
718
+ */
719
+ const reset_auth_state = () => {
720
+ settings_data = null;
721
+ failed_attempts = new Map();
722
+ rate_limits = new Map();
723
+ storage_engine = null;
724
+
725
+ if (process.env.JOYSTICK_DB_SETTINGS) {
726
+ delete process.env.JOYSTICK_DB_SETTINGS;
727
+ }
728
+ };
729
+
730
+ export {
731
+ set_storage_engine,
732
+ create_user,
733
+ get_user,
734
+ update_user,
735
+ reset_user_password,
736
+ delete_user,
737
+ list_users,
738
+ verify_credentials,
739
+ setup_initial_admin,
740
+ get_client_ip,
741
+ is_rate_limited,
742
+ get_auth_stats,
743
+ initialize_user_auth_manager,
744
+ reset_auth_state
745
+ };