Files
incredigo/internal/discover/tea.go
T
leetcrypt 1c0896347b discover: broaden discovery to feed the new drivers
Per the standing directive (dynamic discovery per credential type as each
driver lands):
- env.go: tag DB connection URLs by scheme (postgres/postgresql, mysql/mariadb,
  redis/rediss) with a user:password authority, routing DATABASE_URL/REDIS_URL/
  MYSQL_URL straight to the in-place DB drivers, kept regardless of the
  secret-name heuristic.
- aws.go: emit the driver-ready single-line blob (was the bare secret) and
  resolve region from env / ~/.aws/config.
- ssh.go: content-based key detection (not just id_*) + ~/.ssh/config
  IdentityFile parsing.
- tea.go: new scanner mapping Gitea `tea` CLI config to a driver-ready PAT blob.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 14:48:30 -07:00

99 lines
2.3 KiB
Go

package discover
import (
"context"
"net/url"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"incredigo/internal/vault"
)
func init() { Register(&teaScanner{}) }
// teaScanner reads the Gitea `tea` CLI config (~/.config/tea/config.yml), which
// stores one or more logins as {name, url, user, token}. Because this file is
// unambiguously Gitea, entries are tagged Source "gitea" so the gitea rotation
// driver Detects them directly.
//
// The emitted secret is the driver-ready single-line blob
// "<scheme>://<user>:<token>@<host>/" (see internal/rotate/gitea.go). The token
// name is unknown from tea config, so it is omitted — RevokeOld then degrades to
// the guided worklist for the old-token deletion, which is the correct behavior.
// Read-only.
type teaScanner struct{}
func (t *teaScanner) Name() string { return "gitea" }
func (t *teaScanner) path() string {
if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
return filepath.Join(x, "tea", "config.yml")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "tea", "config.yml")
}
func (t *teaScanner) Available() bool {
_, err := os.Stat(t.path())
return err == nil
}
func (t *teaScanner) Scan(ctx context.Context, v *vault.Vault) ([]Credential, error) {
p := t.path()
b, err := os.ReadFile(p) // read-only
if err != nil {
return nil, err
}
fi, _ := os.Stat(p)
var cfg struct {
Logins []struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
User string `yaml:"user"`
Token string `yaml:"token"`
} `yaml:"logins"`
}
if err := yaml.Unmarshal(b, &cfg); err != nil {
return nil, err
}
var creds []Credential
for _, l := range cfg.Logins {
if l.Token == "" || l.URL == "" {
continue
}
u, err := url.Parse(l.URL)
if err != nil || u.Host == "" {
continue
}
user := l.User
if user == "" {
user = l.Name
}
scheme := u.Scheme
if scheme == "" {
scheme = "https"
}
// Driver-ready blob; the token lives only here (and the vault).
blob := (&url.URL{
Scheme: scheme,
Host: u.Host,
User: url.UserPassword(user, l.Token),
Path: "/",
}).String()
creds = append(creds, Credential{
Source: t.Name(),
Kind: KindToken,
Identity: user + " @ " + u.Host,
Location: p,
Modified: fi.ModTime(),
Secret: v.Store([]byte(blob)),
})
}
return creds, nil
}