use chrono::{Duration, Utc};
use serde_json::Value as JsonValue;
use trust_tasks_rs::TrustTask;
use trust_tasks_rs::specs::vtc::vetting::pcs_challenge::v0_1 as pcs_challenge_spec;
use trust_tasks_rs::specs::vtc::vetting::vetters::event_mode::v0_1 as event_mode_spec;
use trust_tasks_rs::specs::vtc::vetting::vetters::pcs_root::v0_1 as pcs_root_spec;
use trust_tasks_rs::specs::vtc::vetting::vetters::pcs_tokens::v0_1 as pcs_tokens_spec;
use vti_vetting_pcs::issuer::{
RootCredentialWire, RootRequestWire, TokenBatchRequestWire, TokenBatchWire, TokenRequestWire,
};
use super::{pcs_challenge, pcs_event, pcs_issue};
use crate::error::TaskError;
use crate::server::AppState;
pub const PCS_ROOT_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/vetters/pcs-root/0.1";
pub const PCS_TOKENS_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/vetters/pcs-tokens/0.1";
pub const EVENT_MODE_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/vetters/event-mode/0.1";
pub const PCS_CHALLENGE_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/pcs-challenge/0.1";
pub const HIDDEN_PUBLISH_TYPE: &str = "https://trusttasks.org/spec/vtc/vetting/hidden/publish/0.1";
pub const HIDDEN_WITHDRAW_TYPE: &str =
<trust_tasks_rs::specs::vtc::vetting::hidden::withdraw::v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub const HIDDEN_SHOW_TYPE: &str =
<trust_tasks_rs::specs::vtc::vetting::hidden::show::v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub const ROOT_ERR_NOT_A_VETTER: &str = "vtc/vetting/vetters/pcs-root:notAVetter";
pub const ROOT_ERR_WRONG_LABEL: &str = "vtc/vetting/vetters/pcs-root:wrongLabel";
pub const ROOT_ERR_ALREADY_ENROLLED: &str = "vtc/vetting/vetters/pcs-root:alreadyEnrolled";
pub const ROOT_ERR_IDENTIFIER_REBOUND: &str = "vtc/vetting/vetters/pcs-root:identifierRebound";
pub const ROOT_ERR_BAD_REQUEST: &str = "vtc/vetting/vetters/pcs-root:badRequest";
pub const TOKENS_ERR_NOT_A_VETTER: &str = "vtc/vetting/vetters/pcs-tokens:notAVetter";
pub const TOKENS_ERR_LABEL_NOT_LIVE: &str = "vtc/vetting/vetters/pcs-tokens:labelNotLive";
pub const TOKENS_ERR_ALREADY_SERVED: &str = "vtc/vetting/vetters/pcs-tokens:alreadyServed";
pub const TOKENS_ERR_OVER_QUOTA: &str = "vtc/vetting/vetters/pcs-tokens:overQuota";
pub const TOKENS_ERR_BAD_OPENING_PROOF: &str = "vtc/vetting/vetters/pcs-tokens:badOpeningProof";
pub const TOKENS_ERR_TICK_NOT_YET: &str =
trust_tasks_rs::specs::vtc::vetting::vetters::pcs_tokens::v0_1::error_codes::TICK_NOT_YET.code;
pub const TOKENS_ERR_EVENT_REFUSED: &str = "vtc/vetting/vetters/pcs-tokens:eventRefused";
pub const EVENT_ERR_NOT_A_VETTER: &str = "vtc/vetting/vetters/event-mode:notAVetter";
pub const EVENT_ERR_UNKNOWN_EVENT: &str = "vtc/vetting/vetters/event-mode:unknownEvent";
pub const EVENT_ERR_UNKNOWN_TIER: &str = "vtc/vetting/vetters/event-mode:unknownTier";
pub const EVENT_ERR_BAD_WINDOW: &str = "vtc/vetting/vetters/event-mode:badWindow";
pub const HIDDEN_PUBLISH_ERR_NO_SUCH_CRITERION: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::NO_SUCH_CRITERION.code;
pub const HIDDEN_PUBLISH_ERR_NO_VETTING: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::NO_VETTING.code;
pub const HIDDEN_PUBLISH_ERR_APPROVER_NOT_SIGNER: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::APPROVER_NOT_SIGNER
.code;
pub const HIDDEN_PUBLISH_ERR_APPROVER_IN_EVENT: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::APPROVER_IN_EVENT.code;
pub const HIDDEN_PUBLISH_ERR_SIGNER_CHANGED: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::SIGNER_CHANGED.code;
pub const HIDDEN_PUBLISH_ERR_OTHER_CRITERION: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::publish::v0_1::error_codes::OTHER_CRITERION.code;
pub const HIDDEN_WITHDRAW_ERR_NO_SUCH_CRITERION: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::withdraw::v0_1::error_codes::NO_SUCH_CRITERION
.code;
pub const HIDDEN_SHOW_ERR_NO_SUCH_CRITERION: &str =
trust_tasks_rs::specs::vtc::vetting::hidden::show::v0_1::error_codes::NO_SUCH_CRITERION.code;
pub const EVENT_ERR_ALREADY_REQUESTED: &str = "vtc/vetting/vetters/event-mode:alreadyRequested";
pub const EVENT_ERR_EVENT_CLOSED: &str = "vtc/vetting/vetters/event-mode:eventClosed";
pub const CHALLENGE_ERR_NOT_HIDDEN: &str = "vtc/vetting/pcs-challenge:notHiddenVetting";
async fn config_for(state: &AppState) -> Result<super::pcs::HiddenVettingConfig, TaskError> {
let criteria = crate::schemas::accepts::list_accepts(&state.schemas_ks)
.await
.map_err(TaskError::from)?;
for stored in criteria {
if let Some(raw) = stored.hidden_vetting {
return serde_json::from_value(raw).map_err(|e| {
TaskError::from(vti_common::error::AppError::Internal(format!(
"stored hidden-vetting parameters: {e}"
)))
});
}
}
Err(TaskError::declared(
CHALLENGE_ERR_NOT_HIDDEN,
vti_common::error::AppError::Validation(
"this community publishes no criterion that accepts a hidden-vetting proof".into(),
),
))
}
pub async fn handle_pcs_root(
state: &AppState,
member_did: &str,
doc: &TrustTask<JsonValue>,
) -> Result<JsonValue, TaskError> {
let payload: pcs_root_spec::Payload = parse(doc)?;
let config = config_for(state).await?;
let community = community_did(state).await;
let request = RootRequestWire {
label: payload.label.to_string(),
id: payload.id.to_string(),
request: serde_json::to_value(&payload.request).map_err(|e| {
TaskError::from(vti_common::error::AppError::Validation(format!(
"request: {e}"
)))
})?,
};
let answer: RootCredentialWire =
pcs_issue::enrol(state, &community, &config, member_did, &request, Utc::now())
.await
.map_err(root_error)?;
to_value(build::<_, pcs_root_spec::Response>(
pcs_root_spec::Response::builder()
.label(answer.label)
.pre_credential(answer.pre_credential),
)?)
}
pub async fn handle_pcs_tokens(
state: &AppState,
member_did: &str,
doc: &TrustTask<JsonValue>,
) -> Result<JsonValue, TaskError> {
let payload: pcs_tokens_spec::Payload = parse(doc)?;
let config = config_for(state).await?;
let community = community_did(state).await;
let batch = TokenBatchRequestWire {
label: payload.label.to_string(),
tick: u32::try_from(payload.tick).unwrap_or(u32::MAX),
requests: payload
.requests
.iter()
.map(|r| TokenRequestWire {
commitment: r.commitment.to_string(),
opening_proof: r.opening_proof.to_string(),
})
.collect(),
};
let served: TokenBatchWire =
pcs_issue::drip(state, &community, &config, member_did, &batch, Utc::now())
.await
.map_err(tokens_error)?;
let pre_credentials: Vec<pcs_tokens_spec::ResponsePreCredentialsItem> =
convert_vec(served.pre_credentials)?;
to_value(build::<_, pcs_tokens_spec::Response>(
pcs_tokens_spec::Response::builder()
.label(served.label)
.tick(u64::from(served.tick))
.pre_credentials(pre_credentials),
)?)
}
pub async fn handle_event_mode(
state: &AppState,
member_did: &str,
doc: &TrustTask<JsonValue>,
) -> Result<JsonValue, TaskError> {
let payload: event_mode_spec::Payload = parse(doc)?;
let config = config_for(state).await?;
let event_id = payload.event_id.to_string();
let tier = payload.tier.to_string();
let (event_state, group_size, event) = pcs_event::request(
state,
&config,
member_did,
&event_id,
&tier,
(payload.window.start_date, payload.window.end_date),
Utc::now(),
)
.await
.map_err(event_error)?;
let approved = event_state == pcs_event::EventState::Approved;
let mut builder = event_mode_spec::Response::builder()
.event_id(event_id.clone())
.state(event_state.as_str())
.tier(tier.clone())
.window(payload.window)
.group_size(group_size as u64)
.group_floor(i64::try_from(event.group_floor).unwrap_or(i64::MAX));
if approved {
let label: event_mode_spec::ResponseLabel =
event
.label()
.try_into()
.map_err(|e: event_mode_spec::error::ConversionError| {
TaskError::from(vti_common::error::AppError::Internal(format!(
"response: {e}"
)))
})?;
builder = builder
.label(Some(label))
.drip_per_tick(
event
.tier(&tier)
.and_then(|t| std::num::NonZeroU64::new(t.drip_per_tick as u64)),
)
.closes_after(event.closes_after());
}
to_value(build::<_, event_mode_spec::Response>(builder)?)
}
pub async fn handle_pcs_challenge(
state: &AppState,
applicant_did: &str,
doc: &TrustTask<JsonValue>,
) -> Result<JsonValue, TaskError> {
let _payload: pcs_challenge_spec::Payload = parse(doc)?;
config_for(state).await?;
let now = Utc::now();
let ttl = pcs_challenge::DEFAULT_CHALLENGE_TTL;
let challenge = pcs_challenge::issue(&state.join_requests_ks, applicant_did, ttl, now)
.await
.map_err(TaskError::from)?;
to_value(build::<_, pcs_challenge_spec::Response>(
pcs_challenge_spec::Response::builder()
.challenge(challenge)
.expires_at(now + ttl),
)?)
}
async fn community_did(state: &AppState) -> String {
state
.config
.read()
.await
.vtc_did
.clone()
.unwrap_or_default()
}
fn parse<P: serde::de::DeserializeOwned>(doc: &TrustTask<JsonValue>) -> Result<P, TaskError> {
serde_json::from_value(doc.payload.clone()).map_err(|e| {
TaskError::from(vti_common::error::AppError::Validation(format!(
"payload: {e}"
)))
})
}
fn to_value<T: serde::Serialize>(response: T) -> Result<JsonValue, TaskError> {
serde_json::to_value(response).map_err(|e| {
TaskError::from(vti_common::error::AppError::Internal(format!(
"response: {e}"
)))
})
}
fn build<B, T: TryFrom<B>>(builder: B) -> Result<T, TaskError>
where
T::Error: std::fmt::Display,
{
T::try_from(builder).map_err(|e| {
TaskError::from(vti_common::error::AppError::Internal(format!(
"response: {e}"
)))
})
}
fn convert_vec<T, U: TryFrom<T>>(items: Vec<T>) -> Result<Vec<U>, TaskError>
where
U::Error: std::fmt::Display,
{
items
.into_iter()
.map(|item| {
U::try_from(item).map_err(|e| {
TaskError::from(vti_common::error::AppError::Internal(format!(
"response: {e}"
)))
})
})
.collect()
}
fn declared_reason(e: vti_common::error::AppError) -> vti_common::error::AppError {
use vti_common::error::AppError;
match e {
AppError::Validation(reason)
| AppError::Forbidden(reason)
| AppError::NotFound(reason)
| AppError::Conflict(reason) => AppError::Validation(reason),
other => AppError::Validation(other.to_string()),
}
}
fn root_error(e: vti_common::error::AppError) -> TaskError {
let text = e.to_string();
let code = if matches!(e, vti_common::error::AppError::Forbidden(_)) {
ROOT_ERR_NOT_A_VETTER
} else if text.contains("is issuing") {
ROOT_ERR_WRONG_LABEL
} else if text.contains("already holds a credential") {
ROOT_ERR_ALREADY_ENROLLED
} else if text.contains("bound to another PCS identifier") {
ROOT_ERR_IDENTIFIER_REBOUND
} else if text.contains("does not verify") {
ROOT_ERR_BAD_REQUEST
} else {
return TaskError::from(e);
};
TaskError::declared(code, declared_reason(e))
}
fn tokens_error(e: vti_common::error::AppError) -> TaskError {
let text = e.to_string();
let code = if text.contains("event `") {
TOKENS_ERR_EVENT_REFUSED
} else if matches!(e, vti_common::error::AppError::Forbidden(_)) {
TOKENS_ERR_NOT_A_VETTER
} else if text.contains("not a live token label") {
TOKENS_ERR_LABEL_NOT_LIVE
} else if text.contains("has not begun") {
TOKENS_ERR_TICK_NOT_YET
} else if text.contains("already served") {
TOKENS_ERR_ALREADY_SERVED
} else if text.contains("drips") {
TOKENS_ERR_OVER_QUOTA
} else if text.contains("opening proof") {
TOKENS_ERR_BAD_OPENING_PROOF
} else {
return TaskError::from(e);
};
TaskError::declared(code, declared_reason(e))
}
fn event_error(e: vti_common::error::AppError) -> TaskError {
let text = e.to_string();
let code = if matches!(e, vti_common::error::AppError::NotFound(_)) {
EVENT_ERR_UNKNOWN_EVENT
} else if matches!(e, vti_common::error::AppError::Forbidden(_)) {
EVENT_ERR_NOT_A_VETTER
} else if text.contains("is not a tier") {
EVENT_ERR_UNKNOWN_TIER
} else if text.contains("is not inside") {
EVENT_ERR_BAD_WINDOW
} else if text.contains("has already asked") {
EVENT_ERR_ALREADY_REQUESTED
} else if text.contains("closed after") {
EVENT_ERR_EVENT_CLOSED
} else {
return TaskError::from(e);
};
TaskError::declared(code, declared_reason(e))
}
pub use pcs_challenge::DEFAULT_CHALLENGE_TTL as CHALLENGE_TTL;
#[must_use]
pub fn challenge_window() -> Duration {
CHALLENGE_TTL
}
#[cfg(test)]
mod tests {
use super::*;
use vti_common::error::AppError;
#[test]
fn a_declared_refusal_names_its_reason_once() {
let e = root_error(AppError::Validation(
"did:example:v already holds a credential under `vetter/2026-10`".into(),
));
assert_eq!(e.code(), Some(ROOT_ERR_ALREADY_ENROLLED));
assert_eq!(
e.to_string(),
"validation error: did:example:v already holds a credential under `vetter/2026-10`"
);
let e = tokens_error(AppError::Forbidden(
"did:example:v holds no live vetter grant in this community".into(),
));
assert_eq!(e.code(), Some(TOKENS_ERR_NOT_A_VETTER));
assert_eq!(e.to_string().matches("error:").count(), 1, "{e}");
let e = event_error(AppError::NotFound("no event `summit`".into()));
assert_eq!(e.code(), Some(EVENT_ERR_UNKNOWN_EVENT));
assert_eq!(e.to_string().matches("error:").count(), 1, "{e}");
}
}