@rytass/secret-adapter-vault-nestjs 0.3.3 → 0.4.1
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 +466 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,30 +1,482 @@
|
|
|
1
|
-
# Rytass Utils - Secret
|
|
1
|
+
# Rytass Utils - Secret Adapter Vault NestJS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
NestJS module for HashiCorp Vault integration, providing secure secret management with automatic fallback to environment variables. Seamlessly integrates with NestJS dependency injection and configuration system.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- [x] HashiCorp Vault integration for NestJS
|
|
8
|
+
- [x] Automatic fallback to environment variables
|
|
9
|
+
- [x] Global module support
|
|
10
|
+
- [x] TypeScript support with generics
|
|
11
|
+
- [x] Asynchronous secret retrieval
|
|
12
|
+
- [x] Secret writing and deletion
|
|
13
|
+
- [x] Online/offline sync support
|
|
14
|
+
- [x] Connection state management
|
|
15
|
+
- [x] Error handling with graceful degradation
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vault
|
|
21
|
+
# or
|
|
22
|
+
yarn add @rytass/secret-adapter-vault-nestjs @rytass/secret-adapter-vault
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Environment Configuration
|
|
26
|
+
|
|
27
|
+
Configure the following environment variables for Vault connection:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Required for Vault connection
|
|
31
|
+
VAULT_HOST=https://vault.example.com:8200 # Vault service base URL
|
|
32
|
+
VAULT_ACCOUNT=your-username # Vault username
|
|
33
|
+
VAULT_PASSWORD=your-password # Vault password
|
|
34
|
+
|
|
35
|
+
# Optional - defaults to root path if not specified
|
|
36
|
+
VAULT_PATH=/secret/data/myapp # Vault secret path from root
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Usage
|
|
40
|
+
|
|
41
|
+
### Module Setup
|
|
11
42
|
|
|
12
43
|
```typescript
|
|
13
44
|
import { Module } from '@nestjs/common';
|
|
14
|
-
import {
|
|
45
|
+
import { VaultModule } from '@rytass/secret-adapter-vault-nestjs';
|
|
46
|
+
|
|
47
|
+
@Module({
|
|
48
|
+
imports: [
|
|
49
|
+
VaultModule.forRoot({
|
|
50
|
+
path: '/secret/data/myapp', // Vault path for secrets
|
|
51
|
+
fallbackFile: '.env', // Optional: fallback env file
|
|
52
|
+
})
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
export class AppModule {}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Global Module Configuration
|
|
59
|
+
|
|
60
|
+
Make the VaultService available throughout your application:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { Module } from '@nestjs/common';
|
|
64
|
+
import { VaultModule } from '@rytass/secret-adapter-vault-nestjs';
|
|
65
|
+
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [
|
|
68
|
+
VaultModule.forRoot({
|
|
69
|
+
path: '/secret/data/myapp',
|
|
70
|
+
fallbackFile: '.env'
|
|
71
|
+
})
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
export class AppModule {}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Using VaultService
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Injectable } from '@nestjs/common';
|
|
81
|
+
import { VaultService } from '@rytass/secret-adapter-vault-nestjs';
|
|
15
82
|
|
|
16
83
|
@Injectable()
|
|
17
|
-
class
|
|
84
|
+
export class ConfigurationService {
|
|
18
85
|
constructor(private readonly vault: VaultService) {}
|
|
19
86
|
|
|
20
|
-
async
|
|
21
|
-
|
|
87
|
+
async getDatabaseConfig() {
|
|
88
|
+
const host = await this.vault.get<string>('DB_HOST');
|
|
89
|
+
const port = await this.vault.get<number>('DB_PORT');
|
|
90
|
+
const username = await this.vault.get<string>('DB_USERNAME');
|
|
91
|
+
const password = await this.vault.get<string>('DB_PASSWORD');
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
host,
|
|
95
|
+
port,
|
|
96
|
+
username,
|
|
97
|
+
password
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getApiKey(): Promise<string> {
|
|
102
|
+
return this.vault.get<string>('API_KEY');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async updateApiKey(newKey: string): Promise<void> {
|
|
106
|
+
// Update locally and sync to Vault
|
|
107
|
+
await this.vault.set('API_KEY', newKey, true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Advanced Usage
|
|
113
|
+
|
|
114
|
+
### Type-Safe Secret Retrieval
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface DatabaseConfig {
|
|
118
|
+
host: string;
|
|
119
|
+
port: number;
|
|
120
|
+
username: string;
|
|
121
|
+
password: string;
|
|
122
|
+
ssl: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Injectable()
|
|
126
|
+
export class DatabaseService {
|
|
127
|
+
constructor(private readonly vault: VaultService) {}
|
|
128
|
+
|
|
129
|
+
async getConfig(): Promise<DatabaseConfig> {
|
|
130
|
+
// Retrieve complex objects
|
|
131
|
+
return this.vault.get<DatabaseConfig>('database');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getConnectionString(): Promise<string> {
|
|
135
|
+
const config = await this.getConfig();
|
|
136
|
+
return `postgresql://${config.username}:${config.password}@${config.host}:${config.port}/mydb`;
|
|
22
137
|
}
|
|
23
138
|
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Managing Secrets
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
@Injectable()
|
|
145
|
+
export class SecretManagementService {
|
|
146
|
+
constructor(private readonly vault: VaultService) {}
|
|
147
|
+
|
|
148
|
+
// Create or update a secret
|
|
149
|
+
async createSecret(key: string, value: any): Promise<void> {
|
|
150
|
+
// Save locally only
|
|
151
|
+
await this.vault.set(key, value, false);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create and sync to Vault immediately
|
|
155
|
+
async createAndSyncSecret(key: string, value: any): Promise<void> {
|
|
156
|
+
await this.vault.set(key, value, true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Delete a secret
|
|
160
|
+
async removeSecret(key: string): Promise<void> {
|
|
161
|
+
// Delete locally only
|
|
162
|
+
await this.vault.delete(key, false);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Delete and sync removal to Vault
|
|
166
|
+
async removeAndSyncSecret(key: string): Promise<void> {
|
|
167
|
+
await this.vault.delete(key, true);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Fallback Mechanism
|
|
173
|
+
|
|
174
|
+
When Vault is unavailable, the service automatically falls back to environment variables:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
@Injectable()
|
|
178
|
+
export class ResilientConfigService {
|
|
179
|
+
constructor(private readonly vault: VaultService) {}
|
|
180
|
+
|
|
181
|
+
async getConfig() {
|
|
182
|
+
// If Vault is unavailable, this will read from process.env
|
|
183
|
+
const apiUrl = await this.vault.get<string>('API_URL');
|
|
184
|
+
const apiKey = await this.vault.get<string>('API_KEY');
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
apiUrl,
|
|
188
|
+
apiKey
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Environment File Configuration
|
|
195
|
+
|
|
196
|
+
Specify a fallback environment file for when Vault is unavailable:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// app.module.ts
|
|
200
|
+
@Module({
|
|
201
|
+
imports: [
|
|
202
|
+
VaultModule.forRoot({
|
|
203
|
+
path: '/secret/data/production',
|
|
204
|
+
fallbackFile: '.env.production' // Fallback to .env.production file
|
|
205
|
+
})
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
export class AppModule {}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Integration Examples
|
|
212
|
+
|
|
213
|
+
### With TypeORM
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { Module } from '@nestjs/common';
|
|
217
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
218
|
+
import { VaultModule, VaultService } from '@rytass/secret-adapter-vault-nestjs';
|
|
24
219
|
|
|
25
220
|
@Module({
|
|
26
|
-
imports: [
|
|
27
|
-
|
|
221
|
+
imports: [
|
|
222
|
+
VaultModule.forRoot({
|
|
223
|
+
path: '/secret/data/database'
|
|
224
|
+
}),
|
|
225
|
+
TypeOrmModule.forRootAsync({
|
|
226
|
+
imports: [VaultModule],
|
|
227
|
+
inject: [VaultService],
|
|
228
|
+
useFactory: async (vault: VaultService) => ({
|
|
229
|
+
type: 'postgres',
|
|
230
|
+
host: await vault.get<string>('DB_HOST'),
|
|
231
|
+
port: await vault.get<number>('DB_PORT'),
|
|
232
|
+
username: await vault.get<string>('DB_USERNAME'),
|
|
233
|
+
password: await vault.get<string>('DB_PASSWORD'),
|
|
234
|
+
database: await vault.get<string>('DB_NAME'),
|
|
235
|
+
synchronize: false,
|
|
236
|
+
logging: true,
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
],
|
|
28
240
|
})
|
|
29
|
-
class
|
|
241
|
+
export class DatabaseModule {}
|
|
30
242
|
```
|
|
243
|
+
|
|
244
|
+
### With JWT Module
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { Module } from '@nestjs/common';
|
|
248
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
249
|
+
import { VaultModule, VaultService } from '@rytass/secret-adapter-vault-nestjs';
|
|
250
|
+
|
|
251
|
+
@Module({
|
|
252
|
+
imports: [
|
|
253
|
+
VaultModule.forRoot({
|
|
254
|
+
path: '/secret/data/auth'
|
|
255
|
+
}),
|
|
256
|
+
JwtModule.registerAsync({
|
|
257
|
+
imports: [VaultModule],
|
|
258
|
+
inject: [VaultService],
|
|
259
|
+
useFactory: async (vault: VaultService) => ({
|
|
260
|
+
secret: await vault.get<string>('JWT_SECRET'),
|
|
261
|
+
signOptions: {
|
|
262
|
+
expiresIn: await vault.get<string>('JWT_EXPIRY') || '1h'
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
export class AuthModule {}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### With Microservices
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { NestFactory } from '@nestjs/core';
|
|
275
|
+
import { Transport } from '@nestjs/microservices';
|
|
276
|
+
import { VaultService } from '@rytass/secret-adapter-vault-nestjs';
|
|
277
|
+
|
|
278
|
+
async function bootstrap() {
|
|
279
|
+
const app = await NestFactory.create(AppModule);
|
|
280
|
+
const vault = app.get(VaultService);
|
|
281
|
+
|
|
282
|
+
const microservice = await NestFactory.createMicroservice({
|
|
283
|
+
transport: Transport.REDIS,
|
|
284
|
+
options: {
|
|
285
|
+
host: await vault.get<string>('REDIS_HOST'),
|
|
286
|
+
port: await vault.get<number>('REDIS_PORT'),
|
|
287
|
+
password: await vault.get<string>('REDIS_PASSWORD'),
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await app.startAllMicroservices();
|
|
292
|
+
await app.listen(3000);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Error Handling
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
@Injectable()
|
|
300
|
+
export class SafeConfigService {
|
|
301
|
+
constructor(private readonly vault: VaultService) {}
|
|
302
|
+
|
|
303
|
+
async getSensitiveConfig() {
|
|
304
|
+
try {
|
|
305
|
+
const secret = await this.vault.get<string>('SENSITIVE_KEY');
|
|
306
|
+
|
|
307
|
+
if (!secret) {
|
|
308
|
+
throw new Error('Sensitive key not found');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return secret;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// When Vault is down, it falls back to env vars automatically
|
|
314
|
+
console.error('Failed to retrieve secret:', error);
|
|
315
|
+
|
|
316
|
+
// You can implement additional fallback logic
|
|
317
|
+
return process.env.FALLBACK_SENSITIVE_KEY || 'default-value';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async trySaveSecret(key: string, value: string): Promise<boolean> {
|
|
322
|
+
try {
|
|
323
|
+
await this.vault.set(key, value, true);
|
|
324
|
+
return true;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
// Cannot save when in fallback mode
|
|
327
|
+
console.error('Failed to save secret:', error.message);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Best Practices
|
|
335
|
+
|
|
336
|
+
### 1. Always Use Type Parameters
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// Good - Type-safe
|
|
340
|
+
const port = await vault.get<number>('PORT');
|
|
341
|
+
const config = await vault.get<DatabaseConfig>('db_config');
|
|
342
|
+
|
|
343
|
+
// Avoid - Returns any
|
|
344
|
+
const value = await vault.get('SOME_KEY');
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 2. Handle Fallback Scenarios
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
@Injectable()
|
|
351
|
+
export class ConfigService {
|
|
352
|
+
constructor(private readonly vault: VaultService) {}
|
|
353
|
+
|
|
354
|
+
async initialize() {
|
|
355
|
+
try {
|
|
356
|
+
// Try to save a test value to check if Vault is writable
|
|
357
|
+
await this.vault.set('health_check', 'ok', true);
|
|
358
|
+
console.log('Vault is connected and writable');
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.warn('Running in read-only mode with environment variables');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 3. Organize Secrets by Path
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// auth.module.ts
|
|
370
|
+
VaultModule.forRoot({ path: '/secret/data/auth' })
|
|
371
|
+
|
|
372
|
+
// database.module.ts
|
|
373
|
+
VaultModule.forRoot({ path: '/secret/data/database' })
|
|
374
|
+
|
|
375
|
+
// api.module.ts
|
|
376
|
+
VaultModule.forRoot({ path: '/secret/data/external-apis' })
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 4. Use Environment-Specific Paths
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
const environment = process.env.NODE_ENV || 'development';
|
|
383
|
+
|
|
384
|
+
@Module({
|
|
385
|
+
imports: [
|
|
386
|
+
VaultModule.forRoot({
|
|
387
|
+
path: `/secret/data/${environment}`,
|
|
388
|
+
fallbackFile: `.env.${environment}`
|
|
389
|
+
})
|
|
390
|
+
],
|
|
391
|
+
})
|
|
392
|
+
export class AppModule {}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## API Reference
|
|
396
|
+
|
|
397
|
+
### VaultModule
|
|
398
|
+
|
|
399
|
+
#### `forRoot(options: VaultModuleOptions): DynamicModule`
|
|
400
|
+
|
|
401
|
+
Configure the Vault module.
|
|
402
|
+
|
|
403
|
+
**Options:**
|
|
404
|
+
- `path` (string, required): Vault secret path from root
|
|
405
|
+
- `fallbackFile` (string, optional): Path to fallback environment file
|
|
406
|
+
|
|
407
|
+
### VaultService
|
|
408
|
+
|
|
409
|
+
#### `get<T>(key: string): Promise<T>`
|
|
410
|
+
|
|
411
|
+
Retrieve a secret value.
|
|
412
|
+
|
|
413
|
+
**Parameters:**
|
|
414
|
+
- `key`: Secret key name
|
|
415
|
+
|
|
416
|
+
**Returns:** Promise resolving to the secret value
|
|
417
|
+
|
|
418
|
+
#### `set<T>(key: string, value: T, syncToOnline?: boolean): Promise<void>`
|
|
419
|
+
|
|
420
|
+
Store a secret value.
|
|
421
|
+
|
|
422
|
+
**Parameters:**
|
|
423
|
+
- `key`: Secret key name
|
|
424
|
+
- `value`: Value to store
|
|
425
|
+
- `syncToOnline`: Whether to sync immediately to Vault (default: false)
|
|
426
|
+
|
|
427
|
+
#### `delete(key: string, syncToOnline?: boolean): Promise<void>`
|
|
428
|
+
|
|
429
|
+
Delete a secret.
|
|
430
|
+
|
|
431
|
+
**Parameters:**
|
|
432
|
+
- `key`: Secret key name
|
|
433
|
+
- `syncToOnline`: Whether to sync deletion to Vault (default: false)
|
|
434
|
+
|
|
435
|
+
## Migration from ConfigService
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// Before - Using ConfigService
|
|
439
|
+
@Injectable()
|
|
440
|
+
export class OldService {
|
|
441
|
+
constructor(private config: ConfigService) {}
|
|
442
|
+
|
|
443
|
+
getValue() {
|
|
444
|
+
return this.config.get('MY_KEY');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// After - Using VaultService
|
|
449
|
+
@Injectable()
|
|
450
|
+
export class NewService {
|
|
451
|
+
constructor(private vault: VaultService) {}
|
|
452
|
+
|
|
453
|
+
async getValue() {
|
|
454
|
+
return this.vault.get<string>('MY_KEY');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Troubleshooting
|
|
460
|
+
|
|
461
|
+
### Vault Connection Issues
|
|
462
|
+
|
|
463
|
+
If you see fallback warnings:
|
|
464
|
+
1. Check `VAULT_HOST` is accessible
|
|
465
|
+
2. Verify `VAULT_ACCOUNT` and `VAULT_PASSWORD` are correct
|
|
466
|
+
3. Ensure `VAULT_PATH` exists in Vault
|
|
467
|
+
4. Check network connectivity to Vault server
|
|
468
|
+
|
|
469
|
+
### Type Safety
|
|
470
|
+
|
|
471
|
+
Always specify type parameters for better TypeScript support:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// Explicit types prevent runtime errors
|
|
475
|
+
const port = await vault.get<number>('PORT');
|
|
476
|
+
const features = await vault.get<string[]>('FEATURE_FLAGS');
|
|
477
|
+
const config = await vault.get<AppConfig>('APP_CONFIG');
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## License
|
|
481
|
+
|
|
482
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rytass/secret-adapter-vault-nestjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Rytass Sceret Vault nestjs adapter",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rytass",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"reflect-metadata": "*"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@rytass/secret-adapter-vault": "^0.
|
|
27
|
+
"@rytass/secret-adapter-vault": "^0.4.1",
|
|
28
28
|
"regenerator-runtime": "^0.14.1"
|
|
29
29
|
},
|
|
30
30
|
"main": "./index.cjs.js",
|