use std::collections::BTreeMap;
use serde_json::Value;
use crate::error::AppError;
pub(super) async fn resolve_webvh_server(
template_vars: &BTreeMap<String, Value>,
webvh_ks: &crate::store::KeyspaceHandle,
) -> Result<Option<String>, AppError> {
let raw = match template_vars.get("WEBVH_SERVER") {
None | Some(Value::Null) => return Ok(None),
Some(Value::String(s)) => s,
Some(other) => {
let actual = match other {
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::Array(_) => "array",
Value::Object(_) => "object",
_ => "non-string",
};
return Err(AppError::Validation(format!(
"WEBVH_SERVER must be a string (registered webvh-server id), got {actual}"
)));
}
};
let id = raw.trim();
if id.is_empty() {
return Ok(None);
}
if crate::webvh_store::get_server(webvh_ks, id)
.await?
.is_none()
{
return Err(AppError::NotFound(format!(
"WEBVH_SERVER '{id}' is not a registered webvh hosting server on this VTA \
— register it via `vta webvh add-server` first, or omit `WEBVH_SERVER` \
to self-host at the URL"
)));
}
Ok(Some(id.to_string()))
}
pub(super) fn take_webvh_path(
template_vars: &mut BTreeMap<String, Value>,
) -> Result<Option<String>, AppError> {
let removed = match template_vars.remove("WEBVH_PATH") {
None | Some(Value::Null) => return Ok(None),
Some(v) => v,
};
let s = match removed {
Value::String(s) => s,
_ => {
return Err(AppError::Validation(
"WEBVH_PATH must be a non-empty string".into(),
));
}
};
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(AppError::Validation(
"WEBVH_PATH must be a non-empty string".into(),
));
}
Ok(Some(trimmed.to_string()))
}
pub(super) fn reject_root_on_shared_server(
path_mode: &vta_sdk::protocols::did_management::create::WebvhPathMode,
server_managed: bool,
) -> Result<(), AppError> {
use vta_sdk::protocols::did_management::create::WebvhPathMode;
if server_managed && matches!(path_mode, WebvhPathMode::WellKnown) {
return Err(AppError::Validation(
"WEBVH_PATH '.well-known' (root DID) is not available on a shared webvh \
hosting server — a domain's root slot can belong to only one tenant. Omit \
WEBVH_PATH for a server-assigned path, or set an explicit label. Root DIDs \
are only available in serverless mode (omit WEBVH_SERVER and self-host at \
the URL)."
.into(),
));
}
Ok(())
}
pub(super) fn take_webvh_domain(
template_vars: &mut BTreeMap<String, Value>,
) -> Result<Option<String>, AppError> {
let removed = match template_vars.remove("WEBVH_DOMAIN") {
None | Some(Value::Null) => return Ok(None),
Some(v) => v,
};
let s = match removed {
Value::String(s) => s,
_ => {
return Err(AppError::Validation(
"WEBVH_DOMAIN must be a non-empty string".into(),
));
}
};
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(AppError::Validation(
"WEBVH_DOMAIN must be a non-empty string".into(),
));
}
Ok(Some(trimmed.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn vars(pairs: &[(&str, Value)]) -> BTreeMap<String, Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn take_webvh_domain_absent_is_none() {
let mut v = vars(&[]);
assert_eq!(take_webvh_domain(&mut v).unwrap(), None);
}
#[test]
fn take_webvh_domain_null_is_none() {
let mut v = vars(&[("WEBVH_DOMAIN", Value::Null)]);
assert_eq!(take_webvh_domain(&mut v).unwrap(), None);
}
#[test]
fn take_webvh_domain_trims_and_returns() {
let mut v = vars(&[("WEBVH_DOMAIN", json!(" acme.example.com "))]);
assert_eq!(
take_webvh_domain(&mut v).unwrap(),
Some("acme.example.com".to_string())
);
}
#[test]
fn take_webvh_domain_removes_from_map() {
let mut v = vars(&[("WEBVH_DOMAIN", json!("acme.example.com"))]);
let _ = take_webvh_domain(&mut v).unwrap();
assert!(!v.contains_key("WEBVH_DOMAIN"));
}
#[test]
fn take_webvh_domain_empty_string_errors() {
let mut v = vars(&[("WEBVH_DOMAIN", json!(" "))]);
assert!(matches!(
take_webvh_domain(&mut v),
Err(AppError::Validation(_))
));
}
#[test]
fn take_webvh_domain_non_string_errors() {
let mut v = vars(&[("WEBVH_DOMAIN", json!(42))]);
assert!(matches!(
take_webvh_domain(&mut v),
Err(AppError::Validation(_))
));
}
mod reject_root {
use super::super::reject_root_on_shared_server;
use crate::error::AppError;
use vta_sdk::protocols::did_management::create::WebvhPathMode;
#[test]
fn root_on_shared_server_is_rejected() {
let err = reject_root_on_shared_server(&WebvhPathMode::WellKnown, true)
.expect_err("root on a shared server must be rejected");
assert!(
matches!(err, AppError::Validation(_)),
"must be a 400 validation error, got: {err:?}"
);
assert!(
err.to_string().contains(".well-known"),
"error should name the offending value: {err}"
);
}
#[test]
fn root_in_serverless_mode_is_allowed() {
assert!(reject_root_on_shared_server(&WebvhPathMode::WellKnown, false).is_ok());
}
#[test]
fn explicit_path_on_shared_server_is_allowed() {
assert!(
reject_root_on_shared_server(&WebvhPathMode::Explicit("acme".into()), true).is_ok()
);
}
#[test]
fn auto_assign_on_shared_server_is_allowed() {
assert!(reject_root_on_shared_server(&WebvhPathMode::AutoAssign, true).is_ok());
}
}
}