use affinidi_openid4vci::{CredentialOffer, CredentialRequest, CredentialResponse};
use affinidi_openid4vp::DcqlQuery;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const OFFER: &str = "https://trusttasks.org/spec/credential-exchange/offer/1.0";
pub const REQUEST: &str = "https://trusttasks.org/spec/credential-exchange/request/1.0";
pub const ISSUE: &str = "https://trusttasks.org/spec/credential-exchange/issue/1.0";
pub const QUERY: &str = "https://trusttasks.org/spec/credential-exchange/query/1.0";
pub const PRESENT: &str = "https://trusttasks.org/spec/credential-exchange/present/1.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfferBody {
pub credential_offer: CredentialOffer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestBody {
pub credential_request: CredentialRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_response: Option<CredentialResponse>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sealed: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryBody {
pub dcql_query: DcqlQuery,
pub nonce: String,
pub purpose: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresentBody {
pub vp_token: Value,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn query_body_round_trips_with_a_dcql_query() {
let dcql = DcqlQuery::from_json(&json!({
"credentials": [{
"id": "membership",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://openvtc.org/credentials/MembershipCredential"] }
}]
}))
.unwrap();
let body = QueryBody {
dcql_query: dcql,
nonce: "n-123".into(),
purpose: "join the Acme community".into(),
};
let wire = serde_json::to_string(&body).unwrap();
let back: QueryBody = serde_json::from_str(&wire).unwrap();
assert_eq!(back.nonce, "n-123");
assert_eq!(back.purpose, "join the Acme community");
assert_eq!(back.dcql_query.credentials.len(), 1);
}
#[test]
fn issue_body_carries_a_sealed_bundle() {
let body = IssueBody {
credential_response: None,
sealed: Some("-----BEGIN VTA SEALED-----\n…\n-----END VTA SEALED-----".into()),
};
let wire = serde_json::to_value(&body).unwrap();
assert!(wire.get("sealed").is_some());
assert!(
wire.get("credentialResponse").is_none() && wire.get("credential_response").is_none(),
"absent cleartext response is omitted: {wire}"
);
let back: IssueBody = serde_json::from_value(wire).unwrap();
assert!(back.sealed.is_some() && back.credential_response.is_none());
}
#[test]
fn present_body_round_trips() {
let body = PresentBody {
vp_token: json!("<jws>~<disclosure>~<kb-jwt>"),
};
let back: PresentBody =
serde_json::from_str(&serde_json::to_string(&body).unwrap()).unwrap();
assert_eq!(back.vp_token, json!("<jws>~<disclosure>~<kb-jwt>"));
}
#[test]
fn uris_are_versioned_and_distinct() {
let all = [OFFER, REQUEST, ISSUE, QUERY, PRESENT];
for u in all {
assert!(u.starts_with("https://trusttasks.org/spec/credential-exchange/"));
assert!(u.ends_with("/1.0"));
}
for (i, a) in all.iter().enumerate() {
for b in &all[i + 1..] {
assert_ne!(a, b);
}
}
}
}