hydracache 0.53.1

User-facing HydraCache runtime crate.
Documentation
use hydracache::{
    AtRestSealer, CertificateBundle, CertificateRotationWindow, SecurityError,
    StaticAtRestKeyProvider, AT_REST_ARTIFACT_FORMAT_VERSION,
};

#[test]
fn security_lifecycle_at_rest_sealed_bytes_only_persisted() {
    let provider = StaticAtRestKeyProvider::new("k2", b"operator-secret".to_vec()).unwrap();
    let sealer = AtRestSealer::new(provider);

    let sealed = sealer.seal("snapshot", b"tenant-secret-profile").unwrap();

    assert_eq!(sealed.format_version, AT_REST_ARTIFACT_FORMAT_VERSION);
    assert_eq!(sealed.key_id, "k2");
    assert_ne!(sealed.ciphertext, b"tenant-secret-profile");
    assert_eq!(sealer.open(&sealed).unwrap(), b"tenant-secret-profile");
}

#[test]
fn security_lifecycle_undecryptable_artifact_is_rejected_not_served() {
    let provider = StaticAtRestKeyProvider::new("k2", b"operator-secret".to_vec()).unwrap();
    let sealer = AtRestSealer::new(provider);
    let mut sealed = sealer.seal("pitr-log", b"write:user:42").unwrap();

    sealed.ciphertext[0] ^= 0x80;

    assert_eq!(
        sealer.open(&sealed),
        Err(SecurityError::UndecryptableArtifact)
    );
}

#[test]
fn security_lifecycle_key_rotation_accepts_previous_for_reads_only() {
    let old_provider = StaticAtRestKeyProvider::new("k1", b"old-secret".to_vec()).unwrap();
    let old_sealer = AtRestSealer::new(old_provider);
    let old_artifact = old_sealer.seal("snapshot", b"before-rotation").unwrap();

    let rotated_provider = StaticAtRestKeyProvider::new("k2", b"new-secret".to_vec())
        .unwrap()
        .with_previous_key("k1", b"old-secret".to_vec())
        .unwrap();
    let rotated_sealer = AtRestSealer::new(rotated_provider);
    let new_artifact = rotated_sealer.seal("snapshot", b"after-rotation").unwrap();

    assert_eq!(
        rotated_sealer.open(&old_artifact).unwrap(),
        b"before-rotation"
    );
    assert_eq!(new_artifact.key_id, "k2");
}

#[test]
fn security_lifecycle_wrong_key_id_or_secret_fails_closed() {
    let provider = StaticAtRestKeyProvider::new("k2", b"operator-secret".to_vec()).unwrap();
    let sealer = AtRestSealer::new(provider);
    let mut sealed = sealer.seal("snapshot", b"secret").unwrap();

    sealed.key_id = "missing".to_owned();
    assert_eq!(
        sealer.open(&sealed),
        Err(SecurityError::UnknownKey("missing".to_owned()))
    );

    let wrong_provider = StaticAtRestKeyProvider::new("k2", b"wrong-secret".to_vec()).unwrap();
    let wrong_sealer = AtRestSealer::new(wrong_provider);
    sealed.key_id = "k2".to_owned();
    assert_eq!(
        wrong_sealer.open(&sealed),
        Err(SecurityError::UndecryptableArtifact)
    );
}

#[test]
fn security_lifecycle_cert_rotation_window_accepts_old_and_new() {
    let old = CertificateBundle::new("cert-old", "CN=member-a", 2_000).unwrap();
    let new = CertificateBundle::new("cert-new", "CN=member-a", 3_000).unwrap();
    let window = CertificateRotationWindow::new(old.clone()).promote(new.clone());

    assert_eq!(window.current_id(), "cert-new");
    assert!(window.accepts("cert-old", 1_000));
    assert!(window.accepts("cert-new", 1_000));
    assert!(!window.accepts("cert-old", 2_001));
    assert!(!window.accepts("unknown", 1_000));
}