use super::model::{SnapshotEnvelope, SnapshotPayload};
const SCHEMA: &str = "cex.authz.snapshot.v1";
#[derive(Debug, thiserror::Error)]
pub enum VerifyError {
#[error("envelope parse failed: {0}")]
Envelope(String),
#[error("payload parse failed: {0}")]
Payload(String),
#[error("unexpected schema: {0}")]
Schema(String),
#[error("stale version: {got} <= current {current}")]
Stale { got: u64, current: u64 },
}
#[derive(Clone, Default)]
pub struct SnapshotVerifier;
impl SnapshotVerifier {
pub fn new() -> Self {
Self
}
pub fn verify(
&self,
envelope_bytes: &[u8],
current_version: u64,
) -> Result<SnapshotPayload, VerifyError> {
let envelope: SnapshotEnvelope = serde_json::from_slice(envelope_bytes)
.map_err(|e| VerifyError::Envelope(e.to_string()))?;
let payload: SnapshotPayload = serde_json::from_str(&envelope.payload)
.map_err(|e| VerifyError::Payload(e.to_string()))?;
if payload.schema != SCHEMA {
return Err(VerifyError::Schema(payload.schema));
}
if payload.policy_version <= current_version && current_version != 0 {
return Err(VerifyError::Stale {
got: payload.policy_version,
current: current_version,
});
}
Ok(payload)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_envelope(version: u64) -> Vec<u8> {
let payload = serde_json::json!({
"schema": SCHEMA,
"policy_version": version,
"issued_at": "2026-07-08T00:00:00Z",
"permissions": [],
"roles": [],
"grants": [],
"bindings": []
});
let payload_str = serde_json::to_string(&payload).unwrap();
let envelope = serde_json::json!({
"payload": payload_str,
"signature": "",
"key_id": "authz-v1"
});
serde_json::to_vec(&envelope).unwrap()
}
#[test]
fn parses_and_enforces_monotonic_version() {
let v = SnapshotVerifier::new();
let p = v.verify(&make_envelope(5), 0).expect("first snapshot");
assert_eq!(p.policy_version, 5);
assert!(matches!(
v.verify(&make_envelope(5), 5),
Err(VerifyError::Stale { .. })
));
assert!(v.verify(&make_envelope(6), 5).is_ok());
}
#[test]
fn rejects_wrong_schema() {
let v = SnapshotVerifier::new();
let bad = serde_json::to_vec(&serde_json::json!({
"payload": serde_json::json!({"schema":"nope","policy_version":1,"permissions":[],"roles":[],"grants":[],"bindings":[]}).to_string(),
"signature": ""
})).unwrap();
assert!(matches!(v.verify(&bad, 0), Err(VerifyError::Schema(_))));
}
}