Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e6eb772
test: add characterization tests for dbc public API
zeroshade Apr 16, 2026
c2c30b0
refactor: eliminate init() side effects in dbc package
zeroshade Apr 16, 2026
908903b
refactor: move TUI widgets from dbc and config to cmd/dbc
zeroshade Apr 16, 2026
5afdc20
feat: add Client type with functional options
zeroshade Apr 16, 2026
803f87d
feat: implement Client Search, Install, and Uninstall methods
zeroshade Apr 16, 2026
3234d0d
feat: implement Client config management methods
zeroshade Apr 16, 2026
f8b52fe
feat: add auth options layer to Client
zeroshade Apr 16, 2026
e42ad95
chore: deprecate old package-level APIs in favor of Client
zeroshade Apr 16, 2026
742ed44
refactor: update CLI to use Client internally
zeroshade Apr 16, 2026
0f84509
refactor: remove deprecated TUI files from root dbc package
zeroshade Apr 16, 2026
3abdeeb
fix: address code quality issues from F2 review
zeroshade Apr 16, 2026
8a378d9
fix: remove remaining deprecated API usages from cmd/dbc
zeroshade Apr 16, 2026
30a6150
fix: address code review findings from roborev open reviews
zeroshade Apr 17, 2026
3dc3ae6
fix: remove init(), add lazy dbcClient in getDriverRegistry, clean up…
zeroshade Apr 17, 2026
e9ce3f5
fix: extract newDefaultClient helper for DBC_BASE_URL, restore f.Clos…
zeroshade Apr 17, 2026
8bccbb1
fix: remove double-close of tarball file in Install; InflateTarball a…
zeroshade Apr 17, 2026
2e405f4
fix: cleanup tmpdir on downloadPackage error paths, assert credential…
zeroshade Apr 17, 2026
27d0715
fix: propagate newDefaultClient error through sync.Once in getDriverR…
zeroshade Apr 17, 2026
cfda2fb
fix: remove sync.Once from getDriverRegistry, use NewRequestWithConte…
zeroshade Apr 17, 2026
852e835
fix: propagate NewRequestWithContext error in makeRequest, centralize…
zeroshade Apr 17, 2026
7da2fdd
fix: store initDBCClient error at package scope so sync.Once failure …
zeroshade Apr 17, 2026
d6bfbf4
fix: guard makereq retry against nil resp, use fresh request per call…
zeroshade Apr 17, 2026
d84ea70
fix: deep-copy URL in buildLegacyReq so http.Do mutations don't affec…
zeroshade Apr 17, 2026
8e2d0ae
fix: document WithUserAgent transport limitation, remove TestClientSe…
zeroshade Apr 17, 2026
b7d7721
fix: make TestClientSearch fully independent of TestMain by using new…
zeroshade Apr 17, 2026
0452ec0
fix: close response body on non-OK status in DownloadPackage, clone r…
zeroshade Apr 17, 2026
3269f04
fix: explicit body close on non-OK in DownloadPackage, use raw URL st…
zeroshade Apr 17, 2026
17fce89
feat: add AllVersions, VersionInfo, PackageInfo for external iteration
zeroshade Apr 17, 2026
d5c6052
feat: add Client.Download for authenticated tarball streaming
zeroshade Apr 17, 2026
c875180
fix: surface response body in Download errors and add unit tests
zeroshade Apr 17, 2026
e8bacc8
fix: guard nil Registry in GetPackage, harden getHTTPClient, rename D…
zeroshade Apr 17, 2026
a0a2629
fix: lazy credPath init, mutex for credential slice, check oauth requ…
zeroshade Apr 17, 2026
1853221
fix: propagate LicensePath error, hold credMu during load, clean tmpd…
zeroshade Apr 17, 2026
d0177c1
fix: close response body on non-200, use HTTPS for wixl packages, san…
zeroshade Apr 17, 2026
146e5f3
fix: return early from ConfigEnv loadConfig, add default subcommand c…
zeroshade Apr 17, 2026
e2d28cd
fix: check url.Parse error in GetPackage, eliminate data race in Test…
zeroshade Apr 17, 2026
2361e7a
fix: use NewRequestWithContext in legacy makereq, guard MaxVersion on…
zeroshade Apr 17, 2026
29a76cb
fix: check MaxVersion ok in formatDriverInfo for safety
zeroshade Apr 17, 2026
f6622a5
fix: replace panic with os.Exit in GetModelCustom, clarify prerelease…
zeroshade Apr 17, 2026
fa0e2de
fix: check Seek error in InflateTarball, rename extra_folder to extra…
zeroshade Apr 17, 2026
79c3ecd
fix: use deb.debian.org for wixl packages (ftp.de.debian.org lacks va…
zeroshade Apr 17, 2026
b21a7b1
refactor: extract shared helpers, Refresh returns error, MaxVersion r…
zeroshade Apr 17, 2026
150d19a
refactor: atomic license write, extract openAndDecodeDriverList, use …
zeroshade Apr 17, 2026
16fffa4
fix: brace alignment in add.go prerelease check, wrap Refresh error i…
zeroshade Apr 20, 2026
588a625
update comment
zeroshade Apr 28, 2026
294956d
chore: apply workflow security improvements from main (zizmor fixes)
Copilot Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 109 additions & 44 deletions auth/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
package auth

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
Expand Down Expand Up @@ -75,46 +77,52 @@ type Credential struct {
Audience string `toml:"audience,omitempty"`
}

func (t *Credential) Refresh() bool {
func (t *Credential) Refresh() error {
switch t.Type {
case TypeApiKey:
rsp, err := http.DefaultClient.Do(&http.Request{
Method: http.MethodGet,
URL: (*url.URL)(&t.AuthURI),
Header: http.Header{
"authorization": []string{"Bearer " + t.ApiKey},
},
})
if err != nil || rsp.StatusCode != http.StatusOK {
return false
req, err := http.NewRequestWithContext(context.Background(),
http.MethodGet, (*url.URL)(&t.AuthURI).String(), nil)
if err != nil {
return fmt.Errorf("apikey refresh: %w", err)
}
req.Header.Set("Authorization", "Bearer "+t.ApiKey)

rsp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("apikey refresh: %w", err)
}
defer rsp.Body.Close()
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("apikey refresh: status %s", rsp.Status)
}

var tokenResp struct {
Token string `json:"access_token"`
}
if err := json.NewDecoder(rsp.Body).Decode(&tokenResp); err != nil {
return false
return err
}

t.Token = tokenResp.Token
return true
return nil
case TypeToken:
if err := refreshOauth(t); err != nil {
return false
return fmt.Errorf("oauth refresh: %w", err)
}
return true
return nil
}

return false
return fmt.Errorf("unsupported credential type: %s", t.Type)
}

// GetAuthToken returns the current token, refreshing if needed.
// Must not be called while credMu is held (Refresh may acquire it via UpdateCreds).
func (t *Credential) GetAuthToken() string {
if t.Token != "" {
return t.Token
}

if t.Refresh() {
if err := t.Refresh(); err == nil {
_ = UpdateCreds()
return t.Token
}
Expand All @@ -127,18 +135,30 @@ var (
credentialErr error
loaded sync.Once
credPath string
credPathMu sync.Mutex
credMu sync.RWMutex
)

func init() {
var err error
credPath, err = internal.GetCredentialPath()
if err != nil {
panic(fmt.Sprintf("failed to get credential path: %s", err))
func getCredPath() (string, error) {
credPathMu.Lock()
defer credPathMu.Unlock()
if credPath == "" {
var err error
credPath, err = internal.GetCredentialPath()
if err != nil {
return "", fmt.Errorf("failed to get credential path: %w", err)
}
}
return credPath, nil
}

func loadCreds() ([]Credential, error) {
credFile, err := os.Open(credPath)
cp, err := getCredPath()
if err != nil {
return nil, fmt.Errorf("failed to get credential path: %w", err)
}

credFile, err := os.Open(cp)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return []Credential{}, nil
Expand All @@ -163,6 +183,8 @@ func GetCredentials(u *url.URL) (*Credential, error) {
return nil, err
}

credMu.RLock()
defer credMu.RUnlock()
for i, cred := range loadedCredentials {
if cred.RegistryURL.Host == u.Host {
return &loadedCredentials[i], nil
Expand All @@ -174,7 +196,10 @@ func GetCredentials(u *url.URL) (*Credential, error) {

func LoadCredentials() error {
loaded.Do(func() {
loadedCredentials, credentialErr = loadCreds()
creds, err := loadCreds()
credMu.Lock()
loadedCredentials, credentialErr = creds, err
credMu.Unlock()
})
return credentialErr
}
Expand All @@ -184,6 +209,9 @@ func AddCredential(cred Credential, allowOverwrite bool) error {
return err
}

credMu.Lock()
defer credMu.Unlock()

idx := slices.IndexFunc(loadedCredentials, func(c Credential) bool {
return c.RegistryURL.Host == cred.RegistryURL.Host
})
Expand All @@ -196,14 +224,17 @@ func AddCredential(cred Credential, allowOverwrite bool) error {
} else {
loadedCredentials = append(loadedCredentials, cred)
}
return UpdateCreds()
return writeCreds()
}

func RemoveCredential(host Uri) error {
if err := LoadCredentials(); err != nil {
return err
}

credMu.Lock()
defer credMu.Unlock()

idx := slices.IndexFunc(loadedCredentials, func(c Credential) bool {
return c.RegistryURL.Host == host.Host
})
Expand All @@ -213,20 +244,22 @@ func RemoveCredential(host Uri) error {
}

loadedCredentials = append(loadedCredentials[:idx], loadedCredentials[idx+1:]...)
return UpdateCreds()
return writeCreds()
}

func UpdateCreds() error {
if err := LoadCredentials(); err != nil {
return err
// writeCreds persists loadedCredentials to disk.
// Caller must hold credMu.
func writeCreds() error {
cp, err := getCredPath()
if err != nil {
return fmt.Errorf("failed to get credential path: %w", err)
}

err := os.MkdirAll(filepath.Dir(credPath), 0o700)
if err != nil {
if err := os.MkdirAll(filepath.Dir(cp), 0o700); err != nil {
return err
}

f, err := os.OpenFile(credPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600)
f, err := os.OpenFile(cp, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600)
if err != nil {
return err
}
Expand All @@ -239,13 +272,28 @@ func UpdateCreds() error {
})
}

func UpdateCreds() error {
if err := LoadCredentials(); err != nil {
return err
}

credMu.Lock()
defer credMu.Unlock()
return writeCreds()
}

func PurgeCredentials() error {
cp, err := getCredPath()
if err != nil {
return fmt.Errorf("failed to get credential path: %w", err)
}

var fileList = []string{
"credentials.toml",
"columnar.lic",
}

prefix := filepath.Dir(credPath)
prefix := filepath.Dir(cp)

for _, file := range fileList {
fullPath := filepath.Join(prefix, file)
Expand All @@ -268,16 +316,23 @@ var (
ErrLicenseAlreadyExists = errors.New("license already exists (use --force to overwrite)")
)

func LicensePath() string {
return filepath.Join(filepath.Dir(credPath), "columnar.lic")
func LicensePath() (string, error) {
cp, err := getCredPath()
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(cp), "columnar.lic"), nil
}

func InstallLicenseFromFile(srcPath string, force bool) error {
if !force && filepath.Base(srcPath) != "columnar.lic" {
return ErrLicenseWrongFilename
}

destPath := LicensePath()
destPath, err := LicensePath()
if err != nil {
return fmt.Errorf("failed to determine license path: %w", err)
}

if !force {
if _, err := os.Stat(destPath); err == nil {
Expand All @@ -302,8 +357,13 @@ func InstallLicenseFromFile(srcPath string, force bool) error {
}

func FetchColumnarLicense(cred *Credential) error {
licensePath := filepath.Join(filepath.Dir(credPath), "columnar.lic")
_, err := os.Stat(licensePath)
cp, err := getCredPath()
if err != nil {
return fmt.Errorf("failed to get credential path: %w", err)
}

licensePath := filepath.Join(filepath.Dir(cp), "columnar.lic")
_, err = os.Stat(licensePath)
if err == nil { // license exists already
return nil
}
Expand Down Expand Up @@ -332,7 +392,7 @@ func FetchColumnarLicense(cred *Credential) error {
return fmt.Errorf("unsupported credential type: %s", cred.Type)
}

req, err := http.NewRequest(http.MethodGet, licenseURI, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, licenseURI, nil)
if err != nil {
return err
}
Expand All @@ -355,14 +415,19 @@ func FetchColumnarLicense(cred *Credential) error {
}
}

licenseFile, err := os.OpenFile(licensePath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600)
tmp, err := os.CreateTemp(filepath.Dir(licensePath), ".lic.*")
if err != nil {
return err
}
defer licenseFile.Close()
if _, err = licenseFile.ReadFrom(resp.Body); err != nil {
licenseFile.Close()
os.Remove(licensePath)
tmpName := tmp.Name()
defer os.Remove(tmpName)

if _, err := io.Copy(tmp, resp.Body); err != nil {
tmp.Close()
return fmt.Errorf("write license: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close license temp file: %w", err)
}
return err
return os.Rename(tmpName, licensePath)
}
Loading
Loading