use serde::{Deserialize, Serialize};
use serde_json::Value;
use vti_common::error::AppError;
use crate::config::AppConfig;
use crate::config_store::ConfigStore;
pub const DIDS_WEBVH_SERVER: &str = "dids.webvh_server";
pub const LOCAL: &str = "local";
pub const ENV: &str = "VTC_DIDS_WEBVH_SERVER";
const MAX_LEN: usize = 128;
pub const DIDS_DEFAULT_DID_METHOD: &str = "dids.default_did_method";
pub const DIDS_SCID_ALSO_KNOWN_AS: &str = "dids.scid_also_known_as";
pub const ENV_DID_METHOD: &str = "VTC_DIDS_DEFAULT_DID_METHOD";
pub const ENV_SCID_ALSO_KNOWN_AS: &str = "VTC_DIDS_SCID_ALSO_KNOWN_AS";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct DidsConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub webvh_server: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_did_method: Option<vta_sdk::did_scid::DidMethod>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scid_also_known_as: Option<bool>,
}
impl DidsConfig {
pub fn is_empty(&self) -> bool {
self.webvh_server.is_none()
&& self.default_did_method.is_none()
&& self.scid_also_known_as.is_none()
}
}
pub fn validate_did_method(value: &Value) -> Result<vta_sdk::did_scid::DidMethod, AppError> {
value
.as_str()
.ok_or_else(|| "not a string".to_string())
.and_then(|s| s.parse())
.map_err(|e| AppError::Validation(format!("{DIDS_DEFAULT_DID_METHOD}: {e}")))
}
pub async fn live_presentation(
cfg: &AppConfig,
db: &ConfigStore,
) -> Result<(Option<vta_sdk::did_scid::DidMethod>, Option<bool>), AppError> {
let method = match std::env::var(ENV_DID_METHOD).ok() {
Some(v) => Some(Value::String(v)),
None => db.get(DIDS_DEFAULT_DID_METHOD).await?,
}
.and_then(|v| validate_did_method(&v).ok())
.or(cfg.dids.default_did_method);
let alias = match std::env::var(ENV_SCID_ALSO_KNOWN_AS).ok() {
Some(v) => v.parse::<bool>().ok(),
None => db
.get(DIDS_SCID_ALSO_KNOWN_AS)
.await?
.and_then(|v| v.as_bool()),
}
.or(cfg.dids.scid_also_known_as);
Ok((method, alias))
}
pub type Presentation = (Option<vta_sdk::did_scid::DidMethod>, Option<bool>);
pub fn presented_did(
minted: &vta_sdk::protocols::did_management::create::CreateDidWebvhResultBody,
) -> String {
match (&minted.did_method, &minted.scid_did_url) {
(Some(vta_sdk::did_scid::DidMethod::Scid), Some(url)) => url.clone(),
_ => minted.did.clone(),
}
}
pub fn with_presentation(
payload: &mut Value,
presentation: (Option<vta_sdk::did_scid::DidMethod>, Option<bool>),
) -> &'static str {
let (method, alias) = presentation;
if let Some(method) = method {
payload["didMethod"] = Value::String(method.as_str().to_string());
}
if let Some(alias) = alias {
payload["scidAlsoKnownAs"] = Value::Bool(alias);
}
if method.is_some() || alias.is_some() {
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_2
} else {
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_1
}
}
pub fn validate(value: &Value) -> Result<(), AppError> {
let Some(s) = value.as_str() else {
return Err(AppError::Validation(format!(
"{DIDS_WEBVH_SERVER} is a string: a hosting server's id at the community's VTA, or \
\"{LOCAL}\""
)));
};
if s == LOCAL {
return Ok(());
}
let well_formed = !s.is_empty()
&& s.len() <= MAX_LEN
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-'));
if !well_formed {
return Err(AppError::Validation(format!(
"{DIDS_WEBVH_SERVER} names a hosting server by its id at the community's VTA \
(letters, digits, `.`, `_`, `:`, `-`, at most {MAX_LEN}), or is \"{LOCAL}\""
)));
}
Ok(())
}
pub async fn live(cfg: &AppConfig, db: &ConfigStore) -> Result<Option<String>, AppError> {
let layered = match std::env::var(ENV).ok() {
Some(v) => Some(Value::String(v)),
None => db.get(DIDS_WEBVH_SERVER).await?,
};
let value = layered
.filter(|v| validate(v).is_ok())
.and_then(|v| v.as_str().map(str::to_string))
.or_else(|| {
cfg.dids
.webvh_server
.clone()
.filter(|s| validate(&Value::String(s.clone())).is_ok())
});
Ok(value)
}
pub fn server_for(live: Option<&str>, own: Option<&str>) -> Option<String> {
match live {
Some(LOCAL) => None,
Some(server) => Some(server.to_string()),
None => own.map(str::to_string),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_runtime_choice_wins_and_local_means_served_here() {
assert_eq!(server_for(None, None), None);
assert_eq!(server_for(None, Some("hosting")), Some("hosting".into()));
assert_eq!(server_for(Some(LOCAL), Some("hosting")), None);
assert_eq!(
server_for(Some("other"), Some("hosting")),
Some("other".into())
);
}
#[test]
fn a_stated_presentation_takes_the_create_to_1_2() {
let mut payload = json!({ "contextId": "c" });
assert_eq!(
with_presentation(&mut payload, (None, None)),
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_1
);
assert_eq!(payload, json!({ "contextId": "c" }));
assert_eq!(
with_presentation(
&mut payload,
(Some(vta_sdk::did_scid::DidMethod::Scid), Some(true))
),
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_2
);
assert_eq!(
payload,
json!({ "contextId": "c", "didMethod": "scid", "scidAlsoKnownAs": true })
);
assert!(validate_did_method(&json!("webvh")).is_ok());
assert!(validate_did_method(&json!("key")).is_err());
}
#[test]
fn only_local_or_a_server_id_is_accepted() {
assert!(validate(&json!("local")).is_ok());
assert!(validate(&json!("hosting-eu.1")).is_ok());
assert!(validate(&json!("")).is_err());
assert!(validate(&json!("has space")).is_err());
assert!(validate(&json!("x".repeat(129))).is_err());
assert!(validate(&json!(7)).is_err());
}
}