npm package: @rytass/secret-adapter-vault
Description: Rytass Secret Vault adapter
Author: Chia Yu Pai
Latest version: 0.4.5

# Rytass Utils - HashiCorp Vault Secret Adapter

Comprehensive 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.

## Features

### Authentication

- [x] Username/Password authentication (userpass)
- [x] Token-based authentication
- [x] Automatic token renewal
- [x] Configurable token TTL

### Secret Engines

- [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

### Advanced Features

- [x] Automatic caching for offline operations
- [x] Event-driven architecture
- [x] Error handling and retry mechanisms
- [x] TypeScript type safety
- [x] Connection health monitoring

## Installation

```bash
npm install @rytass/secret-adapter-vault
# or
yarn add @rytass/secret-adapter-vault
```

## Basic Usage

### Online Mode (Real-time Operations)

```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<string>('DATABASE_PASSWORD');
console.log('DB Password:', dbPassword);

// Set new secret (async operation)
await onlineVault.set<string>('API_KEY', 'new-api-key-value');

// Delete secret (async operation)
await onlineVault.delete('OLD_CONFIG_KEY');
```

### Offline Mode (Cached Operations)

```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<string>('DATABASE_HOST');
    const dbPort = offlineVault.get<number>('DATABASE_PORT');

    // Modify cached values
    offlineVault.set<string>('NEW_FEATURE_FLAG', 'enabled');

    // 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);
  },
});
```

## Authentication Methods

### Username/Password Authentication

```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
});
```

### Token-based Authentication

```typescript
// Using existing token
const vault = new VaultSecret('secret/myapp', {
  host: 'https://vault.example.com',
  auth: {
    token: 'hvs.1234567890abcdef...', // Pre-existing token
  },
});
```

## Configuration Options

### VaultSecretOptions

| 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                              |

### Authentication Types

```typescript
// Username/Password auth
interface UsernamePasswordAuth {
  account: string;
  password: string;
}

// Token auth
interface TokenAuth {
  token: string;
}

type VaultAuthMethods = UsernamePasswordAuth | TokenAuth;
```

## Advanced Usage

### Environment-specific Configuration

```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
  },
});
```

### Type-safe Secret Management

```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<any>) {}

  async getDatabaseUrl(): Promise<string> {
    return await this.vault.get<string>('DATABASE_URL');
  }

  async getRedisConfig(): Promise<{ url: string; timeout: number }> {
    const url = await this.vault.get<string>('REDIS_URL');
    const timeout = (await this.vault.get<number>('REDIS_TIMEOUT')) || 5000;

    return { url, timeout };
  }

  async getEmailConfig(): Promise<AppSecrets['EMAIL_CONFIG']> {
    return await this.vault.get<AppSecrets['EMAIL_CONFIG']>('EMAIL_CONFIG');
  }

  async updateFeatureFlag(flag: keyof AppSecrets['FEATURE_FLAGS'], enabled: boolean): Promise<void> {
    const currentFlags = (await this.vault.get<AppSecrets['FEATURE_FLAGS']>('FEATURE_FLAGS')) || {};

    await this.vault.set<AppSecrets['FEATURE_FLAGS']>('FEATURE_FLAGS', {
      ...currentFlags,
      [flag]: enabled,
    });
  }
}
```

### Offline Mode with Synchronization Strategies

```typescript
class VaultManager {
  private vault: VaultSecret<any>;
  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<void> {
    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<boolean> {
    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);
    }
  }
}
```

## Integration Examples

### Express.js Application

```typescript
import express from 'express';
import { VaultSecret } from '@rytass/secret-adapter-vault';

class AppConfig {
  private vault: VaultSecret<any>;
  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<void> {
    return new Promise(resolve => {
      if (this.ready) {
        resolve();
      } else {
        this.vault.onReady(() => resolve());
      }
    });
  }

  getDatabaseConfig() {
    return {
      host: this.vault.get<string>('DB_HOST'),
      port: this.vault.get<number>('DB_PORT'),
      database: this.vault.get<string>('DB_NAME'),
      username: this.vault.get<string>('DB_USER'),
      password: this.vault.get<string>('DB_PASSWORD'),
    };
  }

  getJwtSecret(): string {
    return this.vault.get<string>('JWT_SECRET');
  }

  getRedisUrl(): string {
    return this.vault.get<string>('REDIS_URL');
  }
}

// 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);
```

### NestJS Integration

```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<any>;
  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<string> {
    return await this.vault.get<string>('DATABASE_URL');
  }

  async getSecret(key: string): Promise<any> {
    return await this.vault.get(key);
  }

  async updateSecret(key: string, value: any): Promise<void> {
    await this.vault.set(key, value);
  }
}
```

### Microservices Configuration

```typescript
class MicroserviceConfig {
  private vault: VaultSecret<any>;
  private serviceName: string;

  constructor(serviceName: string) {
    this.serviceName = serviceName;
    this.vault = new VaultSecret(`microservices/${serviceName}`, {
      host: process.env.VAULT_HOST!,
      auth: {
        account: `${serviceName}-service`,
        password: process.env[`${serviceName.toUpperCase()}_VAULT_PASSWORD`]!,
      },
      online: false, // Offline for microservice performance
      tokenTTL: 1800000, // 30 minutes
      onReady: () => {
        console.log(`${serviceName} configuration ready`);
        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<string>('HEALTH_CHECK');
      console.log(`${this.serviceName}: Vault health check passed`);
    } catch (error) {
      console.error(`${this.serviceName}: Vault health check failed`, error);
      // Could trigger alerts or restart procedures
    }
  }

  getServiceConfig() {
    return {
      port: this.vault.get<number>('PORT') || 3000,
      logLevel: this.vault.get<string>('LOG_LEVEL') || 'info',
      timeout: this.vault.get<number>('TIMEOUT') || 30000,
      retries: this.vault.get<number>('RETRIES') || 3,
    };
  }

  getDatabaseConfig() {
    return {
      url: this.vault.get<string>('DATABASE_URL'),
      maxConnections: this.vault.get<number>('DB_MAX_CONNECTIONS') || 10,
    };
  }

  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`);
    } catch (error) {
      // Rollback on failure
      this.vault.set(key, oldValue);
      throw new Error(`Secret rotation failed for ${key}: ${error.message}`);
    }
  }
}
```

## Error Handling

```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.
}
```

## Security Best Practices

### Credential Management

```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"
```

### Access Policies

```hcl
# Example 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"]
}
```

### Network Security

```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
```

## Performance Optimization

### Caching Strategies

```typescript
class OptimizedVaultManager {
  private vault: VaultSecret<any>;
  private localCache = new Map<string, { value: any; expiry: number }>();
  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<T>(key: string): Promise<T> {
    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<T>(key);

    this.localCache.set(key, {
      value,
      expiry: Date.now() + this.cacheTimeout,
    });

    return value;
  }

  clearCache(): void {
    this.localCache.clear();
  }

  // Batch operations for efficiency
  async getBatch<T>(keys: string[]): Promise<Record<string, T>> {
    const results: Record<string, T> = {};

    for (const key of keys) {
      results[key] = await this.vault.get<T>(key);
    }

    return results;
  }
}
```

## Testing

```typescript
// Mock Vault for testing
class MockVaultSecret {
  private data = new Map<string, any>();
  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<T>(key: string): T {
    return this.data.get(key);
  }

  set<T>(key: string, value: T): void {
    this.data.set(key, value);
  }

  delete(key: string): void {
    this.data.delete(key);
  }

  async sync(): Promise<void> {
    // 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<string>('TEST_SECRET');
    expect(secret).toBe('test-value');
  });

  it('should store secrets', () => {
    vault.set('NEW_SECRET', 'new-value');
    expect(vault.get('NEW_SECRET')).toBe('new-value');
  });
});
```

## License

MIT