pub mod storage;
pub mod types;
use std::sync::Arc;
use async_stream::stream;
use futures::stream::Stream;
use serde::{Deserialize, Serialize};
use storage::{StorageError, YamlStorage};
use types::{Secret, SecretPath};
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuthEvent {
Secret {
path: String,
value: String,
created_at: Option<String>,
updated_at: Option<String>,
},
SecretInfo {
path: String,
created_at: Option<String>,
updated_at: Option<String>,
},
Success { message: String },
Error { message: String },
}
#[derive(Clone)]
pub struct AuthHub {
storage: Arc<YamlStorage>,
}
impl AuthHub {
pub async fn new() -> Result<Self, StorageError> {
let storage = YamlStorage::default_location()?;
storage.load().await?;
Ok(Self {
storage: Arc::new(storage),
})
}
pub async fn with_storage(storage: YamlStorage) -> Result<Self, StorageError> {
storage.load().await?;
Ok(Self {
storage: Arc::new(storage),
})
}
}
#[plexus_macros::hub_methods(
namespace = "auth",
version = "1.0.0",
description = "Simple secret management with YAML storage",
crate_path = "plexus_core"
)]
impl AuthHub {
#[plexus_macros::hub_method(
description = "Get a secret by path",
params(
secret_key = "Secret key (e.g., 'github/alice/token')"
)
)]
pub async fn get_secret(
&self,
secret_key: String,
) -> impl Stream<Item = AuthEvent> + Send + 'static {
let storage = self.storage.clone();
stream! {
let secret_path = SecretPath::new(secret_key);
match storage.get(&secret_path) {
Ok(secret) => {
yield AuthEvent::Secret {
path: secret.path.to_string(),
value: secret.value,
created_at: secret.created_at.map(|d| d.to_rfc3339()),
updated_at: secret.updated_at.map(|d| d.to_rfc3339()),
};
}
Err(e) => {
yield AuthEvent::Error {
message: format!("Failed to get secret: {}", e),
};
}
}
}
}
#[plexus_macros::hub_method(
description = "Set a secret value",
params(
secret_key = "Secret key (e.g., 'github/alice/token')",
value = "Secret value"
)
)]
pub async fn set_secret(
&self,
secret_key: String,
value: String,
) -> impl Stream<Item = AuthEvent> + Send + 'static {
let storage = self.storage.clone();
stream! {
let secret = Secret::new(secret_key.clone(), value);
match storage.set(secret).await {
Ok(_) => {
yield AuthEvent::Success {
message: format!("Secret set: {}", secret_key),
};
}
Err(e) => {
yield AuthEvent::Error {
message: format!("Failed to set secret: {}", e),
};
}
}
}
}
#[plexus_macros::hub_method(
description = "List secrets matching a prefix",
params(
prefix = "Prefix to filter by (empty string for all secrets)"
)
)]
pub async fn list_secrets(
&self,
prefix: String,
) -> impl Stream<Item = AuthEvent> + Send + 'static {
let storage = self.storage.clone();
stream! {
match storage.list(&prefix) {
Ok(secrets) => {
for info in secrets {
yield AuthEvent::SecretInfo {
path: info.path.to_string(),
created_at: info.created_at.map(|d| d.to_rfc3339()),
updated_at: info.updated_at.map(|d| d.to_rfc3339()),
};
}
}
Err(e) => {
yield AuthEvent::Error {
message: format!("Failed to list secrets: {}", e),
};
}
}
}
}
#[plexus_macros::hub_method(
description = "Delete a secret",
params(
secret_key = "Secret key to delete"
)
)]
pub async fn delete_secret(
&self,
secret_key: String,
) -> impl Stream<Item = AuthEvent> + Send + 'static {
let storage = self.storage.clone();
stream! {
let secret_path = SecretPath::new(secret_key.clone());
match storage.delete(&secret_path).await {
Ok(_) => {
yield AuthEvent::Success {
message: format!("Secret deleted: {}", secret_key),
};
}
Err(e) => {
yield AuthEvent::Error {
message: format!("Failed to delete secret: {}", e),
};
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use futures::StreamExt;
async fn create_test_hub() -> (AuthHub, TempDir) {
let temp = TempDir::new().unwrap();
let storage = YamlStorage::new(temp.path().join("secrets.yaml"));
let hub = AuthHub::with_storage(storage).await.unwrap();
(hub, temp)
}
}