UNPKG

@rytass/secret

Version:
487 lines (373 loc) 12.2 kB
# 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 @rytass/secret # or yarn add @rytass/secret ``` ## Available Adapters - **[@rytass/secret-adapter-vault](https://www.npmjs.com/package/@rytass/secret-adapter-vault)** - HashiCorp Vault adapter - **[@rytass/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'; @Injectable() 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