Files
codex/prs/bolinfest/study/PR-1971-study.md
2025-09-02 15:17:45 -07:00

5.7 KiB
Raw Blame History

DOs

  • Bold the logic: Prefer ChatGPT auth for Free/Plus/Pro/Team plans even if an API key is present.
  • Bold the fallback: Read OPENAI_API_KEY only when auth.json is missing (NotFound) and include_env_var is true.
  • Bold the error handling: If auth.json exists but is malformed, return the error and do not fall back to env.
  • Bold the API key choice: Use API key when auth.json has an API key and no tokens.
  • Bold the enterprise routing: Use API key for Business/Enterprise/Edu or unknown plans.
  • Bold the modeling: Represent plan with PlanType (Known(KnownPlan) or Unknown(String)) and use serde for KnownPlan.
  • Bold the helper: Add read_openai_api_key_from_env() to encapsulate env access.
  • Bold the ChatGPT shape: In ChatGPT mode, set api_key: None and keep only tokens/last_refresh; clear openai_api_key.
  • Bold the display: Expose IdTokenInfo::get_chatgpt_plan_type() for UI and title-case it for display.
  • Bold the tests: Use a minimal URL-safe, no-pad base64 JWT, a LAST_REFRESH constant, and tempdir helpers.
// Auth selection (core decision points)
if let Ok(auth) = try_read_auth_json(&auth_file) {
    let AuthDotJson { openai_api_key, tokens, last_refresh } = auth;

    if let Some(api_key) = &openai_api_key {
        match &tokens {
            Some(tokens) if tokens.is_plan_that_should_use_api_key() => {
                // Business/Enterprise/Edu/Unknown → API key
                return Ok(Some(CodexAuth::from_api_key(api_key)));
            }
            Some(_) => {
                // Free/Plus/Pro/Team → prefer ChatGPT
            }
            None => {
                // API key but no tokens → API key
                return Ok(Some(CodexAuth::from_api_key(api_key)));
            }
        }
    }

    // ChatGPT mode: no API key, keep tokens/last_refresh
    return Ok(Some(CodexAuth {
        api_key: None,
        mode: AuthMode::ChatGPT,
        auth_file,
        auth_dot_json: Arc::new(Mutex::new(Some(AuthDotJson {
            openai_api_key: None,
            tokens,
            last_refresh,
        }))),
    }));
} else if include_env_var && matches!(e.kind(), std::io::ErrorKind::NotFound) {
    // Only when auth.json is missing
    if let Some(k) = read_openai_api_key_from_env() {
        return Ok(Some(CodexAuth::from_api_key(&k)));
    }
    return Ok(None);
} else {
    // Malformed auth.json → surface error
    return Err(e);
}
// Plan modeling
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
enum PlanType {
    Known(KnownPlan),
    Unknown(String),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum KnownPlan { Free, Plus, Pro, Team, Business, Enterprise, Edu }

impl PlanType {
    fn is_plan_that_should_use_api_key(&self) -> bool {
        match self {
            PlanType::Known(KnownPlan::Free | KnownPlan::Plus | KnownPlan::Pro | KnownPlan::Team) => false,
            PlanType::Known(_) | PlanType::Unknown(_) => true,
        }
    }
}

impl TokenData {
    pub(crate) fn is_plan_that_should_use_api_key(&self) -> bool {
        self.id_token.chatgpt_plan_type.as_ref().is_none_or(|p| p.is_plan_that_should_use_api_key())
    }
}
// Env helper
fn read_openai_api_key_from_env() -> Option<String> {
    env::var(OPENAI_API_KEY_ENV_VAR).ok().filter(|s| !s.is_empty())
}
// UI display
let plan_text = info
    .get_chatgpt_plan_type()
    .map(|s| title_case(&s))
    .unwrap_or_else(|| "Unknown".to_string());
lines.push(Line::from(vec!["  • Plan: ".into(), plan_text.into()]));

if let Some(email) = &info.email {
    lines.push(Line::from(vec!["  • Login: ".into(), email.clone().into()]));
}
// Test helper: write a minimal auth.json
let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
    serde_json::to_vec(&json!({ "alg": "none", "typ": "JWT" }))?,
);
let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
    serde_json::to_vec(&json!({
        "email": "user@example.com",
        "https://api.openai.com/auth": {
            "chatgpt_plan_type": "pro",
            "user_id": "user-12345"
        }
    }))?,
);
let fake_jwt = format!("{header_b64}.{payload_b64}.{}", 
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"sig"));

DONTs

  • Bold the wrong fallback: Do not fall back to OPENAI_API_KEY when auth.json exists but is malformed.
  • Bold the plan mistake: Do not use API key for Free/Plus/Pro/Team when tokens indicate those plans.
  • Bold the token ignore: Do not ignore tokens/plan when choosing between ChatGPT and API key.
  • Bold the leakage: Do not carry openai_api_key forward in auth_dot_json when using ChatGPT mode; set it to None.
  • Bold the ambiguity: Do not treat unknown plan strings as ChatGPT-eligible; unknown plans use API key.
  • Bold the env misuse: Do not read OPENAI_API_KEY if include_env_var is false or auth.json is present.
  • Bold the ownership slip: Do not move email out of IdTokenInfo; borrow (&info.email) and clone only when needed for ownership.
  • Bold the string handling: Do not hand-roll plan strings; use PlanType/KnownPlan plus get_chatgpt_plan_type() and title_case.
// WRONG: falling back to env on malformed auth.json
match try_read_auth_json(&auth_file) {
    Err(_) if include_env_var => read_openai_api_key_from_env().map(...), // ✗ Dont do this
    _ => {}
}

// WRONG: treating unknown plan as ChatGPT
match plan_type {
    PlanType::Unknown(_) => use_chatgpt(), // ✗ Should use API key
    _ => {}
}

// WRONG: keeping API key around in ChatGPT mode
CodexAuth { api_key: Some(api_key), mode: AuthMode::ChatGPT, .. } // ✗ api_key must be None