@kibibit/configit 1.0.0-beta.25 → 1.0.0-beta.27
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/README.md +419 -0
- package/lib/scripts/test-vault-comprehensive.d.ts +2 -0
- package/lib/scripts/test-vault-comprehensive.d.ts.map +1 -0
- package/lib/scripts/test-vault-comprehensive.js +422 -0
- package/lib/scripts/test-vault-comprehensive.js.map +1 -0
- package/lib/scripts/test-vault-dynamic.d.ts +2 -0
- package/lib/scripts/test-vault-dynamic.d.ts.map +1 -0
- package/lib/scripts/test-vault-dynamic.js +193 -0
- package/lib/scripts/test-vault-dynamic.js.map +1 -0
- package/lib/scripts/test-vault-gcp-ttl.d.ts +3 -0
- package/lib/scripts/test-vault-gcp-ttl.d.ts.map +1 -0
- package/lib/scripts/test-vault-gcp-ttl.js +218 -0
- package/lib/scripts/test-vault-gcp-ttl.js.map +1 -0
- package/lib/scripts/test-vault.d.ts +2 -0
- package/lib/scripts/test-vault.d.ts.map +1 -0
- package/lib/scripts/test-vault.js +167 -0
- package/lib/scripts/test-vault.js.map +1 -0
- package/lib/src/config.errors.d.ts.map +1 -0
- package/lib/src/config.errors.js.map +1 -0
- package/lib/src/config.model.d.ts.map +1 -0
- package/lib/src/config.model.js.map +1 -0
- package/lib/{config.service.d.ts → src/config.service.d.ts} +10 -1
- package/lib/src/config.service.d.ts.map +1 -0
- package/lib/{config.service.js → src/config.service.js} +75 -9
- package/lib/src/config.service.js.map +1 -0
- package/lib/src/environment.service.d.ts.map +1 -0
- package/lib/src/environment.service.js.map +1 -0
- package/lib/{index.d.ts → src/index.d.ts} +1 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/{index.js → src/index.js} +1 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/json-schema.validator.d.ts.map +1 -0
- package/lib/src/json-schema.validator.js.map +1 -0
- package/lib/src/vault/__tests__/vault-integration.test.d.ts +2 -0
- package/lib/src/vault/__tests__/vault-integration.test.d.ts.map +1 -0
- package/lib/src/vault/__tests__/vault-integration.test.js +190 -0
- package/lib/src/vault/__tests__/vault-integration.test.js.map +1 -0
- package/lib/src/vault/decorators.d.ts +17 -0
- package/lib/src/vault/decorators.d.ts.map +1 -0
- package/lib/src/vault/decorators.js +149 -0
- package/lib/src/vault/decorators.js.map +1 -0
- package/lib/src/vault/index.d.ts +7 -0
- package/lib/src/vault/index.d.ts.map +1 -0
- package/lib/src/vault/index.js +42 -0
- package/lib/src/vault/index.js.map +1 -0
- package/lib/src/vault/secret-refresh-manager.d.ts +23 -0
- package/lib/src/vault/secret-refresh-manager.d.ts.map +1 -0
- package/lib/src/vault/secret-refresh-manager.js +149 -0
- package/lib/src/vault/secret-refresh-manager.js.map +1 -0
- package/lib/src/vault/types.d.ts +149 -0
- package/lib/src/vault/types.d.ts.map +1 -0
- package/lib/src/vault/types.js +4 -0
- package/lib/src/vault/types.js.map +1 -0
- package/lib/src/vault/vault-cache.d.ts +20 -0
- package/lib/src/vault/vault-cache.d.ts.map +1 -0
- package/lib/src/vault/vault-cache.js +139 -0
- package/lib/src/vault/vault-cache.js.map +1 -0
- package/lib/src/vault/vault-integration.d.ts +27 -0
- package/lib/src/vault/vault-integration.d.ts.map +1 -0
- package/lib/src/vault/vault-integration.js +211 -0
- package/lib/src/vault/vault-integration.js.map +1 -0
- package/lib/src/vault/vault-provider.d.ts +37 -0
- package/lib/src/vault/vault-provider.d.ts.map +1 -0
- package/lib/src/vault/vault-provider.js +354 -0
- package/lib/src/vault/vault-provider.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -74
- package/src/config.service.ts +155 -10
- package/src/config.service.vault.spec.ts +859 -0
- package/src/index.ts +1 -0
- package/src/vault/__tests__/vault-integration.test.ts +226 -0
- package/src/vault/decorators.ts +228 -0
- package/src/vault/index.ts +31 -0
- package/src/vault/secret-refresh-manager.ts +241 -0
- package/src/vault/types.ts +487 -0
- package/src/vault/vault-cache.ts +240 -0
- package/src/vault/vault-integration.ts +332 -0
- package/src/vault/vault-provider.ts +576 -0
- package/lib/config.errors.d.ts.map +0 -1
- package/lib/config.errors.js.map +0 -1
- package/lib/config.model.d.ts.map +0 -1
- package/lib/config.model.js.map +0 -1
- package/lib/config.service.d.ts.map +0 -1
- package/lib/config.service.js.map +0 -1
- package/lib/environment.service.d.ts.map +0 -1
- package/lib/environment.service.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/json-schema.validator.d.ts.map +0 -1
- package/lib/json-schema.validator.js.map +0 -1
- /package/lib/{config.errors.d.ts → src/config.errors.d.ts} +0 -0
- /package/lib/{config.errors.js → src/config.errors.js} +0 -0
- /package/lib/{config.model.d.ts → src/config.model.d.ts} +0 -0
- /package/lib/{config.model.js → src/config.model.js} +0 -0
- /package/lib/{environment.service.d.ts → src/environment.service.d.ts} +0 -0
- /package/lib/{environment.service.js → src/environment.service.js} +0 -0
- /package/lib/{json-schema.validator.d.ts → src/json-schema.validator.d.ts} +0 -0
- /package/lib/{json-schema.validator.js → src/json-schema.validator.js} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VaultCache
|
|
3
|
+
* In-memory cache for Vault secrets with TTL management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import nconf from 'nconf';
|
|
7
|
+
|
|
8
|
+
import { IVaultSecret, VaultCacheEntry, VaultPropertyMetadata } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* VaultCache - Manages in-memory cache of Vault secrets
|
|
12
|
+
*/
|
|
13
|
+
export class VaultCache {
|
|
14
|
+
private cache: Map<string, VaultCacheEntry> = new Map();
|
|
15
|
+
private propertyToPath: Map<string, string> = new Map();
|
|
16
|
+
private pathToProperties: Map<string, Set<string>> = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get cached value for a property
|
|
20
|
+
* @returns The cached value or null if not found/expired
|
|
21
|
+
*/
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
get(propertyName: string): any | null {
|
|
24
|
+
const entry = this.cache.get(propertyName);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if expired
|
|
30
|
+
if (Date.now() > entry.expiresAt) {
|
|
31
|
+
this.cache.delete(propertyName);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return entry.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set cached value for a property
|
|
40
|
+
*/
|
|
41
|
+
set(
|
|
42
|
+
propertyName: string,
|
|
43
|
+
vaultPath: string,
|
|
44
|
+
secret: IVaultSecret,
|
|
45
|
+
metadata: VaultPropertyMetadata
|
|
46
|
+
): void {
|
|
47
|
+
// Extract value based on engine type
|
|
48
|
+
const value = this.extractValue(secret, metadata);
|
|
49
|
+
|
|
50
|
+
// Calculate expiration times
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const leaseDurationMs = secret.leaseDuration * 1000;
|
|
53
|
+
// Default 1 hour if no TTL
|
|
54
|
+
const expiresAt = secret.leaseDuration > 0 ? now + leaseDurationMs : now + 3600000;
|
|
55
|
+
|
|
56
|
+
// Calculate refresh time (refresh buffer)
|
|
57
|
+
// metadata.refreshBuffer is in SECONDS, we need milliseconds
|
|
58
|
+
// Default: min(10% of TTL, 5 minutes)
|
|
59
|
+
const defaultBufferMs = Math.min(leaseDurationMs * 0.1, 300000);
|
|
60
|
+
const refreshBufferMs = metadata.refreshBuffer ? metadata.refreshBuffer * 1000 : defaultBufferMs;
|
|
61
|
+
const refreshAt = secret.leaseDuration > 0 ? expiresAt - refreshBufferMs : expiresAt;
|
|
62
|
+
|
|
63
|
+
const entry: VaultCacheEntry = {
|
|
64
|
+
value,
|
|
65
|
+
secret,
|
|
66
|
+
cachedAt: now,
|
|
67
|
+
expiresAt,
|
|
68
|
+
refreshAt,
|
|
69
|
+
propertyName,
|
|
70
|
+
vaultPath
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this.cache.set(propertyName, entry);
|
|
74
|
+
|
|
75
|
+
// Update mappings
|
|
76
|
+
this.propertyToPath.set(propertyName, vaultPath);
|
|
77
|
+
|
|
78
|
+
const properties = this.pathToProperties.get(vaultPath) || new Set();
|
|
79
|
+
properties.add(propertyName);
|
|
80
|
+
this.pathToProperties.set(vaultPath, properties);
|
|
81
|
+
|
|
82
|
+
// Inject into nconf overrides (highest priority)
|
|
83
|
+
// Merge with existing overrides to avoid overwriting other secrets
|
|
84
|
+
// Access the overrides store directly to get current values
|
|
85
|
+
const overridesStore = (nconf as any).stores?.overrides;
|
|
86
|
+
const existingOverrides = overridesStore?.store || {};
|
|
87
|
+
|
|
88
|
+
// Merge and set all overrides at once
|
|
89
|
+
nconf.overrides({
|
|
90
|
+
...existingOverrides,
|
|
91
|
+
[propertyName]: value
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract value from secret based on engine type and metadata
|
|
97
|
+
*/
|
|
98
|
+
private extractValue(secret: IVaultSecret, metadata: VaultPropertyMetadata): any {
|
|
99
|
+
const { engine, key } = metadata;
|
|
100
|
+
|
|
101
|
+
switch (engine) {
|
|
102
|
+
case 'kv-v1':
|
|
103
|
+
// KV v1: data is flat
|
|
104
|
+
return key ? secret.data[key] : secret.data;
|
|
105
|
+
|
|
106
|
+
case 'kv-v2':
|
|
107
|
+
// KV v2: data is nested under 'data' key
|
|
108
|
+
const kv2Data = secret.data?.data || secret.data;
|
|
109
|
+
return key ? kv2Data[key] : kv2Data;
|
|
110
|
+
|
|
111
|
+
case 'database':
|
|
112
|
+
case 'aws':
|
|
113
|
+
case 'azure':
|
|
114
|
+
case 'gcp':
|
|
115
|
+
// Dynamic secrets: data contains credentials
|
|
116
|
+
return key ? secret.data[key] : secret.data;
|
|
117
|
+
|
|
118
|
+
default:
|
|
119
|
+
// Default: try to extract by key, fallback to entire data
|
|
120
|
+
return key ? secret.data[key] : secret.data;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Invalidate cache entry by path
|
|
126
|
+
*/
|
|
127
|
+
invalidate(vaultPath: string): void {
|
|
128
|
+
const properties = this.pathToProperties.get(vaultPath);
|
|
129
|
+
if (!properties) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const propertyName of properties) {
|
|
134
|
+
this.cache.delete(propertyName);
|
|
135
|
+
this.propertyToPath.delete(propertyName);
|
|
136
|
+
// Remove from nconf overrides
|
|
137
|
+
nconf.remove(propertyName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.pathToProperties.delete(vaultPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Invalidate cache entry by property name
|
|
145
|
+
*/
|
|
146
|
+
invalidateProperty(propertyName: string): void {
|
|
147
|
+
const vaultPath = this.propertyToPath.get(propertyName);
|
|
148
|
+
if (vaultPath) {
|
|
149
|
+
this.invalidate(vaultPath);
|
|
150
|
+
} else {
|
|
151
|
+
// Just remove this property
|
|
152
|
+
this.cache.delete(propertyName);
|
|
153
|
+
nconf.remove(propertyName);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get cache entry for a property
|
|
159
|
+
*/
|
|
160
|
+
getEntry(propertyName: string): VaultCacheEntry | undefined {
|
|
161
|
+
return this.cache.get(propertyName);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all properties for a Vault path
|
|
166
|
+
*/
|
|
167
|
+
getPropertiesForPath(vaultPath: string): string[] {
|
|
168
|
+
const properties = this.pathToProperties.get(vaultPath);
|
|
169
|
+
return properties ? Array.from(properties) : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get Vault path for a property
|
|
174
|
+
*/
|
|
175
|
+
getPathForProperty(propertyName: string): string | undefined {
|
|
176
|
+
return this.propertyToPath.get(propertyName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if cache entry exists and is valid
|
|
181
|
+
*/
|
|
182
|
+
has(propertyName: string): boolean {
|
|
183
|
+
const entry = this.cache.get(propertyName);
|
|
184
|
+
if (!entry) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if expired
|
|
189
|
+
if (Date.now() > entry.expiresAt) {
|
|
190
|
+
this.cache.delete(propertyName);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get all cache entries that need refresh
|
|
199
|
+
*/
|
|
200
|
+
getEntriesNeedingRefresh(): VaultCacheEntry[] {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const entries: VaultCacheEntry[] = [];
|
|
203
|
+
|
|
204
|
+
for (const entry of this.cache.values()) {
|
|
205
|
+
if (entry.refreshAt <= now && entry.expiresAt > now) {
|
|
206
|
+
entries.push(entry);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return entries;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clear all cache entries
|
|
215
|
+
*/
|
|
216
|
+
clear(): void {
|
|
217
|
+
// Remove all from nconf overrides
|
|
218
|
+
for (const propertyName of this.cache.keys()) {
|
|
219
|
+
nconf.remove(propertyName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.cache.clear();
|
|
223
|
+
this.propertyToPath.clear();
|
|
224
|
+
this.pathToProperties.clear();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get cache size
|
|
229
|
+
*/
|
|
230
|
+
size(): number {
|
|
231
|
+
return this.cache.size;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all cached property names
|
|
236
|
+
*/
|
|
237
|
+
getCachedProperties(): string[] {
|
|
238
|
+
return Array.from(this.cache.keys());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VaultIntegration
|
|
3
|
+
* High-level integration class for Vault secrets management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getAllVaultMetadata } from './decorators';
|
|
7
|
+
import { SecretRefreshManager } from './secret-refresh-manager';
|
|
8
|
+
import {
|
|
9
|
+
IVaultConfigOptions,
|
|
10
|
+
IVaultHealthDetails,
|
|
11
|
+
VaultHealth,
|
|
12
|
+
VaultPropertyMetadata
|
|
13
|
+
} from './types';
|
|
14
|
+
import { VaultCache } from './vault-cache';
|
|
15
|
+
import { VaultProvider } from './vault-provider';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* VaultIntegration - High-level API for Vault secrets integration
|
|
19
|
+
*/
|
|
20
|
+
export class VaultIntegration {
|
|
21
|
+
private provider: VaultProvider;
|
|
22
|
+
private cache: VaultCache;
|
|
23
|
+
private refreshManager: SecretRefreshManager;
|
|
24
|
+
private initialized = false;
|
|
25
|
+
private config: IVaultConfigOptions;
|
|
26
|
+
private errors: Array<{ timestamp: number; path: string; error: string; retryable: boolean }> = [];
|
|
27
|
+
|
|
28
|
+
constructor(config: IVaultConfigOptions) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.provider = new VaultProvider(config);
|
|
31
|
+
this.cache = new VaultCache();
|
|
32
|
+
const refreshBuffer = config.refreshBuffer || 300; // Default 5 minutes
|
|
33
|
+
this.refreshManager = new SecretRefreshManager(this.provider, this.cache, refreshBuffer);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize Vault connection and authenticate
|
|
38
|
+
* Must be called before using Vault integration
|
|
39
|
+
*/
|
|
40
|
+
async initialize(): Promise<void> {
|
|
41
|
+
if (this.initialized) {
|
|
42
|
+
return; // Already initialized
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await this.provider.initialize();
|
|
47
|
+
this.initialized = true;
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
const errorMessage = error?.message || 'Unknown error';
|
|
50
|
+
this.recordError('', this.sanitizeError(errorMessage), false);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load secrets for a config class or instance
|
|
57
|
+
* Scans for @VaultPath decorators and loads secrets
|
|
58
|
+
*/
|
|
59
|
+
async loadSecrets<T extends object>(configOrClass: T | (new () => T)): Promise<void> {
|
|
60
|
+
if (!this.initialized) {
|
|
61
|
+
throw new Error('VaultIntegration not initialized. Call initialize() first.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Determine if we got a class or instance
|
|
65
|
+
const isClass = typeof configOrClass === 'function';
|
|
66
|
+
const targetClass = isClass ? configOrClass : (configOrClass.constructor as new () => T);
|
|
67
|
+
const targetInstance = isClass ? null : configOrClass;
|
|
68
|
+
|
|
69
|
+
// Get all Vault metadata from decorators
|
|
70
|
+
const vaultMetadata = getAllVaultMetadata(targetClass);
|
|
71
|
+
|
|
72
|
+
if (Object.keys(vaultMetadata).length === 0) {
|
|
73
|
+
return; // No Vault properties
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Group properties by full Vault path (including engine prefix)
|
|
77
|
+
const pathGroups = this.groupByFullPath(vaultMetadata);
|
|
78
|
+
|
|
79
|
+
// Load secrets for each path
|
|
80
|
+
for (const [ fullPath, properties ] of pathGroups.entries()) {
|
|
81
|
+
try {
|
|
82
|
+
const secret = await this.provider.read(fullPath);
|
|
83
|
+
|
|
84
|
+
// Cache secret for each property
|
|
85
|
+
for (const property of properties) {
|
|
86
|
+
// Merge global refreshBuffer config with property metadata
|
|
87
|
+
const propertyWithDefaults = {
|
|
88
|
+
...property,
|
|
89
|
+
// Use property-specific refreshBuffer, fall back to global config
|
|
90
|
+
refreshBuffer: property.refreshBuffer ?? this.config.refreshBuffer
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.cache.set(property.propertyName, fullPath, secret, propertyWithDefaults);
|
|
94
|
+
|
|
95
|
+
// Extract and set the specific key value to the instance
|
|
96
|
+
if (targetInstance) {
|
|
97
|
+
const key = property.key || property.propertyName;
|
|
98
|
+
const value = secret.data[key];
|
|
99
|
+
(targetInstance as any)[property.propertyName] = value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Schedule refresh if secret has TTL
|
|
103
|
+
if (secret.leaseDuration > 0) {
|
|
104
|
+
this.refreshManager.scheduleRefresh(property.propertyName, propertyWithDefaults, targetInstance);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
const errorMessage = error?.message || 'Unknown error';
|
|
109
|
+
const sanitizedError = this.sanitizeError(errorMessage);
|
|
110
|
+
this.recordError(fullPath, sanitizedError, this.isRetryableError(error));
|
|
111
|
+
|
|
112
|
+
// Handle fallback strategy
|
|
113
|
+
const fallback = this.config.fallback;
|
|
114
|
+
if (fallback?.required !== false) {
|
|
115
|
+
// Required secret - throw error
|
|
116
|
+
throw new Error(`Failed to load required secret from ${ this.sanitizePath(fullPath) }: ${ sanitizedError }`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Optional secret - log warning and continue
|
|
120
|
+
console.warn(`Failed to load optional secret from ${ this.sanitizePath(fullPath) }: ${ sanitizedError }`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get secret value synchronously from cache
|
|
127
|
+
*/
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
getSecret(propertyName: string): any | null {
|
|
130
|
+
return this.cache.get(propertyName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if Vault integration is initialized
|
|
135
|
+
*/
|
|
136
|
+
isInitialized(): boolean {
|
|
137
|
+
return this.initialized;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get Vault health status
|
|
142
|
+
*/
|
|
143
|
+
getHealth(): VaultHealth {
|
|
144
|
+
const refreshStatus = this.refreshManager.getRefreshStatus();
|
|
145
|
+
const lastRefreshTime = refreshStatus.length > 0 ?
|
|
146
|
+
Math.max(...refreshStatus.map((s) => s.lastRefresh)) :
|
|
147
|
+
0;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
connected: this.initialized && this.provider.isAuthenticated(),
|
|
151
|
+
authenticated: this.provider.isAuthenticated(),
|
|
152
|
+
cacheSize: this.cache.size(),
|
|
153
|
+
refreshQueueSize: refreshStatus.filter((s) => s.scheduled).length,
|
|
154
|
+
lastRefreshTime,
|
|
155
|
+
errors: this.errors.slice(-10) // Last 10 errors
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get detailed health information
|
|
161
|
+
*/
|
|
162
|
+
getHealthDetails(): IVaultHealthDetails {
|
|
163
|
+
const refreshStatus = this.refreshManager.getRefreshStatus();
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
connected: this.initialized && this.provider.isAuthenticated(),
|
|
167
|
+
authenticated: this.provider.isAuthenticated(),
|
|
168
|
+
cacheSize: this.cache.size(),
|
|
169
|
+
refreshQueueSize: refreshStatus.filter((s) => s.scheduled).length,
|
|
170
|
+
lastRefreshTime: refreshStatus.length > 0 ?
|
|
171
|
+
Math.max(...refreshStatus.map((s) => s.lastRefresh)) :
|
|
172
|
+
0,
|
|
173
|
+
errors: this.errors.slice(-10), // Last 10 errors
|
|
174
|
+
refreshStatus
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Invalidate cache for a Vault path
|
|
180
|
+
*/
|
|
181
|
+
invalidateCache(vaultPath: string): void {
|
|
182
|
+
this.cache.invalidate(vaultPath);
|
|
183
|
+
// Cancel refresh for properties using this path
|
|
184
|
+
const properties = this.cache.getPropertiesForPath(vaultPath);
|
|
185
|
+
for (const propertyName of properties) {
|
|
186
|
+
this.refreshManager.cancelRefresh(propertyName);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Invalidate cache for a property
|
|
192
|
+
*/
|
|
193
|
+
invalidateProperty(propertyName: string): void {
|
|
194
|
+
this.cache.invalidateProperty(propertyName);
|
|
195
|
+
this.refreshManager.cancelRefresh(propertyName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Shutdown gracefully - stop all refresh workers
|
|
200
|
+
*/
|
|
201
|
+
shutdown(): void {
|
|
202
|
+
this.refreshManager.shutdown();
|
|
203
|
+
this.cache.clear();
|
|
204
|
+
this.initialized = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Group properties by Vault path
|
|
209
|
+
*/
|
|
210
|
+
private groupByPath(metadata: Record<string, VaultPropertyMetadata>): Map<string, VaultPropertyMetadata[]> {
|
|
211
|
+
const groups = new Map<string, VaultPropertyMetadata[]>();
|
|
212
|
+
|
|
213
|
+
for (const property of Object.values(metadata)) {
|
|
214
|
+
const path = property.path;
|
|
215
|
+
if (!groups.has(path)) {
|
|
216
|
+
groups.set(path, []);
|
|
217
|
+
}
|
|
218
|
+
groups.get(path)!.push(property);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return groups;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Group properties by full Vault path (including engine prefix)
|
|
226
|
+
*/
|
|
227
|
+
private groupByFullPath(metadata: Record<string, VaultPropertyMetadata>): Map<string, VaultPropertyMetadata[]> {
|
|
228
|
+
const groups = new Map<string, VaultPropertyMetadata[]>();
|
|
229
|
+
|
|
230
|
+
for (const property of Object.values(metadata)) {
|
|
231
|
+
const fullPath = this.constructFullPath(property.path, property.engine);
|
|
232
|
+
if (!groups.has(fullPath)) {
|
|
233
|
+
groups.set(fullPath, []);
|
|
234
|
+
}
|
|
235
|
+
groups.get(fullPath)!.push(property);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return groups;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Construct full Vault path based on engine type
|
|
243
|
+
*/
|
|
244
|
+
private constructFullPath(path: string, engine: string): string {
|
|
245
|
+
switch (engine) {
|
|
246
|
+
case 'kv1':
|
|
247
|
+
case 'kv-v1':
|
|
248
|
+
return `secret/${ path }`;
|
|
249
|
+
case 'kv2':
|
|
250
|
+
case 'kv-v2':
|
|
251
|
+
return `secret/data/${ path }`;
|
|
252
|
+
case 'database':
|
|
253
|
+
return path.startsWith('database/') ? path : `database/${ path }`;
|
|
254
|
+
default:
|
|
255
|
+
return path;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Record error for health monitoring
|
|
261
|
+
*/
|
|
262
|
+
private recordError(path: string, error: string, retryable: boolean): void {
|
|
263
|
+
this.errors.push({
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
path: this.sanitizePath(path),
|
|
266
|
+
error,
|
|
267
|
+
retryable
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Keep only last 100 errors
|
|
271
|
+
if (this.errors.length > 100) {
|
|
272
|
+
this.errors.shift();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if error is retryable
|
|
278
|
+
*/
|
|
279
|
+
private isRetryableError(error: any): boolean {
|
|
280
|
+
const errorMessage = error?.message || '';
|
|
281
|
+
const errorCode = error?.code || '';
|
|
282
|
+
const statusCode = error?.statusCode || error?.response?.statusCode;
|
|
283
|
+
|
|
284
|
+
const retryablePatterns = [ 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', '5xx' ];
|
|
285
|
+
|
|
286
|
+
for (const pattern of retryablePatterns) {
|
|
287
|
+
if (pattern.includes('xx') && statusCode) {
|
|
288
|
+
const codePrefix = parseInt(pattern[0]);
|
|
289
|
+
const statusPrefix = Math.floor(statusCode / 100);
|
|
290
|
+
if (statusPrefix === codePrefix) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
} else if (errorMessage.includes(pattern) || errorCode.includes(pattern)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Sanitize error message
|
|
303
|
+
*/
|
|
304
|
+
private sanitizeError(message: string): string {
|
|
305
|
+
const sensitivePatterns = [ /password/i, /secret/i, /key/i, /token/i, /credential/i ];
|
|
306
|
+
let sanitized = message;
|
|
307
|
+
|
|
308
|
+
sensitivePatterns.forEach((pattern) => {
|
|
309
|
+
sanitized = sanitized.replace(
|
|
310
|
+
new RegExp(`${ pattern.source }[:=]\\s*[^\\s,}]+`, 'gi'),
|
|
311
|
+
`${ pattern.source }: ***`
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return sanitized;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Sanitize path for logging
|
|
320
|
+
*/
|
|
321
|
+
private sanitizePath(path: string): string {
|
|
322
|
+
const segments = path.split('/');
|
|
323
|
+
if (segments.length > 0) {
|
|
324
|
+
const lastSegment = segments[segments.length - 1];
|
|
325
|
+
const sensitivePatterns = [ /password/i, /secret/i, /key/i, /token/i, /credential/i ];
|
|
326
|
+
if (sensitivePatterns.some((pattern) => pattern.test(lastSegment))) {
|
|
327
|
+
segments[segments.length - 1] = '***';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return segments.join('/');
|
|
331
|
+
}
|
|
332
|
+
}
|