// Package secrets abstracts secret retrieval across backends (SOPS, Infisical). // Phase 5: SOPS → Infisical migration with SOPS DR fallback. package secrets import ( "context" "errors" "sync" "time" ) var ErrNotFound = errors.New("secret not found") var ErrBackendUnavailable = errors.New("secret backend unavailable") // Backend is the interface for retrieving and storing secrets. type Backend interface { // Get retrieves a secret value by path/key. Get(ctx context.Context, key string) (string, error) // List returns all secret keys available in this backend. List(ctx context.Context) ([]string, error) // Set stores a secret value. Used during migration. Set(ctx context.Context, key string, value string) error // Name returns a human-readable backend identifier. Name() string } // Manager holds a primary and fallback backend. If the primary fails, // it falls back to the secondary. type Manager struct { primary Backend fallback Backend cache map[string]cachedSecret mu sync.RWMutex cacheTTL time.Duration } type cachedSecret struct { value string expiresAt time.Time } // NewManager creates a secret manager with primary and fallback backends. func NewManager(primary, fallback Backend) *Manager { return &Manager{ primary: primary, fallback: fallback, cache: make(map[string]cachedSecret), cacheTTL: 5 * time.Minute, } } // Get retrieves a secret from primary, falling back to secondary on error. func (m *Manager) Get(ctx context.Context, key string) (string, error) { m.mu.RLock() if cached, ok := m.cache[key]; ok && time.Now().Before(cached.expiresAt) { m.mu.RUnlock() return cached.value, nil } m.mu.RUnlock() val, err := m.primary.Get(ctx, key) if err == nil { m.mu.Lock() m.cache[key] = cachedSecret{value: val, expiresAt: time.Now().Add(m.cacheTTL)} m.mu.Unlock() return val, nil } if m.fallback != nil { val, fallbackErr := m.fallback.Get(ctx, key) if fallbackErr == nil { return val, nil } } return "", err } // List returns all keys from the primary backend. func (m *Manager) List(ctx context.Context) ([]string, error) { keys, err := m.primary.List(ctx) if err != nil && m.fallback != nil { return m.fallback.List(ctx) } return keys, err } // Set stores a secret in the primary backend (used during migration). func (m *Manager) Set(ctx context.Context, key string, value string) error { return m.primary.Set(ctx, key, value) } // PrimaryName returns the name of the primary backend. func (m *Manager) PrimaryName() string { return m.primary.Name() } // InvalidateCache clears cached secrets. func (m *Manager) InvalidateCache() { m.mu.Lock() m.cache = make(map[string]cachedSecret) m.mu.Unlock() }