use super::error::{AgentNameError, AppError, ValidationKind};
const RESERVED_NAMES: &[&str] = &[
".well-known",
"api",
"auth",
"dids",
"stats",
"acl",
"health",
];
fn path_err(msg: impl Into<String>) -> AppError {
AppError::validation(ValidationKind::InvalidPath, msg)
}
fn validate_segment(segment: &str) -> Result<(), AppError> {
if segment.len() < 2 || segment.len() > 63 {
return Err(path_err(
"each path segment must be between 2 and 63 characters",
));
}
if !segment
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(path_err(
"path segments must contain only lowercase letters, digits, and hyphens",
));
}
let first = segment.as_bytes()[0];
let last = segment.as_bytes()[segment.len() - 1];
if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
return Err(path_err(
"each path segment must start and end with an alphanumeric character",
));
}
Ok(())
}
pub fn validate_custom_path(path: &str) -> Result<(), AppError> {
if path.is_empty() {
return Err(path_err("path must not be empty"));
}
if path.len() > 255 {
return Err(path_err("path must be at most 255 characters"));
}
if path.starts_with('/') || path.ends_with('/') {
return Err(path_err("path must not start or end with '/'"));
}
for (i, segment) in path.split('/').enumerate() {
if segment.is_empty() {
return Err(path_err(
"path must not contain empty segments (double slashes)",
));
}
validate_segment(segment)?;
if i == 0 && RESERVED_NAMES.contains(&segment) {
return Err(path_err(format!(
"'{segment}' is a reserved name and cannot be used as the first path segment",
)));
}
}
Ok(())
}
pub fn validate_mnemonic(mnemonic: &str) -> Result<(), AppError> {
if mnemonic == ".well-known" {
return Ok(());
}
validate_custom_path(mnemonic)
}
const RESERVED_AGENT_NAMES: &[&str] = &[
"abuse",
"admin",
"administrator",
"api",
"help",
"hostmaster",
"info",
"postmaster",
"root",
"security",
"support",
"sysadmin",
"webmaster",
"well-known",
];
pub const ROOT_DID_MNEMONIC: &str = ".well-known";
pub fn validate_agent_name(name: &str) -> Result<(), AppError> {
let name = name.strip_prefix('@').unwrap_or(name);
if name.is_empty() {
return Ok(());
}
validate_segment(name)?;
if RESERVED_AGENT_NAMES.contains(&name) {
return Err(AppError::AgentName(AgentNameError::Reserved));
}
Ok(())
}
pub fn validate_agent_name_binding(name: &str, mnemonic: &str) -> Result<(), AppError> {
validate_agent_name(name)?;
let bare = name.strip_prefix('@').unwrap_or(name);
if bare.is_empty() && mnemonic != ROOT_DID_MNEMONIC {
return Err(AppError::AgentName(AgentNameError::Reserved));
}
Ok(())
}