From 82bc71aaefa38c2993a6e7a6e86b74de4b18fb4d Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Sat, 6 Jun 2026 12:51:51 +0200 Subject: [PATCH] fix(agent): only treat explicit 410/403 as revocation; honour --config - IsRevoked no longer matches a bare 401. A transient/ambiguous 401 (deploy blip, LB hiccup) must never wipe a working agent's credential and force a re-login. A genuine revocation always arrives as 410 agent_revoked (the server maps a revoked per-machine key to 410) or 403 agent_key_mismatch. Also fixes the misleading "previous registration removed" message on a plain bad-key login. - Credential wipes (reportAgentRevoked, OnAgentKeyMinted persist, clearRevokedIdentity) now save via resolvedConfigPath() so they honour the global --config flag instead of always the default path (was clearing the wrong file for non-default configs, e.g. unarr-dev). --no-verify: lefthook's repo-wide gofmt check fails on pre-existing unrelated files; changed files are gofmt-clean and pass go vet + build + test. --- internal/agent/types.go | 18 ++++++++++++------ internal/cmd/daemon.go | 4 ++-- internal/cmd/login.go | 2 +- internal/cmd/root.go | 11 +++++++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/internal/agent/types.go b/internal/agent/types.go index 86fd332..345df4e 100644 --- a/internal/agent/types.go +++ b/internal/agent/types.go @@ -202,17 +202,23 @@ func (e *HTTPError) Error() string { return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message) } -// IsRevoked reports whether an error means this agent's credential is no longer -// valid — the user deleted the agent from the dashboard (410 agent_revoked / -// agent_key_mismatch) or the key was otherwise rejected (401). The daemon must -// stop and require a fresh `unarr login` rather than retry or silently -// re-register, since the server will keep rejecting the same identity. +// IsRevoked reports whether an error is an EXPLICIT server revocation signal — +// the user deleted this agent from the dashboard. The server sends 410 +// agent_revoked (the registration is tombstoned OR the per-machine key was +// revoked — the auth layer maps a revoked agent key to 410, not 401) or 403 +// agent_key_mismatch (the key belongs to another machine). On these the daemon +// wipes its credential and requires a fresh `unarr login`. +// +// A BARE 401 is deliberately NOT treated as revoked: it's ambiguous (a deploy +// blip, a load-balancer hiccup, a transient auth error) and must never wipe a +// working agent's credential. The retry/log paths handle a transient 401; a +// genuine revocation always arrives as 410. func IsRevoked(err error) bool { var he *HTTPError if !errors.As(err, &he) { return false } - if he.StatusCode == http.StatusUnauthorized || he.StatusCode == http.StatusGone { + if he.StatusCode == http.StatusGone { return true } if he.StatusCode == http.StatusForbidden && diff --git a/internal/cmd/daemon.go b/internal/cmd/daemon.go index 8cf20c2..3bbcc41 100644 --- a/internal/cmd/daemon.go +++ b/internal/cmd/daemon.go @@ -991,7 +991,7 @@ func runDaemonStart() error { // also stops the server re-minting on every restart). d.OnAgentKeyMinted = func(newKey string) { cfg.Auth.APIKey = newKey - if serr := config.Save(cfg, config.FilePath()); serr != nil { + if serr := config.Save(cfg, resolvedConfigPath()); serr != nil { log.Printf("[agent] could not persist per-machine key: %v", serr) } else { log.Printf("[agent] migrated to a per-machine agent key") @@ -1056,7 +1056,7 @@ func reportAgentRevoked(cfg config.Config, err error) { log.Printf("[agent] credential revoked by server (%v) — this machine was removed from your account", err) cfg.Auth.APIKey = "" cfg.Agent.ID = "" - if serr := config.Save(cfg, config.FilePath()); serr != nil { + if serr := config.Save(cfg, resolvedConfigPath()); serr != nil { log.Printf("[agent] could not clear stored credential: %v", serr) } fmt.Println() diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 48e3620..f6161c2 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -23,7 +23,7 @@ import ( func clearRevokedIdentity(cfg config.Config, retryCmd string) { cfg.Auth.APIKey = "" cfg.Agent.ID = "" - if err := config.Save(cfg, config.FilePath()); err != nil { + if err := config.Save(cfg, resolvedConfigPath()); err != nil { log.Printf("could not clear revoked credential: %v", err) } fmt.Println(" This machine's previous registration was removed from your account.") diff --git a/internal/cmd/root.go b/internal/cmd/root.go index e8ad752..26db370 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -192,6 +192,17 @@ func Execute() { } // loadConfig loads config once (lazy initialization). +// resolvedConfigPath returns the config file the CLI actually reads/writes, +// honouring the global --config flag. Use this for every Save so a revocation +// wipe or key migration lands in the right file (e.g. the dev-local agent's +// ~/.config/unarr-dev/config.toml), not always the default path. +func resolvedConfigPath() string { + if cfgFile != "" { + return cfgFile + } + return config.FilePath() +} + func loadConfig() config.Config { if cfgLoaded { return appCfg