Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 215 additions & 53 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ url = "2"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yml = "0.0.12"
serde-saphyr = { version = "2", default-features = false, features = ["serialize", "deserialize"] }
serde_yaml = "0.9"
toml = "1.1"
regex = "1"
Expand Down Expand Up @@ -81,7 +81,7 @@ chacha20poly1305 = "0.10"
argon2 = "0.5"

# JWS signing for the headless RFC 7523 jwt-bearer client (RS256 / ES256)
jsonwebtoken = "9"
jsonwebtoken = { version = "10.4", features = ["rust_crypto"] }

# Async utilities
futures = "0.3"
Expand Down
10 changes: 6 additions & 4 deletions src/analysis/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub fn get_git_branch(repo_path: &Path) -> Option<String> {
///
/// When this returns `true`, the caller should skip persisting the cache entry.
fn is_analysis_cache_oversized(cached: &CachedAnalysis) -> bool {
let size_result = serde_yml::to_string(cached)
let size_result = crate::yaml::to_string(cached)
.map(|s| s.len())
.map_err(|e| e.to_string());
is_analysis_cache_oversized_inner(size_result)
Expand Down Expand Up @@ -1031,7 +1031,7 @@ cached_analysis:
analyzed_at: "2024-01-01T00:00:00Z"
"#;

let cfg: config::Config = serde_yml::from_str(yaml).expect("should parse old cache YAML");
let cfg: config::Config = crate::yaml::from_str(yaml).expect("should parse old cache YAML");
let cached = cfg.cached_analysis.expect("should have cached_analysis");
assert_eq!(cached.repo_path, "/home/user/project");
assert_eq!(cached.head_commit.as_deref(), Some("abc123def456"));
Expand Down Expand Up @@ -1141,7 +1141,7 @@ cached_analysis:
};

// Sanity-check the fixture.
let size = serde_yml::to_string(&big_cached)
let size = crate::yaml::to_string(&big_cached)
.map(|s| s.len())
.unwrap_or(0);
assert!(size > CACHE_MAX_SIZE_BYTES, "fixture must exceed limit");
Expand Down Expand Up @@ -1211,7 +1211,9 @@ cached_analysis:
analysis: big_analysis,
analyzed_at: chrono::Utc::now(),
};
let size = serde_yml::to_string(&cached).map(|s| s.len()).unwrap_or(0);
let size = crate::yaml::to_string(&cached)
.map(|s| s.len())
.unwrap_or(0);
assert!(
size > CACHE_MAX_SIZE_BYTES,
"test fixture must exceed the limit ({size} bytes vs {CACHE_MAX_SIZE_BYTES} bytes)"
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/static_analyzer/monorepo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ fn detect_pnpm(root: &Path) -> Result<Option<MonorepoInfo>, std::io::Error> {
packages: Option<Vec<String>>,
}

let workspace: PnpmWorkspace = serde_yml::from_str(&content).map_err(|e| {
let workspace: PnpmWorkspace = crate::yaml::from_repo_str(&content).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid pnpm-workspace.yaml: {e}"),
Expand Down
20 changes: 7 additions & 13 deletions src/auth/jwt_bearer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,8 @@ mod tests {

/// Decode an assertion's claims WITHOUT checking the signature or the time
/// claims — for tests that only inspect `jti`/`exp`/`iat` values.
fn decode_claims_only(assertion: &str, key: &DecodingKey, alg: Algorithm) -> AssertionClaims {
let mut validation = Validation::new(alg);
validation.insecure_disable_signature_validation();
validation.validate_exp = false;
validation.validate_aud = false;
decode::<AssertionClaims>(assertion, key, &validation)
fn decode_claims_only(assertion: &str) -> AssertionClaims {
jsonwebtoken::dangerous::insecure_decode::<AssertionClaims>(assertion)
.expect("decode claims")
.claims
}
Expand Down Expand Up @@ -618,17 +614,16 @@ mod tests {

#[test]
fn each_assertion_gets_a_fresh_unique_jti() {
let (priv_pem, pub_pem) = ec_keypair();
let (priv_pem, _pub) = ec_keypair();
let a =
build_and_sign_assertion(&params(AssertionAlgorithm::Es256), &priv_pem, 1_700_000_000)
.unwrap();
let b =
build_and_sign_assertion(&params(AssertionAlgorithm::Es256), &priv_pem, 1_700_000_000)
.unwrap();

let key = DecodingKey::from_ec_pem(&pub_pem).unwrap();
let ja = decode_claims_only(&a, &key, Algorithm::ES256).jti;
let jb = decode_claims_only(&b, &key, Algorithm::ES256).jti;
let ja = decode_claims_only(&a).jti;
let jb = decode_claims_only(&b).jti;
assert_ne!(
ja, jb,
"each assertion must carry a unique jti (anti-replay)"
Expand All @@ -637,14 +632,13 @@ mod tests {

#[test]
fn lifetime_is_clamped_to_the_server_cap() {
let (priv_pem, pub_pem) = ec_keypair();
let (priv_pem, _pub) = ec_keypair();
let mut p = params(AssertionAlgorithm::Es256);
p.lifetime_seconds = 10_000; // absurd; must clamp to 300
let now = 1_700_000_000;
let assertion = build_and_sign_assertion(&p, &priv_pem, now).unwrap();

let key = DecodingKey::from_ec_pem(&pub_pem).unwrap();
let claims = decode_claims_only(&assertion, &key, Algorithm::ES256);
let claims = decode_claims_only(&assertion);
assert_eq!(claims.exp - claims.iat, MAX_ASSERTION_LIFETIME_SECONDS);
}

Expand Down
25 changes: 23 additions & 2 deletions src/auth/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ pub fn load_from(path: &Path) -> Result<Option<StoredCredentials>, ActualError>
let contents = String::from_utf8(bytes).map_err(|e| {
credentials_error(format!("Credentials file is not valid UTF-8: {e}"))
})?;
let creds = serde_yml::from_str(&contents)
let creds = crate::yaml::from_str(&contents)
.map_err(|e| credentials_error(format!("Failed to parse credentials: {e}")))?;
Ok(Some(creds))
}
Expand All @@ -157,7 +157,7 @@ pub fn save_to(creds: &StoredCredentials, path: &Path) -> Result<(), ActualError
std::fs::create_dir_all(parent)
.map_err(|e| credentials_error(format!("Failed to create config directory: {e}")))?;
}
let yaml = serde_yml::to_string(creds)
let yaml = crate::yaml::to_string(creds)
.map_err(|e| credentials_error(format!("Failed to serialize credentials: {e}")))?;
write_secure(path, &yaml)
}
Expand Down Expand Up @@ -491,4 +491,25 @@ mod tests {
let err = save_to(&sample(), Path::new("/")).unwrap_err();
assert!(err.to_string().contains("Failed to"), "got: {err}");
}

/// A credentials file serde_yml 0.0.12 wrote reads back unchanged.
#[test]
fn test_reads_credentials_written_by_serde_yml() {
let dir = tempdir().unwrap();
let path = dir.path().join("credentials.yaml");
std::fs::write(
&path,
include_str!("../../tests/fixtures/serde_yml/credentials.yaml"),
)
.unwrap();
let expected: serde_json::Value = serde_json::from_str(include_str!(
"../../tests/fixtures/serde_yml/credentials.json"
))
.unwrap();
let creds = load_from(&path).unwrap().expect("credentials present");
assert_eq!(serde_json::to_value(&creds).unwrap(), expected);

save_to(&creds, &path).unwrap();
assert_eq!(load_from(&path).unwrap(), Some(creds));
}
}
26 changes: 24 additions & 2 deletions src/cli/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn run_with_resolver(
}

fn serialize_config_for_display(cfg: &config::Config) -> Result<String, ActualError> {
serde_yml::to_string(cfg)
crate::yaml::to_string(cfg)
.map_err(|e| ActualError::ConfigError(format!("Failed to serialize config to YAML: {e}")))
}

Expand Down Expand Up @@ -561,7 +561,7 @@ mod tests {
#[test]
fn test_show_after_setting_api_key_redacts_in_yaml() {
// Verify that the redact_yaml function correctly handles the YAML that
// serde_yml would produce for a set API key.
// the config serializer would produce for a set API key.
let yaml_with_key = "anthropic_api_key: sk-ant-actual-key\nmodel: ~\n";
let redacted = redact_yaml(yaml_with_key);
assert!(
Expand Down Expand Up @@ -707,4 +707,26 @@ mod tests {
"show should succeed when no API keys are set"
);
}

/// `config show` redacts line by line, so it depends on the serializer
/// keeping each key's value on the key's own line, long values included.
#[test]
fn test_show_redacts_long_and_quoted_api_keys() {
let long = format!("sk-ant-api03-{}", "x".repeat(300));
let quoted = "secret: with #hash, 'single' and \"double\" quotes ".repeat(6);
let cfg = config::Config {
anthropic_api_key: Some(long.clone()),
openai_api_key: Some(quoted.clone()),
cursor_api_key: Some("cursor-secret".to_string()),
..config::Config::default()
};
let shown = redact_yaml(&serialize_config_for_display(&cfg).unwrap());
for fragment in ["xxxxxxxxxx", "#hash", "'single'", "double", "cursor-secret"] {
assert!(
!shown.contains(fragment),
"{fragment} leaked into:\n{shown}"
);
}
assert_eq!(shown.matches("[redacted]").count(), 3, "got:\n{shown}");
}
}
2 changes: 1 addition & 1 deletion src/cli/commands/sync/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub(crate) fn store_tailoring_cache(
root_dir: &Path,
output: &TailoringOutput,
) {
let size_result = serde_yml::to_string(output)
let size_result = crate::yaml::to_string(output)
.map(|s| s.len())
.map_err(|e| e.to_string());
store_tailoring_cache_inner(config, cfg_path, cache_key, root_dir, output, size_result);
Expand Down
6 changes: 3 additions & 3 deletions src/cli/commands/sync/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4538,8 +4538,8 @@ mod tests {
};

// Serialize to YAML and back
let yaml = serde_yml::to_string(&cached).unwrap();
let restored: CachedTailoring = serde_yml::from_str(&yaml).unwrap();
let yaml = crate::yaml::to_string(&cached).unwrap();
let restored: CachedTailoring = crate::yaml::from_str(&yaml).unwrap();
assert_eq!(cached.cache_key, restored.cache_key);
assert_eq!(cached.tailoring, restored.tailoring);
}
Expand All @@ -4550,7 +4550,7 @@ mod tests {
// miss) rather than failing to parse the entire Config.
use crate::config::Config;
let yaml = "cached_tailoring:\n garbage_field: some_value\n";
let cfg: Config = serde_yml::from_str(yaml).expect("config must parse despite bad cache");
let cfg: Config = crate::yaml::from_str(yaml).expect("config must parse despite bad cache");
assert!(
cfg.cached_tailoring.is_none(),
"corrupt cached_tailoring should be treated as absent"
Expand Down
4 changes: 2 additions & 2 deletions src/config/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub fn load_from(path: &Path) -> Result<Config, ActualError> {
}
let contents = String::from_utf8(bytes)
.map_err(|e| config_error(format!("Config file is not valid UTF-8: {e}")))?;
serde_yml::from_str(&contents)
crate::yaml::from_str(&contents)
.map_err(|e| config_error(format!("Failed to parse config YAML: {e}")))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Expand Down Expand Up @@ -130,7 +130,7 @@ pub fn save_to(config: &Config, path: &Path) -> Result<(), ActualError> {
.map_err(|e| config_error(format!("Failed to create config directory: {e}")))?;
}

let yaml = serde_yml::to_string(config)
let yaml = crate::yaml::to_string(config)
.map_err(|e| config_error(format!("Failed to serialize config to YAML: {e}")))?;
write_config_secure(path, &yaml)?;

Expand Down
Loading
Loading