auth: surface contexts read errors; cleaner not-logged-in UX · Entire
auth: surface contexts read errors; cleaner not-logged-in UX
6621920→main·toothbrush·1mo ago·6 files·+78 added/-27 removed
Address two review comments:
ResolveControlPlaneTarget already failed loud on a contexts.json read/parse error; make
auth status(resolveStatusTarget) symmetric — surface a genuine load error instead of swallowing it into the legacy fallback. A missing file still reads as "no contexts" (not an error), so this only fires on real corruption/IO failure, which the user must see before a control-plane mutation acts as a stale identity.providerSource.BearerAuth no longer prefixes the active-context error. NewRefreshingLoginProvider already returns a tailored message naming the context, its login server, and the exact re-login command; surface it verbatim. The bare ErrNotLoggedIn sentinel (static fallback path) still gets the standard 'entire login' hint.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
52317c4f2e8fView transcript
Changes
6
cmd/entire/cli
Mauth.go+22/-13
auth
Mcontrol_plane_test.go+15
Mauth_context_test.go+22/-1
Mlogout.go+4/-1
internal/coreapi
Mclient.go+8/-5
Mclient_test.go+7/-7
166 unmodified lines
// active contexts.json context wins (so `auth use` retargets status onto that login server); otherwise it falls back to the legacy keyring entry keyed by the configured auth host.
func resolveStatusTarget(store tokenStore, listContexts contextsProvider, fallbackBaseURL string) statusTarget {
// A genuine contexts.json read/parse error is surfaced, not swallowed — a missing file reads as "no contexts" (no error), so an error here means the file is corrupt or unreadable, which the user must see. This keeps status symmetric with the control-plane commands (auth.ResolveControlPlaneTarget), which fail the same way rather than silently degrading to a stale identity.
func resolveStatusTarget(store tokenStore, listContexts contextsProvider, fallbackBaseURL string) (statusTarget, error) {
all, current, err := listContexts()
total := 0
if err == nil {
total = len(all)
for _, c := range all {
if c.Name != current || c.CoreURL == "" {
continue
}
if tok, terr := auth.LoginTokenForContext(c); terr == nil && tok != "" {
return statusTarget{coreURL: c.CoreURL, token: tok, activeContext: c.Name, totalContexts: total}
}
}
}
if err != nil {
return statusTarget{}, fmt.Errorf("load contexts: %w", err)
}
total := len(all)
for _, c := range all {
if c.Name != current || c.CoreURL == "" {
continue
}
if tok, terr := auth.LoginTokenForContext(c); terr == nil && tok != "" {
return statusTarget{coreURL: c.CoreURL, token: tok, activeContext: c.Name, totalContexts: total}, nil
}
}
tok, gerr := store.GetToken(fallbackBaseURL)
if gerr != nil {
tok = "" // best-effort: a keyring read failure just reads as "no token"
}
return statusTarget{coreURL: fallbackBaseURL, token: tok, totalContexts: total}
}
// defaultFetchProfile fetches a user's profile from coreURL's GET /me with the
func TestResolveControlPlaneTarget_CorruptContextsErrors(t *testing.T) {
configDir := t.TempDir()
t.Setenv("ENTIRE_CONFIG_DIR", configDir)
t.Setenv(api.AuthBaseURLEnvVar, "")
if err := os.WriteFile(filepath.Join(configDir, "contexts.json"), []byte("{ not valid json"), 0o600); err != nil {
t.Fatalf("write corrupt contexts.json: %v", err)
}
if _, err := ResolveControlPlaneTarget(); err == nil {
t.Fatal("want an error when contexts.json is corrupt, got nil")
}
}
// With no active context, the target falls back to the configured auth origin
// — the default when ENTIRE_AUTH_BASE_URL is unset, or the env value when set
// (the env var is the fallback host).
func (p *providerSource) BearerAuth(ctx context.Context, _ OperationName) (BearerAuth, error) {
token, err := p.provide(ctx)
if err != nil {
// Only suggest login when the user genuinely isn't logged in.
if errors.Is(err, auth.ErrNotLoggedIn) {
return BearerAuth{}, fmt.Errorf("not logged in — run 'entire login': %w", err)
}
return BearerAuth{}, fmt.Errorf("resolve control-plane token: %w", err)
}
return BearerAuth{Token: token}, nil
}
func TestResolveStatusTarget_CorruptContextsErrors(t *testing.T) {
cfgDir := t.TempDir()
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)
restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json"))
t.Cleanup(restore)
if err := os.WriteFile(filepath.Join(cfgDir, "contexts.json"), []byte("{ not valid json"), 0o600); err != nil {
t.Fatalf("write corrupt contexts.json: %v", err)
}
if _, err := resolveStatusTarget(auth.NewContextStore(), auth.Contexts, "https://fallback.example.com"); err == nil {
t.Fatal("want an error when contexts.json is corrupt, got nil")
}
}
// makeContextJWT builds a JWT-shaped token (non-"none" alg) carrying the
// given claims, which is all RecordLoginContext needs.