@rodit/rodit-auth-be 9.11.14

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,1614 @@
1
+ /**
2
+ * Authentication State Manager for RODiT operations
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const logger = require("../../services/logger");
8
+ const config = require("../../services/configsdk");
9
+ const { createLogContext, logErrorWithMetrics } = logger;
10
+
11
+ // authenticationmw is not required at module load; fetch helpers use lazy require() so this
12
+ // module and middleware do not form a circular dependency at startup.
13
+
14
+ const baseModuleContext = createLogContext("AuthStateManager", "module", {
15
+ loadedAt: new Date().toISOString()
16
+ });
17
+
18
+ logger.debugWithContext("Loading statemanager.js module", baseModuleContext);
19
+
20
+ /**
21
+ * Singleton class for managing authentication state
22
+ * This includes RODiT configurations, JWT tokens, and public keys
23
+ */
24
+ class AuthStateManager {
25
+ constructor(asmoptions = {}) {
26
+ // Allow bypassing singleton pattern for testing
27
+ if (!asmoptions.bypassSingleton && AuthStateManager.instance) {
28
+ return AuthStateManager.instance;
29
+ }
30
+
31
+ // Separate variables for own key and peer key
32
+ this.ownBase64urlJwkPublicKey = null;
33
+ this.peerBase64urlJwkPublicKey = null;
34
+
35
+ // Other existing properties
36
+ this.config_own_rodit = null;
37
+ this.signportalJwtToken = null;
38
+ this.jwtToken = null;
39
+
40
+ // Session management
41
+ this.sessions = new Map();
42
+
43
+ // Store instance ID for debugging multiple instances
44
+ this.instanceId = ulid();
45
+ this.isTestInstance = asmoptions.bypassSingleton || false;
46
+
47
+ // Only set singleton instance if not bypassing
48
+ if (!asmoptions.bypassSingleton) {
49
+ AuthStateManager.instance = this;
50
+ }
51
+
52
+ logger.debugWithContext("AuthStateManager instance created", {
53
+ ...baseModuleContext,
54
+ instanceId: this.instanceId,
55
+ isTestInstance: this.isTestInstance,
56
+ isSingleton: !asmoptions.bypassSingleton
57
+ });
58
+ }
59
+
60
+ // Methods for own public key
61
+ async setOwnBase64urlJwkPublicKey(key) {
62
+ const requestId = ulid();
63
+ const startTime = Date.now();
64
+
65
+ const baseContext = createLogContext(
66
+ "AuthStateManager",
67
+ "setOwnBase64urlJwkPublicKey",
68
+ {
69
+ requestId,
70
+ keyLength: key ? key.length : 0,
71
+ keyFirstChars: key ? key.substring(0, 10) + '...' : 'null'
72
+ }
73
+ );
74
+
75
+ logger.debugWithContext("Setting own base64url JWK public key", baseContext);
76
+
77
+ try {
78
+ this.ownBase64urlJwkPublicKey = key;
79
+
80
+ const duration = Date.now() - startTime;
81
+ logger.debugWithContext("Successfully set own base64url JWK public key", {
82
+ ...baseContext,
83
+ duration
84
+ });
85
+
86
+ // Add metric for key operations
87
+ logger.metric("auth_key_operations", duration, {
88
+ operation: "set",
89
+ keyType: "own",
90
+ result: "success"
91
+ });
92
+
93
+ return key;
94
+ } catch (error) {
95
+ const duration = Date.now() - startTime;
96
+
97
+ logErrorWithMetrics(
98
+ "Failed to set own base64url JWK public key",
99
+ {
100
+ ...baseContext,
101
+ duration
102
+ },
103
+ error,
104
+ "auth_key_operations_error",
105
+ {
106
+ operation: "set",
107
+ keyType: "own",
108
+ result: "error",
109
+ duration
110
+ }
111
+ );
112
+
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ getOwnBase64urlJwkPublicKey() {
118
+ const requestId = ulid();
119
+ const startTime = Date.now();
120
+
121
+ const hasKey = !!this.ownBase64urlJwkPublicKey;
122
+ const baseContext = createLogContext(
123
+ "AuthStateManager",
124
+ "getOwnBase64urlJwkPublicKey",
125
+ {
126
+ requestId,
127
+ hasKey,
128
+ keyLength: hasKey ? this.ownBase64urlJwkPublicKey.length : 0
129
+ }
130
+ );
131
+
132
+ logger.debugWithContext("Getting own base64url JWK public key", baseContext);
133
+
134
+ try {
135
+ const duration = Date.now() - startTime;
136
+
137
+ logger.debugWithContext("Retrieved own base64url JWK public key", {
138
+ ...baseContext,
139
+ duration
140
+ });
141
+
142
+ // Add metric for key operations
143
+ logger.metric("auth_key_operations", duration, {
144
+ operation: "get",
145
+ keyType: "own",
146
+ result: "success",
147
+ hasKey
148
+ });
149
+
150
+ return this.ownBase64urlJwkPublicKey;
151
+ } catch (error) {
152
+ const duration = Date.now() - startTime;
153
+
154
+ logErrorWithMetrics(
155
+ "Failed to get own base64url JWK public key",
156
+ {
157
+ ...baseContext,
158
+ duration
159
+ },
160
+ error,
161
+ "auth_key_operations_error",
162
+ {
163
+ operation: "get",
164
+ keyType: "own",
165
+ result: "error",
166
+ duration
167
+ }
168
+ );
169
+
170
+ throw error;
171
+ }
172
+ }
173
+
174
+ // Methods for peer public key
175
+ async setPeerBase64urlJwkPublicKey(key) {
176
+ const requestId = ulid();
177
+ const startTime = Date.now();
178
+
179
+ const baseContext = createLogContext(
180
+ "AuthStateManager",
181
+ "setPeerBase64urlJwkPublicKey",
182
+ {
183
+ requestId,
184
+ keyLength: key ? key.length : 0,
185
+ keyFirstChars: key ? key.substring(0, 10) + '...' : 'null'
186
+ }
187
+ );
188
+
189
+ logger.debugWithContext("Setting peer base64url JWK public key", baseContext);
190
+
191
+ try {
192
+ this.peerBase64urlJwkPublicKey = key;
193
+
194
+ const duration = Date.now() - startTime;
195
+ logger.debugWithContext("Successfully set peer base64url JWK public key", {
196
+ ...baseContext,
197
+ duration
198
+ });
199
+
200
+ // Add metric for key operations
201
+ logger.metric("auth_key_operations", duration, {
202
+ operation: "set",
203
+ keyType: "peer",
204
+ result: "success"
205
+ });
206
+
207
+ return key;
208
+ } catch (error) {
209
+ const duration = Date.now() - startTime;
210
+
211
+ logErrorWithMetrics(
212
+ "Failed to set peer base64url JWK public key",
213
+ {
214
+ ...baseContext,
215
+ duration
216
+ },
217
+ error,
218
+ "auth_key_operations_error",
219
+ {
220
+ operation: "set",
221
+ keyType: "peer",
222
+ result: "error",
223
+ duration
224
+ }
225
+ );
226
+
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ getPeerBase64urlJwkPublicKey() {
232
+ const requestId = ulid();
233
+ const startTime = Date.now();
234
+
235
+ const hasKey = !!this.peerBase64urlJwkPublicKey;
236
+ const baseContext = createLogContext(
237
+ "AuthStateManager",
238
+ "getPeerBase64urlJwkPublicKey",
239
+ {
240
+ requestId,
241
+ hasKey,
242
+ keyLength: hasKey ? this.peerBase64urlJwkPublicKey.length : 0
243
+ }
244
+ );
245
+
246
+ logger.debugWithContext("Getting peer base64url JWK public key", baseContext);
247
+
248
+ try {
249
+ const duration = Date.now() - startTime;
250
+
251
+ logger.debugWithContext("Retrieved peer base64url JWK public key", {
252
+ ...baseContext,
253
+ duration
254
+ });
255
+
256
+ // Add metric for key operations
257
+ logger.metric("auth_key_operations", duration, {
258
+ operation: "get",
259
+ keyType: "peer",
260
+ result: "success",
261
+ hasKey
262
+ });
263
+
264
+ return this.peerBase64urlJwkPublicKey;
265
+ } catch (error) {
266
+ const duration = Date.now() - startTime;
267
+
268
+ logErrorWithMetrics(
269
+ "Failed to get peer base64url JWK public key",
270
+ {
271
+ ...baseContext,
272
+ duration
273
+ },
274
+ error,
275
+ "auth_key_operations_error",
276
+ {
277
+ operation: "get",
278
+ keyType: "peer",
279
+ result: "error",
280
+ duration
281
+ }
282
+ );
283
+
284
+ throw error;
285
+ }
286
+ }
287
+
288
+ // RODiT configuration management
289
+ async setConfigOwnRodit(config_own_rodit) {
290
+ const requestId = ulid();
291
+ const startTime = Date.now();
292
+
293
+ const baseContext = createLogContext(
294
+ "AuthStateManager",
295
+ "setConfigOwnRodit",
296
+ {
297
+ requestId,
298
+ hasConfig: !!config_own_rodit
299
+ }
300
+ );
301
+
302
+ logger.debugWithContext("Setting own RODiT configuration", baseContext);
303
+
304
+ try {
305
+ // Ensure private key is in Uint8Array format for nacl.sign.detached
306
+ if (config_own_rodit && config_own_rodit.own_rodit_bytes_private_key) {
307
+ const privateKey = config_own_rodit.own_rodit_bytes_private_key;
308
+
309
+ // Check if the private key is already a Uint8Array
310
+ if (!(privateKey instanceof Uint8Array)) {
311
+ logger.debugWithContext("Converting private key to Uint8Array", {
312
+ ...baseContext,
313
+ privateKeyType: typeof privateKey,
314
+ isBuffer: Buffer.isBuffer(privateKey)
315
+ });
316
+
317
+ // Convert Buffer to Uint8Array
318
+ if (Buffer.isBuffer(privateKey)) {
319
+ config_own_rodit.own_rodit_bytes_private_key = new Uint8Array(privateKey);
320
+ }
321
+ // Convert base64/hex string to Uint8Array
322
+ else if (typeof privateKey === 'string') {
323
+ try {
324
+ // Try to decode as base64 first
325
+ const buffer = Buffer.from(privateKey, 'base64');
326
+ config_own_rodit.own_rodit_bytes_private_key = new Uint8Array(buffer);
327
+ } catch (conversionError) {
328
+ logger.warnWithContext("Failed to convert private key string to Uint8Array", {
329
+ ...baseContext,
330
+ error: conversionError.message
331
+ });
332
+ throw new Error("Private key must be convertible to Uint8Array");
333
+ }
334
+ } else {
335
+ logger.errorWithContext("Private key is in an unsupported format", {
336
+ ...baseContext,
337
+ privateKeyType: typeof privateKey
338
+ });
339
+ throw new Error("Private key must be a Buffer, string, or Uint8Array");
340
+ }
341
+
342
+ logger.debugWithContext("Successfully converted private key to Uint8Array", {
343
+ ...baseContext,
344
+ convertedKeyLength: config_own_rodit.own_rodit_bytes_private_key.length
345
+ });
346
+ }
347
+ }
348
+
349
+ this.config_own_rodit = config_own_rodit;
350
+
351
+ const duration = Date.now() - startTime;
352
+ logger.debugWithContext("Successfully set own RODiT configuration", {
353
+ ...baseContext,
354
+ duration
355
+ });
356
+
357
+ // Add metric for configuration operations
358
+ logger.metric("auth_config_operations", duration, {
359
+ operation: "set",
360
+ configType: "own_rodit",
361
+ result: "success"
362
+ });
363
+
364
+ return config_own_rodit;
365
+ } catch (error) {
366
+ const duration = Date.now() - startTime;
367
+
368
+ logErrorWithMetrics(
369
+ "Failed to set own RODiT configuration",
370
+ {
371
+ ...baseContext,
372
+ duration
373
+ },
374
+ error,
375
+ "auth_config_operations_error",
376
+ {
377
+ operation: "set",
378
+ configType: "own_rodit",
379
+ result: "error",
380
+ duration
381
+ }
382
+ );
383
+
384
+ throw error;
385
+ }
386
+ }
387
+
388
+ getConfigOwnRodit() {
389
+ const requestId = ulid();
390
+ const startTime = Date.now();
391
+
392
+ const hasConfig = !!this.config_own_rodit;
393
+ const baseContext = createLogContext(
394
+ "AuthStateManager",
395
+ "getConfigOwnRodit",
396
+ {
397
+ requestId,
398
+ hasConfig
399
+ }
400
+ );
401
+
402
+ logger.debugWithContext("Getting own RODiT configuration", baseContext);
403
+
404
+ try {
405
+ const duration = Date.now() - startTime;
406
+
407
+ logger.debugWithContext("Retrieved own RODiT configuration", {
408
+ ...baseContext,
409
+ duration
410
+ });
411
+
412
+ // Add metric for configuration operations
413
+ logger.metric("auth_config_operations", duration, {
414
+ operation: "get",
415
+ configType: "own_rodit",
416
+ result: "success",
417
+ hasConfig
418
+ });
419
+
420
+ return this.config_own_rodit;
421
+ } catch (error) {
422
+ const duration = Date.now() - startTime;
423
+
424
+ logErrorWithMetrics(
425
+ "Failed to get own RODiT configuration",
426
+ {
427
+ ...baseContext,
428
+ duration
429
+ },
430
+ error,
431
+ "auth_config_operations_error",
432
+ {
433
+ operation: "get",
434
+ configType: "own_rodit",
435
+ result: "error",
436
+ duration
437
+ }
438
+ );
439
+
440
+ throw error;
441
+ }
442
+ }
443
+
444
+ // JWT token management
445
+ async setSignPortalJwtToken(token) {
446
+ const requestId = ulid();
447
+ const startTime = Date.now();
448
+
449
+ const baseContext = createLogContext(
450
+ "AuthStateManager",
451
+ "setSignPortalJwtToken",
452
+ {
453
+ requestId,
454
+ hasToken: !!token
455
+ }
456
+ );
457
+
458
+ try {
459
+ this.signportalJwtToken = token;
460
+
461
+ const duration = Date.now() - startTime;
462
+ // Add metric for token operations
463
+ logger.metric("auth_token_operations", duration, {
464
+ operation: "set",
465
+ tokenType: "signportal",
466
+ result: "success"
467
+ });
468
+
469
+ return token;
470
+ } catch (error) {
471
+ const duration = Date.now() - startTime;
472
+
473
+ logErrorWithMetrics(
474
+ "Failed to set SignPortal JWT token",
475
+ {
476
+ ...baseContext,
477
+ duration
478
+ },
479
+ error,
480
+ "auth_token_operations_error",
481
+ {
482
+ operation: "set",
483
+ tokenType: "signportal",
484
+ result: "error",
485
+ duration
486
+ }
487
+ );
488
+
489
+ throw error;
490
+ }
491
+ }
492
+
493
+ getSignPortalJwtToken() {
494
+ const requestId = ulid();
495
+ const startTime = Date.now();
496
+
497
+ const hasToken = !!this.signportalJwtToken;
498
+ const baseContext = createLogContext(
499
+ "AuthStateManager",
500
+ "getSignPortalJwtToken",
501
+ {
502
+ requestId,
503
+ hasToken
504
+ }
505
+ );
506
+
507
+ try {
508
+ const duration = Date.now() - startTime;
509
+
510
+ // Add metric for token operations
511
+ logger.metric("auth_token_operations", duration, {
512
+ operation: "get",
513
+ tokenType: "signportal",
514
+ result: "success",
515
+ hasToken
516
+ });
517
+
518
+ return this.signportalJwtToken;
519
+ } catch (error) {
520
+ const duration = Date.now() - startTime;
521
+
522
+ logErrorWithMetrics(
523
+ "Failed to get SignPortal JWT token",
524
+ {
525
+ ...baseContext,
526
+ duration
527
+ },
528
+ error,
529
+ "auth_token_operations_error",
530
+ {
531
+ operation: "get",
532
+ tokenType: "signportal",
533
+ result: "error",
534
+ duration
535
+ }
536
+ );
537
+
538
+ throw error;
539
+ }
540
+ }
541
+
542
+ async setJwtToken(token) {
543
+ const requestId = ulid();
544
+ const startTime = Date.now();
545
+
546
+ const baseContext = createLogContext(
547
+ "AuthStateManager",
548
+ "setJwtToken",
549
+ {
550
+ requestId,
551
+ hasToken: !!token
552
+ }
553
+ );
554
+
555
+ try {
556
+ this.jwtToken = token;
557
+
558
+ const duration = Date.now() - startTime;
559
+ // Add metric for token operations
560
+ logger.metric("auth_token_operations", duration, {
561
+ operation: "set",
562
+ tokenType: "jwt",
563
+ result: "success"
564
+ });
565
+
566
+ return token;
567
+ } catch (error) {
568
+ const duration = Date.now() - startTime;
569
+
570
+ logErrorWithMetrics(
571
+ "Failed to set JWT token",
572
+ {
573
+ ...baseContext,
574
+ duration
575
+ },
576
+ error,
577
+ "auth_token_operations_error",
578
+ {
579
+ operation: "set",
580
+ tokenType: "jwt",
581
+ result: "error",
582
+ duration
583
+ }
584
+ );
585
+
586
+ throw error;
587
+ }
588
+ }
589
+
590
+ getJwtToken() {
591
+ const requestId = ulid();
592
+ const startTime = Date.now();
593
+
594
+ const hasToken = !!this.jwtToken;
595
+ const baseContext = createLogContext(
596
+ "AuthStateManager",
597
+ "getJwtToken",
598
+ {
599
+ requestId,
600
+ hasToken
601
+ }
602
+ );
603
+
604
+ try {
605
+ const duration = Date.now() - startTime;
606
+
607
+ // Add metric for token operations
608
+ logger.metric("auth_token_operations", duration, {
609
+ operation: "get",
610
+ tokenType: "jwt",
611
+ result: "success",
612
+ hasToken
613
+ });
614
+
615
+ return this.jwtToken;
616
+ } catch (error) {
617
+ const duration = Date.now() - startTime;
618
+
619
+ logErrorWithMetrics(
620
+ "Failed to get JWT token",
621
+ {
622
+ ...baseContext,
623
+ duration
624
+ },
625
+ error,
626
+ "auth_token_operations_error",
627
+ {
628
+ operation: "get",
629
+ tokenType: "jwt",
630
+ result: "error",
631
+ duration
632
+ }
633
+ );
634
+
635
+ throw error;
636
+ }
637
+ }
638
+
639
+ // Session management
640
+ createSession(sessionData) {
641
+ const requestId = ulid();
642
+ const startTime = Date.now();
643
+
644
+ const baseContext = createLogContext(
645
+ "AuthStateManager",
646
+ "createSession",
647
+ {
648
+ requestId,
649
+ sessionId: sessionData?.id
650
+ }
651
+ );
652
+
653
+ logger.debugWithContext("Creating new session", baseContext);
654
+
655
+ try {
656
+ if (!sessionData || !sessionData.id) {
657
+ const error = new Error("Session data must include an ID");
658
+
659
+ logErrorWithMetrics(
660
+ "Failed to create session: missing ID",
661
+ baseContext,
662
+ error,
663
+ "session_operations_error",
664
+ {
665
+ operation: "create",
666
+ result: "error",
667
+ reason: "missing_id"
668
+ }
669
+ );
670
+
671
+ throw error;
672
+ }
673
+
674
+ const sessionWithTimestamp = {
675
+ ...sessionData,
676
+ lastAccessedAt: Math.floor(Date.now() / 1000)
677
+ };
678
+
679
+ this.sessions.set(sessionData.id, sessionWithTimestamp);
680
+
681
+ const duration = Date.now() - startTime;
682
+ logger.debugWithContext("Successfully created session", {
683
+ ...baseContext,
684
+ duration,
685
+ sessionData: {
686
+ id: sessionData.id,
687
+ lastAccessedAt: sessionWithTimestamp.lastAccessedAt
688
+ }
689
+ });
690
+
691
+ // Add metric for session operations
692
+ logger.metric("session_operations", duration, {
693
+ operation: "create",
694
+ result: "success"
695
+ });
696
+
697
+ return sessionWithTimestamp;
698
+ } catch (error) {
699
+ if (error.message !== "Session data must include an ID") {
700
+ const duration = Date.now() - startTime;
701
+
702
+ logErrorWithMetrics(
703
+ "Failed to create session",
704
+ {
705
+ ...baseContext,
706
+ duration
707
+ },
708
+ error,
709
+ "session_operations_error",
710
+ {
711
+ operation: "create",
712
+ result: "error",
713
+ duration
714
+ }
715
+ );
716
+ }
717
+
718
+ throw error;
719
+ }
720
+ }
721
+
722
+ getSession(sessionId) {
723
+ const requestId = ulid();
724
+ const startTime = Date.now();
725
+
726
+ const baseContext = createLogContext(
727
+ "AuthStateManager",
728
+ "getSession",
729
+ {
730
+ requestId,
731
+ sessionId
732
+ }
733
+ );
734
+
735
+ logger.debugWithContext("Getting session", baseContext);
736
+
737
+ try {
738
+ const session = this.sessions.get(sessionId);
739
+ const hasSession = !!session;
740
+
741
+ const duration = Date.now() - startTime;
742
+ logger.debugWithContext("Session retrieval complete", {
743
+ ...baseContext,
744
+ duration,
745
+ found: hasSession
746
+ });
747
+
748
+ // Add metric for session operations
749
+ logger.metric("session_operations", duration, {
750
+ operation: "get",
751
+ result: "success",
752
+ found: hasSession
753
+ });
754
+
755
+ return session;
756
+ } catch (error) {
757
+ const duration = Date.now() - startTime;
758
+
759
+ logErrorWithMetrics(
760
+ "Failed to get session",
761
+ {
762
+ ...baseContext,
763
+ duration
764
+ },
765
+ error,
766
+ "session_operations_error",
767
+ {
768
+ operation: "get",
769
+ result: "error",
770
+ duration
771
+ }
772
+ );
773
+
774
+ throw error;
775
+ }
776
+ }
777
+
778
+ updateSession(sessionId, updates) {
779
+ const requestId = ulid();
780
+ const startTime = Date.now();
781
+
782
+ const baseContext = createLogContext(
783
+ "AuthStateManager",
784
+ "updateSession",
785
+ {
786
+ requestId,
787
+ sessionId
788
+ }
789
+ );
790
+
791
+ logger.debugWithContext("Updating session", baseContext);
792
+
793
+ try {
794
+ if (!this.sessions.has(sessionId)) {
795
+ const duration = Date.now() - startTime;
796
+
797
+ logger.debugWithContext("Session not found for update", {
798
+ ...baseContext,
799
+ duration
800
+ });
801
+
802
+ // Add metric for session operations
803
+ logger.metric("session_operations", duration, {
804
+ operation: "update",
805
+ result: "not_found"
806
+ });
807
+
808
+ return null;
809
+ }
810
+
811
+ const session = this.sessions.get(sessionId);
812
+ const updatedSession = {
813
+ ...session,
814
+ ...updates,
815
+ lastAccessedAt: Math.floor(Date.now() / 1000)
816
+ };
817
+
818
+ this.sessions.set(sessionId, updatedSession);
819
+
820
+ const duration = Date.now() - startTime;
821
+ logger.debugWithContext("Successfully updated session", {
822
+ ...baseContext,
823
+ duration
824
+ });
825
+
826
+ // Add metric for session operations
827
+ logger.metric("session_operations", duration, {
828
+ operation: "update",
829
+ result: "success"
830
+ });
831
+
832
+ return updatedSession;
833
+ } catch (error) {
834
+ const duration = Date.now() - startTime;
835
+
836
+ logErrorWithMetrics(
837
+ "Failed to update session",
838
+ {
839
+ ...baseContext,
840
+ duration
841
+ },
842
+ error,
843
+ "session_operations_error",
844
+ {
845
+ operation: "update",
846
+ result: "error",
847
+ duration
848
+ }
849
+ );
850
+
851
+ throw error;
852
+ }
853
+ }
854
+
855
+ deleteSession(sessionId) {
856
+ const requestId = ulid();
857
+ const startTime = Date.now();
858
+
859
+ const baseContext = createLogContext(
860
+ "AuthStateManager",
861
+ "deleteSession",
862
+ {
863
+ requestId,
864
+ sessionId
865
+ }
866
+ );
867
+
868
+ logger.debugWithContext("Deleting session", baseContext);
869
+
870
+ try {
871
+ if (!this.sessions.has(sessionId)) {
872
+ const duration = Date.now() - startTime;
873
+
874
+ logger.debugWithContext("Session not found for deletion", {
875
+ ...baseContext,
876
+ duration
877
+ });
878
+
879
+ // Add metric for session operations
880
+ logger.metric("session_operations", duration, {
881
+ operation: "delete",
882
+ result: "not_found"
883
+ });
884
+
885
+ return false;
886
+ }
887
+
888
+ const deleted = this.sessions.delete(sessionId);
889
+
890
+ const duration = Date.now() - startTime;
891
+ logger.debugWithContext("Session deletion complete", {
892
+ ...baseContext,
893
+ duration,
894
+ deleted
895
+ });
896
+
897
+ // Add metric for session operations
898
+ logger.metric("session_operations", duration, {
899
+ operation: "delete",
900
+ result: deleted ? "success" : "failed"
901
+ });
902
+
903
+ return deleted;
904
+ } catch (error) {
905
+ const duration = Date.now() - startTime;
906
+
907
+ logErrorWithMetrics(
908
+ "Failed to delete session",
909
+ {
910
+ ...baseContext,
911
+ duration
912
+ },
913
+ error,
914
+ "session_operations_error",
915
+ {
916
+ operation: "delete",
917
+ result: "error",
918
+ duration
919
+ }
920
+ );
921
+
922
+ throw error;
923
+ }
924
+ }
925
+
926
+ getAllSessions() {
927
+ const requestId = ulid();
928
+ const startTime = Date.now();
929
+
930
+ const baseContext = createLogContext(
931
+ "AuthStateManager",
932
+ "getAllSessions",
933
+ {
934
+ requestId
935
+ }
936
+ );
937
+
938
+ logger.debugWithContext("Getting all sessions", baseContext);
939
+
940
+ try {
941
+ const sessions = Array.from(this.sessions.values());
942
+
943
+ const duration = Date.now() - startTime;
944
+ logger.debugWithContext("Retrieved all sessions", {
945
+ ...baseContext,
946
+ duration,
947
+ sessionCount: sessions.length
948
+ });
949
+
950
+ // Add metric for session operations
951
+ logger.metric("session_operations", duration, {
952
+ operation: "getAll",
953
+ result: "success",
954
+ sessionCount: sessions.length
955
+ });
956
+
957
+ return sessions;
958
+ } catch (error) {
959
+ const duration = Date.now() - startTime;
960
+
961
+ logErrorWithMetrics(
962
+ "Failed to get all sessions",
963
+ {
964
+ ...baseContext,
965
+ duration
966
+ },
967
+ error,
968
+ "session_operations_error",
969
+ {
970
+ operation: "getAll",
971
+ result: "error",
972
+ duration
973
+ }
974
+ );
975
+
976
+ throw error;
977
+ }
978
+ }
979
+
980
+ getPortalUrl(serviceProviderId, port) {
981
+ const requestId = ulid();
982
+ const startTime = Date.now();
983
+
984
+ const baseContext = createLogContext(
985
+ "AuthStateManager",
986
+ "getPortalUrl",
987
+ {
988
+ requestId,
989
+ serviceProviderId,
990
+ port
991
+ }
992
+ );
993
+
994
+ logger.debugWithContext("Generating portal URL", baseContext);
995
+
996
+ try {
997
+ // Validate serviceProviderId
998
+ if (!serviceProviderId) {
999
+ const error = new Error("serviceProviderId is undefined in getPortalUrl");
1000
+
1001
+ logErrorWithMetrics(
1002
+ "Missing serviceProviderId parameter",
1003
+ baseContext,
1004
+ error,
1005
+ "portal_url_error",
1006
+ {
1007
+ result: "error",
1008
+ reason: "missing_provider_id"
1009
+ }
1010
+ );
1011
+
1012
+ throw error;
1013
+ }
1014
+
1015
+ // Use configured SignPortal endpoint (falls back to SDK defaults if not provided)
1016
+ let configuredUrlRaw = config.get("SIGNPORTAL_API_URL");
1017
+
1018
+ if (typeof configuredUrlRaw !== "string" || configuredUrlRaw.trim() === "") {
1019
+ const error = new Error("SIGNPORTAL_API_URL configuration is missing or empty");
1020
+
1021
+ logErrorWithMetrics(
1022
+ "Missing SignPortal configuration",
1023
+ {
1024
+ ...baseContext,
1025
+ serviceProviderId,
1026
+ configuredUrlRaw
1027
+ },
1028
+ error,
1029
+ "portal_url_error",
1030
+ {
1031
+ result: "error",
1032
+ reason: "missing_configuration"
1033
+ }
1034
+ );
1035
+
1036
+ throw error;
1037
+ }
1038
+
1039
+ configuredUrlRaw = configuredUrlRaw.trim();
1040
+
1041
+ // Ensure URL has protocol for URL parser friendliness
1042
+ let preparedUrl = configuredUrlRaw;
1043
+ if (!/^https?:\/\//i.test(preparedUrl)) {
1044
+ preparedUrl = `https://${preparedUrl}`;
1045
+ }
1046
+
1047
+ let portalUrl;
1048
+ try {
1049
+ const parsed = new URL(preparedUrl);
1050
+ if (port && !parsed.port) {
1051
+ parsed.port = String(port);
1052
+ }
1053
+ portalUrl = parsed.toString().replace(/\/$/, "");
1054
+ } catch (parseError) {
1055
+ const error = new Error("Invalid SIGNPORTAL_API_URL configuration");
1056
+ error.cause = parseError;
1057
+
1058
+ logErrorWithMetrics(
1059
+ "Failed to parse SignPortal configuration URL",
1060
+ {
1061
+ ...baseContext,
1062
+ serviceProviderId,
1063
+ configuredUrlRaw,
1064
+ preparedUrl,
1065
+ port
1066
+ },
1067
+ error,
1068
+ "portal_url_error",
1069
+ {
1070
+ result: "error",
1071
+ reason: "invalid_configuration_url"
1072
+ }
1073
+ );
1074
+
1075
+ throw error;
1076
+ }
1077
+
1078
+ const duration = Date.now() - startTime;
1079
+ logger.debugWithContext("Successfully generated portal URL", {
1080
+ ...baseContext,
1081
+ duration,
1082
+ portalUrl,
1083
+ configuredUrlRaw
1084
+ });
1085
+
1086
+ // Add metric for portal URL generation
1087
+ logger.metric("portal_url_operations", duration, {
1088
+ result: "success"
1089
+ });
1090
+
1091
+ return portalUrl;
1092
+ } catch (error) {
1093
+ const duration = Date.now() - startTime;
1094
+
1095
+ // Only log errors that haven't been logged already
1096
+ if (!error.logged) {
1097
+ logErrorWithMetrics(
1098
+ "Unexpected error generating portal URL",
1099
+ {
1100
+ ...baseContext,
1101
+ duration
1102
+ },
1103
+ error,
1104
+ "portal_url_error",
1105
+ {
1106
+ result: "error",
1107
+ reason: "unexpected",
1108
+ duration
1109
+ }
1110
+ );
1111
+ }
1112
+
1113
+ throw error;
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Performs a fetch operation with comprehensive error handling and logging for monitoring
1119
+ *
1120
+ * @param {string} url - The URL to fetch from
1121
+ * @param {Object} fwehoptions - Fetch fwehoptions including method, headers, etc.
1122
+ * @returns {Promise<Object>} - The response data or error object
1123
+ */
1124
+ async fetchWithErrorHandling(url, fwehoptions, retryCount = 0) {
1125
+ const requestId = ulid();
1126
+ const startTime = Date.now();
1127
+ const operation = fwehoptions?.method || "POST";
1128
+ const urlObj = new URL(url);
1129
+ const endpoint = urlObj.pathname;
1130
+ const MAX_AUTH_RETRIES = 1; // Retries for expired tokens
1131
+ const MAX_RATE_LIMIT_RETRIES = 3; // Retries for rate limiting
1132
+
1133
+ logger.debug("API request initiated", {
1134
+ component: "APIClient",
1135
+ method: "fetchWithErrorHandling",
1136
+ requestId,
1137
+ url: endpoint,
1138
+ operation,
1139
+ retryCount,
1140
+ });
1141
+
1142
+ try {
1143
+ // Get the current JWT token for authentication
1144
+ const jwt_token = this.getJwtToken();
1145
+
1146
+ // Add authorization and tracking headers
1147
+ fwehoptions.headers = {
1148
+ ...fwehoptions.headers,
1149
+ ...(jwt_token ? { Authorization: `Bearer ${jwt_token}` } : {}),
1150
+ "X-Request-ID": requestId,
1151
+ };
1152
+
1153
+ // Make the API request
1154
+ const response = await fetch(url, fwehoptions);
1155
+ const responseTime = Date.now() - startTime;
1156
+
1157
+ // Check for a renewed token in response headers
1158
+ const newToken = response.headers.get("New-Token");
1159
+ if (newToken) {
1160
+ try {
1161
+ await this.setJwtToken(newToken);
1162
+ logger.info("Authentication token refreshed from header", {
1163
+ component: "APIClient",
1164
+ method: "fetchWithErrorHandling",
1165
+ requestId,
1166
+ });
1167
+ } catch (tokenError) {
1168
+ logger.error("Failed to update JWT token", {
1169
+ component: "APIClient",
1170
+ method: "fetchWithErrorHandling",
1171
+ requestId,
1172
+ error: tokenError.message,
1173
+ });
1174
+ }
1175
+ }
1176
+
1177
+ // Record response time metrics
1178
+ logger.metric("api_request_duration_milliseconds", responseTime, {
1179
+ endpoint,
1180
+ method: operation,
1181
+ status: response.status,
1182
+ });
1183
+
1184
+ // Handle 401 Unauthorized with retry for token expiration
1185
+ if (response.status === 401 && retryCount < MAX_AUTH_RETRIES) {
1186
+ const responseData = await response.json();
1187
+
1188
+ // Only retry for expired tokens
1189
+ if (responseData.error && responseData.error.code === "TOKEN_EXPIRED") {
1190
+ logger.info("Token expired, attempting login refresh", {
1191
+ component: "APIClient",
1192
+ method: "fetchWithErrorHandling",
1193
+ requestId,
1194
+ });
1195
+
1196
+ // Try to login again to get a fresh token
1197
+ // This implementation depends on your authentication flow
1198
+ try {
1199
+ const config_own_rodit = this.getConfigOwnRodit();
1200
+ if (config_own_rodit && config_own_rodit.own_rodit) {
1201
+ // Lazy require: authenticationmw pulls in statemanager; avoid top-level cycle
1202
+ const { login_server } = require("../middleware/authenticationmw");
1203
+ const loginResult = await login_server(config_own_rodit);
1204
+
1205
+ if (loginResult && loginResult.jwt_token) {
1206
+ // Save the new token
1207
+ await this.setJwtToken(loginResult.jwt_token);
1208
+
1209
+ // Retry the request with the new token
1210
+ return this.fetchWithErrorHandling(url, fwehoptions, retryCount + 1);
1211
+ }
1212
+ }
1213
+ } catch (loginError) {
1214
+ logger.error("Failed to refresh token through login", {
1215
+ component: "APIClient",
1216
+ method: "fetchWithErrorHandling",
1217
+ requestId,
1218
+ error: loginError.message,
1219
+ });
1220
+ }
1221
+ }
1222
+ }
1223
+
1224
+ // Handle 429 Too Many Requests with retry and exponential backoff
1225
+ if (response.status === 429 && retryCount < MAX_RATE_LIMIT_RETRIES) {
1226
+ // Get retry-after header or default to exponential backoff
1227
+ const retryAfter = response.headers.get('Retry-After');
1228
+ let waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, retryCount) * 1000;
1229
+
1230
+ // Cap the wait time at 30 seconds
1231
+ waitTime = Math.min(waitTime, 30000);
1232
+
1233
+ // Log rate limiting information
1234
+ logger.warn("Rate limit exceeded", {
1235
+ component: "APIClient",
1236
+ method: "fetchWithErrorHandling",
1237
+ requestId,
1238
+ url: endpoint,
1239
+ statusCode: response.status,
1240
+ retryCount,
1241
+ retryAfter: retryAfter || 'not specified',
1242
+ waitTime: waitTime / 1000,
1243
+ event: "rate_limit_exceeded",
1244
+ maxRequests: response.headers.get('X-RateLimit-Limit'),
1245
+ windowMinutes: response.headers.get('X-RateLimit-Window') || 15,
1246
+ });
1247
+
1248
+ // Record rate limit metric
1249
+ logger.metric("api_rate_limit_exceeded_total", 1, {
1250
+ endpoint,
1251
+ method: operation,
1252
+ });
1253
+
1254
+ // Wait for the specified time before retrying
1255
+ await new Promise(resolve => setTimeout(resolve, waitTime));
1256
+
1257
+ // Retry the request
1258
+ return this.fetchWithErrorHandling(url, fwehoptions, retryCount + 1);
1259
+ }
1260
+
1261
+ // Parse response as JSON for all status codes
1262
+ let responseData;
1263
+ try {
1264
+ responseData = await response.json();
1265
+ } catch (parseError) {
1266
+ // Handle non-JSON responses - clone response to avoid double-read error
1267
+ try {
1268
+ const responseClone = response.clone();
1269
+ const text = await responseClone.text();
1270
+ responseData = {
1271
+ rawResponse: text.substring(0, 100), // Only include a preview
1272
+ parseError: parseError.message,
1273
+ };
1274
+ } catch (textError) {
1275
+ // If both JSON and text parsing fail, create a minimal response
1276
+ responseData = {
1277
+ rawResponse: "Unable to parse response",
1278
+ parseError: parseError.message,
1279
+ textError: textError.message,
1280
+ };
1281
+ }
1282
+ }
1283
+
1284
+ if (!response.ok) {
1285
+ // Handle error responses
1286
+ logger.error("API request failed", {
1287
+ component: "APIClient",
1288
+ method: "fetchWithErrorHandling",
1289
+ requestId,
1290
+ url: endpoint,
1291
+ statusCode: response.status,
1292
+ errorDetails: responseData,
1293
+ });
1294
+
1295
+ // Record error metrics
1296
+ logger.metric("api_request_errors_total", 1, {
1297
+ endpoint,
1298
+ method: operation,
1299
+ status: response.status,
1300
+ });
1301
+
1302
+ return {
1303
+ error: responseData.error || "RequestFailed",
1304
+ message:
1305
+ responseData.message || `Request failed: ${response.statusText}`,
1306
+ statusCode: response.status,
1307
+ details: responseData,
1308
+ };
1309
+ }
1310
+
1311
+ // Log successful request
1312
+ logger.debug("API request completed", {
1313
+ component: "APIClient",
1314
+ method: "fetchWithErrorHandling",
1315
+ requestId,
1316
+ url: endpoint,
1317
+ statusCode: response.status,
1318
+ duration: responseTime,
1319
+ });
1320
+
1321
+ return responseData;
1322
+ } catch (error) {
1323
+ const errorDuration = Date.now() - startTime;
1324
+
1325
+ // Log detailed error information
1326
+ logger.error("Fetch operation failed", {
1327
+ component: "APIClient",
1328
+ method: "fetchWithErrorHandling",
1329
+ requestId,
1330
+ url: endpoint,
1331
+ errorMessage: error.message,
1332
+ errorStack: error.stack,
1333
+ duration: errorDuration,
1334
+ });
1335
+
1336
+ // Return a standardized error object
1337
+ return {
1338
+ error: "RequestFailed",
1339
+ message: error.message,
1340
+ isNetworkError:
1341
+ error.message.includes("fetch") || error.message.includes("network"),
1342
+ };
1343
+ }
1344
+ }
1345
+
1346
+ /**
1347
+ * Performs a fetch operation with comprehensive error handling and logging for monitoring
1348
+ *
1349
+ * @param {string} url - The URL to fetch from
1350
+ * @param {Object} fwehspoptions - Fetch fwehspoptions including method, headers, etc.
1351
+ * @returns {Promise<Object>} - The response data or error object
1352
+ */
1353
+ async fetchWithErrorHandlingSignPortal(url, fwehspoptions, retryCount = 0) {
1354
+ const requestId = ulid();
1355
+ const startTime = Date.now();
1356
+ const operation = fwehspoptions?.method || "POST";
1357
+ const urlObj = new URL(url);
1358
+ const endpoint = urlObj.pathname;
1359
+ const MAX_RETRIES = 1; // Only retry once for expired tokens
1360
+
1361
+ logger.debug("API request initiated", {
1362
+ component: "APIClient",
1363
+ method: "fetchWithErrorHandling",
1364
+ requestId,
1365
+ url: endpoint,
1366
+ operation,
1367
+ retryCount,
1368
+ });
1369
+
1370
+ try {
1371
+ // Get the current JWT token for authentication
1372
+ const jwt_token = this.getSignPortalJwtToken();
1373
+
1374
+ // Add authorization and tracking headers
1375
+ fwehspoptions.headers = {
1376
+ ...fwehspoptions.headers,
1377
+ ...(jwt_token ? { Authorization: `Bearer ${jwt_token}` } : {}),
1378
+ "X-Request-ID": requestId,
1379
+ };
1380
+
1381
+ // Make the API request
1382
+ const response = await fetch(url, fwehspoptions);
1383
+ const responseTime = Date.now() - startTime;
1384
+
1385
+ // Check for a renewed token in response headers
1386
+ const newToken = response.headers.get("New-Token");
1387
+ if (newToken) {
1388
+ try {
1389
+ await this.setSignPortalJwtToken(newToken);
1390
+ logger.info("Authentication token refreshed from header", {
1391
+ component: "APIClient",
1392
+ method: "fetchWithErrorHandling",
1393
+ requestId,
1394
+ });
1395
+ } catch (tokenError) {
1396
+ logger.error("Failed to update JWT token", {
1397
+ component: "APIClient",
1398
+ method: "fetchWithErrorHandling",
1399
+ requestId,
1400
+ error: tokenError.message,
1401
+ });
1402
+ }
1403
+ }
1404
+
1405
+ // Record response time metrics
1406
+ logger.metric("api_request_duration_milliseconds", responseTime, {
1407
+ endpoint,
1408
+ method: operation,
1409
+ status: response.status,
1410
+ });
1411
+
1412
+ // Handle 401 Unauthorized with retry for token expiration or session errors
1413
+ if (response.status === 401 && retryCount < MAX_RETRIES) {
1414
+ logger.info("Received 401 Unauthorized, attempting to re-authenticate with SignPortal", {
1415
+ component: "APIClient",
1416
+ method: "fetchWithErrorHandling",
1417
+ requestId,
1418
+ retryCount,
1419
+ });
1420
+
1421
+ // Try to login again to get a fresh token
1422
+ try {
1423
+ const config_own_rodit = this.getConfigOwnRodit();
1424
+ if (config_own_rodit && config_own_rodit.own_rodit) {
1425
+ // Lazy require: authenticationmw pulls in statemanager; avoid top-level cycle
1426
+ const { login_portal } = require("../middleware/authenticationmw");
1427
+
1428
+ // Extract port from URL if available
1429
+ const urlObj = new URL(url);
1430
+ const port = urlObj.port ? parseInt(urlObj.port) : 8443;
1431
+
1432
+ logger.debug("Attempting portal re-authentication", {
1433
+ component: "APIClient",
1434
+ method: "fetchWithErrorHandling",
1435
+ requestId,
1436
+ port,
1437
+ });
1438
+
1439
+ const loginResult = await login_portal(config_own_rodit, port);
1440
+
1441
+ if (loginResult && loginResult.jwt_token) {
1442
+ // Save the new token
1443
+ await this.setSignPortalJwtToken(loginResult.jwt_token);
1444
+
1445
+ logger.info("Successfully re-authenticated with SignPortal, retrying request", {
1446
+ component: "APIClient",
1447
+ method: "fetchWithErrorHandling",
1448
+ requestId,
1449
+ retryCount: retryCount + 1,
1450
+ });
1451
+
1452
+ // Retry the request with the new token
1453
+ return this.fetchWithErrorHandlingSignPortal(url, fwehspoptions, retryCount + 1);
1454
+ } else {
1455
+ logger.error("Portal re-authentication failed: no JWT token received", {
1456
+ component: "APIClient",
1457
+ method: "fetchWithErrorHandling",
1458
+ requestId,
1459
+ loginError: loginResult?.error,
1460
+ loginReason: loginResult?.reason,
1461
+ });
1462
+ }
1463
+ } else {
1464
+ logger.error("Cannot re-authenticate: config_own_rodit not available", {
1465
+ component: "APIClient",
1466
+ method: "fetchWithErrorHandling",
1467
+ requestId,
1468
+ });
1469
+ }
1470
+ } catch (loginError) {
1471
+ logger.error("Failed to refresh token through portal login", {
1472
+ component: "APIClient",
1473
+ method: "fetchWithErrorHandling",
1474
+ requestId,
1475
+ error: loginError.message,
1476
+ stack: loginError.stack,
1477
+ });
1478
+ }
1479
+ }
1480
+
1481
+ // Parse response as JSON for all status codes
1482
+ let responseData;
1483
+ try {
1484
+ responseData = await response.json();
1485
+ } catch (parseError) {
1486
+ // Handle non-JSON responses - clone response to avoid double-read error
1487
+ try {
1488
+ const responseClone = response.clone();
1489
+ const text = await responseClone.text();
1490
+ responseData = {
1491
+ rawResponse: text.substring(0, 100), // Only include a preview
1492
+ parseError: parseError.message,
1493
+ };
1494
+ } catch (textError) {
1495
+ // If both JSON and text parsing fail, create a minimal response
1496
+ responseData = {
1497
+ rawResponse: "Unable to parse response",
1498
+ parseError: parseError.message,
1499
+ textError: textError.message,
1500
+ };
1501
+ }
1502
+ }
1503
+
1504
+ if (!response.ok) {
1505
+ // Handle error responses
1506
+ logger.error("API request failed", {
1507
+ component: "APIClient",
1508
+ method: "fetchWithErrorHandling",
1509
+ requestId,
1510
+ url: endpoint,
1511
+ statusCode: response.status,
1512
+ errorDetails: responseData,
1513
+ });
1514
+
1515
+ // Record error metrics
1516
+ logger.metric("api_request_errors_total", 1, {
1517
+ endpoint,
1518
+ method: operation,
1519
+ status: response.status,
1520
+ });
1521
+
1522
+ return {
1523
+ error: responseData.error || "RequestFailed",
1524
+ message:
1525
+ responseData.message || `Request failed: ${response.statusText}`,
1526
+ statusCode: response.status,
1527
+ details: responseData,
1528
+ };
1529
+ }
1530
+
1531
+ // Log successful request
1532
+ logger.debug("API request completed", {
1533
+ component: "APIClient",
1534
+ method: "fetchWithErrorHandling",
1535
+ requestId,
1536
+ url: endpoint,
1537
+ statusCode: response.status,
1538
+ duration: responseTime,
1539
+ });
1540
+
1541
+ return responseData;
1542
+ } catch (error) {
1543
+ const errorDuration = Date.now() - startTime;
1544
+
1545
+ // Log detailed error information
1546
+ logger.error("Fetch operation failed", {
1547
+ component: "APIClient",
1548
+ method: "fetchWithErrorHandling",
1549
+ requestId,
1550
+ url: endpoint,
1551
+ errorMessage: error.message,
1552
+ errorStack: error.stack,
1553
+ duration: errorDuration,
1554
+ });
1555
+
1556
+ // Return a standardized error object
1557
+ return {
1558
+ error: "RequestFailed",
1559
+ message: error.message,
1560
+ isNetworkError:
1561
+ error.message.includes("fetch") || error.message.includes("network"),
1562
+ };
1563
+ }
1564
+ }
1565
+
1566
+ /**
1567
+ * Create a new test instance that bypasses the singleton pattern
1568
+ * This is useful for testing multiple concurrent sessions
1569
+ * @param {Object} cioptions - Configuration cioptions for the test instance
1570
+ * @returns {AuthStateManager} New test instance
1571
+ */
1572
+ static createTestInstance(cioptions = {}) {
1573
+ const testOptions = {
1574
+ ...cioptions,
1575
+ bypassSingleton: true
1576
+ };
1577
+
1578
+ const testInstance = new AuthStateManager(testOptions);
1579
+
1580
+ logger.debugWithContext("Created test instance of AuthStateManager", {
1581
+ ...baseModuleContext,
1582
+ instanceId: testInstance.instanceId,
1583
+ isTestInstance: testInstance.isTestInstance
1584
+ });
1585
+
1586
+ return testInstance;
1587
+ }
1588
+
1589
+ /**
1590
+ * Get the singleton instance
1591
+ * @returns {AuthStateManager} Singleton instance
1592
+ */
1593
+ static getInstance() {
1594
+ if (!AuthStateManager.instance) {
1595
+ AuthStateManager.instance = new AuthStateManager();
1596
+ }
1597
+ return AuthStateManager.instance;
1598
+ }
1599
+
1600
+ /**
1601
+ * Reset singleton instance (for testing purposes)
1602
+ */
1603
+ static resetInstance() {
1604
+ logger.debugWithContext("Resetting AuthStateManager singleton instance", baseModuleContext);
1605
+ AuthStateManager.instance = null;
1606
+ }
1607
+ }
1608
+
1609
+ // Create and export a singleton instance
1610
+ const stateManager = new AuthStateManager();
1611
+
1612
+ // Export both the singleton instance and the class
1613
+ module.exports = stateManager;
1614
+ module.exports.AuthStateManager = AuthStateManager;