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:

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

52317c4f2e8fView transcript

Changes

6

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.