| package amp |
|
|
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| "time" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| |
| type SecretSource interface { |
| Get(ctx context.Context) (string, error) |
| } |
|
|
| |
| type cachedSecret struct { |
| value string |
| expiresAt time.Time |
| } |
|
|
| |
| |
| |
| |
| type MultiSourceSecret struct { |
| explicitKey string |
| envKey string |
| filePath string |
| cacheTTL time.Duration |
|
|
| mu sync.RWMutex |
| cache *cachedSecret |
| } |
|
|
| |
| func NewMultiSourceSecret(explicitKey string, cacheTTL time.Duration) *MultiSourceSecret { |
| if cacheTTL == 0 { |
| cacheTTL = 5 * time.Minute |
| } |
|
|
| 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, |
| } |
| } |
|
|
| |
| 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, |
| } |
| } |
|
|
| |
| |
| func (s *MultiSourceSecret) Get(ctx context.Context) (string, error) { |
| |
| if s.explicitKey != "" { |
| return s.explicitKey, nil |
| } |
|
|
| |
| if envValue := strings.TrimSpace(os.Getenv(s.envKey)); envValue != "" { |
| return envValue, nil |
| } |
|
|
| |
| |
| 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() |
|
|
| |
| key, err := s.readFromFile() |
| if err != nil { |
| |
| s.updateCache("") |
| return "", err |
| } |
|
|
| |
| s.updateCache(key) |
| return key, nil |
| } |
|
|
| |
| func (s *MultiSourceSecret) readFromFile() (string, error) { |
| content, err := os.ReadFile(s.filePath) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return "", nil |
| } |
| 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 |
| } |
|
|
| |
| func (s *MultiSourceSecret) updateCache(value string) { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| s.cache = &cachedSecret{ |
| value: value, |
| expiresAt: time.Now().Add(s.cacheTTL), |
| } |
| } |
|
|
| |
| func (s *MultiSourceSecret) InvalidateCache() { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| s.cache = nil |
| } |
|
|
| |
| func (s *MultiSourceSecret) UpdateExplicitKey(key string) { |
| if s == nil { |
| return |
| } |
| s.mu.Lock() |
| s.explicitKey = strings.TrimSpace(key) |
| s.cache = nil |
| s.mu.Unlock() |
| } |
|
|
| |
| type StaticSecretSource struct { |
| key string |
| } |
|
|
| |
| func NewStaticSecretSource(key string) *StaticSecretSource { |
| return &StaticSecretSource{key: strings.TrimSpace(key)} |
| } |
|
|
| |
| func (s *StaticSecretSource) Get(ctx context.Context) (string, error) { |
| return s.key, nil |
| } |
|
|
| |
| |
| |
| type MappedSecretSource struct { |
| defaultSource SecretSource |
| mu sync.RWMutex |
| lookup map[string]string |
| } |
|
|
| |
| func NewMappedSecretSource(defaultSource SecretSource) *MappedSecretSource { |
| return &MappedSecretSource{ |
| defaultSource: defaultSource, |
| lookup: make(map[string]string), |
| } |
| } |
|
|
| |
| |
| |
| func (s *MappedSecretSource) Get(ctx context.Context) (string, error) { |
| |
| clientKey := getClientAPIKeyFromContext(ctx) |
| if clientKey != "" { |
| s.mu.RLock() |
| if upstreamKey, ok := s.lookup[clientKey]; ok && upstreamKey != "" { |
| s.mu.RUnlock() |
| return upstreamKey, nil |
| } |
| s.mu.RUnlock() |
| } |
|
|
| |
| return s.defaultSource.Get(ctx) |
| } |
|
|
| |
| |
| func (s *MappedSecretSource) UpdateMappings(entries []config.AmpUpstreamAPIKeyEntry) { |
| newLookup := make(map[string]string) |
|
|
| for _, entry := range entries { |
| upstreamKey := strings.TrimSpace(entry.UpstreamAPIKey) |
| if upstreamKey == "" { |
| continue |
| } |
| for _, clientKey := range entry.APIKeys { |
| trimmedKey := strings.TrimSpace(clientKey) |
| if trimmedKey == "" { |
| continue |
| } |
| if _, exists := newLookup[trimmedKey]; exists { |
| |
| log.Warnf("amp upstream-api-keys: client API key appears in multiple entries; using first mapping.") |
| continue |
| } |
| newLookup[trimmedKey] = upstreamKey |
| } |
| } |
|
|
| s.mu.Lock() |
| s.lookup = newLookup |
| s.mu.Unlock() |
| } |
|
|
| |
| func (s *MappedSecretSource) UpdateDefaultExplicitKey(key string) { |
| if ms, ok := s.defaultSource.(*MultiSourceSecret); ok { |
| ms.UpdateExplicitKey(key) |
| } |
| } |
|
|
| |
| func (s *MappedSecretSource) InvalidateCache() { |
| if ms, ok := s.defaultSource.(*MultiSourceSecret); ok { |
| ms.InvalidateCache() |
| } |
| } |
|
|