package store import ( "context" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/config" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth") // GitTokenStore persists token records and auth metadata using git as the backing storage.type GitTokenStore struct { mu sync.Mutex dirLock sync.RWMutex baseDir string repoDir string configDir string remote string username string password string} // NewGitTokenStore creates a token store that saves credentials to disk through the// TokenStorage implementation embedded in the token record.func NewGitTokenStore(remote, username, password string) *GitTokenStore { return &GitTokenStore{ remote: remote, username: username, password: password, }} // SetBaseDir updates the default directory used for auth JSON persistence when no explicit path is provided.func (s *GitTokenStore) SetBaseDir(dir string) { clean := strings.TrimSpace(dir) if clean == "" { s.dirLock.Lock() s.baseDir = "" s.repoDir = "" s.configDir = "" s.dirLock.Unlock() return } if abs, err := filepath.Abs(clean); err == nil { clean = abs } repoDir := filepath.Dir(clean) if repoDir == "" || repoDir == "." { repoDir = clean } configDir := filepath.Join(repoDir, "config") s.dirLock.Lock() s.baseDir = clean s.repoDir = repoDir s.configDir = configDir s.dirLock.Unlock()} // AuthDir returns the directory used for auth persistence.func (s *GitTokenStore) AuthDir() string { return s.baseDirSnapshot()} // ConfigPath returns the managed config file path.func (s *GitTokenStore) ConfigPath() string { s.dirLock.RLock() defer s.dirLock.RUnlock() if s.configDir == "" { return "" } return filepath.Join(s.configDir, "config.yaml")} // EnsureRepository prepares the local git working tree by cloning or opening the repository.func (s *GitTokenStore) EnsureRepository() error { s.dirLock.Lock() if s.remote == "" { s.dirLock.Unlock() return fmt.Errorf("git token store: remote not configured") } if s.baseDir == "" { s.dirLock.Unlock() return fmt.Errorf("git token store: base directory not configured") } repoDir := s.repoDir if repoDir == "" { repoDir = filepath.Dir(s.baseDir) if repoDir == "" || repoDir == "." { repoDir = s.baseDir } s.repoDir = repoDir } if s.configDir == "" { s.configDir = filepath.Join(repoDir, "config") } authDir := filepath.Join(repoDir, "auths") configDir := filepath.Join(repoDir, "config") gitDir := filepath.Join(repoDir, ".git") authMethod := s.gitAuth() var initPaths []string if _, err := os.Stat(gitDir); errors.Is(err, fs.ErrNotExist) { if errMk := os.MkdirAll(repoDir, 0o700); errMk != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create repo dir: %w", errMk) } if _, errClone := git.PlainClone(repoDir, &git.CloneOptions{Auth: authMethod, URL: s.remote}); errClone != nil { if errors.Is(errClone, transport.ErrEmptyRemoteRepository) { _ = os.RemoveAll(gitDir) repo, errInit := git.PlainInit(repoDir, false) if errInit != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: init empty repo: %w", errInit) } if _, errRemote := repo.Remote("origin"); errRemote != nil { if _, errCreate := repo.CreateRemote(&config.RemoteConfig{ Name: "origin", URLs: []string{s.remote}, }); errCreate != nil && !errors.Is(errCreate, git.ErrRemoteExists) { s.dirLock.Unlock() return fmt.Errorf("git token store: configure remote: %w", errCreate) } } if err := os.MkdirAll(authDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth dir: %w", err) } if err := os.MkdirAll(configDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config dir: %w", err) } if err := ensureEmptyFile(filepath.Join(authDir, ".gitkeep")); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth placeholder: %w", err) } if err := ensureEmptyFile(filepath.Join(configDir, ".gitkeep")); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config placeholder: %w", err) } initPaths = []string{ filepath.Join("auths", ".gitkeep"), filepath.Join("config", ".gitkeep"), } } else { s.dirLock.Unlock() return fmt.Errorf("git token store: clone remote: %w", errClone) } } } else if err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: stat repo: %w", err) } else { repo, errOpen := git.PlainOpen(repoDir) if errOpen != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: open repo: %w", errOpen) } worktree, errWorktree := repo.Worktree() if errWorktree != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: worktree: %w", errWorktree) } if errPull := worktree.Pull(&git.PullOptions{Auth: authMethod, RemoteName: "origin"}); errPull != nil { switch { case errors.Is(errPull, git.NoErrAlreadyUpToDate), errors.Is(errPull, git.ErrUnstagedChanges), errors.Is(errPull, git.ErrNonFastForwardUpdate): // Ignore clean syncs, local edits, and remote divergence—local changes win. case errors.Is(errPull, transport.ErrAuthenticationRequired), errors.Is(errPull, plumbing.ErrReferenceNotFound), errors.Is(errPull, transport.ErrEmptyRemoteRepository): // Ignore authentication prompts and empty remote references on initial sync. default: s.dirLock.Unlock() return fmt.Errorf("git token store: pull: %w", errPull) } } } if err := os.MkdirAll(s.baseDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth dir: %w", err) } if err := os.MkdirAll(s.configDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config dir: %w", err) } s.dirLock.Unlock() if len(initPaths) > 0 { s.mu.Lock() err := s.commitAndPushLocked("Initialize git token store", initPaths...) s.mu.Unlock() if err != nil { return err } } return nil} // Save persists token storage and metadata to the resolved auth file path.func (s *GitTokenStore) Save(_ context.Context, auth *cliproxyauth.Auth) (string, error) { if auth == nil { return "", fmt.Errorf("auth filestore: auth is nil") } path, err := s.resolveAuthPath(auth) if err != nil { return "", err } if path == "" { return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID) } if auth.Disabled { if _, statErr := os.Stat(path); os.IsNotExist(statErr) { return "", nil } } if err = s.EnsureRepository(); err != nil { return "", err } s.mu.Lock() defer s.mu.Unlock() if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", fmt.Errorf("auth filestore: create dir failed: %w", err) } switch { case auth.Storage != nil: if err = auth.Storage.SaveTokenToFile(path); err != nil { return "", err } case auth.Metadata != nil: raw, errMarshal := json.Marshal(auth.Metadata) if errMarshal != nil { return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal) } if existing, errRead := os.ReadFile(path); errRead == nil { if jsonEqual(existing, raw) { return path, nil } } else if !os.IsNotExist(errRead) { return "", fmt.Errorf("auth filestore: read existing failed: %w", errRead) } tmp := path + ".tmp" if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil { return "", fmt.Errorf("auth filestore: write temp failed: %w", errWrite) } if errRename := os.Rename(tmp, path); errRename != nil { return "", fmt.Errorf("auth filestore: rename failed: %w", errRename) } default: return "", fmt.Errorf("auth filestore: nothing to persist for %s", auth.ID) } if auth.Attributes == nil { auth.Attributes = make(map[string]string) } auth.Attributes["path"] = path if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID } relPath, errRel := s.relativeToRepo(path) if errRel != nil { return "", errRel } messageID := auth.ID if strings.TrimSpace(messageID) == "" { messageID = filepath.Base(path) } if errCommit := s.commitAndPushLocked(fmt.Sprintf("Update auth %s", strings.TrimSpace(messageID)), relPath); errCommit != nil { return "", errCommit } return path, nil} // List enumerates all auth JSON files under the configured directory.func (s *GitTokenStore) List(_ context.Context) ([]*cliproxyauth.Auth, error) { if err := s.EnsureRepository(); err != nil { return nil, err } dir := s.baseDirSnapshot() if dir == "" { return nil, fmt.Errorf("auth filestore: directory not configured") } entries := make([]*cliproxyauth.Auth, 0) err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() { return nil } if !strings.HasSuffix(strings.ToLower(d.Name()), ".json") { return nil } auth, err := s.readAuthFile(path, dir) if err != nil { return nil } if auth != nil { entries = append(entries, auth) } return nil }) if err != nil { return nil, err } return entries, nil} // Delete removes the auth file.func (s *GitTokenStore) Delete(_ context.Context, id string) error { id = strings.TrimSpace(id) if id == "" { return fmt.Errorf("auth filestore: id is empty") } path, err := s.resolveDeletePath(id) if err != nil { return err } if err = s.EnsureRepository(); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() if err = os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("auth filestore: delete failed: %w", err) } if err == nil { rel, errRel := s.relativeToRepo(path) if errRel != nil { return errRel } messageID := id if errCommit := s.commitAndPushLocked(fmt.Sprintf("Delete auth %s", messageID), rel); errCommit != nil { return errCommit } } return nil} // PersistAuthFiles commits and pushes the provided paths to the remote repository.// It no-ops when the store is not fully configured or when there are no paths.func (s *GitTokenStore) PersistAuthFiles(_ context.Context, message string, paths ...string) error { if len(paths) == 0 { return nil } if err := s.EnsureRepository(); err != nil { return err } filtered := make([]string, 0, len(paths)) for _, p := range paths { trimmed := strings.TrimSpace(p) if trimmed == "" { continue } rel, err := s.relativeToRepo(trimmed) if err != nil { return err } filtered = append(filtered, rel) } if len(filtered) == 0 { return nil } s.mu.Lock() defer s.mu.Unlock() if strings.TrimSpace(message) == "" { message = "Sync watcher updates" } return s.commitAndPushLocked(message, filtered...)} func (s *GitTokenStore) resolveDeletePath(id string) (string, error) { if strings.ContainsRune(id, os.PathSeparator) || filepath.IsAbs(id) { return id, nil } dir := s.baseDirSnapshot() if dir == "" { return "", fmt.Errorf("auth filestore: directory not configured") } return filepath.Join(dir, id), nil} func (s *GitTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) } if len(data) == 0 { return nil, nil } metadata := make(map[string]any) if err = json.Unmarshal(data, &metadata); err != nil { return nil, fmt.Errorf("unmarshal auth json: %w", err) } provider, _ := metadata["type"].(string) if provider == "" { provider = "unknown" } info, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("stat file: %w", err) } id := s.idFor(path, baseDir) auth := &cliproxyauth.Auth{ ID: id, Provider: provider, FileName: id, Label: s.labelFor(metadata), Status: cliproxyauth.StatusActive, Attributes: map[string]string{"path": path}, Metadata: metadata, CreatedAt: info.ModTime(), UpdatedAt: info.ModTime(), LastRefreshedAt: time.Time{}, NextRefreshAfter: time.Time{}, } if email, ok := metadata["email"].(string); ok && email != "" { auth.Attributes["email"] = email } return auth, nil} func (s *GitTokenStore) idFor(path, baseDir string) string { if baseDir == "" { return path } rel, err := filepath.Rel(baseDir, path) if err != nil { return path } return rel} func (s *GitTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error) { if auth == nil { return "", fmt.Errorf("auth filestore: auth is nil") } if auth.Attributes != nil { if p := strings.TrimSpace(auth.Attributes["path"]); p != "" { return p, nil } } if fileName := strings.TrimSpace(auth.FileName); fileName != "" { if filepath.IsAbs(fileName) { return fileName, nil } if dir := s.baseDirSnapshot(); dir != "" { return filepath.Join(dir, fileName), nil } return fileName, nil } if auth.ID == "" { return "", fmt.Errorf("auth filestore: missing id") } if filepath.IsAbs(auth.ID) { return auth.ID, nil } dir := s.baseDirSnapshot() if dir == "" { return "", fmt.Errorf("auth filestore: directory not configured") } return filepath.Join(dir, auth.ID), nil} func (s *GitTokenStore) labelFor(metadata map[string]any) string { if metadata == nil { return "" } if v, ok := metadata["label"].(string); ok && v != "" { return v } if v, ok := metadata["email"].(string); ok && v != "" { return v } if project, ok := metadata["project_id"].(string); ok && project != "" {Lines 1–500 of 749. Load more
package store import ( "context" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/config" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/plumbing/transport/http" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" ) // GitTokenStore persists token records and auth metadata using git as the backing storage. type GitTokenStore struct { mu sync.Mutex dirLock sync.RWMutex baseDir string repoDir string configDir string remote string username string password string } // NewGitTokenStore creates a token store that saves credentials to disk through the // TokenStorage implementation embedded in the token record. func NewGitTokenStore(remote, username, password string) *GitTokenStore { return &GitTokenStore{ remote: remote, username: username, password: password, } } // SetBaseDir updates the default directory used for auth JSON persistence when no explicit path is provided. func (s *GitTokenStore) SetBaseDir(dir string) { clean := strings.TrimSpace(dir) if clean == "" { s.dirLock.Lock() s.baseDir = "" s.repoDir = "" s.configDir = "" s.dirLock.Unlock() return } if abs, err := filepath.Abs(clean); err == nil { clean = abs } repoDir := filepath.Dir(clean) if repoDir == "" || repoDir == "." { repoDir = clean } configDir := filepath.Join(repoDir, "config") s.dirLock.Lock() s.baseDir = clean s.repoDir = repoDir s.configDir = configDir s.dirLock.Unlock() } // AuthDir returns the directory used for auth persistence. func (s *GitTokenStore) AuthDir() string { return s.baseDirSnapshot() } // ConfigPath returns the managed config file path. func (s *GitTokenStore) ConfigPath() string { s.dirLock.RLock() defer s.dirLock.RUnlock() if s.configDir == "" { return "" } return filepath.Join(s.configDir, "config.yaml") } // EnsureRepository prepares the local git working tree by cloning or opening the repository. func (s *GitTokenStore) EnsureRepository() error { s.dirLock.Lock() if s.remote == "" { s.dirLock.Unlock() return fmt.Errorf("git token store: remote not configured") } if s.baseDir == "" { s.dirLock.Unlock() return fmt.Errorf("git token store: base directory not configured") } repoDir := s.repoDir if repoDir == "" { repoDir = filepath.Dir(s.baseDir) if repoDir == "" || repoDir == "." { repoDir = s.baseDir } s.repoDir = repoDir } if s.configDir == "" { s.configDir = filepath.Join(repoDir, "config") } authDir := filepath.Join(repoDir, "auths") configDir := filepath.Join(repoDir, "config") gitDir := filepath.Join(repoDir, ".git") authMethod := s.gitAuth() var initPaths []string if _, err := os.Stat(gitDir); errors.Is(err, fs.ErrNotExist) { if errMk := os.MkdirAll(repoDir, 0o700); errMk != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create repo dir: %w", errMk) } if _, errClone := git.PlainClone(repoDir, &git.CloneOptions{Auth: authMethod, URL: s.remote}); errClone != nil { if errors.Is(errClone, transport.ErrEmptyRemoteRepository) { _ = os.RemoveAll(gitDir) repo, errInit := git.PlainInit(repoDir, false) if errInit != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: init empty repo: %w", errInit) } if _, errRemote := repo.Remote("origin"); errRemote != nil { if _, errCreate := repo.CreateRemote(&config.RemoteConfig{ Name: "origin", URLs: []string{s.remote}, }); errCreate != nil && !errors.Is(errCreate, git.ErrRemoteExists) { s.dirLock.Unlock() return fmt.Errorf("git token store: configure remote: %w", errCreate) } } if err := os.MkdirAll(authDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth dir: %w", err) } if err := os.MkdirAll(configDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config dir: %w", err) } if err := ensureEmptyFile(filepath.Join(authDir, ".gitkeep")); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth placeholder: %w", err) } if err := ensureEmptyFile(filepath.Join(configDir, ".gitkeep")); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config placeholder: %w", err) } initPaths = []string{ filepath.Join("auths", ".gitkeep"), filepath.Join("config", ".gitkeep"), } } else { s.dirLock.Unlock() return fmt.Errorf("git token store: clone remote: %w", errClone) } } } else if err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: stat repo: %w", err) } else { repo, errOpen := git.PlainOpen(repoDir) if errOpen != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: open repo: %w", errOpen) } worktree, errWorktree := repo.Worktree() if errWorktree != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: worktree: %w", errWorktree) } if errPull := worktree.Pull(&git.PullOptions{Auth: authMethod, RemoteName: "origin"}); errPull != nil { switch { case errors.Is(errPull, git.NoErrAlreadyUpToDate), errors.Is(errPull, git.ErrUnstagedChanges), errors.Is(errPull, git.ErrNonFastForwardUpdate): // Ignore clean syncs, local edits, and remote divergence—local changes win. case errors.Is(errPull, transport.ErrAuthenticationRequired), errors.Is(errPull, plumbing.ErrReferenceNotFound), errors.Is(errPull, transport.ErrEmptyRemoteRepository): // Ignore authentication prompts and empty remote references on initial sync. default: s.dirLock.Unlock() return fmt.Errorf("git token store: pull: %w", errPull) } } } if err := os.MkdirAll(s.baseDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create auth dir: %w", err) } if err := os.MkdirAll(s.configDir, 0o700); err != nil { s.dirLock.Unlock() return fmt.Errorf("git token store: create config dir: %w", err) } s.dirLock.Unlock() if len(initPaths) > 0 { s.mu.Lock() err := s.commitAndPushLocked("Initialize git token store", initPaths...) s.mu.Unlock() if err != nil { return err } } return nil } // Save persists token storage and metadata to the resolved auth file path. func (s *GitTokenStore) Save(_ context.Context, auth *cliproxyauth.Auth) (string, error) { if auth == nil { return "", fmt.Errorf("auth filestore: auth is nil") } path, err := s.resolveAuthPath(auth) if err != nil { return "", err } if path == "" { return "", fmt.Errorf("auth filestore: missing file path attribute for %s", auth.ID) } if auth.Disabled { if _, statErr := os.Stat(path); os.IsNotExist(statErr) { return "", nil } } if err = s.EnsureRepository(); err != nil { return "", err } s.mu.Lock() defer s.mu.Unlock() if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", fmt.Errorf("auth filestore: create dir failed: %w", err) } switch { case auth.Storage != nil: if err = auth.Storage.SaveTokenToFile(path); err != nil { return "", err } case auth.Metadata != nil: raw, errMarshal := json.Marshal(auth.Metadata) if errMarshal != nil { return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal) } if existing, errRead := os.ReadFile(path); errRead == nil { if jsonEqual(existing, raw) { return path, nil } } else if !os.IsNotExist(errRead) { return "", fmt.Errorf("auth filestore: read existing failed: %w", errRead) } tmp := path + ".tmp" if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil { return "", fmt.Errorf("auth filestore: write temp failed: %w", errWrite) } if errRename := os.Rename(tmp, path); errRename != nil { return "", fmt.Errorf("auth filestore: rename failed: %w", errRename) } default: return "", fmt.Errorf("auth filestore: nothing to persist for %s", auth.ID) } if auth.Attributes == nil { auth.Attributes = make(map[string]string) } auth.Attributes["path"] = path if strings.TrimSpace(auth.FileName) == "" { auth.FileName = auth.ID } relPath, errRel := s.relativeToRepo(path) if errRel != nil { return "", errRel } messageID := auth.ID if strings.TrimSpace(messageID) == "" { messageID = filepath.Base(path) } if errCommit := s.commitAndPushLocked(fmt.Sprintf("Update auth %s", strings.TrimSpace(messageID)), relPath); errCommit != nil { return "", errCommit } return path, nil } // List enumerates all auth JSON files under the configured directory. func (s *GitTokenStore) List(_ context.Context) ([]*cliproxyauth.Auth, error) { if err := s.EnsureRepository(); err != nil { return nil, err } dir := s.baseDirSnapshot() if dir == "" { return nil, fmt.Errorf("auth filestore: directory not configured") } entries := make([]*cliproxyauth.Auth, 0) err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() { return nil } if !strings.HasSuffix(strings.ToLower(d.Name()), ".json") { return nil } auth, err := s.readAuthFile(path, dir) if err != nil { return nil } if auth != nil { entries = append(entries, auth) } return nil }) if err != nil { return nil, err } return entries, nil } // Delete removes the auth file. func (s *GitTokenStore) Delete(_ context.Context, id string) error { id = strings.TrimSpace(id) if id == "" { return fmt.Errorf("auth filestore: id is empty") } path, err := s.resolveDeletePath(id) if err != nil { return err } if err = s.EnsureRepository(); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() if err = os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("auth filestore: delete failed: %w", err) } if err == nil { rel, errRel := s.relativeToRepo(path) if errRel != nil { return errRel } messageID := id if errCommit := s.commitAndPushLocked(fmt.Sprintf("Delete auth %s", messageID), rel); errCommit != nil { return errCommit } } return nil } // PersistAuthFiles commits and pushes the provided paths to the remote repository. // It no-ops when the store is not fully configured or when there are no paths. func (s *GitTokenStore) PersistAuthFiles(_ context.Context, message string, paths ...string) error { if len(paths) == 0 { return nil } if err := s.EnsureRepository(); err != nil { return err } filtered := make([]string, 0, len(paths)) for _, p := range paths { trimmed := strings.TrimSpace(p) if trimmed == "" { continue } rel, err := s.relativeToRepo(trimmed) if err != nil { return err } filtered = append(filtered, rel) } if len(filtered) == 0 { return nil } s.mu.Lock() defer s.mu.Unlock() if strings.TrimSpace(message) == "" { message = "Sync watcher updates" } return s.commitAndPushLocked(message, filtered...) } func (s *GitTokenStore) resolveDeletePath(id string) (string, error) { if strings.ContainsRune(id, os.PathSeparator) || filepath.IsAbs(id) { return id, nil } dir := s.baseDirSnapshot() if dir == "" { return "", fmt.Errorf("auth filestore: directory not configured") } return filepath.Join(dir, id), nil } func (s *GitTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) } if len(data) == 0 { return nil, nil } metadata := make(map[string]any) if err = json.Unmarshal(data, &metadata); err != nil { return nil, fmt.Errorf("unmarshal auth json: %w", err) } provider, _ := metadata["type"].(string) if provider == "" { provider = "unknown" } info, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("stat file: %w", err) } id := s.idFor(path, baseDir) auth := &cliproxyauth.Auth{ ID: id, Provider: provider, FileName: id, Label: s.labelFor(metadata), Status: cliproxyauth.StatusActive, Attributes: map[string]string{"path": path}, Metadata: metadata, CreatedAt: info.ModTime(), UpdatedAt: info.ModTime(), LastRefreshedAt: time.Time{}, NextRefreshAfter: time.Time{}, } if email, ok := metadata["email"].(string); ok && email != "" { auth.Attributes["email"] = email } return auth, nil } func (s *GitTokenStore) idFor(path, baseDir string) string { if baseDir == "" { return path } rel, err := filepath.Rel(baseDir, path) if err != nil { return path } return rel } func (s *GitTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error) { if auth == nil { return "", fmt.Errorf("auth filestore: auth is nil") } if auth.Attributes != nil { if p := strings.TrimSpace(auth.Attributes["path"]); p != "" { return p, nil } } if fileName := strings.TrimSpace(auth.FileName); fileName != "" { if filepath.IsAbs(fileName) { return fileName, nil } if dir := s.baseDirSnapshot(); dir != "" { return filepath.Join(dir, fileName), nil } return fileName, nil } if auth.ID == "" { return "", fmt.Errorf("auth filestore: missing id") } if filepath.IsAbs(auth.ID) { return auth.ID, nil } dir := s.baseDirSnapshot() if dir == "" { return "", fmt.Errorf("auth filestore: directory not configured") } return filepath.Join(dir, auth.ID), nil } func (s *GitTokenStore) labelFor(metadata map[string]any) string { if metadata == nil { return "" } if v, ok := metadata["label"].(string); ok && v != "" { return v } if v, ok := metadata["email"].(string); ok && v != "" { return v } if project, ok := metadata["project_id"].(string); ok && project != "" {