Land release blocker fixes and workspace cleanup
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

This commit is contained in:
2026-04-23 00:16:09 +01:00
parent d7363cc913
commit 9b0067b4c0
36 changed files with 1529 additions and 418 deletions

View File

@@ -1,29 +1,77 @@
/// Store an API key in the OS keychain.
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
/// Store an API key in Kon's process-local keystore.
///
/// Stub implementation using environment variables until the `keyring` crate is
/// added. Keys are only held in-process and lost on exit.
/// Keys are held in memory for the lifetime of the process and are lost on
/// exit. This avoids the undefined behaviour of mutating process environment
/// variables from arbitrary threads while keeping the public API safe.
///
/// # Safety note
/// `std::env::set_var` is deprecated in Rust 2024 edition and is **not**
/// thread-safe — mutating the environment while other threads read it is
/// undefined behaviour. This is acceptable during single-threaded app init
/// but must not be called from async/multi-threaded contexts.
/// `retrieve_api_key` still falls back to `KON_API_KEY_<PROVIDER>` environment
/// variables so externally injected secrets continue to work.
///
/// TODO: Replace with the `keyring` crate (or platform-native credential
/// storage) so keys persist across sessions and are accessed safely.
#[allow(deprecated)] // set_var deprecated in Rust 2024 edition
pub fn store_api_key(provider: &str, key: &str) {
// SAFETY: Only safe when called from a single-threaded context (e.g. app
// initialisation). See doc comment above.
std::env::set_var(format!("KON_API_KEY_{}", provider.to_uppercase()), key);
api_key_store()
.lock()
.unwrap()
.insert(provider_env_key(provider), key.to_string());
}
/// Retrieve an API key from the OS keychain.
/// Retrieve an API key from Kon's process-local keystore.
///
/// Stub implementation using environment variables until the `keyring` crate is
/// added. Returns `None` if no key has been stored this session.
///
/// TODO: Replace with the `keyring` crate alongside `store_api_key`.
/// Returns a previously stored in-memory key when present, otherwise falls
/// back to the read-only `KON_API_KEY_<PROVIDER>` environment variable so
/// operator-supplied secrets still work.
pub fn retrieve_api_key(provider: &str) -> Option<String> {
std::env::var(format!("KON_API_KEY_{}", provider.to_uppercase())).ok()
let env_key = provider_env_key(provider);
api_key_store()
.lock()
.unwrap()
.get(&env_key)
.cloned()
.or_else(|| std::env::var(env_key).ok())
}
fn api_key_store() -> &'static Mutex<HashMap<String, String>> {
static STORE: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
STORE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn provider_env_key(provider: &str) -> String {
format!("KON_API_KEY_{}", provider.to_uppercase())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn unique_provider(prefix: &str) -> String {
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
format!("{prefix}_{}", NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
#[test]
fn stored_key_is_retrievable_without_env_mutation() {
let provider = unique_provider("provider");
store_api_key(&provider, "secret-token");
assert_eq!(
retrieve_api_key(&provider),
Some("secret-token".to_string())
);
}
#[test]
fn providers_do_not_overlap() {
let first = unique_provider("first");
let second = unique_provider("second");
store_api_key(&first, "alpha");
store_api_key(&second, "beta");
assert_eq!(retrieve_api_key(&first), Some("alpha".to_string()));
assert_eq!(retrieve_api_key(&second), Some("beta".to_string()));
}
}