codex-switch 0.1.13

Local CLI account switcher for Codex
const MAX_REDACTED_ERROR_BODY_CHARS: usize = 2048;

pub(crate) fn redact_known_secrets(value: String, secrets: &[&str]) -> String {
    let mut redacted = value;
    for secret in secrets.iter().copied().filter(|secret| !secret.is_empty()) {
        redacted = redacted.replace(secret, "<redacted>");

        let encoded = urlencoding::encode(secret);
        if encoded != secret {
            redacted = redacted.replace(encoded.as_ref(), "<redacted>");
        }
    }

    truncate_error_body(redacted)
}

fn truncate_error_body(value: String) -> String {
    let mut chars = value.chars();
    let truncated = chars
        .by_ref()
        .take(MAX_REDACTED_ERROR_BODY_CHARS)
        .collect::<String>();

    if chars.next().is_some() {
        format!("{truncated}...")
    } else {
        value
    }
}

#[cfg(test)]
mod tests {
    use super::{MAX_REDACTED_ERROR_BODY_CHARS, redact_known_secrets};

    #[test]
    fn redacts_raw_and_url_encoded_secret_values() {
        let secret = "secret value/with-symbols";
        let body = format!("raw={secret} encoded={}", urlencoding::encode(secret));

        let redacted = redact_known_secrets(body, &[secret]);

        assert!(!redacted.contains(secret));
        assert!(!redacted.contains(urlencoding::encode(secret).as_ref()));
        assert_eq!(redacted, "raw=<redacted> encoded=<redacted>");
    }

    #[test]
    fn truncates_large_error_bodies_after_redaction() {
        let redacted = redact_known_secrets("x".repeat(MAX_REDACTED_ERROR_BODY_CHARS + 10), &[]);

        assert_eq!(redacted.chars().count(), MAX_REDACTED_ERROR_BODY_CHARS + 3);
        assert!(redacted.ends_with("..."));
    }
}