use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use async_trait::async_trait;
use awaken_server_contract::contract::config_store::{
ConfigChangeEvent, ConfigChangeKind, ConfigChangeNotifier, ConfigChangeSubscriber, ConfigStore,
};
use awaken_server_contract::contract::storage::StorageError;
use awaken_stores::InMemoryStore;
use serde_json::{Value, json};
use tokio::sync::broadcast;
#[async_trait]
trait TokenSource: Send + Sync {
async fn fresh_token(&self) -> Result<String, StorageError>;
}
struct CountingTokenSource {
counter: AtomicU64,
}
#[async_trait]
impl TokenSource for CountingTokenSource {
async fn fresh_token(&self) -> Result<String, StorageError> {
let n = self.counter.fetch_add(1, Ordering::SeqCst);
Ok(format!("sk-rotated-{n:08}"))
}
}
struct RotatingApiKeyStore<S: ConfigStore + 'static> {
inner: Arc<S>,
notifier: broadcast::Sender<ConfigChangeEvent>,
namespace: String,
target_id: String,
source: Arc<dyn TokenSource>,
}
impl<S: ConfigStore + 'static> RotatingApiKeyStore<S> {
fn new(
inner: Arc<S>,
namespace: impl Into<String>,
target_id: impl Into<String>,
source: Arc<dyn TokenSource>,
) -> Self {
let (notifier, _) = broadcast::channel(32);
Self {
inner,
notifier,
namespace: namespace.into(),
target_id: target_id.into(),
source,
}
}
async fn rotate_once(&self) -> Result<(), StorageError> {
let fresh = self.source.fresh_token().await?;
let mut current = self
.inner
.get(&self.namespace, &self.target_id)
.await?
.unwrap_or_else(|| json!({"id": &self.target_id}));
if let Some(object) = current.as_object_mut() {
object.insert("api_key".into(), Value::String(fresh));
}
self.inner
.put(&self.namespace, &self.target_id, ¤t)
.await?;
let _ = self.notifier.send(ConfigChangeEvent {
namespace: self.namespace.clone(),
id: self.target_id.clone(),
kind: ConfigChangeKind::Put,
});
Ok(())
}
fn start_rotation(self: &Arc<Self>, interval: Duration) -> tokio::task::JoinHandle<()> {
let me = Arc::clone(self);
tokio::spawn(async move {
loop {
tokio::time::sleep(interval).await;
if let Err(error) = me.rotate_once().await {
tracing::warn!(error = %error, "credential rotation failed");
}
}
})
}
}
#[async_trait]
impl<S: ConfigStore + 'static> ConfigStore for RotatingApiKeyStore<S> {
async fn get(&self, namespace: &str, id: &str) -> Result<Option<Value>, StorageError> {
self.inner.get(namespace, id).await
}
async fn list(
&self,
namespace: &str,
offset: usize,
limit: usize,
) -> Result<Vec<(String, Value)>, StorageError> {
self.inner.list(namespace, offset, limit).await
}
async fn put(&self, namespace: &str, id: &str, value: &Value) -> Result<(), StorageError> {
self.inner.put(namespace, id, value).await
}
async fn delete(&self, namespace: &str, id: &str) -> Result<(), StorageError> {
self.inner.delete(namespace, id).await
}
}
#[async_trait]
impl<S: ConfigStore + 'static> ConfigChangeNotifier for RotatingApiKeyStore<S> {
async fn subscribe(&self) -> Result<Box<dyn ConfigChangeSubscriber>, StorageError> {
Ok(Box::new(BroadcastSubscriber {
receiver: self.notifier.subscribe(),
}))
}
}
struct BroadcastSubscriber {
receiver: broadcast::Receiver<ConfigChangeEvent>,
}
#[async_trait]
impl ConfigChangeSubscriber for BroadcastSubscriber {
async fn next(&mut self) -> Result<ConfigChangeEvent, StorageError> {
self.receiver
.recv()
.await
.map_err(|error| StorageError::Io(format!("rotation broadcast: {error}")))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let inner = Arc::new(InMemoryStore::new());
inner
.put(
"providers",
"openai",
&json!({
"id": "openai",
"adapter": "openai",
"api_key": "sk-initial-12345",
}),
)
.await?;
let source = Arc::new(CountingTokenSource {
counter: AtomicU64::new(0),
});
let rotating = Arc::new(RotatingApiKeyStore::new(
Arc::clone(&inner),
"providers",
"openai",
source,
));
let mut subscriber = rotating.subscribe().await?;
rotating.rotate_once().await?;
let event = subscriber.next().await?;
println!("manual rotation emitted: {event:?}");
let handle = rotating.start_rotation(Duration::from_millis(100));
tokio::time::sleep(Duration::from_millis(250)).await;
handle.abort();
let after = rotating
.get("providers", "openai")
.await?
.expect("entry should be present after rotation");
println!("provider after several rotations: {after}");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn rotate_once_replaces_api_key_and_emits_event() {
let inner = Arc::new(InMemoryStore::new());
inner
.put(
"providers",
"openai",
&json!({
"id": "openai",
"adapter": "openai",
"api_key": "sk-initial",
"base_url": "https://example.com/v1",
}),
)
.await
.unwrap();
let rotating = Arc::new(RotatingApiKeyStore::new(
Arc::clone(&inner),
"providers",
"openai",
Arc::new(CountingTokenSource {
counter: AtomicU64::new(7),
}),
));
let mut subscriber = rotating.subscribe().await.unwrap();
rotating.rotate_once().await.unwrap();
let event = subscriber.next().await.unwrap();
assert_eq!(event.namespace, "providers");
assert_eq!(event.id, "openai");
assert_eq!(event.kind, ConfigChangeKind::Put);
let stored = rotating.get("providers", "openai").await.unwrap().unwrap();
let api_key = stored
.get("api_key")
.and_then(Value::as_str)
.expect("api_key should be present");
assert_eq!(api_key, "sk-rotated-00000007");
assert_eq!(
stored.get("base_url").and_then(Value::as_str),
Some("https://example.com/v1")
);
}
#[tokio::test]
async fn rotate_once_creates_entry_when_missing() {
let inner = Arc::new(InMemoryStore::new());
let rotating = Arc::new(RotatingApiKeyStore::new(
Arc::clone(&inner),
"providers",
"fresh",
Arc::new(CountingTokenSource {
counter: AtomicU64::new(0),
}),
));
rotating.rotate_once().await.unwrap();
let stored = rotating.get("providers", "fresh").await.unwrap().unwrap();
assert_eq!(
stored.get("api_key").and_then(Value::as_str),
Some("sk-rotated-00000000")
);
}
}