From 6252039fc508ee412b932786bf3938b7dc223ad4 Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 16 Apr 2026 21:06:35 +0000 Subject: [PATCH 1/3] feat: add user reset-apikey command Adds `hf user reset-apikey ` to regenerate a user API key. Requires user.manage permission. Returns the new key (shown once only). Co-Authored-By: Claude Opus 4.6 --- cmd/hf/main.go | 5 +++++ internal/commands/user.go | 42 +++++++++++++++++++++++++++++++++++++++ internal/help/leaf.go | 1 + internal/help/surface.go | 1 + 4 files changed, 49 insertions(+) diff --git a/cmd/hf/main.go b/cmd/hf/main.go index cbea4e5..783be85 100644 --- a/cmd/hf/main.go +++ b/cmd/hf/main.go @@ -341,6 +341,11 @@ func handleUserCommand(subCmd string, args []string) { output.Error("usage: hf user delete ") } commands.RunUserDelete(filtered[0], tokenFlag) + case "reset-apikey": + if len(filtered) < 1 { + output.Error("usage: hf user reset-apikey ") + } + commands.RunUserResetAPIKey(filtered[0], tokenFlag) default: output.Errorf("hf user %s is not implemented yet", subCmd) } diff --git a/internal/commands/user.go b/internal/commands/user.go index 8381a5d..c2c36d4 100644 --- a/internal/commands/user.go +++ b/internal/commands/user.go @@ -390,3 +390,45 @@ func RunUserDelete(username, tokenFlag string) { } fmt.Printf("user deleted: %s\n", username) } + +// resetAPIKeyResponse matches the backend reset-apikey response. +type resetAPIKeyResponse struct { + UserID int `json:"user_id"` + Username string `json:"username"` + APIKey string `json:"api_key"` + Message string `json:"message"` +} + +// RunUserResetAPIKey implements `hf user reset-apikey `. +func RunUserResetAPIKey(username, tokenFlag string) { + token := ResolveToken(tokenFlag) + cfg, err := config.Load() + if err != nil { + output.Errorf("config error: %v", err) + } + c := client.New(cfg.BaseURL, token) + data, err := c.Post("/users/"+username+"/reset-apikey", nil) + if err != nil { + output.Errorf("failed to reset API key: %v", err) + } + + if output.JSONMode { + var raw json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + output.Errorf("invalid JSON response: %v", err) + } + output.PrintJSON(raw) + return + } + + var r resetAPIKeyResponse + if err := json.Unmarshal(data, &r); err != nil { + fmt.Printf("API key reset for: %s\n", username) + return + } + output.PrintKeyValue( + "username", r.Username, + "api-key", r.APIKey, + "message", r.Message, + ) +} diff --git a/internal/help/leaf.go b/internal/help/leaf.go index 9a771c8..8eba619 100644 --- a/internal/help/leaf.go +++ b/internal/help/leaf.go @@ -114,6 +114,7 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) { "user/activate": {Summary: "Activate a user", Usage: []string{"hf user activate "}, Flags: authFlagHelp()}, "user/deactivate": {Summary: "Deactivate a user", Usage: []string{"hf user deactivate "}, Flags: authFlagHelp()}, "user/delete": {Summary: "Delete a user", Usage: []string{"hf user delete "}, Flags: authFlagHelp()}, + "user/reset-apikey": {Summary: "Reset a user's API key", Usage: []string{"hf user reset-apikey "}, Flags: authFlagHelp(), Notes: []string{"The new API key is shown once and cannot be retrieved again."}}, "role/list": {Summary: "List roles", Usage: []string{"hf role list"}, Flags: authFlagHelp()}, "role/get": {Summary: "Show a role by name", Usage: []string{"hf role get "}, Flags: authFlagHelp()}, "role/create": {Summary: "Create a role", Usage: []string{"hf role create --name [--desc ] [--global ]"}, Flags: authFlagHelp()}, diff --git a/internal/help/surface.go b/internal/help/surface.go index 38a9d48..4954209 100644 --- a/internal/help/surface.go +++ b/internal/help/surface.go @@ -40,6 +40,7 @@ func CommandSurface() []Group { {Name: "activate", Description: "Activate a user", Permitted: has(perms, "user.manage")}, {Name: "deactivate", Description: "Deactivate a user", Permitted: has(perms, "user.manage")}, {Name: "delete", Description: "Delete a user", Permitted: has(perms, "user.manage")}, + {Name: "reset-apikey", Description: "Reset a user's API key", Permitted: has(perms, "user.manage")}, }, }, { From 53b5b88fc2e1c44090550559ca34f613c8561f04 Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 16 Apr 2026 21:08:00 +0000 Subject: [PATCH 2/3] feat: user reset-apikey supports acc-mgr-token auth Allows reset-apikey to use --acc-mgr-token or auto-resolve from secret-mgr in padded-cell mode, enabling API key provisioning without an existing user Bearer token. Co-Authored-By: Claude Opus 4.6 --- cmd/hf/main.go | 2 +- internal/commands/user.go | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cmd/hf/main.go b/cmd/hf/main.go index 783be85..2716e1f 100644 --- a/cmd/hf/main.go +++ b/cmd/hf/main.go @@ -345,7 +345,7 @@ func handleUserCommand(subCmd string, args []string) { if len(filtered) < 1 { output.Error("usage: hf user reset-apikey ") } - commands.RunUserResetAPIKey(filtered[0], tokenFlag) + commands.RunUserResetAPIKey(filtered[0], tokenFlag, accMgrTokenFlag) default: output.Errorf("hf user %s is not implemented yet", subCmd) } diff --git a/internal/commands/user.go b/internal/commands/user.go index c2c36d4..af2555c 100644 --- a/internal/commands/user.go +++ b/internal/commands/user.go @@ -400,13 +400,28 @@ type resetAPIKeyResponse struct { } // RunUserResetAPIKey implements `hf user reset-apikey `. -func RunUserResetAPIKey(username, tokenFlag string) { - token := ResolveToken(tokenFlag) +func RunUserResetAPIKey(username, tokenFlag, accMgrTokenFlag string) { cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } - c := client.New(cfg.BaseURL, token) + + // Try acc-mgr-token first (allows provisioning without existing user token) + var c *client.Client + if accMgrTokenFlag != "" { + c = client.NewWithAPIKey(cfg.BaseURL, accMgrTokenFlag) + } else if mode.IsPaddedCell() { + if tok, err := passmgr.GetAccountManagerToken(); err == nil && tok != "" { + c = client.NewWithAPIKey(cfg.BaseURL, tok) + } else { + token := ResolveToken(tokenFlag) + c = client.New(cfg.BaseURL, token) + } + } else { + token := ResolveToken(tokenFlag) + c = client.New(cfg.BaseURL, token) + } + data, err := c.Post("/users/"+username+"/reset-apikey", nil) if err != nil { output.Errorf("failed to reset API key: %v", err) From 6dae4902575f6bca5ce015c6300e8428a1d22396 Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 16 Apr 2026 21:11:00 +0000 Subject: [PATCH 3/3] refactor: rename pass_mgr to secret-mgr The secret manager binary was renamed from pass_mgr to secret-mgr. Update all references in CLI code, mode detection, and help text. Co-Authored-By: Claude Opus 4.6 --- internal/commands/config.go | 2 +- internal/help/leaf.go | 6 +++--- internal/mode/mode.go | 6 +++--- internal/passmgr/passmgr.go | 24 ++++++++++++------------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/commands/config.go b/internal/commands/config.go index 54a895a..d43cd27 100644 --- a/internal/commands/config.go +++ b/internal/commands/config.go @@ -20,7 +20,7 @@ func RunConfigURL(url string) { fmt.Printf("base-url set to %s\n", url) } -// RunConfigAccMgrToken stores the account-manager token via pass_mgr. +// RunConfigAccMgrToken stores the account-manager token via secret-mgr. func RunConfigAccMgrToken(token string) { if token == "" { output.Error("usage: hf config --acc-mgr-token ") diff --git a/internal/help/leaf.go b/internal/help/leaf.go index 8eba619..aacb544 100644 --- a/internal/help/leaf.go +++ b/internal/help/leaf.go @@ -95,9 +95,9 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) { Notes: []string{"Writes base-url into .hf-config.json next to the hf binary."}, }, "config/acc-mgr-token": { - Summary: "Store the account-manager token via pass_mgr", + Summary: "Store the account-manager token via secret-mgr", Usage: []string{"hf config --acc-mgr-token "}, - Notes: []string{"Only available in padded-cell mode with pass_mgr installed."}, + Notes: []string{"Only available in padded-cell mode with secret-mgr installed."}, }, "user/create": { Summary: "Create a user account", @@ -105,7 +105,7 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) { Flags: accountManagerFlagHelp(), Notes: []string{ "This command uses the account-manager token flow, not the normal user token flow.", - "In padded-cell mode, --acc-mgr-token is hidden and password generation can fall back to pass_mgr.", + "In padded-cell mode, --acc-mgr-token is hidden and password generation can fall back to secret-mgr.", }, }, "user/list": {Summary: "List users", Usage: []string{"hf user list"}, Flags: authFlagHelp()}, diff --git a/internal/mode/mode.go b/internal/mode/mode.go index 3c52c1f..c6271b1 100644 --- a/internal/mode/mode.go +++ b/internal/mode/mode.go @@ -12,7 +12,7 @@ type RuntimeMode int const ( // ManualMode requires explicit --token / --acc-mgr-token flags. ManualMode RuntimeMode = iota - // PaddedCellMode resolves secrets via pass_mgr automatically. + // PaddedCellMode resolves secrets via secret-mgr automatically. PaddedCellMode ) @@ -21,11 +21,11 @@ var ( detectOnce sync.Once ) -// Detect checks whether pass_mgr is available and returns the runtime mode. +// Detect checks whether secret-mgr is available and returns the runtime mode. // The result is cached after the first call. func Detect() RuntimeMode { detectOnce.Do(func() { - _, err := exec.LookPath("pass_mgr") + _, err := exec.LookPath("secret-mgr") if err == nil { detectedMode = PaddedCellMode } else { diff --git a/internal/passmgr/passmgr.go b/internal/passmgr/passmgr.go index df1c6eb..525522a 100644 --- a/internal/passmgr/passmgr.go +++ b/internal/passmgr/passmgr.go @@ -1,4 +1,4 @@ -// Package passmgr wraps calls to the pass_mgr binary for secret resolution. +// Package passmgr wraps calls to the secret-mgr binary for secret resolution. package passmgr import ( @@ -7,49 +7,49 @@ import ( "strings" ) -// GetSecret calls: pass_mgr get-secret [--public] --key +// GetSecret calls: secret-mgr get-secret [--public] --key func GetSecret(key string, public bool) (string, error) { args := []string{"get-secret"} if public { args = append(args, "--public") } args = append(args, "--key", key) - out, err := exec.Command("pass_mgr", args...).Output() + out, err := exec.Command("secret-mgr", args...).Output() if err != nil { - return "", fmt.Errorf("pass_mgr get-secret --key %s failed: %w", key, err) + return "", fmt.Errorf("secret-mgr get-secret --key %s failed: %w", key, err) } return strings.TrimSpace(string(out)), nil } -// SetSecret calls: pass_mgr set [--public] --key --secret +// SetSecret calls: secret-mgr set [--public] --key --secret func SetSecret(key, secret string, public bool) error { args := []string{"set"} if public { args = append(args, "--public") } args = append(args, "--key", key, "--secret", secret) - if err := exec.Command("pass_mgr", args...).Run(); err != nil { - return fmt.Errorf("pass_mgr set --key %s failed: %w", key, err) + if err := exec.Command("secret-mgr", args...).Run(); err != nil { + return fmt.Errorf("secret-mgr set --key %s failed: %w", key, err) } return nil } -// GeneratePassword calls: pass_mgr generate --key --username +// GeneratePassword calls: secret-mgr generate --key --username func GeneratePassword(key, username string) (string, error) { args := []string{"generate", "--key", key, "--username", username} - out, err := exec.Command("pass_mgr", args...).Output() + out, err := exec.Command("secret-mgr", args...).Output() if err != nil { - return "", fmt.Errorf("pass_mgr generate failed: %w", err) + return "", fmt.Errorf("secret-mgr generate failed: %w", err) } return strings.TrimSpace(string(out)), nil } -// GetToken retrieves the normal hf-token via pass_mgr. +// GetToken retrieves the normal hf-token via secret-mgr. func GetToken() (string, error) { return GetSecret("hf-token", false) } -// GetAccountManagerToken retrieves the public hf-acc-mgr-token via pass_mgr. +// GetAccountManagerToken retrieves the public hf-acc-mgr-token via secret-mgr. func GetAccountManagerToken() (string, error) { return GetSecret("hf-acc-mgr-token", true) }