@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.
- package/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -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;
|