import { DatabaseInterface } from '@happyvertical/sql'; import { Secret } from '../models/Secret.js'; import { SecretAuditLog } from '../models/SecretAuditLog.js'; /** * Options for creating a SecretService */ export interface SecretServiceOptions { /** Database connection */ db: DatabaseInterface; /** Environment variable containing the AMK (64 hex chars) */ amkEnvVar?: string; /** AMK key identifier */ amkKeyId?: string; /** Enable audit logging (default: true) */ auditEnabled?: boolean; } /** * Options for storing a secret */ export interface StoreSecretOptions { /** Human-readable description */ description?: string; /** Category for organization */ category?: string; /** Optional expiration date */ expiresAt?: Date; /** Additional metadata */ metadata?: Record; } /** * Result of retrieving a secret */ export interface RetrievedSecret { /** The decrypted secret value */ value: string; /** Secret metadata */ name: string; description: string; category: string; expiresAt: Date | null; createdAt: Date; lastAccessedAt: Date | null; accessCount: number; metadata: Record; } export type SecretKeyDriftIssueSeverity = 'info' | 'warning' | 'error'; export type SecretKeyDriftIssueCode = 'amk_unavailable' | 'active_secrets_without_usable_active_key' | 'missing_active_tenant_encryption_key' | 'multiple_active_tenant_encryption_keys' | 'active_tenant_encryption_key_amk_mismatch' | 'active_tenant_encryption_key_unwrap_failed' | 'secret_envelope_invalid_json' | 'secret_envelope_invalid_wrapped_key' | 'secret_envelope_missing_tenant_encryption_key' | 'secret_envelope_unwrap_failed' | 'smrt_tenant_keys_query_failed' | 'smrt_tenant_keys_not_mirrored'; export type SecretKeyDriftRepairAction = 'delete-unrecoverable-secret' | 'delete-unusable-tenant-encryption-key' | 'store-fresh-secret-value' | 'none'; export interface SecretKeyDriftIssue { code: SecretKeyDriftIssueCode; severity: SecretKeyDriftIssueSeverity; message: string; repairAction: SecretKeyDriftRepairAction; secretId?: string; secretName?: string; keyId?: string; sourceTable?: 'secrets' | 'tenant_encryption_keys' | 'tenant_keys'; details?: Record; } export interface DiagnoseTenantSecretKeyDriftOptions { /** * Limit secret-envelope checks to these names. Tenant key checks still run. */ secretNames?: string[]; } export interface SecretKeyDriftReport { tenantId: string; checkedAt: Date; ok: boolean; summary: { activeSecretCount: number; tenantEncryptionKeyCount: number; activeTenantEncryptionKeyCount: number; usableActiveTenantEncryptionKeyCount: number; smrtTenantKeyCount: number; activeSmrtTenantKeyCount: number; }; issues: SecretKeyDriftIssue[]; } export interface RepairTenantSecretKeyDriftOptions extends DiagnoseTenantSecretKeyDriftOptions { /** * Preview affected rows without deleting anything. */ dryRun?: boolean; /** * Required for destructive repair. This deletes encrypted values/key rows * that cannot be used with the currently configured AMK. */ confirmDeleteUnrecoverableData?: boolean; } export interface SecretKeyDriftRepairResult { tenantId: string; dryRun: boolean; issuesBefore: SecretKeyDriftIssue[]; remainingIssues: SecretKeyDriftIssue[]; wouldDeleteSecrets: number; wouldDeleteTenantEncryptionKeys: number; deletedSecrets: number; deletedTenantEncryptionKeys: number; secretNames: string[]; tenantEncryptionKeyIds: string[]; } export declare class SecretKeyDriftError extends Error { readonly code = "SECRET_KEY_DRIFT"; readonly tenantId: string; readonly report: SecretKeyDriftReport; readonly cause?: Error; constructor(message: string, tenantId: string, report: SecretKeyDriftReport, cause?: Error); } /** * SecretService provides high-level operations for managing per-tenant secrets. * * It integrates with: * - `@happyvertical/secrets` for envelope encryption * - `@happyvertical/smrt-tenancy` for tenant context * - Audit logging for compliance * * @example * ```typescript * import { SecretService } from '@happyvertical/smrt-secrets'; * import { withTenant } from '@happyvertical/smrt-tenancy'; * * const service = await SecretService.create({ db }); * * await withTenant({ tenantId: 'tenant-123' }, async () => { * // Store a secret * await service.store('stripe-api-key', 'sk_live_xxx', { * category: 'api-keys', * description: 'Stripe production API key' * }); * * // Retrieve the secret * const secret = await service.retrieve('stripe-api-key'); * console.log(secret.value); // 'sk_live_xxx' * * // List secret names (without values) * const secrets = await service.list(); * * // Rotate tenant's encryption key * await service.rotateKey(); * * // Delete a secret * await service.delete('stripe-api-key'); * }); * ``` */ export declare class SecretService { private db; private secretStore; private secrets; private tenantKeys; private auditLogs; private auditEnabled; private amkEnvVar; private amkKeyId; private constructor(); /** * Create a new SecretService instance */ static create(options: SecretServiceOptions): Promise; /** * Store a secret for the current tenant */ store(name: string, value: string, options?: StoreSecretOptions): Promise; /** * Store a secret for a specific tenant. * * This is useful for integrations that already resolved tenant ownership but * may be running outside the application's ambient tenant context. */ storeForTenant(tenantId: string, name: string, value: string, options?: StoreSecretOptions): Promise; /** * Retrieve a secret for the current tenant */ retrieve(name: string): Promise; /** * Retrieve a secret for a specific tenant. */ retrieveForTenant(tenantId: string, name: string): Promise; /** * Diagnose tenant secret/key drift without exposing decrypted values. */ diagnoseTenantSecretKeyDrift(tenantId: string, options?: DiagnoseTenantSecretKeyDriftOptions): Promise; /** * Diagnose drift for the current tenant context. */ diagnoseCurrentTenantSecretKeyDrift(options?: DiagnoseTenantSecretKeyDriftOptions): Promise; /** * Delete unrecoverable secret/key rows identified by diagnosis. * * This never attempts to recover or expose secret values. Use dryRun first * to preview destructive changes. */ repairTenantSecretKeyDrift(tenantId: string, options?: RepairTenantSecretKeyDriftOptions): Promise; /** * List secrets for the current tenant (names only, not values) */ list(options?: { category?: string; }): Promise; /** * Delete a secret */ delete(name: string): Promise; /** * Disable a secret (soft delete) */ disable(name: string): Promise; /** * Enable a disabled secret */ enable(name: string): Promise; /** * Rotate the tenant's encryption key * * This creates a new TDEK and marks the old one as retired. * Existing secrets remain encrypted with the old key and can still * be decrypted (the old key is kept in retired state). * * For full re-encryption, call reencryptAll() after rotation. */ rotateKey(): Promise; /** * Re-encrypt all secrets with the current active key * * Call this after key rotation to ensure all secrets use the new key. * This is optional but recommended for security. */ reencryptAll(): Promise<{ success: number; failed: number; }>; /** * Get audit logs for the current tenant */ getAuditLogs(options?: { secretName?: string; limit?: number; }): Promise; /** * Get secret categories for the current tenant */ getCategories(): Promise; /** * Check if a secret exists for the current tenant */ exists(name: string): Promise; private listActiveSecretRowsForDiagnosis; private listTenantEncryptionKeyRows; private listSmrtTenantKeysForDiagnosis; private getConfiguredAmkForDiagnosis; private checkWrappedKey; private getWrappedKeyFingerprint; private parseSecretEnvelopeForDiagnosis; private deleteRowsByIds; private auditSecretDriftRepairDeletes; private rowsFromResult; private classifyTenantKeyFailure; private shouldClassifyTenantKeyFailure; private getSecretErrorCode; private toError; private getCurrentUserId; private audit; private serializeMetadata; } //# sourceMappingURL=SecretService.d.ts.map