Rytass Secret Vault adapter
npm install @rytass/secret-adapter-vaultComprehensive secret management adapter for HashiCorp Vault, providing secure storage and retrieval of sensitive configuration data. Features both online and offline modes with automatic token management, caching mechanisms, and support for Key-Value V2 secret engines.
- [x] Username/Password authentication (userpass)
- [x] Token-based authentication
- [x] Automatic token renewal
- [x] Configurable token TTL
- [x] Key-Value V2 engine support
- [x] Online mode (real-time operations)
- [x] Offline mode (cached operations)
- [x] Version control and conflict resolution
- [x] Batch synchronization
- [x] Automatic caching for offline operations
- [x] Event-driven architecture
- [x] Error handling and retry mechanisms
- [x] TypeScript type safety
- [x] Connection health monitoring
``bash`
npm install @rytass/secret-adapter-vaultor
yarn add @rytass/secret-adapter-vault
`typescript
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Online mode - all operations hit Vault directly
const onlineVault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
online: true, // Enable online mode
auth: {
account: 'myapp-service',
password: 'secure-password',
},
});
// Wait for connection to be ready
await new Promise(resolve => {
onlineVault.onReady(() => resolve(undefined));
});
// Get secret (async operation)
const dbPassword = await onlineVault.get
console.log('DB Password:', dbPassword);
// Set new secret (async operation)
await onlineVault.set
// Delete secret (async operation)
await onlineVault.delete('OLD_CONFIG_KEY');
`
`typescript
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Offline mode - operations use local cache
const offlineVault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
online: false, // Default offline mode
auth: {
account: 'myapp-service',
password: 'secure-password',
},
onReady: async () => {
console.log('Vault cache ready for offline operations');
// All operations are synchronous and use cache
const dbHost = offlineVault.get
const dbPort = offlineVault.get
// Modify cached values
offlineVault.set
// Delete from cache
offlineVault.delete('DEPRECATED_CONFIG');
// Sync changes to Vault server
try {
await offlineVault.sync();
console.log('Successfully synced changes to Vault');
} catch (error) {
console.error('Sync failed:', error);
}
},
onError: error => {
console.error('Vault connection error:', error);
},
});
`
`typescript`
const vault = new VaultSecret('secret/myapp', {
host: 'https://vault.example.com',
auth: {
account: 'service-account',
password: 'strong-password',
},
tokenTTL: 3600000, // 1 hour in milliseconds
});
`typescript`
// Using existing token
const vault = new VaultSecret('secret/myapp', {
host: 'https://vault.example.com',
auth: {
token: 'hvs.1234567890abcdef...', // Pre-existing token
},
});
| Property | Type | Required | Default | Description |
| ---------- | ------------------ | -------- | --------- | ------------------------------------------- |
| host | string | Yes | - | Vault server URL |auth
| | VaultAuthMethods | Yes | - | Authentication configuration |online
| | boolean | No | false | Enable online mode for real-time operations |tokenTTL
| | number | No | 2764724 | Token TTL in milliseconds |onReady
| | function | No | - | Callback when vault is ready |onError
| | function | No | - | Error callback |
`typescript
// Username/Password auth
interface UsernamePasswordAuth {
account: string;
password: string;
}
// Token auth
interface TokenAuth {
token: string;
}
type VaultAuthMethods = UsernamePasswordAuth | TokenAuth;
`
`typescript
// Development environment
const devVault = new VaultSecret('apps/myapp/dev', {
host: 'https://vault-dev.company.com',
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: true, // Real-time for development
onReady: () => {
console.log('Development vault ready');
},
});
// Production environment
const prodVault = new VaultSecret('apps/myapp/prod', {
host: 'https://vault-prod.company.com',
auth: {
account: process.env.VAULT_PROD_USERNAME!,
password: process.env.VAULT_PROD_PASSWORD!,
},
online: false, // Cached for production performance
tokenTTL: 7200000, // 2 hours
onReady: () => {
console.log('Production vault cache loaded');
},
onError: error => {
console.error('Production vault error:', error);
// Implement alerting/monitoring
},
});
`
`typescript
// Define secret schema
interface AppSecrets {
DATABASE_URL: string;
REDIS_URL: string;
JWT_SECRET: string;
API_TIMEOUT: number;
FEATURE_FLAGS: {
newFeature: boolean;
betaAccess: boolean;
};
EMAIL_CONFIG: {
smtp_host: string;
smtp_port: number;
username: string;
password: string;
};
}
class SecretManager {
constructor(private vault: VaultSecret
async getDatabaseUrl(): Promise
return await this.vault.get
}
async getRedisConfig(): Promise<{ url: string; timeout: number }> {
const url = await this.vault.get
const timeout = (await this.vault.get
return { url, timeout };
}
async getEmailConfig(): Promise
return await this.vault.get
}
async updateFeatureFlag(flag: keyof AppSecrets['FEATURE_FLAGS'], enabled: boolean): Promise
const currentFlags = (await this.vault.get
await this.vault.set
...currentFlags,
[flag]: enabled,
});
}
}
`
`typescript
class VaultManager {
private vault: VaultSecret
private syncInterval?: NodeJS.Timeout;
constructor(secretPath: string, vaultConfig: any) {
this.vault = new VaultSecret(secretPath, {
...vaultConfig,
online: false,
onReady: () => {
this.startPeriodicSync();
},
});
}
// Periodic synchronization
private startPeriodicSync() {
this.syncInterval = setInterval(async () => {
try {
await this.vault.sync();
console.log('Periodic sync completed');
} catch (error) {
console.error('Periodic sync failed:', error);
}
}, 300000); // Sync every 5 minutes
}
// Force sync with conflict resolution
async forceSyncWithBackup(): Promise
try {
// Create backup of current cache
const currentCache = { ...this.vault.getCacheData() };
// Force sync (ignore version conflicts)
await this.vault.sync(true);
console.log('Force sync completed successfully');
} catch (error) {
console.error('Force sync failed:', error);
// Could implement rollback logic here
throw error;
}
}
// Conditional sync based on last update
async smartSync(maxCacheAge: number = 600000): Promise
const lastSync = this.getLastSyncTime();
const now = Date.now();
if (now - lastSync > maxCacheAge) {
try {
await this.vault.sync();
this.setLastSyncTime(now);
return true;
} catch (error) {
console.error('Smart sync failed:', error);
return false;
}
}
return false; // No sync needed
}
private getLastSyncTime(): number {
// Implementation depends on storage mechanism
return parseInt(localStorage.getItem('vault_last_sync') || '0');
}
private setLastSyncTime(timestamp: number): void {
localStorage.setItem('vault_last_sync', timestamp.toString());
}
destroy(): void {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
}
}
`
`typescript
import express from 'express';
import { VaultSecret } from '@rytass/secret-adapter-vault';
class AppConfig {
private vault: VaultSecret
private ready = false;
constructor() {
this.vault = new VaultSecret('apps/express-api/config', {
host: process.env.VAULT_HOST!,
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: false, // Use cache for better performance
onReady: () => {
this.ready = true;
console.log('Application configuration loaded from Vault');
},
onError: error => {
console.error('Vault configuration error:', error);
process.exit(1);
},
});
}
async waitForReady(): Promise
return new Promise(resolve => {
if (this.ready) {
resolve();
} else {
this.vault.onReady(() => resolve());
}
});
}
getDatabaseConfig() {
return {
host: this.vault.get
port: this.vault.get
database: this.vault.get
username: this.vault.get
password: this.vault.get
};
}
getJwtSecret(): string {
return this.vault.get
}
getRedisUrl(): string {
return this.vault.get
}
}
// Usage
const config = new AppConfig();
async function startServer() {
// Wait for configuration to load
await config.waitForReady();
const app = express();
// Use configuration
const dbConfig = config.getDatabaseConfig();
const jwtSecret = config.getJwtSecret();
// Setup database connection
// Setup JWT middleware
// ... other setup
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(Server running on port ${port});
});
}
startServer().catch(console.error);
`
`typescript
// Use with @rytass/secret-adapter-vault-nestjs for full NestJS integration
import { Injectable, OnModuleInit } from '@nestjs/common';
import { VaultSecret } from '@rytass/secret-adapter-vault';
@Injectable()
export class ConfigService implements OnModuleInit {
private vault: VaultSecret
private configLoaded = false;
async onModuleInit() {
this.vault = new VaultSecret('apps/nestjs-api/config', {
host: process.env.VAULT_HOST!,
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
online: true, // Real-time for NestJS
onReady: () => {
this.configLoaded = true;
},
});
// Wait for vault to be ready
await new Promise(resolve => {
if (this.configLoaded) {
resolve(undefined);
} else {
this.vault.onReady(() => resolve(undefined));
}
});
}
async getDatabaseUrl(): Promise
return await this.vault.get
}
async getSecret(key: string): Promise
return await this.vault.get(key);
}
async updateSecret(key: string, value: any): Promise
await this.vault.set(key, value);
}
}
`
`typescript
class MicroserviceConfig {
private vault: VaultSecret
private serviceName: string;
constructor(serviceName: string) {
this.serviceName = serviceName;
this.vault = new VaultSecret(microservices/${serviceName}, {${serviceName}-service
host: process.env.VAULT_HOST!,
auth: {
account: ,${serviceName.toUpperCase()}_VAULT_PASSWORD
password: process.env[]!,${serviceName} configuration ready
},
online: false, // Offline for microservice performance
tokenTTL: 1800000, // 30 minutes
onReady: () => {
console.log();
this.registerHealthCheck();
},
});
}
private registerHealthCheck() {
// Register with service discovery/health check system
setInterval(() => {
this.healthCheck();
}, 30000);
}
private async healthCheck() {
try {
// Test vault connectivity
await this.vault.get
console.log(${this.serviceName}: Vault health check passed);${this.serviceName}: Vault health check failed
} catch (error) {
console.error(, error);
// Could trigger alerts or restart procedures
}
}
getServiceConfig() {
return {
port: this.vault.get
logLevel: this.vault.get
timeout: this.vault.get
retries: this.vault.get
};
}
getDatabaseConfig() {
return {
url: this.vault.get
maxConnections: this.vault.get
};
}
async rotateSecret(key: string, newValue: any) {
// Store old value for rollback
const oldValue = this.vault.get(key);
try {
this.vault.set(key, newValue);
await this.vault.sync();
console.log(Secret ${key} rotated successfully);Secret rotation failed for ${key}: ${error.message}
} catch (error) {
// Rollback on failure
this.vault.set(key, oldValue);
throw new Error();`
}
}
}
`typescript
import { VaultSecret } from '@rytass/secret-adapter-vault';
// Comprehensive error handling
const vault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com',
auth: {
account: 'service-account',
password: 'secure-password',
},
online: true,
onReady: () => {
console.log('Vault connection established');
},
onError: error => {
console.error('Vault error occurred:', error);
// Handle different error types
if (error.message.includes('authentication')) {
console.error('Authentication failed - check credentials');
// Could trigger credential rotation
} else if (error.message.includes('network')) {
console.error('Network error - vault unreachable');
// Could trigger fallback to cached values
} else if (error.message.includes('permission')) {
console.error('Permission denied - check vault policies');
}
// Implement alerting/monitoring
sendAlert(Vault error: ${error.message});
},
});
// Graceful error handling in operations
async function safeGetSecret(key: string, defaultValue?: any) {
try {
const value = await vault.get(key);
return value !== undefined ? value : defaultValue;
} catch (error) {
console.error(Failed to get secret ${key}:, error);
return defaultValue;
}
}
async function safeSetSecret(key: string, value: any, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await vault.set(key, value);
return true;
} catch (error) {
console.error(Attempt ${attempt} failed for setting ${key}:, error);
if (attempt === maxRetries) {
throw new Error(Failed to set secret ${key} after ${maxRetries} attempts);
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
return false;
}
function sendAlert(message: string) {
// Implement your alerting mechanism
// Could be email, Slack, PagerDuty, etc.
}
`
`bash`Environment variables for credentials
export VAULT_HOST="https://vault.company.com"
export VAULT_USERNAME="myapp-service"
export VAULT_PASSWORD="$(cat /run/secrets/vault-password)"
export VAULT_TOKEN_TTL="3600000"
`hclExample Vault policy for application
path "apps/myapp/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "shared/database/*" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
`
`typescript
// Use HTTPS and validate certificates
const vault = new VaultSecret('apps/myapp/config', {
host: 'https://vault.company.com', // Always use HTTPS
auth: {
account: process.env.VAULT_USERNAME!,
password: process.env.VAULT_PASSWORD!,
},
// Additional security options could be added
});
// Implement network timeouts
const VAULT_TIMEOUT = 30000; // 30 seconds
`
`typescript
class OptimizedVaultManager {
private vault: VaultSecret
private localCache = new Map
private cacheTimeout = 300000; // 5 minutes
constructor(secretPath: string, vaultConfig: any) {
this.vault = new VaultSecret(secretPath, {
...vaultConfig,
online: false, // Use offline mode for performance
});
}
async getCached
const cached = this.localCache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.value;
}
// Cache miss or expired
const value = await this.vault.get
this.localCache.set(key, {
value,
expiry: Date.now() + this.cacheTimeout,
});
return value;
}
clearCache(): void {
this.localCache.clear();
}
// Batch operations for efficiency
async getBatch
const results: Record
for (const key of keys) {
results[key] = await this.vault.get
}
return results;
}
}
`
`typescript
// Mock Vault for testing
class MockVaultSecret {
private data = new Map
private ready = true;
constructor(
private path: string,
private options: any,
) {
// Populate with test data
this.data.set('TEST_SECRET', 'test-value');
this.data.set('DB_HOST', 'localhost');
this.data.set('DB_PORT', 5432);
}
get
return this.data.get(key);
}
set
this.data.set(key, value);
}
delete(key: string): void {
this.data.delete(key);
}
async sync(): Promise
// Mock sync operation
}
onReady(callback: () => void): void {
if (this.ready) {
callback();
}
}
}
// Test example
describe('VaultSecret', () => {
let vault: MockVaultSecret;
beforeEach(() => {
vault = new MockVaultSecret('test/secrets', {});
});
it('should retrieve secrets', () => {
const secret = vault.get
expect(secret).toBe('test-value');
});
it('should store secrets', () => {
vault.set('NEW_SECRET', 'new-value');
expect(vault.get('NEW_SECRET')).toBe('new-value');
});
});
``
MIT