package token import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/natefinch/atomic" ) // profileStore keeps one token per profile under the XDG state directory. // It exists because vault-cli storage is single-valued (~/.vault-token): // logging into "prod" would silently clobber "dev". Uses the same // write-then-atomic-rename pattern as Vault's own InternalTokenHelper. type profileStore struct { path string } // NewProfileStore returns a Store scoped to one profile name, rooted at // stateDir (see config.StateDir), e.g. stateDir/tokens/.token. func NewProfileStore(stateDir, profile string) Store { return profileStore{path: filepath.Join(stateDir, "tokens", sanitize(profile)+".token")} } // NewFileStore is a profileStore pinned to an explicit path, used for // token.file in the config. func NewFileStore(path string) Store { return profileStore{path: path} } func sanitize(name string) string { return strings.NewReplacer("/", "_", "\\", "_", "..", "_").Replace(name) } func (s profileStore) Kind() string { return "profile" } func (s profileStore) Location() string { return s.path } func (s profileStore) Get(context.Context) (string, error) { b, err := os.ReadFile(s.path) if os.IsNotExist(err) { return "", nil } if err != nil { return "", err } return strings.TrimSpace(string(b)), nil } func (s profileStore) Store(_ context.Context, tok string) error { dir := filepath.Dir(s.path) if err := os.MkdirAll(dir, 0o700); err != nil { return fmt.Errorf("creating token directory %q: %w", dir, err) } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, []byte(tok), 0o600); err != nil { return err } return atomic.ReplaceFile(tmp, s.path) } func (s profileStore) Erase(context.Context) error { if err := os.Remove(s.path); err != nil && !os.IsNotExist(err) { return err } return nil }