@rytass/secret
Version:
Rytass Secret Manager
487 lines (373 loc) • 12.2 kB
Markdown
# Rytass Utils - Secret Manager
An abstract base class for secret management adapters in the Rytass ecosystem. Provides a unified interface for managing secrets across different secret storage backends like HashiCorp Vault, AWS Secrets Manager, and more.
## Features
- [x] Abstract base class for secret management
- [x] Project-based secret organization
- [x] Generic type support for secret values
- [x] CRUD operations for secrets
- [x] TypeScript support
- [x] Extensible architecture
## Installation
```bash
npm install /secret
# or
yarn add /secret
```
## Available Adapters
- **[/secret-adapter-vault](https://www.npmjs.com/package/@rytass/secret-adapter-vault)** - HashiCorp Vault adapter
- **[/secret-adapter-vault-nestjs](https://www.npmjs.com/package/@rytass/secret-adapter-vault-nestjs)** - NestJS module for Vault integration
## Basic Usage
### Implementing a Secret Manager
```typescript
import { SecretManager } from '@rytass/secret';
class MySecretManager extends SecretManager {
constructor(project: string) {
super(project);
}
async get<T>(key: string): Promise<T> {
// Implement secret retrieval logic
const secret = await this.retrieveFromBackend(key);
return secret as T;
}
async set<T>(key: string, value: T): Promise<void> {
// Implement secret storage logic
await this.storeToBackend(key, value);
}
async delete(key: string): Promise<void> {
// Implement secret deletion logic
await this.deleteFromBackend(key);
}
private async retrieveFromBackend(key: string): Promise<any> {
// Your implementation here
}
private async storeToBackend(key: string, value: any): Promise<void> {
// Your implementation here
}
private async deleteFromBackend(key: string): Promise<void> {
// Your implementation here
}
}
```
### Using with Vault Adapter
```typescript
import { VaultSecretService } from '@rytass/secret-adapter-vault';
// Create Vault secret manager
const secretManager = new VaultSecretService('my-project', {
baseURL: 'https://vault.example.com',
token: 'your-vault-token',
version: 'v1',
});
// Store a secret
await secretManager.set('database-password', 'super-secure-password');
// Retrieve a secret
const password = await secretManager.get<string>('database-password');
console.log('Database password:', password);
// Delete a secret
await secretManager.delete('old-api-key');
```
## Core Concepts
### Project-based Organization
The `SecretManager` class organizes secrets by project:
```typescript
// Different projects can have isolated secret namespaces
const productionSecrets = new VaultSecretService('production', options);
const stagingSecrets = new VaultSecretService('staging', options);
// Same key, different values per project
await productionSecrets.set('api-key', 'prod-key-12345');
await stagingSecrets.set('api-key', 'staging-key-67890');
```
### Generic Type Support
Store and retrieve typed secrets:
```typescript
interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
}
// Store complex objects
await secretManager.set<DatabaseConfig>('db-config', {
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret123',
});
// Retrieve with proper typing
const dbConfig = await secretManager.get<DatabaseConfig>('db-config');
console.log(`Connecting to ${dbConfig.host}:${dbConfig.port}`);
```
## Advanced Usage
### Configuration Management
```typescript
class ConfigManager {
constructor(private secretManager: SecretManager) {}
async getDatabaseConfig(): Promise<DatabaseConfig> {
return this.secretManager.get<DatabaseConfig>('database');
}
async getApiKeys(): Promise<{ [service: string]: string }> {
return this.secretManager.get<Record<string, string>>('api-keys');
}
async updateSecret(key: string, value: any): Promise<void> {
await this.secretManager.set(key, value);
console.log(`Secret '${key}' updated successfully`);
}
}
// Usage
const configManager = new ConfigManager(secretManager);
const dbConfig = await configManager.getDatabaseConfig();
```
### Error Handling
```typescript
class SafeSecretManager extends SecretManager {
constructor(
project: string,
private fallbackManager?: SecretManager,
) {
super(project);
}
async get<T>(key: string): Promise<T> {
try {
return await this.primaryGet<T>(key);
} catch (error) {
console.warn(`Primary secret retrieval failed for '${key}':`, error);
if (this.fallbackManager) {
console.log(`Attempting fallback for '${key}'`);
return this.fallbackManager.get<T>(key);
}
throw error;
}
}
private async primaryGet<T>(key: string): Promise<T> {
// Primary secret retrieval implementation
throw new Error('Not implemented');
}
async set<T>(key: string, value: T): Promise<void> {
// Implementation
}
async delete(key: string): Promise<void> {
// Implementation
}
}
```
### Caching Layer
```typescript
class CachedSecretManager extends SecretManager {
private cache = new Map<string, { value: any; expiry: number }>();
private ttl = 5 * 60 * 1000; // 5 minutes
constructor(
project: string,
private baseManager: SecretManager,
) {
super(project);
}
async get<T>(key: string): Promise<T> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.value as T;
}
const value = await this.baseManager.get<T>(key);
this.cache.set(key, {
value,
expiry: Date.now() + this.ttl,
});
return value;
}
async set<T>(key: string, value: T): Promise<void> {
await this.baseManager.set(key, value);
// Invalidate cache
this.cache.delete(key);
}
async delete(key: string): Promise<void> {
await this.baseManager.delete(key);
this.cache.delete(key);
}
clearCache(): void {
this.cache.clear();
}
}
```
## Integration Examples
### Express.js Configuration
```typescript
import express from 'express';
import { VaultSecretService } from '@rytass/secret-adapter-vault';
const app = express();
const secrets = new VaultSecretService('api-server', {
baseURL: process.env.VAULT_URL!,
token: process.env.VAULT_TOKEN!,
});
// Load configuration from secrets
async function loadConfig() {
const config = {
port: await secrets.get<number>('server-port'),
jwtSecret: await secrets.get<string>('jwt-secret'),
dbConfig: await secrets.get<DatabaseConfig>('database'),
apiKeys: await secrets.get<Record<string, string>>('external-apis'),
};
return config;
}
// Start server with secret-based configuration
loadConfig()
.then(config => {
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
})
.catch(error => {
console.error('Failed to load configuration:', error);
process.exit(1);
});
```
### NestJS Integration
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { VaultSecretService } from '@rytass/secret-adapter-vault';
export class SecretsService implements OnModuleInit {
private secretManager: VaultSecretService;
constructor(private configService: ConfigService) {}
async onModuleInit() {
this.secretManager = new VaultSecretService(this.configService.get('PROJECT_NAME'), {
baseURL: this.configService.get('VAULT_URL'),
token: this.configService.get('VAULT_TOKEN'),
});
}
async getDatabasePassword(): Promise<string> {
return this.secretManager.get<string>('database-password');
}
async getApiKey(service: string): Promise<string> {
const apiKeys = await this.secretManager.get<Record<string, string>>('api-keys');
return apiKeys[service];
}
async updateSecret(key: string, value: any): Promise<void> {
await this.secretManager.set(key, value);
}
}
```
### Docker Secrets Integration
```typescript
import { readFileSync } from 'fs';
import { SecretManager } from '@rytass/secret';
class DockerSecretManager extends SecretManager {
private secretsPath = '/run/secrets';
async get<T>(key: string): Promise<T> {
try {
const secretPath = `${this.secretsPath}/${key}`;
const content = readFileSync(secretPath, 'utf8').trim();
// Try to parse as JSON, fallback to string
try {
return JSON.parse(content) as T;
} catch {
return content as T;
}
} catch (error) {
throw new Error(`Failed to read Docker secret '${key}': ${error.message}`);
}
}
async set<T>(key: string, value: T): Promise<void> {
throw new Error('Docker secrets are read-only');
}
async delete(key: string): Promise<void> {
throw new Error('Docker secrets cannot be deleted');
}
}
// Usage in containerized environment
const dockerSecrets = new DockerSecretManager('myapp');
const dbPassword = await dockerSecrets.get<string>('db-password');
```
## API Reference
### SecretManager
The abstract base class that all secret managers must extend.
#### Constructor
```typescript
constructor(project: string)
```
#### Properties
| Property | Type | Description |
| --------- | -------- | --------------------------------------- |
| `project` | `string` | The project name for organizing secrets |
#### Methods
| Method | Description |
| ------------------------------------------------------ | ------------------------ |
| `get<T>(key: string): Promise<T> \| T` | Retrieve a secret by key |
| `set<T>(key: string, value: T): Promise<void> \| void` | Store a secret |
| `delete(key: string): Promise<void> \| void` | Delete a secret |
## Best Practices
### Security
- Never log secret values
- Use environment variables for secret manager credentials
- Implement proper access controls
- Regularly rotate secrets
- Use least-privilege principles
### Performance
- Implement caching for frequently accessed secrets
- Use connection pooling for secret backends
- Cache secret manager instances
- Implement circuit breakers for resilience
### Organization
- Use consistent naming conventions for secrets
- Group related secrets logically
- Document secret schemas and usage
- Implement secret lifecycle management
### Error Handling
- Implement graceful fallbacks
- Log security events appropriately
- Handle network failures gracefully
- Validate secret formats before use
## Development
### Creating a Custom Adapter
```typescript
import { SecretManager } from '@rytass/secret';
export interface MyBackendOptions {
endpoint: string;
apiKey: string;
}
export class MyBackendSecretManager extends SecretManager {
constructor(
project: string,
private options: MyBackendOptions,
) {
super(project);
}
async get<T>(key: string): Promise<T> {
// Implement your backend-specific logic
const response = await fetch(`${this.options.endpoint}/secrets/${key}`, {
headers: {
Authorization: `Bearer ${this.options.apiKey}`,
},
});
if (!response.ok) {
throw new Error(`Failed to retrieve secret: ${response.statusText}`);
}
const data = await response.json();
return data.value as T;
}
async set<T>(key: string, value: T): Promise<void> {
const response = await fetch(`${this.options.endpoint}/secrets/${key}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.options.apiKey}`,
},
body: JSON.stringify({ value }),
});
if (!response.ok) {
throw new Error(`Failed to store secret: ${response.statusText}`);
}
}
async delete(key: string): Promise<void> {
const response = await fetch(`${this.options.endpoint}/secrets/${key}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${this.options.apiKey}`,
},
});
if (!response.ok) {
throw new Error(`Failed to delete secret: ${response.statusText}`);
}
}
}
```
## License
MIT