@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,617 @@
1
+ /**
2
+ * Vault-based credential storage
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const config = require('../../services/configsdk');
7
+ const { ulid } = require("ulid");
8
+ const logger = require("../../services/logger");
9
+ const { createLogContext, logErrorWithMetrics } = logger;
10
+ const { validateAndExtractCredentials } = require("../../services/utils");
11
+
12
+ logger.debugWithContext("Loading vaultcredentialstoremw.js module", createLogContext(
13
+ "ModuleLoader",
14
+ "moduleInitialization",
15
+ {
16
+ module: "vaultcredentialstoremw.js",
17
+ loadedAt: new Date().toISOString()
18
+ }
19
+ ));
20
+
21
+ class VaultCredentialManager {
22
+ constructor() {
23
+ this.vault = require("node-vault")();
24
+ this.vault.endpoint = config.get("VAULT_ENDPOINT");
25
+ this.vault.apiVersion = "v1";
26
+ this.roleId = config.get("VAULT_ROLE_ID");
27
+ this.secretId = config.get("VAULT_SECRET_ID");
28
+ this.renewalInterval = 60 * 60 * 1000; // 1 hour in milliseconds
29
+ this.vaultInitialized = false;
30
+ this.vaultPath = null;
31
+ this.credentials = {};
32
+ }
33
+
34
+ async getVaultToken() {
35
+ const requestId = ulid();
36
+ const startTime = Date.now();
37
+
38
+ // Create a base context for this method
39
+ const baseContext = createLogContext(
40
+ "CredentialManager",
41
+ "getVaultToken",
42
+ { requestId }
43
+ );
44
+
45
+ logger.infoWithContext("Attempting Vault authentication", {
46
+ ...baseContext,
47
+ result: 'call',
48
+ reason: 'Vault authentication requested'
49
+ });
50
+
51
+ try {
52
+ const result = await this.vault.approleLogin({
53
+ role_id: this.roleId,
54
+ secret_id: this.secretId,
55
+ });
56
+
57
+ logger.infoWithContext("Vault authentication successful", {
58
+ ...baseContext,
59
+ duration: Date.now() - startTime,
60
+ result: 'success',
61
+ reason: 'Vault authentication succeeded'
62
+ });
63
+ logger.metric("vault_authentication_duration_ms", Date.now() - startTime, {
64
+ component: "CredentialManager",
65
+ result: 'success',
66
+ reason: 'Vault authentication succeeded'
67
+ });
68
+
69
+ return result.auth.client_token;
70
+ } catch (error) {
71
+ logger.metric("vault_authentication_duration_ms", Date.now() - startTime, {
72
+ component: "CredentialManager",
73
+ result: 'failure',
74
+ reason: error.message || 'Vault authentication failed'
75
+ });
76
+ logErrorWithMetrics(
77
+ "Error authenticating with Vault",
78
+ {
79
+ ...baseContext,
80
+ duration: Date.now() - startTime,
81
+ result: 'failure',
82
+ reason: error.message || 'Vault authentication failed'
83
+ },
84
+ error,
85
+ "vault_authentication_error",
86
+ { error_type: "auth_failure" }
87
+ );
88
+ throw new Error("Error 108: Vault authentication failed");
89
+ }
90
+ }
91
+
92
+ async initialize() {
93
+ const requestId = ulid();
94
+ const startTime = Date.now();
95
+ const baseContext = createLogContext('CredentialManager', 'initialize', { requestId });
96
+ const log = (message, extra = {}) => logger.infoWithContext(message, { ...baseContext, ...extra });
97
+ const logError = (error, context = {}) => logErrorWithMetrics(
98
+ error.message,
99
+ { ...baseContext, ...context, result: 'failure' },
100
+ error,
101
+ 'vault_initialization_error',
102
+ { error_type: error.errorType || 'initialization_failure' }
103
+ );
104
+
105
+ log('Starting vault initialization', { result: 'call', reason: 'Vault initialization requested' });
106
+
107
+ if (this.vaultInitialized) {
108
+ log('Vault already initialized', {
109
+ duration: Date.now() - startTime,
110
+ result: 'success',
111
+ reason: 'Vault was already initialized'
112
+ });
113
+ return this.vault;
114
+ }
115
+
116
+ try {
117
+ // Initialize secret storage token
118
+ this.vault.token = await this.getVaultToken();
119
+ log('Checking Vault health status', { result: 'call', reason: 'Vault health status check requested' });
120
+
121
+ // Check Vault health
122
+ const health = await this.vault.health();
123
+
124
+ if (!health.initialized || health.sealed) {
125
+ const error = new Error(health.sealed ? 'Error 110: Vault is sealed' : 'Error 109: Vault is not initialized');
126
+ error.errorType = health.sealed ? 'sealed' : 'not_initialized';
127
+ throw error;
128
+ }
129
+
130
+ // Load and validate config
131
+ if (!config || typeof config.get !== 'function') {
132
+ const error = new Error('Config object is not properly initialized');
133
+ error.errorType = 'config_initialization_error';
134
+ throw error;
135
+ }
136
+
137
+ this.vaultPath = config.get('VAULT_RODIT_KEYVALUE_PATH');
138
+ log('Retrieved vault path from config', {
139
+ vaultPath: this.vaultPath,
140
+ result: 'success',
141
+ reason: 'Vault path retrieved from config'
142
+ });
143
+
144
+ this.vaultInitialized = true;
145
+ const duration = Date.now() - startTime;
146
+
147
+ log('Vault initialized successfully', {
148
+ vaultInitialized: true,
149
+ vaultPath: this.vaultPath,
150
+ duration,
151
+ result: 'success',
152
+ reason: 'Vault initialized successfully'
153
+ });
154
+
155
+ logger.metric('vault_initialization_duration_ms', duration, {
156
+ component: 'CredentialManager',
157
+ result: 'success',
158
+ reason: 'Vault initialized successfully'
159
+ });
160
+
161
+ return this.vault;
162
+ } catch (error) {
163
+ const duration = Date.now() - startTime;
164
+ this.vaultInitialized = false;
165
+
166
+ logError(error, {
167
+ duration,
168
+ reason: error.message || 'Vault initialization failed',
169
+ errorType: error.code || 'UNKNOWN_ERROR'
170
+ });
171
+
172
+ logger.metric('vault_initialization_duration_ms', duration, {
173
+ component: 'CredentialManager',
174
+ result: 'failure',
175
+ reason: error.message || 'Vault initialization failed',
176
+ errorType: error.code || 'UNKNOWN_ERROR'
177
+ });
178
+
179
+ logger.metric('vault_initialization_errors_total', 1, {
180
+ component: 'CredentialManager',
181
+ errorType: error.code || 'UNKNOWN_ERROR'
182
+ });
183
+
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ async setupSecretStorageTokenRenewal() {
189
+ const requestId = ulid();
190
+ const startTime = Date.now();
191
+
192
+ // Create a base context for this method
193
+ const baseContext = createLogContext(
194
+ "CredentialManager",
195
+ "setupSecretStorageTokenRenewal",
196
+ { requestId }
197
+ );
198
+
199
+ logger.debugWithContext("Starting secret storage token renewal setup", baseContext);
200
+
201
+ try {
202
+ // Get secret storage token info to determine TTL
203
+ const tokenInfo = await this.vault.tokenLookupSelf();
204
+ const ttlSeconds = tokenInfo.data.ttl;
205
+
206
+ // Calculate renewal time (renew at 80% of TTL)
207
+ const renewalTimeMs = (ttlSeconds * 0.8) * 1000;
208
+
209
+ logger.infoWithContext("Setting up secret storage token renewal", {
210
+ ...baseContext,
211
+ ttlSeconds,
212
+ renewalIntervalMs: renewalTimeMs || this.renewalInterval,
213
+ nextRenewalAt: new Date(Date.now() + (renewalTimeMs || this.renewalInterval)).toISOString(),
214
+ duration: Date.now() - startTime
215
+ });
216
+
217
+ const interval = renewalTimeMs || this.renewalInterval;
218
+
219
+ setInterval(async () => {
220
+ const renewalRequestId = ulid();
221
+ const renewalStartTime = Date.now();
222
+
223
+ // Create a context for the renewal operation
224
+ const renewalContext = createLogContext(
225
+ "CredentialManager",
226
+ "secretStorageTokenRenewal",
227
+ { requestId: renewalRequestId }
228
+ );
229
+
230
+ logger.debugWithContext("Attempting to renew secret storage token", renewalContext);
231
+
232
+ try {
233
+ // Renew secret storage token instead of re-authenticating
234
+ const renewResponse = await this.vault.tokenRenew();
235
+
236
+ logger.infoWithContext("Successfully renewed secret storage token", {
237
+ ...renewalContext,
238
+ newTtl: renewResponse.auth?.lease_duration || "unknown",
239
+ duration: Date.now() - renewalStartTime
240
+ });
241
+
242
+ // Add metrics for successful renewal
243
+ logger.metric("vault_token_renewal_duration_ms", Date.now() - renewalStartTime, {
244
+ success: true,
245
+ component: "CredentialManager"
246
+ });
247
+ } catch (error) {
248
+ logErrorWithMetrics(
249
+ "Error renewing secret storage token, attempting re-authentication",
250
+ {
251
+ ...renewalContext,
252
+ duration: Date.now() - renewalStartTime
253
+ },
254
+ error,
255
+ "vault_token_renewal_error",
256
+ { error_type: "renewal_failure" }
257
+ );
258
+
259
+ try {
260
+ const token = await this.getVaultToken();
261
+ this.vault.token = token;
262
+
263
+ logger.infoWithContext("Successfully re-authenticated secret storage with Vault", {
264
+ ...renewalContext,
265
+ duration: Date.now() - renewalStartTime
266
+ });
267
+
268
+ // Add metrics for successful re-authentication
269
+ logger.metric("vault_token_reauthentication_duration_ms", Date.now() - renewalStartTime, {
270
+ success: true,
271
+ component: "CredentialManager"
272
+ });
273
+ } catch (reAuthError) {
274
+ logErrorWithMetrics(
275
+ "Failed to re-authenticate secret storage with Vault",
276
+ {
277
+ ...renewalContext,
278
+ duration: Date.now() - renewalStartTime
279
+ },
280
+ reAuthError,
281
+ "vault_token_reauthentication_error",
282
+ { error_type: "reauthentication_failure" }
283
+ );
284
+ }
285
+ }
286
+ }, interval);
287
+
288
+ return true;
289
+ } catch (error) {
290
+ logErrorWithMetrics(
291
+ "Error setting up secret storage token renewal",
292
+ {
293
+ ...baseContext,
294
+ duration: Date.now() - startTime
295
+ },
296
+ error,
297
+ "vault_token_renewal_setup_error",
298
+ { error_type: "setup_failure" }
299
+ );
300
+ return false;
301
+ }
302
+ }
303
+
304
+ async getRoditFromVault(vaultPath, secretKey) {
305
+ const requestId = ulid();
306
+ const startTime = Date.now();
307
+
308
+ // Create a base context for this method
309
+ const baseContext = createLogContext(
310
+ "CredentialManager",
311
+ "getRoditFromVault",
312
+ {
313
+ requestId,
314
+ vaultPath,
315
+ secretKey
316
+ }
317
+ );
318
+
319
+ this.validateVaultParameters(vaultPath, secretKey);
320
+
321
+ try {
322
+ logger.debugWithContext("Retrieving data from Vault", {
323
+ ...baseContext,
324
+ path: `secret/data/${vaultPath}`,
325
+ apiEndpoint: this.vault.endpoint,
326
+ hasToken: !!this.vault.token
327
+ });
328
+
329
+ const result = await this.vault.read(`secret/data/${vaultPath}`);
330
+ const secretData = result.data.data[secretKey];
331
+
332
+ if (!secretData) {
333
+ const error = new Error(
334
+ `Error 048: No data found for ${secretKey} at secret/data/${vaultPath}`
335
+ );
336
+
337
+ logErrorWithMetrics(
338
+ "No data found in Vault path",
339
+ baseContext,
340
+ error,
341
+ "vault_data_retrieval_error",
342
+ { error_type: "data_not_found" }
343
+ );
344
+
345
+ throw error;
346
+ }
347
+
348
+ logger.debugWithContext("Successfully retrieved data from Vault", {
349
+ ...baseContext,
350
+ duration: Date.now() - startTime
351
+ });
352
+
353
+ const parsedData = this.parseSecretData(secretData, secretKey);
354
+ return validateAndExtractCredentials(parsedData, logger);
355
+ } catch (error) {
356
+ logErrorWithMetrics(
357
+ "Error retrieving Rodit config from Vault",
358
+ {
359
+ ...baseContext,
360
+ duration: Date.now() - startTime,
361
+ errorDetails: error.response?.data || error.response || "No details available",
362
+ statusCode: error.response?.statusCode
363
+ },
364
+ error,
365
+ "vault_data_retrieval_error",
366
+ { error_type: "retrieval_failure" }
367
+ );
368
+ throw error;
369
+ }
370
+ }
371
+
372
+ validateVaultParameters(vaultPath, secretKey) {
373
+ const requestId = ulid();
374
+
375
+ // Create a base context for this method
376
+ const baseContext = createLogContext(
377
+ "CredentialManager",
378
+ "validateVaultParameters",
379
+ {
380
+ requestId,
381
+ vaultPath,
382
+ secretKey
383
+ }
384
+ );
385
+
386
+ logger.debugWithContext("Validating Vault parameters", baseContext);
387
+
388
+ if (!this.vault || typeof this.vault.read !== "function") {
389
+ const error = new Error("Error 051: Invalid vault object");
390
+ logErrorWithMetrics(
391
+ "Invalid vault object",
392
+ baseContext,
393
+ error,
394
+ "vault_parameter_validation_error",
395
+ { error_type: "invalid_vault" }
396
+ );
397
+ throw error;
398
+ }
399
+ if (!vaultPath || typeof vaultPath !== "string") {
400
+ const error = new Error("Error 052: Invalid VAULT_RODIT_KEYVALUE_PATH");
401
+ logErrorWithMetrics(
402
+ "Invalid vault path",
403
+ baseContext,
404
+ error,
405
+ "vault_parameter_validation_error",
406
+ { error_type: "invalid_path" }
407
+ );
408
+ throw error;
409
+ }
410
+ if (!secretKey || typeof secretKey !== "string") {
411
+ const error = new Error("Error 047: Invalid or missing secretKey parameter");
412
+ logErrorWithMetrics(
413
+ "Invalid secret key",
414
+ baseContext,
415
+ error,
416
+ "vault_parameter_validation_error",
417
+ { error_type: "invalid_key" }
418
+ );
419
+ throw error;
420
+ }
421
+
422
+ logger.debugWithContext("Vault parameters validated successfully", baseContext);
423
+ }
424
+
425
+ parseSecretData(secretData, secretKey) {
426
+ const requestId = ulid();
427
+
428
+ // Create a base context for this method
429
+ const baseContext = createLogContext(
430
+ "CredentialManager",
431
+ "parseSecretData",
432
+ {
433
+ requestId,
434
+ secretKey,
435
+ dataType: typeof secretData
436
+ }
437
+ );
438
+
439
+ logger.debugWithContext("Parsing secret data", baseContext);
440
+
441
+ if (typeof secretData === "string") {
442
+ try {
443
+ const parsedData = JSON.parse(secretData);
444
+ logger.debugWithContext("Successfully parsed secret data", baseContext);
445
+ return parsedData;
446
+ } catch (parseError) {
447
+ const error = new Error(`Error 046: Invalid JSON format in ${secretKey}`);
448
+ logErrorWithMetrics(
449
+ "Failed to parse secret data",
450
+ baseContext,
451
+ parseError,
452
+ "vault_data_parsing_error",
453
+ { error_type: "invalid_json" }
454
+ );
455
+ throw error;
456
+ }
457
+ }
458
+
459
+ logger.debugWithContext("Secret data already in object format", baseContext);
460
+ return secretData;
461
+ }
462
+
463
+ async getCredentials(type) {
464
+ const requestId = ulid();
465
+ const startTime = Date.now();
466
+ const maxRetries = 2; // Maximum number of retry attempts
467
+ let retryCount = 0;
468
+ let lastError = null;
469
+
470
+ // Create a base context for this method
471
+ const baseContext = createLogContext(
472
+ "CredentialManager",
473
+ "getCredentials",
474
+ {
475
+ requestId,
476
+ credentialType: type
477
+ }
478
+ );
479
+
480
+ logger.debugWithContext("Retrieving credentials", baseContext);
481
+
482
+ // Use cached credentials if available
483
+ if (this.credentials[type]) {
484
+ logger.debugWithContext("Using cached credentials", baseContext);
485
+ return this.credentials[type];
486
+ }
487
+
488
+ // Make sure vault is initialized
489
+ if (!this.vaultInitialized) {
490
+ logger.debugWithContext("Vault not initialized, initializing now", baseContext);
491
+ await this.initialize();
492
+ }
493
+
494
+ // Retry logic for vault operations
495
+ while (retryCount <= maxRetries) {
496
+ try {
497
+ // Make the accountType consistent with the type parameter
498
+ const accountType = `account_${type}`;
499
+ const vaultPath = `${this.vaultPath}/${type}`;
500
+
501
+ logger.debugWithContext("Fetching credentials from vault", {
502
+ ...baseContext,
503
+ vaultPath,
504
+ attempt: retryCount + 1,
505
+ maxAttempts: maxRetries + 1
506
+ });
507
+
508
+ const vaultData = await this.getRoditFromVault(
509
+ vaultPath,
510
+ accountType
511
+ );
512
+
513
+ // Add detailed logging about the vault data received
514
+ logger.debugWithContext("Vault data received", {
515
+ ...baseContext,
516
+ hasVaultData: !!vaultData,
517
+ dataKeys: vaultData ? Object.keys(vaultData) : [],
518
+ hasPrivateKey: vaultData && !!vaultData.private_key
519
+ });
520
+
521
+ // Safely check for private_key before using it
522
+ if (!vaultData || !vaultData.private_key || typeof vaultData.private_key !== "string") {
523
+ const error = new Error(`Invalid or missing private_key for ${type}`);
524
+ logErrorWithMetrics(
525
+ "Invalid private key format",
526
+ baseContext,
527
+ error,
528
+ "credential_retrieval_error",
529
+ { error_type: "invalid_private_key" }
530
+ );
531
+ throw error;
532
+ }
533
+
534
+ // Cache the credentials for future use
535
+ this.credentials[type] = vaultData;
536
+
537
+ const duration = Date.now() - startTime;
538
+ logger.infoWithContext("Successfully retrieved credentials", {
539
+ ...baseContext,
540
+ duration,
541
+ accountId: vaultData.account_id // Safe to log account ID
542
+ });
543
+
544
+ // Emit metrics for dashboards
545
+ logger.metric("credential_retrieval_duration_ms", duration, {
546
+ success: true,
547
+ credentialType: type,
548
+ component: "CredentialManager"
549
+ });
550
+
551
+ return vaultData;
552
+ } catch (error) {
553
+ lastError = error;
554
+
555
+ // Log the error but don't throw yet if we have retries left
556
+ const isRetrying = retryCount < maxRetries;
557
+ const duration = Date.now() - startTime;
558
+
559
+ if (isRetrying) {
560
+ logger.warnWithContext(`Retryable error retrieving credentials`, {
561
+ ...baseContext,
562
+ duration,
563
+ errorMessage: error.message,
564
+ errorCode: error.code || "UNKNOWN_ERROR",
565
+ attempt: retryCount + 1,
566
+ willRetry: true
567
+ });
568
+
569
+ // Emit metrics for dashboards
570
+ logger.metric("credential_retrieval_duration_ms", duration, {
571
+ success: false,
572
+ credentialType: type,
573
+ errorType: error.code || "UNKNOWN_ERROR",
574
+ component: "CredentialManager",
575
+ retryAttempt: retryCount
576
+ });
577
+
578
+ retryCount++;
579
+ await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
580
+ continue;
581
+ }
582
+
583
+ // If we've exhausted retries, log and throw the last error
584
+ logErrorWithMetrics(
585
+ "Failed to retrieve credentials after all retry attempts",
586
+ {
587
+ ...baseContext,
588
+ duration,
589
+ attempts: retryCount + 1,
590
+ maxAttempts: maxRetries + 1
591
+ },
592
+ lastError,
593
+ "credential_retrieval_error",
594
+ { error_type: "max_retries_exceeded" }
595
+ );
596
+
597
+ // Emit metrics for dashboards
598
+ logger.metric("credential_retrieval_errors_total", 1, {
599
+ credentialType: type,
600
+ errorType: error.code || "UNKNOWN_ERROR",
601
+ component: "CredentialManager"
602
+ });
603
+
604
+ throw lastError;
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ const vaultManager = new VaultCredentialManager();
611
+
612
+ module.exports = {
613
+ initializeCredentialStore: () => vaultManager.initialize(),
614
+ setupSecretStorageTokenRenewal: () => vaultManager.setupSecretStorageTokenRenewal(),
615
+ getCredentials: (type) => vaultManager.getCredentials(type),
616
+ vault: vaultManager.vault,
617
+ };