@happyvertical/smrt-secrets
Per-tenant secret management with envelope encryption (AMK to TDEK to secret), key rotation, and audit logging.
v0.20.44Envelope EncryptionKey RotationAudit Trail
Overview
smrt-secrets provides per-tenant secret storage using a three-layer envelope encryption chain. An Application Master Key (AMK) from the environment wraps per-tenant Data Encryption Keys (TDEK), which encrypt individual secret values. Every operation is audit-logged.
Installation
bash
npm install @happyvertical/smrt-secretsRequires the SMRT_SECRET_MASTER_KEY environment variable (64 hex characters) as
the Application Master Key.
Quick Start
typescript
import { SecretService } from '@happyvertical/smrt-secrets';
import { withTenant } from '@happyvertical/smrt-tenancy';
// Create the service (reads AMK from env by default)
const service = await SecretService.create({ db });
await withTenant({ tenantId: 'tenant-123' }, async () => {
// Store a secret (upserts if name already exists)
await service.store('stripe-api-key', 'sk_live_xxx', {
category: 'api-keys',
description: 'Stripe production key',
expiresAt: new Date('2027-01-01'),
});
// Retrieve and decrypt (increments accessCount)
const { value, accessCount } = await service.retrieve('stripe-api-key');
// List secret names (values never included)
const secrets = await service.list({ category: 'api-keys' });
// Disable/enable without deleting
await service.disable('stripe-api-key');
await service.enable('stripe-api-key');
// Rotate the tenant's encryption key
await service.rotateKey();
// Re-encrypt all secrets with the new key (separate step)
await service.reencryptAll();
// Query audit logs
const logs = await service.getAuditLogs({ secretName: 'stripe-api-key' });
// Hard delete
await service.delete('stripe-api-key');
});Core Models
Secret
typescript
class Secret extends SmrtObject {
name: string
encryptedValue: string // JSON envelope (encrypted)
category?: string
description?: string
status: 'active' | 'disabled' | 'expired'
expiresAt?: Date
accessCount: number
lastAccessedAt?: Date
// No API/MCP exposure (security)
// CLI: list-only
}TenantKey
typescript
class TenantKey extends SmrtObject {
tenantId: string
wrappedKey: string // TDEK wrapped by AMK
keyVersion: number
status: 'active' | 'rotating' | 'retired' | 'compromised'
// NOT tenant-scoped (tracks keys FOR tenants)
}SecretAuditLog
typescript
class SecretAuditLog extends SmrtObject {
secretName: string
action: 'create' | 'read' | 'update' | 'delete' | 'rotate_key' | 'disable' | 'enable'
result: 'success' | 'failure' | 'denied'
userId?: string
ipAddress?: string
userAgent?: string
// Immutable: CLI list-only
}Key Rotation
typescript
// Encryption chain:
// AMK (env var, 256-bit)
// -> wraps TDEK (per-tenant, auto-generated)
// -> encrypts secret value (stored as JSON envelope)
// Key rotation creates a new TDEK and retires the old one
await service.rotateKey();
// IMPORTANT: rotateKey() does NOT auto-re-encrypt secrets
// Retired keys are kept for decryption until re-encryption
await service.reencryptAll();
// Returns: { success: number, failed: number }
// TenantKey statuses:
// active - current encryption key
// rotating - transitional during rotation
// retired - kept for decryption of old secrets
// compromised - should not be used
// Cleanup retired keys after 90 days
// TenantKeyCollection.cleanupRetiredKeys()Best Practices
DOs
- Set
SMRT_SECRET_MASTER_KEYas a 64-character hex string - Call
reencryptAll()separately afterrotateKey() - Use categories to organize secrets logically
- Set expiration dates on time-sensitive credentials
- Monitor audit logs for unexpected access patterns
DON'Ts
- Don't assume
rotateKey()re-encrypts secrets automatically - Don't expose Secret or TenantKey models via API or MCP
- Don't skip the tenant context when storing/retrieving secrets
- Don't ignore that
retrieve()increments accessCount on every read - Don't hard-delete retired keys before calling
reencryptAll()