1use napi::bindgen_prelude::*;
7use napi_derive::napi;
8use uuid::Uuid;
9
10use ves_stark_air::Policy;
11use ves_stark_primitives::{
12 witness_commitment_hex_to_u64, CommerceAuthorizationReceipt, CompliancePublicInputs,
13 PayloadAmountBinding, PolicyParams,
14};
15use ves_stark_prover::{ComplianceProver, ComplianceWitness};
16use ves_stark_verifier::{
17 verify_agent_authorization_proof_auto_with_amount_binding, verify_compliance_proof_auto_bound,
18 verify_compliance_proof_auto_with_amount_binding, VerifierError,
19};
20
21fn bigint_to_u64(value: &BigInt, field_name: &str) -> Result<u64> {
22 let (sign_bit, value, lossless) = value.get_u64();
23 if sign_bit {
24 return Err(Error::new(
25 Status::InvalidArg,
26 format!("{} must be non-negative", field_name),
27 ));
28 }
29 if !lossless {
30 return Err(Error::new(
31 Status::InvalidArg,
32 format!("{} must fit in u64", field_name),
33 ));
34 }
35 Ok(value)
36}
37
38fn parse_witness_commitment(witness_commitment: Vec<String>) -> Result<[u64; 4]> {
39 if witness_commitment.len() != 4 {
40 return Err(Error::new(
41 Status::InvalidArg,
42 format!(
43 "Witness commitment must have exactly 4 elements, got {}",
44 witness_commitment.len()
45 ),
46 ));
47 }
48
49 let mut commitment = [0u64; 4];
50 for (idx, value) in witness_commitment.iter().enumerate() {
51 let parsed = value.parse::<u64>().map_err(|_| {
52 Error::new(
53 Status::InvalidArg,
54 "Invalid witness commitment element".to_string(),
55 )
56 })?;
57 commitment[idx] = parsed;
58 }
59
60 Ok(commitment)
61}
62
63fn verifier_error_to_napi(err: VerifierError) -> Error {
64 let status = match err {
65 VerifierError::PublicInputMismatch(_)
66 | VerifierError::InvalidHexFormat { .. }
67 | VerifierError::DeserializationError(_)
68 | VerifierError::InvalidPolicyHash { .. }
69 | VerifierError::PolicyMismatch { .. }
70 | VerifierError::LimitMismatch { .. }
71 | VerifierError::PayloadAmountBindingRequired(_)
72 | VerifierError::WitnessCommitmentMismatch
73 | VerifierError::ProofTooLarge { .. }
74 | VerifierError::UnsupportedProofVersion { .. } => Status::InvalidArg,
75 VerifierError::InvalidProofStructure(_)
76 | VerifierError::FriVerificationFailed(_)
77 | VerifierError::ConstraintCheckFailed(_)
78 | VerifierError::VerificationFailed(_) => Status::GenericFailure,
79 };
80
81 Error::new(status, format!("Verification error: {}", err))
82}
83
84fn bind_public_inputs_to_commitment(
85 mut public_inputs: CompliancePublicInputs,
86 witness_commitment: &[u64; 4],
87) -> Result<CompliancePublicInputs> {
88 public_inputs = public_inputs
89 .bind_witness_commitment(witness_commitment)
90 .map_err(|e| {
91 Error::new(
92 Status::InvalidArg,
93 format!("Failed to bind witness commitment to public inputs: {}", e),
94 )
95 })?;
96 Ok(public_inputs)
97}
98
99fn parse_authorization_receipt(receipt: serde_json::Value) -> Result<CommerceAuthorizationReceipt> {
100 serde_json::from_value(receipt).map_err(|e| {
101 Error::new(
102 Status::InvalidArg,
103 format!("Invalid authorization receipt object: {}", e),
104 )
105 })
106}
107
108fn parse_payload_amount_binding(binding: serde_json::Value) -> Result<PayloadAmountBinding> {
109 serde_json::from_value(binding).map_err(|e| {
110 Error::new(
111 Status::InvalidArg,
112 format!("Invalid payload amount binding object: {}", e),
113 )
114 })
115}
116
117#[napi(object)]
119pub struct JsCompliancePublicInputs {
120 pub event_id: String,
122 pub tenant_id: String,
124 pub store_id: String,
126 pub sequence_number: BigInt,
128 pub payload_kind: u32,
130 pub payload_plain_hash: String,
132 pub payload_cipher_hash: String,
134 pub event_signing_hash: String,
136 pub policy_id: String,
138 pub policy_params: serde_json::Value,
140 pub policy_hash: String,
142 pub witness_commitment: Option<String>,
144 pub authorization_receipt_hash: Option<String>,
146 pub amount_binding_hash: Option<String>,
148}
149
150#[napi(object)]
152pub struct JsComplianceProof {
153 pub proof_bytes: Buffer,
155 pub proof_hash: String,
157 pub proving_time_ms: i64,
159 pub proof_size: i64,
161 pub witness_commitment: Vec<String>,
163 pub witness_commitment_hex: String,
165}
166
167#[napi(object)]
169pub struct JsVerificationResult {
170 pub valid: bool,
172 pub verification_time_ms: i64,
174 pub error: Option<String>,
176 pub policy_id: String,
178 pub policy_limit: BigInt,
180}
181
182fn convert_public_inputs(js: &JsCompliancePublicInputs) -> Result<CompliancePublicInputs> {
184 let event_id = Uuid::parse_str(&js.event_id)
185 .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid event_id UUID: {}", e)))?;
186 let tenant_id = Uuid::parse_str(&js.tenant_id)
187 .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid tenant_id UUID: {}", e)))?;
188 let store_id = Uuid::parse_str(&js.store_id)
189 .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid store_id UUID: {}", e)))?;
190
191 Ok(CompliancePublicInputs {
192 event_id,
193 tenant_id,
194 store_id,
195 sequence_number: bigint_to_u64(&js.sequence_number, "sequence_number")?,
196 payload_kind: js.payload_kind,
197 payload_plain_hash: js.payload_plain_hash.clone(),
198 payload_cipher_hash: js.payload_cipher_hash.clone(),
199 event_signing_hash: js.event_signing_hash.clone(),
200 policy_id: js.policy_id.clone(),
201 policy_params: PolicyParams(js.policy_params.clone()),
202 policy_hash: js.policy_hash.clone(),
203 witness_commitment: js.witness_commitment.clone(),
204 authorization_receipt_hash: js.authorization_receipt_hash.clone(),
205 amount_binding_hash: js.amount_binding_hash.clone(),
206 })
207}
208
209#[napi]
221pub fn prove(
222 amount: BigInt,
223 public_inputs: JsCompliancePublicInputs,
224 policy_type: String,
225 policy_limit: BigInt,
226) -> Result<JsComplianceProof> {
227 let amount = bigint_to_u64(&amount, "amount")?;
228 let policy_limit = bigint_to_u64(&policy_limit, "policy_limit")?;
229
230 let rust_inputs = convert_public_inputs(&public_inputs)?;
232 if rust_inputs.policy_id != policy_type {
233 return Err(Error::new(
234 Status::InvalidArg,
235 format!(
236 "policyType {} does not match publicInputs.policyId {}",
237 policy_type, rust_inputs.policy_id
238 ),
239 ));
240 }
241
242 let policy = Policy::from_public_inputs(&rust_inputs.policy_id, &rust_inputs.policy_params)
243 .map_err(|e| {
244 Error::new(
245 Status::InvalidArg,
246 format!("Invalid policy parameters for {}: {}", policy_type, e),
247 )
248 })?;
249 if policy.limit() != policy_limit {
250 return Err(Error::new(
251 Status::InvalidArg,
252 format!(
253 "policyLimit {} does not match publicInputs policy limit {}",
254 policy_limit,
255 policy.limit()
256 ),
257 ));
258 }
259 if !policy.validate_amount(amount) {
260 return Err(Error::new(
261 Status::InvalidArg,
262 format!(
263 "amount must be {} policy limit for {}",
264 match policy_type.as_str() {
265 "aml.threshold" => "<",
266 _ => "<=",
267 },
268 policy_type
269 ),
270 ));
271 }
272
273 let witness = ComplianceWitness::try_new(amount, rust_inputs).map_err(|e| {
275 Error::new(
276 Status::InvalidArg,
277 format!("Invalid witness/public inputs: {}", e),
278 )
279 })?;
280
281 let prover = ComplianceProver::with_policy(policy);
283 let proof = prover.prove(&witness).map_err(|e| {
284 Error::new(
285 Status::GenericFailure,
286 format!("Proof generation failed: {}", e),
287 )
288 })?;
289
290 let witness_commitment: Vec<String> = proof
291 .witness_commitment
292 .iter()
293 .map(|value| value.to_string())
294 .collect();
295 let witness_commitment_hex = proof.witness_commitment_hex.clone().ok_or_else(|| {
296 Error::new(
297 Status::GenericFailure,
298 "Missing witness_commitment_hex in proof".to_string(),
299 )
300 })?;
301
302 Ok(JsComplianceProof {
303 proof_bytes: Buffer::from(proof.proof_bytes),
304 proof_hash: proof.proof_hash,
305 proving_time_ms: proof.metadata.proving_time_ms as i64,
306 proof_size: proof.metadata.proof_size as i64,
307 witness_commitment,
308 witness_commitment_hex,
309 })
310}
311
312#[napi]
322pub fn verify(
323 proof_bytes: Buffer,
324 public_inputs: JsCompliancePublicInputs,
325 witness_commitment: Vec<String>,
326) -> Result<JsVerificationResult> {
327 let rust_inputs = convert_public_inputs(&public_inputs)?;
329
330 let commitment = parse_witness_commitment(witness_commitment)?;
331 let rust_inputs = bind_public_inputs_to_commitment(rust_inputs, &commitment)?;
332
333 let result = verify_compliance_proof_auto_bound(&proof_bytes, &rust_inputs);
335
336 match result {
337 Ok(verification) => Ok(JsVerificationResult {
338 valid: verification.valid,
339 verification_time_ms: verification.verification_time_ms as i64,
340 error: verification.error,
341 policy_id: verification.policy_id,
342 policy_limit: BigInt::from(verification.policy_limit),
343 }),
344 Err(e) => Err(verifier_error_to_napi(e)),
345 }
346}
347
348#[napi]
354pub fn verify_hex(
355 proof_bytes: Buffer,
356 public_inputs: JsCompliancePublicInputs,
357 witness_commitment_hex: String,
358) -> Result<JsVerificationResult> {
359 let rust_inputs = convert_public_inputs(&public_inputs)?;
361
362 let commitment = witness_commitment_hex_to_u64(&witness_commitment_hex).map_err(|e| {
363 Error::new(
364 Status::InvalidArg,
365 format!("Invalid witnessCommitmentHex: {}", e),
366 )
367 })?;
368 let rust_inputs = bind_public_inputs_to_commitment(rust_inputs, &commitment)?;
369
370 let result = verify_compliance_proof_auto_bound(&proof_bytes, &rust_inputs);
372
373 match result {
374 Ok(verification) => Ok(JsVerificationResult {
375 valid: verification.valid,
376 verification_time_ms: verification.verification_time_ms as i64,
377 error: verification.error,
378 policy_id: verification.policy_id,
379 policy_limit: BigInt::from(verification.policy_limit),
380 }),
381 Err(e) => Err(verifier_error_to_napi(e)),
382 }
383}
384
385#[napi]
387pub fn verify_with_amount_binding(
388 proof_bytes: Buffer,
389 public_inputs: JsCompliancePublicInputs,
390 amount_binding: serde_json::Value,
391) -> Result<JsVerificationResult> {
392 let rust_inputs = convert_public_inputs(&public_inputs)?;
393 let binding = parse_payload_amount_binding(amount_binding)?;
394
395 let result =
396 verify_compliance_proof_auto_with_amount_binding(&proof_bytes, &rust_inputs, &binding);
397
398 match result {
399 Ok(verification) => Ok(JsVerificationResult {
400 valid: verification.valid,
401 verification_time_ms: verification.verification_time_ms as i64,
402 error: verification.error,
403 policy_id: verification.policy_id,
404 policy_limit: BigInt::from(verification.policy_limit),
405 }),
406 Err(e) => Err(verifier_error_to_napi(e)),
407 }
408}
409
410#[napi]
412pub fn verify_agent_authorization(
413 proof_bytes: Buffer,
414 public_inputs: JsCompliancePublicInputs,
415 witness_commitment: Vec<String>,
416 receipt: serde_json::Value,
417) -> Result<JsVerificationResult> {
418 let rust_inputs = convert_public_inputs(&public_inputs)?;
419 let commitment = parse_witness_commitment(witness_commitment)?;
420 let rust_inputs = bind_public_inputs_to_commitment(rust_inputs, &commitment)?;
421 let receipt = parse_authorization_receipt(receipt)?;
422 let binding = rust_inputs
423 .payload_amount_binding(receipt.amount)
424 .map_err(|e| verifier_error_to_napi(VerifierError::PublicInputMismatch(format!("{e}"))))?;
425
426 let result = verify_agent_authorization_proof_auto_with_amount_binding(
427 &proof_bytes,
428 &rust_inputs,
429 &binding,
430 &receipt,
431 );
432
433 match result {
434 Ok(verification) => Ok(JsVerificationResult {
435 valid: verification.valid,
436 verification_time_ms: verification.verification_time_ms as i64,
437 error: verification.error,
438 policy_id: verification.policy_id,
439 policy_limit: BigInt::from(verification.policy_limit),
440 }),
441 Err(e) => Err(verifier_error_to_napi(e)),
442 }
443}
444
445#[napi]
447pub fn verify_agent_authorization_hex(
448 proof_bytes: Buffer,
449 public_inputs: JsCompliancePublicInputs,
450 witness_commitment_hex: String,
451 receipt: serde_json::Value,
452) -> Result<JsVerificationResult> {
453 let rust_inputs = convert_public_inputs(&public_inputs)?;
454 let commitment = witness_commitment_hex_to_u64(&witness_commitment_hex).map_err(|e| {
455 Error::new(
456 Status::InvalidArg,
457 format!("Invalid witnessCommitmentHex: {}", e),
458 )
459 })?;
460 let rust_inputs = bind_public_inputs_to_commitment(rust_inputs, &commitment)?;
461 let receipt = parse_authorization_receipt(receipt)?;
462 let binding = rust_inputs
463 .payload_amount_binding(receipt.amount)
464 .map_err(|e| verifier_error_to_napi(VerifierError::PublicInputMismatch(format!("{e}"))))?;
465
466 let result = verify_agent_authorization_proof_auto_with_amount_binding(
467 &proof_bytes,
468 &rust_inputs,
469 &binding,
470 &receipt,
471 );
472
473 match result {
474 Ok(verification) => Ok(JsVerificationResult {
475 valid: verification.valid,
476 verification_time_ms: verification.verification_time_ms as i64,
477 error: verification.error,
478 policy_id: verification.policy_id,
479 policy_limit: BigInt::from(verification.policy_limit),
480 }),
481 Err(e) => Err(verifier_error_to_napi(e)),
482 }
483}
484
485#[napi]
488pub fn verify_agent_authorization_with_amount_binding(
489 proof_bytes: Buffer,
490 public_inputs: JsCompliancePublicInputs,
491 amount_binding: serde_json::Value,
492 receipt: serde_json::Value,
493) -> Result<JsVerificationResult> {
494 let rust_inputs = convert_public_inputs(&public_inputs)?;
495 let binding = parse_payload_amount_binding(amount_binding)?;
496 let receipt = parse_authorization_receipt(receipt)?;
497
498 let result = verify_agent_authorization_proof_auto_with_amount_binding(
499 &proof_bytes,
500 &rust_inputs,
501 &binding,
502 &receipt,
503 );
504
505 match result {
506 Ok(verification) => Ok(JsVerificationResult {
507 valid: verification.valid,
508 verification_time_ms: verification.verification_time_ms as i64,
509 error: verification.error,
510 policy_id: verification.policy_id,
511 policy_limit: BigInt::from(verification.policy_limit),
512 }),
513 Err(e) => Err(verifier_error_to_napi(e)),
514 }
515}
516
517#[napi]
523pub fn compute_policy_hash(policy_id: String, policy_params: serde_json::Value) -> Result<String> {
524 let params = PolicyParams(policy_params);
525 let hash = ves_stark_primitives::compute_policy_hash(&policy_id, ¶ms).map_err(|e| {
526 Error::new(
527 Status::GenericFailure,
528 format!("Failed to compute policy hash: {}", e),
529 )
530 })?;
531 Ok(hash.to_hex())
532}
533
534#[napi]
539pub fn create_aml_threshold_params(threshold: BigInt) -> Result<serde_json::Value> {
540 let threshold = bigint_to_u64(&threshold, "threshold")?;
541 Ok(serde_json::json!({ "threshold": threshold }))
542}
543
544#[napi]
549pub fn create_order_total_cap_params(cap: BigInt) -> Result<serde_json::Value> {
550 let cap = bigint_to_u64(&cap, "cap")?;
551 Ok(serde_json::json!({ "cap": cap }))
552}
553
554#[napi]
560pub fn create_agent_authorization_params(
561 max_total: BigInt,
562 intent_hash: String,
563) -> Result<serde_json::Value> {
564 let max_total = bigint_to_u64(&max_total, "max_total")?;
565 let params = PolicyParams::agent_authorization(max_total, &intent_hash).map_err(|e| {
566 Error::new(
567 Status::InvalidArg,
568 format!("Invalid agent authorization params: {}", e),
569 )
570 })?;
571 Ok(params.to_json_value())
572}
573
574#[napi]
576pub fn create_payload_amount_binding(
577 public_inputs: JsCompliancePublicInputs,
578 amount: BigInt,
579) -> Result<serde_json::Value> {
580 let rust_inputs = convert_public_inputs(&public_inputs)?;
581 let amount = bigint_to_u64(&amount, "amount")?;
582
583 let binding = rust_inputs.payload_amount_binding(amount).map_err(|e| {
584 Error::new(
585 Status::InvalidArg,
586 format!("Invalid payload amount binding inputs: {}", e),
587 )
588 })?;
589
590 serde_json::to_value(&binding).map_err(|e| {
591 Error::new(
592 Status::GenericFailure,
593 format!("Failed to serialize payload amount binding: {}", e),
594 )
595 })
596}