mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
167 lines
4.3 KiB
Go
167 lines
4.3 KiB
Go
package amp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SecretSource provides Amp API keys with configurable precedence and caching
|
|
type SecretSource interface {
|
|
Get(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// cachedSecret holds a secret value with expiration
|
|
type cachedSecret struct {
|
|
value string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// MultiSourceSecret implements precedence-based secret lookup:
|
|
// 1. Explicit config value (highest priority)
|
|
// 2. Environment variable AMP_API_KEY
|
|
// 3. File-based secret (lowest priority)
|
|
type MultiSourceSecret struct {
|
|
explicitKey string
|
|
envKey string
|
|
filePath string
|
|
cacheTTL time.Duration
|
|
|
|
mu sync.RWMutex
|
|
cache *cachedSecret
|
|
}
|
|
|
|
// NewMultiSourceSecret creates a secret source with precedence and caching
|
|
func NewMultiSourceSecret(explicitKey string, cacheTTL time.Duration) *MultiSourceSecret {
|
|
if cacheTTL == 0 {
|
|
cacheTTL = 5 * time.Minute // Default 5 minute cache
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
filePath := filepath.Join(home, ".local", "share", "amp", "secrets.json")
|
|
|
|
return &MultiSourceSecret{
|
|
explicitKey: strings.TrimSpace(explicitKey),
|
|
envKey: "AMP_API_KEY",
|
|
filePath: filePath,
|
|
cacheTTL: cacheTTL,
|
|
}
|
|
}
|
|
|
|
// NewMultiSourceSecretWithPath creates a secret source with a custom file path (for testing)
|
|
func NewMultiSourceSecretWithPath(explicitKey string, filePath string, cacheTTL time.Duration) *MultiSourceSecret {
|
|
if cacheTTL == 0 {
|
|
cacheTTL = 5 * time.Minute
|
|
}
|
|
|
|
return &MultiSourceSecret{
|
|
explicitKey: strings.TrimSpace(explicitKey),
|
|
envKey: "AMP_API_KEY",
|
|
filePath: filePath,
|
|
cacheTTL: cacheTTL,
|
|
}
|
|
}
|
|
|
|
// Get retrieves the Amp API key using precedence: config > env > file
|
|
// Results are cached for cacheTTL duration to avoid excessive file reads
|
|
func (s *MultiSourceSecret) Get(ctx context.Context) (string, error) {
|
|
// Precedence 1: Explicit config key (highest priority, no caching needed)
|
|
if s.explicitKey != "" {
|
|
return s.explicitKey, nil
|
|
}
|
|
|
|
// Precedence 2: Environment variable
|
|
if envValue := strings.TrimSpace(os.Getenv(s.envKey)); envValue != "" {
|
|
return envValue, nil
|
|
}
|
|
|
|
// Precedence 3: File-based secret (lowest priority, cached)
|
|
// Check cache first
|
|
s.mu.RLock()
|
|
if s.cache != nil && time.Now().Before(s.cache.expiresAt) {
|
|
value := s.cache.value
|
|
s.mu.RUnlock()
|
|
return value, nil
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Cache miss or expired - read from file
|
|
key, err := s.readFromFile()
|
|
if err != nil {
|
|
// Cache empty result to avoid repeated file reads on missing files
|
|
s.updateCache("")
|
|
return "", err
|
|
}
|
|
|
|
// Cache the result
|
|
s.updateCache(key)
|
|
return key, nil
|
|
}
|
|
|
|
// readFromFile reads the Amp API key from the secrets file
|
|
func (s *MultiSourceSecret) readFromFile() (string, error) {
|
|
content, err := os.ReadFile(s.filePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil // Missing file is not an error, just no key available
|
|
}
|
|
return "", fmt.Errorf("failed to read amp secrets from %s: %w", s.filePath, err)
|
|
}
|
|
|
|
var secrets map[string]string
|
|
if err := json.Unmarshal(content, &secrets); err != nil {
|
|
return "", fmt.Errorf("failed to parse amp secrets from %s: %w", s.filePath, err)
|
|
}
|
|
|
|
key := strings.TrimSpace(secrets["apiKey@https://ampcode.com/"])
|
|
return key, nil
|
|
}
|
|
|
|
// updateCache updates the cached secret value
|
|
func (s *MultiSourceSecret) updateCache(value string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.cache = &cachedSecret{
|
|
value: value,
|
|
expiresAt: time.Now().Add(s.cacheTTL),
|
|
}
|
|
}
|
|
|
|
// InvalidateCache clears the cached secret, forcing a fresh read on next Get
|
|
func (s *MultiSourceSecret) InvalidateCache() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.cache = nil
|
|
}
|
|
|
|
// UpdateExplicitKey refreshes the config-provided key and clears cache.
|
|
func (s *MultiSourceSecret) UpdateExplicitKey(key string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
s.explicitKey = strings.TrimSpace(key)
|
|
s.cache = nil
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// StaticSecretSource returns a fixed API key (for testing)
|
|
type StaticSecretSource struct {
|
|
key string
|
|
}
|
|
|
|
// NewStaticSecretSource creates a secret source with a fixed key
|
|
func NewStaticSecretSource(key string) *StaticSecretSource {
|
|
return &StaticSecretSource{key: strings.TrimSpace(key)}
|
|
}
|
|
|
|
// Get returns the static API key
|
|
func (s *StaticSecretSource) Get(ctx context.Context) (string, error) {
|
|
return s.key, nil
|
|
}
|