use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg(all(feature = "wasm", feature = "wasm-bindgen-futures"))]
use wasm_bindgen::prelude::*;
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChunkValidationResult {
pub valid: bool,
pub schema_errors: Vec<String>,
pub ttl_errors: Vec<String>,
pub crypto_errors: Vec<String>,
pub inspector_errors: Vec<String>,
pub policy_errors: Vec<String>,
}
#[wasm_bindgen]
pub trait StreamingValidator {
fn on_progress(&self, current: u32, total: u32);
}
#[wasm_bindgen]
pub struct StreamingValidationState {
pub chunks_processed: u32,
pub chunks_total: u32,
pub items_processed: u32,
pub items_total: u32,
pub valid: bool,
}
impl StreamingValidationState {
pub fn new() -> Self {
Self {
chunks_processed: 0,
chunks_total: 0,
items_processed: 0,
items_total: 0,
valid: false,
}
}
pub fn update_chunk(&mut self, current: u32, total: u32) {
self.chunks_processed = current;
self.chunks_total = total;
}
pub fn update_item(&mut self, current: u32, total: u32) {
self.items_processed = current;
self.items_total = total;
}
pub fn update_valid(&mut self, valid: bool) {
self.valid = valid;
}
}
#[wasm_bindgen]
pub fn validate_chunk(
manifest_chunk: &str,
policy_json: &str,
) -> Result<ChunkValidationResult, String> {
let manifest = match serde_json::from_str::<serde_json::Value>(manifest_chunk) {
Ok(v) => v,
Err(e) => {
return Err(format!("Invalid manifest JSON: {}", e));
}
};
let policy = match serde_json::from_str::<serde_json::Value>(policy_json) {
Ok(v) => v,
Err(e) => {
return Err(format!("Invalid policy JSON: {}", e));
}
};
let kya_version = String::new();
let agent_id = String::new();
let verification_method = String::new();
let proof = Vec::new();
let expires_at = String::new();
if let serde_json::Value::Object(map) = &manifest {
if let Some(serde_json::Value::String(ver)) = map.get("kyaVersion") {
kya_version = ver.clone();
}
if let Some(serde_json::Value::String(id)) = map.get("agentId") {
agent_id = id.clone();
}
if let Some(serde_json::Value::String(method)) = map.get("verificationMethod") {
verification_method = method.clone();
}
if let Some(serde_json::Value::Array(p)) = map.get("proof") {
proof = p.clone();
}
if let Some(serde_json::Value::String(exp)) = map.get("expiresAt") {
expires_at = exp.clone();
}
}
let mut schema_errors = Vec::new();
let mut ttl_errors = Vec::new();
let mut crypto_errors = Vec::new();
let mut inspector_errors = Vec::new();
let mut policy_errors = Vec::new();
if !kya_version.starts_with("1.") {
schema_errors.push(format!(
"KYA version must start with '1.', got '{}'",
kya_version
));
}
if !agent_id.starts_with("did:") {
schema_errors.push(format!("Agent ID must be a DID, got '{}'", agent_id));
}
if verification_method.is_empty() {
schema_errors.push("Verification method cannot be empty".to_string());
}
if !expires_at.is_empty() {
if chrono::DateTime::parse_from_rfc3339(&expires_at).is_err() {
ttl_errors.push(format!(
"Invalid expiresAt format, must be ISO 8601, got '{}'",
expires_at
));
}
}
if let Some(serde_json::Value::Object(policy_map)) = &policy {
if let Some(serde_json::Value::Array(rules)) = policy_map.get("rules") {
let needs_crypto = false;
let needs_schema = false;
let needs_inspector = false;
for rule in rules.as_array().unwrap_or(&vec![]) {
if let Some(serde_json::Value::Object(rule_obj)) = rule.as_object() {
if let Some(serde_json::Value::String(rule_type)) = rule_obj.get("type") {
match rule_type.as_str() {
"crypto" => needs_crypto = true,
"schema" => needs_schema = true,
"inspector" => needs_inspector = true,
_ => {}
}
}
}
}
if verification_method.contains("cryptographic") && !needs_crypto {
policy_errors.push(format!("Policy uses cryptographic verification but has no crypto rules, verification: '{}'", verification_method));
}
if verification_method.contains("TEE") && !needs_inspector {
policy_errors.push(format!(
"Policy uses TEE verification but has no inspector rules, verification: '{}'",
verification_method
));
}
}
}
if verification_method.contains("cryptographic") {
if let Some(serde_json::Value::Array(proofs)) = &proof {
for proof_item in proofs.as_array().unwrap_or(&vec![]) {
if let Some(serde_json::Value::Object(obj)) = proof_item.as_object() {
if let Some(serde_json::Value::String(purpose)) = obj.get("proofPurpose") {
match purpose.as_str() {
"signature" => {
if obj.get("signature").is_none() {
crypto_errors.push(
"Cryptographic proof must include signature".to_string(),
);
}
}
_ => {}
}
}
}
}
}
}
let valid = schema_errors.is_empty()
&& ttl_errors.is_empty()
&& crypto_errors.is_empty()
&& inspector_errors.is_empty()
&& policy_errors.is_empty();
Ok(ChunkValidationResult {
valid,
schema_errors,
ttl_errors,
crypto_errors,
inspector_errors,
policy_errors,
})
}
#[wasm_bindgen]
pub fn finalize_validation(
chunk_results: Vec<ChunkValidationResult>,
state: &StreamingValidationState,
) -> Result<(HashMap<String, Value>, ValidationReport), String> {
let mut all_schema_errors = Vec::new();
let mut all_ttl_errors = Vec::new();
let mut all_crypto_errors = Vec::new();
let mut all_inspector_errors = Vec::new();
let mut all_policy_errors = Vec::new();
for result in &chunk_results {
all_schema_errors.extend(result.schema_errors.iter().cloned());
all_ttl_errors.extend(result.ttl_errors.iter().cloned());
all_crypto_errors.extend(result.crypto_errors.iter().cloned());
all_inspector_errors.extend(result.inspector_errors.iter().cloned());
all_policy_errors.extend(result.policy_errors.iter().cloned());
}
let total_errors = all_schema_errors.len()
+ all_ttl_errors.len()
+ all_crypto_errors.len()
+ all_inspector_errors.len()
+ all_policy_errors.len();
let valid = total_errors == 0;
let mut report = HashMap::new();
report.insert(
"valid".to_string(),
valid.to_string().to_json().unwrap().into(),
);
report.insert(
"totalErrors".to_string(),
total_errors.to_string().to_json().unwrap().into(),
);
report.insert(
"schemaErrors".to_string(),
serde_json::to_value(all_schema_errors).unwrap().into(),
);
report.insert(
"ttlErrors".to_string(),
serde_json::to_value(all_ttl_errors).unwrap().into(),
);
report.insert(
"cryptoErrors".to_string(),
serde_json::to_value(all_crypto_errors).unwrap().into(),
);
report.insert(
"inspectorErrors".to_string(),
serde_json::to_value(all_inspector_errors).unwrap().into(),
);
report.insert(
"policyErrors".to_string(),
serde_json::to_value(all_policy_errors).unwrap().into(),
);
Ok((
report,
ValidationReport {
valid,
schemaErrors: all_schema_errors,
ttlErrors: all_ttl_errors,
cryptoErrors: all_crypto_errors,
inspectorErrors: all_inspector_errors,
policyErrors: all_policy_errors,
totalErrors,
},
))
}
#[wasm_bindgen]
pub struct StreamingValidator {
pub chunks_processed: u32,
pub chunks_total: u32,
pub items_processed: u32,
pub items_total: u32,
pub valid: bool,
pub validator: Box<dyn StreamingValidator>,
pub state: StreamingValidationState,
}
#[wasm_bindgen]
impl StreamingValidator {
pub fn new(validator: Box<dyn StreamingValidator>) -> Self {
Self {
chunks_processed: 0,
chunks_total: 0,
items_processed: 0,
items_total: 0,
valid: false,
validator,
state: StreamingValidationState::new(),
}
}
pub fn add_chunk(&mut self, chunk: &str, total_chunks: u32) -> Result<(), String> {
self.chunks_total = total_chunks;
match self.validator.validate_chunk(chunk) {
Ok(_) => {
self.chunks_processed += 1;
self.state.update_chunk(self.chunks_processed, total_chunks);
}
Err(e) => {
return Err(e);
}
}
Ok(())
}
pub fn finish(self) -> Result<ValidationReport, String> {
let mut report = HashMap::new();
report.insert(
"valid".to_string(),
self.state.valid.to_string().to_json().unwrap().into(),
);
report.insert(
"totalErrors".to_string(),
"0".to_string().to_json().unwrap().into(),
);
Ok(ValidationReport {
valid: self.state.valid,
schemaErrors: vec![],
ttlErrors: vec![],
cryptoErrors: vec![],
inspectorErrors: vec![],
policyErrors: vec![],
totalErrors: 0,
})
}
}
#[wasm_bindgen]
pub fn create_streaming_validator(validator: Box<dyn StreamingValidator>) -> StreamingValidator {
StreamingValidator::new(validator)
}
#[wasm_bindgen]
pub fn get_streaming_state(validator: &StreamingValidator) -> StreamingValidationState {
validator.state
}
#[wasm_bindgen]
pub fn reset_streaming_state(validator: &mut StreamingValidator) {
validator.state = StreamingValidationState::new();
}