From 0d59534ce58182350a500bdd2e61c226432e6d30 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 15:00:01 -0400 Subject: [PATCH 1/2] fix(hoverclient): send token: null (not "") on signin auth.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hover's React signin added 'magic token' sign-in. auth.json branches on the 'token' field: a non-null value (including empty string "") routes to magic-token validation, which fails an empty token with a generic 'Invalid username or password.' — even when username/password are correct. The browser sends token: null for password sign-in (verified via DevTools Network); the plugin was sending "". Send null to match. Diagnosis: cookieless probe ruled out CSRF/session; username confirmed exact; null vs "" was the only remaining difference vs the working browser request. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/hoverclient/client.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/hoverclient/client.go b/pkg/hoverclient/client.go index 2f9be20..e9a07c9 100644 --- a/pkg/hoverclient/client.go +++ b/pkg/hoverclient/client.go @@ -102,7 +102,12 @@ func (c *Client) ensureLoginLocked(ctx context.Context) error { "username": c.creds.Username, "password": c.creds.Password, "remember": false, - "token": "", + // token MUST be JSON null, not "". Hover's signin branches on this + // field for its "magic token" sign-in: a non-null token (even "") + // routes to magic-token validation, which fails an empty token with + // a generic "Invalid username or password." The browser sends null + // for password sign-in; match it exactly (verified via DevTools). + "token": nil, }) if err != nil { return fmt.Errorf("hover signin step 1: %w", err) From 37ae99e23d0c1a462b7e3e9531fd408c07719097 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 15:02:43 -0400 Subject: [PATCH 2/2] test(hoverclient): guard token is JSON null (regression for magic-token 401) Strengthen TestClient_Login_UsesBrowserSigninShape: the token field must be JSON null, not "". Was a key-presence check that passed for both. Proven: fails with 'token must be JSON null, got ""' if the fix regresses to "". --- pkg/hoverclient/client_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/hoverclient/client_test.go b/pkg/hoverclient/client_test.go index 71d7e1d..9df89fc 100644 --- a/pkg/hoverclient/client_test.go +++ b/pkg/hoverclient/client_test.go @@ -143,8 +143,16 @@ func TestClient_Login_UsesBrowserSigninShape(t *testing.T) { t.Fatalf("Login: %v", err) } - if _, ok := authBody["token"]; !ok { + // Regression guard (do not weaken to a key-presence check): token MUST + // be JSON null, not "". A non-null token — including the empty string — + // routes Hover's auth.json to its "magic token" sign-in path, which + // fails an empty token with a generic "Invalid username or password" + // even when username + password are correct. Sending "" cost a + // multi-day live debugging session. See client.go ensureLoginLocked. + if v, ok := authBody["token"]; !ok { t.Fatalf("auth body missing token field: %#v", authBody) + } else if v != nil { + t.Fatalf("token must be JSON null, got %#v — a non-null token (incl. \"\") triggers magic-token signin and 401s with valid creds", v) } if got := headers.Get("Origin"); got != hoverHost { t.Errorf("Origin = %q, want %q", got, hoverHost)