@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.
Files changed (98) hide show
  1. package/README.md +419 -0
  2. package/lib/scripts/test-vault-comprehensive.d.ts +2 -0
  3. package/lib/scripts/test-vault-comprehensive.d.ts.map +1 -0
  4. package/lib/scripts/test-vault-comprehensive.js +422 -0
  5. package/lib/scripts/test-vault-comprehensive.js.map +1 -0
  6. package/lib/scripts/test-vault-dynamic.d.ts +2 -0
  7. package/lib/scripts/test-vault-dynamic.d.ts.map +1 -0
  8. package/lib/scripts/test-vault-dynamic.js +193 -0
  9. package/lib/scripts/test-vault-dynamic.js.map +1 -0
  10. package/lib/scripts/test-vault-gcp-ttl.d.ts +3 -0
  11. package/lib/scripts/test-vault-gcp-ttl.d.ts.map +1 -0
  12. package/lib/scripts/test-vault-gcp-ttl.js +218 -0
  13. package/lib/scripts/test-vault-gcp-ttl.js.map +1 -0
  14. package/lib/scripts/test-vault.d.ts +2 -0
  15. package/lib/scripts/test-vault.d.ts.map +1 -0
  16. package/lib/scripts/test-vault.js +167 -0
  17. package/lib/scripts/test-vault.js.map +1 -0
  18. package/lib/src/config.errors.d.ts.map +1 -0
  19. package/lib/src/config.errors.js.map +1 -0
  20. package/lib/src/config.model.d.ts.map +1 -0
  21. package/lib/src/config.model.js.map +1 -0
  22. package/lib/{config.service.d.ts → src/config.service.d.ts} +10 -1
  23. package/lib/src/config.service.d.ts.map +1 -0
  24. package/lib/{config.service.js → src/config.service.js} +75 -9
  25. package/lib/src/config.service.js.map +1 -0
  26. package/lib/src/environment.service.d.ts.map +1 -0
  27. package/lib/src/environment.service.js.map +1 -0
  28. package/lib/{index.d.ts → src/index.d.ts} +1 -0
  29. package/lib/src/index.d.ts.map +1 -0
  30. package/lib/{index.js → src/index.js} +1 -0
  31. package/lib/src/index.js.map +1 -0
  32. package/lib/src/json-schema.validator.d.ts.map +1 -0
  33. package/lib/src/json-schema.validator.js.map +1 -0
  34. package/lib/src/vault/__tests__/vault-integration.test.d.ts +2 -0
  35. package/lib/src/vault/__tests__/vault-integration.test.d.ts.map +1 -0
  36. package/lib/src/vault/__tests__/vault-integration.test.js +190 -0
  37. package/lib/src/vault/__tests__/vault-integration.test.js.map +1 -0
  38. package/lib/src/vault/decorators.d.ts +17 -0
  39. package/lib/src/vault/decorators.d.ts.map +1 -0
  40. package/lib/src/vault/decorators.js +149 -0
  41. package/lib/src/vault/decorators.js.map +1 -0
  42. package/lib/src/vault/index.d.ts +7 -0
  43. package/lib/src/vault/index.d.ts.map +1 -0
  44. package/lib/src/vault/index.js +42 -0
  45. package/lib/src/vault/index.js.map +1 -0
  46. package/lib/src/vault/secret-refresh-manager.d.ts +23 -0
  47. package/lib/src/vault/secret-refresh-manager.d.ts.map +1 -0
  48. package/lib/src/vault/secret-refresh-manager.js +149 -0
  49. package/lib/src/vault/secret-refresh-manager.js.map +1 -0
  50. package/lib/src/vault/types.d.ts +149 -0
  51. package/lib/src/vault/types.d.ts.map +1 -0
  52. package/lib/src/vault/types.js +4 -0
  53. package/lib/src/vault/types.js.map +1 -0
  54. package/lib/src/vault/vault-cache.d.ts +20 -0
  55. package/lib/src/vault/vault-cache.d.ts.map +1 -0
  56. package/lib/src/vault/vault-cache.js +139 -0
  57. package/lib/src/vault/vault-cache.js.map +1 -0
  58. package/lib/src/vault/vault-integration.d.ts +27 -0
  59. package/lib/src/vault/vault-integration.d.ts.map +1 -0
  60. package/lib/src/vault/vault-integration.js +211 -0
  61. package/lib/src/vault/vault-integration.js.map +1 -0
  62. package/lib/src/vault/vault-provider.d.ts +37 -0
  63. package/lib/src/vault/vault-provider.d.ts.map +1 -0
  64. package/lib/src/vault/vault-provider.js +354 -0
  65. package/lib/src/vault/vault-provider.js.map +1 -0
  66. package/lib/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +14 -74
  68. package/src/config.service.ts +155 -10
  69. package/src/config.service.vault.spec.ts +859 -0
  70. package/src/index.ts +1 -0
  71. package/src/vault/__tests__/vault-integration.test.ts +226 -0
  72. package/src/vault/decorators.ts +228 -0
  73. package/src/vault/index.ts +31 -0
  74. package/src/vault/secret-refresh-manager.ts +241 -0
  75. package/src/vault/types.ts +487 -0
  76. package/src/vault/vault-cache.ts +240 -0
  77. package/src/vault/vault-integration.ts +332 -0
  78. package/src/vault/vault-provider.ts +576 -0
  79. package/lib/config.errors.d.ts.map +0 -1
  80. package/lib/config.errors.js.map +0 -1
  81. package/lib/config.model.d.ts.map +0 -1
  82. package/lib/config.model.js.map +0 -1
  83. package/lib/config.service.d.ts.map +0 -1
  84. package/lib/config.service.js.map +0 -1
  85. package/lib/environment.service.d.ts.map +0 -1
  86. package/lib/environment.service.js.map +0 -1
  87. package/lib/index.d.ts.map +0 -1
  88. package/lib/index.js.map +0 -1
  89. package/lib/json-schema.validator.d.ts.map +0 -1
  90. package/lib/json-schema.validator.js.map +0 -1
  91. /package/lib/{config.errors.d.ts → src/config.errors.d.ts} +0 -0
  92. /package/lib/{config.errors.js → src/config.errors.js} +0 -0
  93. /package/lib/{config.model.d.ts → src/config.model.d.ts} +0 -0
  94. /package/lib/{config.model.js → src/config.model.js} +0 -0
  95. /package/lib/{environment.service.d.ts → src/environment.service.d.ts} +0 -0
  96. /package/lib/{environment.service.js → src/environment.service.js} +0 -0
  97. /package/lib/{json-schema.validator.d.ts → src/json-schema.validator.d.ts} +0 -0
  98. /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
+ }