harnessd 0.1.0

The harness daemon: API server (axum WS + REST), agent runtime host, and CLI (init/pair/doctor).
//! Device-token auth + pairing helpers (design doc §8).
//!
//! Tokens and one-time pairing codes are random; the store only ever holds their
//! **SHA-256 hashes**, so a database leak never yields a usable credential. Pairing is
//! DB-backed (not in-memory) so `harnessd pair` in one process and the running daemon
//! that completes the handshake share the same one-time codes.

use rand::RngCore;
use sha2::{Digest, Sha256};

/// Hex-encoded SHA-256 of the input. Used to store token/code hashes.
pub fn hash(input: &str) -> String {
    let digest = Sha256::digest(input.as_bytes());
    hex(&digest)
}

/// A cryptographically-random hex string of `bytes` bytes (2×`bytes` hex chars).
pub fn random_hex(bytes: usize) -> String {
    let mut buf = vec![0u8; bytes];
    rand::thread_rng().fill_bytes(&mut buf);
    hex(&buf)
}

/// An 8-digit human-typeable pairing code.
pub fn generate_code() -> String {
    let mut buf = [0u8; 4];
    rand::thread_rng().fill_bytes(&mut buf);
    let n = u32::from_be_bytes(buf) % 100_000_000;
    format!("{n:08}")
}

fn hex(bytes: &[u8]) -> String {
    use std::fmt::Write;
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(s, "{b:02x}");
    }
    s
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hash_is_stable_and_hex() {
        let h = hash("hello");
        assert_eq!(h.len(), 64);
        assert_eq!(h, hash("hello"));
        assert_ne!(h, hash("hell0"));
    }

    #[test]
    fn code_is_8_digits() {
        let c = generate_code();
        assert_eq!(c.len(), 8);
        assert!(c.chars().all(|c| c.is_ascii_digit()));
    }

    #[test]
    fn random_hex_len() {
        assert_eq!(random_hex(32).len(), 64);
        assert_ne!(random_hex(16), random_hex(16));
    }
}