vauban-claim 0.1.0

Vauban Claim Algebra — reference implementation of draft-vauban-claim-algebra-00 (post-quantum claim sextuplet + 5 composition operators, canonical CBOR/JSON codec).
Documentation
//! Temporal Frame primitive (CDDL §5.4).

use serde::{Deserialize, Serialize};

use crate::error::CompositionError;

/// Temporal Frame — POSIX-epoch validity window.
///
/// CDDL invariants T-1..T-4 enforced at construction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TemporalFrame {
    /// Issuance time (REQUIRED, T-1).
    #[serde(rename = "issued-at")]
    pub issued_at: u64,
    /// Earliest validity time.
    #[serde(rename = "valid-from", default, skip_serializing_if = "Option::is_none")]
    pub valid_from: Option<u64>,
    /// Latest validity time.
    #[serde(rename = "valid-until", default, skip_serializing_if = "Option::is_none")]
    pub valid_until: Option<u64>,
    /// Revocation timestamp — sticky (T-3).
    #[serde(rename = "revoked-at", default, skip_serializing_if = "Option::is_none")]
    pub revoked_at: Option<u64>,
    /// Wall-clock at which evidence was collected.
    #[serde(rename = "observed-at", default, skip_serializing_if = "Option::is_none")]
    pub observed_at: Option<u64>,
}

impl TemporalFrame {
    /// Construct and validate (T-1, T-2).
    pub fn new(
        issued_at: u64,
        valid_from: Option<u64>,
        valid_until: Option<u64>,
    ) -> Result<Self, CompositionError> {
        let f = Self {
            issued_at,
            valid_from,
            valid_until,
            revoked_at: None,
            observed_at: None,
        };
        f.validate_shape()?;
        Ok(f)
    }

    /// Enforce T-2: when both bounds present, `valid_from <= valid_until`.
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        if let (Some(vf), Some(vu)) = (self.valid_from, self.valid_until) {
            if vf > vu {
                return Err(CompositionError::Invariant(
                    "T-2: valid_from must be <= valid_until",
                ));
            }
        }
        Ok(())
    }

    /// Earliest validity time (issued_at as floor if no valid_from).
    pub fn not_before(&self) -> u64 { self.valid_from.unwrap_or(self.issued_at) }
    /// Latest validity time (none = unbounded forward).
    pub fn not_after(&self) -> Option<u64> { self.valid_until }
    /// Whether the Claim is currently revoked.
    pub fn is_revoked(&self) -> bool { self.revoked_at.is_some() }

    /// Whether the Claim has been marked revoked at or before `now` (T-3).
    pub fn is_revoked_at(&self, now: u64) -> bool {
        matches!(self.revoked_at, Some(t) if t <= now)
    }

    /// Intersection used for ∧ (Conjunction §6.1 C-3).
    /// Returns `None` iff intersection is empty.
    pub fn intersect(&self, other: &Self) -> Option<Self> {
        let issued_at = self.issued_at.max(other.issued_at);
        let valid_from = match (self.valid_from, other.valid_from) {
            (Some(a), Some(b)) => Some(a.max(b)),
            (Some(x), None) | (None, Some(x)) => Some(x),
            (None, None) => None,
        };
        let valid_until = match (self.valid_until, other.valid_until) {
            (Some(a), Some(b)) => Some(a.min(b)),
            (Some(x), None) | (None, Some(x)) => Some(x),
            (None, None) => None,
        };
        if let (Some(vf), Some(vu)) = (valid_from, valid_until) {
            if vf > vu {
                return None;
            }
        }
        let revoked_at = match (self.revoked_at, other.revoked_at) {
            (Some(a), Some(b)) => Some(a.min(b)),
            (Some(x), None) | (None, Some(x)) => Some(x),
            (None, None) => None,
        };
        Some(Self {
            issued_at,
            valid_from,
            valid_until,
            revoked_at,
            observed_at: None,
        })
    }
}