Skip to main content

@happyvertical/smrt-secrets

Per-tenant secret management with envelope encryption for the s-m-r-t framework. Uses a three-layer encryption chain: Application Master Key (AMK) wraps per-tenant Data Encryption Keys (TDEK), which encrypt individual secret values.

Installation​

pnpm add @happyvertical/smrt-secrets

Requires the SMRT_SECRET_MASTER_KEY environment variable (64 hex characters) as the AMK.

Usage​

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

// Diagnose tenant secret/key drift without exposing values
const drift = await service.diagnoseTenantSecretKeyDrift('tenant-123');

// Preview and then explicitly repair unrecoverable drifted rows
await service.repairTenantSecretKeyDrift('tenant-123', { dryRun: true });
await service.repairTenantSecretKeyDrift('tenant-123', {
confirmDeleteUnrecoverableData: true,
});

// Hard delete
await service.delete('stripe-api-key');
});

Encryption chain​

AMK (env var, 256-bit)
└─ wraps TDEK (per-tenant, auto-generated)
└─ encrypts secret value (stored as JSON envelope)

The AMK is loaded from SMRT_SECRET_MASTER_KEY (configurable via amkEnvVar option). Each tenant gets its own TDEK on first secret storage. TDEKs are stored in wrapped form -- they can only be unwrapped using the AMK. Secret values are encrypted by the unwrapped TDEK and persisted as EncryptedEnvelope JSON.

Key rotation​

rotateKey() creates a new TDEK version and retires the old one. Existing secrets remain readable because retired keys are kept for decryption. Call reencryptAll() separately after rotation to re-encrypt all secrets with the new key. The method returns { success, failed } counts.

TenantKey statuses: active (current encryption key), rotating (transitional), retired (kept for decryption), compromised (should not be used).

Drift diagnosis and repair​

diagnoseTenantSecretKeyDrift(tenantId) reports tenant secret/key drift without returning secret values. It checks active secrets rows, lower-level tenant_encryption_keys rows used by @happyvertical/secrets, and s-m-r-t-visible tenant_keys rows so operators can distinguish missing secrets from stale or unusable key material.

repairTenantSecretKeyDrift(tenantId, { dryRun: true }) previews rows that would be deleted. A destructive repair requires confirmDeleteUnrecoverableData: true; it deletes only encrypted secret/key rows identified as unrecoverable with the currently configured AMK. After repair, store fresh secret values with storeForTenant() or store().

Audit logging​

Every secret operation (create, read, update, delete, rotate_key, disable, enable) is logged to SecretAuditLog with user ID, action, result (success/failure/denied), and optional IP/user-agent. Audit logging is enabled by default; failures are logged to console rather than thrown so they do not block secret operations.

Security model​

The Secret and TenantKey models have no API or MCP exposure to prevent accidental leakage. CLI access is limited to listing names. The TenantKey model is deliberately not tenant-scoped -- it tracks keys FOR tenants rather than being owned BY them.

API​

Models​

ExportDescription
SecretEncrypted secret record with status, expiration, and access tracking
SecretAuditLogImmutable audit trail for all secret operations
TenantKeyPer-tenant data encryption key in wrapped form

Collections​

ExportDescription
SecretCollectionSecret queries with tenant filtering and category support
SecretAuditLogCollectionAudit log queries with filtering and pagination
TenantKeyCollectionKey management including retired key cleanup

Services​

ExportDescription
SecretServiceHigh-level API: store, retrieve, rotate, delete, audit, diagnose and repair key drift

Functions​

ExportDescription
createAuditEntryBuild an audit log entry for a secret operation

Error Classes (re-exported from @happyvertical/secrets)​

SecretError, AMKUnavailableError, DecryptionError, EncryptionError, InvalidKeyFormatError, KeyNotFoundError, KeyRotationError, StoreNotInitializedError, TenantKeyMissingError

Key Types​

SecretServiceOptions, StoreSecretOptions, RetrievedSecret, DiagnoseTenantSecretKeyDriftOptions, SecretKeyDriftReport, SecretKeyDriftIssue, RepairTenantSecretKeyDriftOptions, SecretKeyDriftRepairResult, ListSecretsOptions, ListAuditLogsOptions, SecretStatus, SecretAuditAction, SecretAuditResult, TenantKeyStatus, ApplicationMasterKey, EncryptedEnvelope, TenantDataEncryptionKey, SecretStore

Dependencies​

  • @happyvertical/smrt-core -- ORM and code generation
  • @happyvertical/smrt-tenancy -- tenant context for scoped operations
  • @happyvertical/secrets -- envelope encryption primitives
  • @happyvertical/sql -- database interface
  • @happyvertical/utils -- shared utilities