use serde_json::Value;
use super::constraints::{check_constraints, CheckResult};
use super::jws::{sha256_b64u, AgentKey};
use super::mandate::{merchant_matches, L2View, VCT_CHECKOUT_FINAL, VCT_PAYMENT_FINAL};
use super::sd_jwt::{
create_disclosure, delegate_ref, hash_disclosure, presentation_hash, selective_presentation,
SdJwt,
};
use super::ViError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineItem {
pub id: String,
pub quantity: u32,
}
#[derive(Debug, Clone)]
pub struct L3Request {
pub nonce: String,
pub iat: u64,
pub exp: u64,
pub iss: Option<String>,
pub aud_network: String,
pub aud_merchant: String,
pub payee: Value,
pub payment_amount: Value,
pub checkout_jwt: String,
pub line_items: Vec<LineItem>,
pub attestation: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct L3Bundle {
pub l3a: SdJwt,
pub l3b: SdJwt,
pub l2_payment_presentation: String,
pub l2_checkout_presentation: String,
pub checkout_hash: String,
pub constraint_result: CheckResult,
}
pub fn payment_fulfillment(req: &L3Request, l2: &L2View) -> Value {
serde_json::json!({
"payment_amount": req.payment_amount,
"payee": req.payee,
"allowed_merchants": l2.allowed_payees(),
})
}
pub fn checkout_fulfillment(req: &L3Request, l2: &L2View) -> Value {
serde_json::json!({
"merchant": req.payee,
"line_items": req.line_items.iter().map(|li| serde_json::json!({"id": li.id, "quantity": li.quantity})).collect::<Vec<_>>(),
"allowed_merchants": l2.allowed_merchants(),
})
}
pub fn check_request(req: &L3Request, l2: &L2View) -> CheckResult {
let mut pay = check_constraints(
&l2.payment_constraints(),
&payment_fulfillment(req, l2),
l2.autonomous,
);
let chk = check_constraints(
&l2.checkout_constraints(),
&checkout_fulfillment(req, l2),
l2.autonomous,
);
pay.satisfied &= chk.satisfied;
pay.violations.extend(chk.violations);
pay.checked.extend(chk.checked);
pay.skipped.extend(chk.skipped);
if req.exp <= req.iat || req.exp - req.iat > 3600 {
pay.satisfied = false;
pay.violations
.push("L3 exp must be after iat and at most 3600 s later".into());
}
pay
}
pub fn build_l3(l2: &L2View, key: &AgentKey, req: &L3Request) -> Result<L3Bundle, ViError> {
if !l2.autonomous {
return Err(ViError::Mandate(
"L3 credentials exist only for autonomous (open) mandates".into(),
));
}
let payment = l2
.payment
.as_ref()
.ok_or_else(|| ViError::Mandate("L2 has no payment mandate".into()))?;
let checkout = l2
.checkout
.as_ref()
.ok_or_else(|| ViError::Mandate("L2 has no checkout mandate".into()))?;
if let Some(kid) = &l2.agent_kid {
if *kid != key.kid {
return Err(ViError::Mandate(format!(
"L2 delegates to kid '{kid}', this key is '{}'",
key.kid
)));
}
}
let ours = key.public_jwk();
if ours.x != l2.agent_jwk.x || ours.y != l2.agent_jwk.y {
return Err(ViError::Mandate("L2 cnf.jwk is not this key".into()));
}
let constraint_result = check_request(req, l2);
if !constraint_result.satisfied {
return Err(ViError::Mandate(format!(
"request violates the mandate: {}",
constraint_result.violations.join("; ")
)));
}
let checkout_hash = sha256_b64u(req.checkout_jwt.as_bytes());
let base = l2.base_jwt();
let merchant_disc = l2.merchant_disclosure(&req.payee).ok_or_else(|| {
ViError::Mandate("payee is not among the L2's disclosed merchants".into())
})?;
let mut item_discs: Vec<String> = Vec::new();
for li in &req.line_items {
let d = l2.item_disclosure(&li.id).ok_or_else(|| {
ViError::Mandate(format!(
"item {} is not among the L2's disclosed acceptable items",
li.id
))
})?;
if !item_discs.contains(&d.disclosure) {
item_discs.push(d.disclosure.clone());
}
}
let mut pay_pres_discs = vec![payment.disclosure.clone(), merchant_disc.disclosure.clone()];
pay_pres_discs.dedup();
let l2_payment_presentation = selective_presentation(&base, &pay_pres_discs);
let mut chk_pres_discs = vec![checkout.disclosure.clone()];
chk_pres_discs.extend(item_discs);
let l2_checkout_presentation = selective_presentation(&base, &chk_pres_discs);
let payment_instrument = l2
.payment_instrument()
.ok_or_else(|| ViError::Mandate("L2 payment mandate has no payment_instrument".into()))?;
let payee = l2
.allowed_payees()
.into_iter()
.chain(l2.allowed_merchants())
.find(|m| merchant_matches(m, &req.payee))
.unwrap_or_else(|| req.payee.clone());
let header = serde_json::json!({"alg": "ES256", "typ": "kb-sd-jwt", "kid": key.kid});
let final_merchant = create_disclosure(None, &payee, None);
let final_payment = create_disclosure(
None,
&serde_json::json!({
"vct": VCT_PAYMENT_FINAL,
"transaction_id": checkout_hash,
"payee": payee,
"payment_amount": req.payment_amount,
"payment_instrument": payment_instrument,
}),
None,
);
let mut pa = serde_json::json!({
"nonce": req.nonce,
"aud": req.aud_network,
"sd_hash": presentation_hash(&l2_payment_presentation),
"iat": req.iat,
"exp": req.exp,
"delegate_payload": [delegate_ref(&hash_disclosure(&final_merchant)), delegate_ref(&hash_disclosure(&final_payment))],
"_sd_alg": "sha-256",
});
if let Some(iss) = &req.iss {
pa["iss"] = Value::String(iss.clone());
}
if let Some(att) = &req.attestation {
pa["agent_attestation"] = att.clone();
}
let l3a = SdJwt::create(&header, &pa, vec![final_merchant, final_payment], key);
let mut final_checkout_v = serde_json::json!({
"vct": VCT_CHECKOUT_FINAL,
"checkout_jwt": req.checkout_jwt,
"checkout_hash": checkout_hash,
});
if !req.line_items.is_empty() {
final_checkout_v["line_items"] = Value::Array(
req.line_items
.iter()
.map(|li| serde_json::json!({"id": li.id, "quantity": li.quantity}))
.collect(),
);
}
let final_checkout = create_disclosure(None, &final_checkout_v, None);
let mut pb = serde_json::json!({
"nonce": req.nonce,
"aud": req.aud_merchant,
"sd_hash": presentation_hash(&l2_checkout_presentation),
"iat": req.iat,
"exp": req.exp,
"delegate_payload": [delegate_ref(&hash_disclosure(&final_checkout))],
"_sd_alg": "sha-256",
});
if let Some(iss) = &req.iss {
pb["iss"] = Value::String(iss.clone());
}
if let Some(att) = &req.attestation {
pb["agent_attestation"] = att.clone();
}
let l3b = SdJwt::create(&header, &pb, vec![final_checkout], key);
Ok(L3Bundle {
l3a,
l3b,
l2_payment_presentation,
l2_checkout_presentation,
checkout_hash,
constraint_result,
})
}