use serde_json::{json, Value};
use crate::audit::{audit_call, CallStatus};
use crate::backend;
use crate::errors::{McpError, McpErrorKind};
use crate::session::Session;
pub fn call(session: &Session, raw: Value) -> Result<Value, McpError> {
let namespace = parse_namespace(&raw)?;
let vault = match backend::open_vault(session) {
Ok(v) => v,
Err(e) => {
audit_call(
session,
"tsafe_list_keys",
None,
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&e.message),
);
return Err(e);
}
};
let mut keys: Vec<String> = vault
.list()
.into_iter()
.filter(|k| session.is_in_scope(k))
.filter(|k| namespace_matches(namespace.as_deref(), k))
.map(|k| k.to_string())
.collect();
keys.sort();
drop(vault);
audit_call(
session,
"tsafe_list_keys",
None,
Vec::new(),
None,
None,
CallStatus::Success,
None,
);
Ok(json!(keys))
}
fn parse_namespace(raw: &Value) -> Result<Option<String>, McpError> {
let obj = match raw.as_object() {
Some(o) => o,
None if raw.is_null() => return Ok(None),
None => {
return Err(McpError::new(
McpErrorKind::InvalidParams,
"expected an object",
))
}
};
for k in obj.keys() {
if k != "namespace" {
return Err(McpError::new(
McpErrorKind::InvalidParams,
format!("unknown field '{k}'"),
));
}
}
obj.get("namespace")
.map(|v| {
v.as_str().map(str::to_string).ok_or_else(|| {
McpError::new(McpErrorKind::InvalidParams, "'namespace' must be a string")
})
})
.transpose()
}
fn namespace_matches(namespace: Option<&str>, key: &str) -> bool {
match namespace {
None => true,
Some("") => true,
Some(ns) => {
key.starts_with(&format!("{ns}/")) || key.starts_with(&format!("{ns}.")) || key == ns
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_namespace_accepts_string_and_none() {
assert!(parse_namespace(&json!({})).unwrap().is_none());
assert_eq!(
parse_namespace(&json!({"namespace": "demo"}))
.unwrap()
.as_deref(),
Some("demo")
);
}
#[test]
fn parse_namespace_rejects_unknown_field() {
let err = parse_namespace(&json!({"foo": "bar"})).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidParams);
}
#[test]
fn namespace_matches_dotted_or_slashed() {
assert!(namespace_matches(None, "anything"));
assert!(namespace_matches(Some("demo"), "demo/foo"));
assert!(namespace_matches(Some("demo"), "demo.bar"));
assert!(!namespace_matches(Some("demo"), "other/foo"));
assert!(namespace_matches(Some("demo"), "demo")); }
}