Skip to content

Commit 49f3383

Browse files
committed
bump
1 parent c9d1090 commit 49f3383

1 file changed

Lines changed: 147 additions & 31 deletions

File tree

src/auth.rs

Lines changed: 147 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,12 @@ fn unwarned_stale_ai_provider_secrets(
667667
.collect()
668668
}
669669

670+
fn ai_provider_key_staleness_warning_message(secret_name: &str) -> String {
671+
format!(
672+
"We recommend disabling and rotating AI provider secrets periodically. {secret_name} has not been rotated in over 6 months."
673+
)
674+
}
675+
670676
async fn maybe_warn_ai_provider_key_staleness(base: &BaseArgs, ctx: &LoginContext) {
671677
if base.json
672678
|| ui::is_quiet()
@@ -722,10 +728,7 @@ async fn warn_ai_provider_key_staleness(ctx: &LoginContext) -> Result<()> {
722728
for secret in &to_warn {
723729
ui::print_command_status(
724730
ui::CommandStatus::Warning,
725-
&format!(
726-
"We recommend disabling and rotating AI provider secrets periodically. This {} has not been rotated in over 6 months.",
727-
secret.name
728-
),
731+
&ai_provider_key_staleness_warning_message(&secret.name),
729732
);
730733
}
731734
state
@@ -3233,36 +3236,56 @@ mod tests {
32333236
.with_timezone(&Utc)
32343237
}
32353238

3239+
fn ai_provider_secret(
3240+
id: Option<&str>,
3241+
name: &str,
3242+
secret_type: Option<&str>,
3243+
preview_secret: Option<&str>,
3244+
secret_updated_at: Option<&str>,
3245+
updated_at: Option<&str>,
3246+
created: Option<&str>,
3247+
) -> AiProviderSecret {
3248+
AiProviderSecret {
3249+
id: id.map(str::to_string),
3250+
name: name.to_string(),
3251+
r#type: secret_type.map(str::to_string),
3252+
preview_secret: preview_secret.map(str::to_string),
3253+
secret_updated_at: secret_updated_at.map(str::to_string),
3254+
updated_at: updated_at.map(str::to_string),
3255+
created: created.map(str::to_string),
3256+
}
3257+
}
3258+
32363259
#[test]
32373260
fn stale_ai_provider_secrets_returns_configured_keys_older_than_six_months() {
32383261
let secrets = vec![
3239-
AiProviderSecret {
3240-
id: Some("openai-secret".to_string()),
3241-
name: "OPENAI_API_KEY".to_string(),
3242-
r#type: Some("openai".to_string()),
3243-
preview_secret: Some("********".to_string()),
3244-
secret_updated_at: Some("2025-11-12T00:00:00Z".to_string()),
3245-
updated_at: None,
3246-
created: None,
3247-
},
3248-
AiProviderSecret {
3249-
id: Some("anthropic-secret".to_string()),
3250-
name: "ANTHROPIC_API_KEY".to_string(),
3251-
r#type: Some("anthropic".to_string()),
3252-
preview_secret: Some("********".to_string()),
3253-
secret_updated_at: Some("2025-11-14T00:00:00Z".to_string()),
3254-
updated_at: None,
3255-
created: None,
3256-
},
3257-
AiProviderSecret {
3258-
id: Some("gemini-secret".to_string()),
3259-
name: "GEMINI_API_KEY".to_string(),
3260-
r#type: Some("google".to_string()),
3261-
preview_secret: None,
3262-
secret_updated_at: Some("2025-01-01T00:00:00Z".to_string()),
3263-
updated_at: None,
3264-
created: None,
3265-
},
3262+
ai_provider_secret(
3263+
Some("openai-secret"),
3264+
"OPENAI_API_KEY",
3265+
Some("openai"),
3266+
Some("********"),
3267+
Some("2025-11-12T00:00:00Z"),
3268+
None,
3269+
None,
3270+
),
3271+
ai_provider_secret(
3272+
Some("anthropic-secret"),
3273+
"ANTHROPIC_API_KEY",
3274+
Some("anthropic"),
3275+
Some("********"),
3276+
Some("2025-11-14T00:00:00Z"),
3277+
None,
3278+
None,
3279+
),
3280+
ai_provider_secret(
3281+
Some("gemini-secret"),
3282+
"GEMINI_API_KEY",
3283+
Some("google"),
3284+
None,
3285+
Some("2025-01-01T00:00:00Z"),
3286+
None,
3287+
None,
3288+
),
32663289
];
32673290

32683291
let stale = stale_ai_provider_secrets("org-id", &secrets, dt("2026-05-13T00:00:00Z"));
@@ -3276,6 +3299,66 @@ mod tests {
32763299
);
32773300
}
32783301

3302+
#[test]
3303+
fn stale_ai_provider_secrets_uses_timestamp_fallbacks_and_ignores_invalid_dates() {
3304+
let secrets = vec![
3305+
ai_provider_secret(
3306+
None,
3307+
"OPENAI_API_KEY",
3308+
Some("openai"),
3309+
Some("********"),
3310+
None,
3311+
Some("2025-11-12T00:00:00Z"),
3312+
None,
3313+
),
3314+
ai_provider_secret(
3315+
None,
3316+
"ANTHROPIC_API_KEY",
3317+
Some("anthropic"),
3318+
Some("********"),
3319+
None,
3320+
Some("not-a-date"),
3321+
Some("2025-01-01T00:00:00Z"),
3322+
),
3323+
ai_provider_secret(
3324+
None,
3325+
"GEMINI_API_KEY",
3326+
Some("google"),
3327+
Some("********"),
3328+
None,
3329+
None,
3330+
None,
3331+
),
3332+
];
3333+
3334+
let stale = stale_ai_provider_secrets("org-id", &secrets, dt("2026-05-13T00:00:00Z"));
3335+
3336+
assert_eq!(
3337+
stale,
3338+
vec![StaleAiProviderSecret {
3339+
name: "OPENAI_API_KEY".to_string(),
3340+
warning_key: "org-id:openai:2025-11-12T00:00:00Z".to_string(),
3341+
}]
3342+
);
3343+
}
3344+
3345+
#[test]
3346+
fn stale_ai_provider_secrets_does_not_treat_exact_six_month_cutoff_as_stale() {
3347+
let secrets = vec![ai_provider_secret(
3348+
Some("openai-secret"),
3349+
"OPENAI_API_KEY",
3350+
Some("openai"),
3351+
Some("********"),
3352+
Some("2025-11-13T00:00:00Z"),
3353+
None,
3354+
None,
3355+
)];
3356+
3357+
let stale = stale_ai_provider_secrets("org-id", &secrets, dt("2026-05-13T00:00:00Z"));
3358+
3359+
assert!(stale.is_empty());
3360+
}
3361+
32793362
#[test]
32803363
fn unwarned_stale_ai_provider_secrets_skips_previously_warned_key_versions() {
32813364
let already_warned = StaleAiProviderSecret {
@@ -3296,6 +3379,39 @@ mod tests {
32963379
assert_eq!(unwarned, vec![newly_stale]);
32973380
}
32983381

3382+
#[test]
3383+
fn ai_provider_key_staleness_warning_message_includes_key_name() {
3384+
assert_eq!(
3385+
ai_provider_key_staleness_warning_message("OPENAI_API_KEY"),
3386+
"We recommend disabling and rotating AI provider secrets periodically. OPENAI_API_KEY has not been rotated in over 6 months."
3387+
);
3388+
}
3389+
3390+
#[tokio::test]
3391+
async fn ai_provider_warning_state_round_trips_through_global_config_dir() {
3392+
let _guard = env_test_lock().lock().await;
3393+
let previous_xdg_config_home = env::var_os("XDG_CONFIG_HOME");
3394+
let previous_appdata = env::var_os("APPDATA");
3395+
let config_dir = TempDir::new().expect("create temp config dir");
3396+
env::set_var("XDG_CONFIG_HOME", config_dir.path());
3397+
env::set_var("APPDATA", config_dir.path());
3398+
3399+
let state = AiProviderKeyStalenessWarningState {
3400+
warned: BTreeSet::from([
3401+
"org-id:openai-secret:2025-11-12T00:00:00Z".to_string(),
3402+
"org-id:anthropic-secret:2025-11-10T00:00:00Z".to_string(),
3403+
]),
3404+
};
3405+
3406+
save_ai_provider_warning_state(&state).expect("save warning state");
3407+
let loaded = load_ai_provider_warning_state();
3408+
3409+
restore_env_var("XDG_CONFIG_HOME", previous_xdg_config_home);
3410+
restore_env_var("APPDATA", previous_appdata);
3411+
3412+
assert_eq!(loaded.warned, state.warned);
3413+
}
3414+
32993415
fn env_test_lock() -> &'static Mutex<()> {
33003416
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
33013417
LOCK.get_or_init(|| Mutex::new(()))

0 commit comments

Comments
 (0)