mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
9c3dc0ee3a
* fix(auth): canonicalize Google Antigravity provider and enhance credential management * fix(auth): improve error handling in credential storage tests * fix(auth): stabilize canonical provider merge precedence
247 lines
5.5 KiB
Go
247 lines
5.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/fileutil"
|
|
)
|
|
|
|
type AuthCredential struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
|
Provider string `json:"provider"`
|
|
AuthMethod string `json:"auth_method"`
|
|
Email string `json:"email,omitempty"`
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
}
|
|
|
|
type AuthStore struct {
|
|
Credentials map[string]*AuthCredential `json:"credentials"`
|
|
}
|
|
|
|
const (
|
|
providerGoogleAntigravity = "google-antigravity"
|
|
providerAntigravityAlias = "antigravity"
|
|
)
|
|
|
|
func (c *AuthCredential) IsExpired() bool {
|
|
if c.ExpiresAt.IsZero() {
|
|
return false
|
|
}
|
|
return time.Now().After(c.ExpiresAt)
|
|
}
|
|
|
|
func (c *AuthCredential) NeedsRefresh() bool {
|
|
if c.ExpiresAt.IsZero() {
|
|
return false
|
|
}
|
|
return time.Now().Add(5 * time.Minute).After(c.ExpiresAt)
|
|
}
|
|
|
|
func authFilePath() string {
|
|
return filepath.Join(config.GetHome(), "auth.json")
|
|
}
|
|
|
|
func canonicalProvider(provider string) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(provider))
|
|
switch normalized {
|
|
case providerAntigravityAlias:
|
|
return providerGoogleAntigravity
|
|
default:
|
|
return normalized
|
|
}
|
|
}
|
|
|
|
func cloneCredential(cred *AuthCredential) *AuthCredential {
|
|
if cred == nil {
|
|
return nil
|
|
}
|
|
cp := *cred
|
|
return &cp
|
|
}
|
|
|
|
func mergeCredentials(primary, secondary *AuthCredential) *AuthCredential {
|
|
if primary == nil {
|
|
return cloneCredential(secondary)
|
|
}
|
|
|
|
merged := *primary
|
|
if secondary == nil {
|
|
return &merged
|
|
}
|
|
if merged.AccessToken == "" {
|
|
merged.AccessToken = secondary.AccessToken
|
|
}
|
|
if merged.RefreshToken == "" {
|
|
merged.RefreshToken = secondary.RefreshToken
|
|
}
|
|
if merged.AccountID == "" {
|
|
merged.AccountID = secondary.AccountID
|
|
}
|
|
if merged.ExpiresAt.IsZero() {
|
|
merged.ExpiresAt = secondary.ExpiresAt
|
|
}
|
|
if merged.Provider == "" {
|
|
merged.Provider = secondary.Provider
|
|
}
|
|
if merged.AuthMethod == "" {
|
|
merged.AuthMethod = secondary.AuthMethod
|
|
}
|
|
if merged.Email == "" {
|
|
merged.Email = secondary.Email
|
|
}
|
|
if merged.ProjectID == "" {
|
|
merged.ProjectID = secondary.ProjectID
|
|
}
|
|
|
|
return &merged
|
|
}
|
|
|
|
func shouldPreferCredential(
|
|
candidate *AuthCredential,
|
|
candidateCanonical bool,
|
|
current *AuthCredential,
|
|
currentCanonical bool,
|
|
) bool {
|
|
if candidate == nil {
|
|
return false
|
|
}
|
|
if current == nil {
|
|
return true
|
|
}
|
|
|
|
switch {
|
|
case candidate.ExpiresAt.After(current.ExpiresAt):
|
|
return true
|
|
case current.ExpiresAt.After(candidate.ExpiresAt):
|
|
return false
|
|
case candidateCanonical != currentCanonical:
|
|
return candidateCanonical
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normalizeStore(store *AuthStore) {
|
|
if store == nil {
|
|
return
|
|
}
|
|
if store.Credentials == nil {
|
|
store.Credentials = make(map[string]*AuthCredential)
|
|
return
|
|
}
|
|
|
|
normalized := make(map[string]*AuthCredential, len(store.Credentials))
|
|
canonicalFlags := make(map[string]bool, len(store.Credentials))
|
|
|
|
for provider, cred := range store.Credentials {
|
|
normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
|
|
canonical := canonicalProvider(provider)
|
|
normalizedCred := cloneCredential(cred)
|
|
if normalizedCred != nil {
|
|
normalizedCred.Provider = canonicalProvider(normalizedCred.Provider)
|
|
if normalizedCred.Provider == "" {
|
|
normalizedCred.Provider = canonical
|
|
}
|
|
}
|
|
|
|
current := normalized[canonical]
|
|
currentCanonical := canonicalFlags[canonical]
|
|
candidateCanonical := normalizedProvider == canonical
|
|
|
|
if shouldPreferCredential(normalizedCred, candidateCanonical, current, currentCanonical) {
|
|
normalized[canonical] = mergeCredentials(normalizedCred, current)
|
|
canonicalFlags[canonical] = candidateCanonical
|
|
continue
|
|
}
|
|
|
|
normalized[canonical] = mergeCredentials(current, normalizedCred)
|
|
}
|
|
|
|
store.Credentials = normalized
|
|
}
|
|
|
|
func LoadStore() (*AuthStore, error) {
|
|
path := authFilePath()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return &AuthStore{Credentials: make(map[string]*AuthCredential)}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var store AuthStore
|
|
if err := json.Unmarshal(data, &store); err != nil {
|
|
return nil, err
|
|
}
|
|
normalizeStore(&store)
|
|
return &store, nil
|
|
}
|
|
|
|
func SaveStore(store *AuthStore) error {
|
|
path := authFilePath()
|
|
data, err := json.MarshalIndent(store, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use unified atomic write utility with explicit sync for flash storage reliability.
|
|
return fileutil.WriteFileAtomic(path, data, 0o600)
|
|
}
|
|
|
|
func GetCredential(provider string) (*AuthCredential, error) {
|
|
store, err := LoadStore()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cred, ok := store.Credentials[canonicalProvider(provider)]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
func SetCredential(provider string, cred *AuthCredential) error {
|
|
store, err := LoadStore()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
canonical := canonicalProvider(provider)
|
|
normalized := cloneCredential(cred)
|
|
if normalized != nil {
|
|
normalized.Provider = canonicalProvider(normalized.Provider)
|
|
if normalized.Provider == "" {
|
|
normalized.Provider = canonical
|
|
}
|
|
}
|
|
|
|
store.Credentials[canonical] = normalized
|
|
return SaveStore(store)
|
|
}
|
|
|
|
func DeleteCredential(provider string) error {
|
|
store, err := LoadStore()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(store.Credentials, canonicalProvider(provider))
|
|
return SaveStore(store)
|
|
}
|
|
|
|
func DeleteAllCredentials() error {
|
|
path := authFilePath()
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|