Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-estate-side/src/lib.rs

//! # monetize-estate-side — the estate side of the ONE monetize's wire
//!
//! Monetize never calls into an estate (G1-pull, G16). Every estate calls the ONE monetize, and
//! what that call looks like is the same for every product. Until P4a each product carried its
//! own copy (A28 §4: Holger's `holger-fleet` and `holger-estate-front/src/shop_door.rs`,
//! Gunnar's `gunnar-repair` and `gunnar-front/src/shop_door.rs`). This crate holds the one copy:
//!
//! | piece | module | feature |
//! |---|---|---|
//! | the box pump: the box key, `Fleet.Report` / `Orders` / `Ask`, verify under the monetize key, run through the product's executor, ack in the next report, `fleet/pulled.json` | [`pump`] | `pump` |
//! | the pump's jera round (report, long-poll, run, report again; the refusal lines) | [`pump::duty`] | `pump` |
//! | the sign-in-again link a sold box mints and mails: the order's checks, the lifetime, the words (pure; P4c) | [`sign_in`] | none |
//! | the front's signed `SoldEstate` client (Upgrade, Recharge, TakeMaterial, UpdateTake, UpdateReport), its constants, its bench seam | [`sold_door`] | `sold-door` |
//!
//! What stays the product's: its order kinds and executor ([`pump::PumpBox`]), its health
//! words, and what its front does with the shop's answers. The shared update step is
//! Skidbladnir's (`nordisk_pairing::frontside::update`); [`sold_door::SoldDoor`] is its `Shop`.
//!
//! **The SoldEstate client is thin** (TASKS-4-v2 T16, A28 H16): the SoldEstate → Fleet merge
//! swaps its transport ([`sold_door::Wire`]) and grows nothing here.

#[cfg(feature = "pump")]
pub mod pump;
pub mod sign_in;
#[cfg(feature = "sold-door")]
pub mod sold_door;

/// Unix ms now (0 before the epoch).
pub fn now_unix_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// 32 bytes → 64 hex.
pub fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

/// 64 hex (or 44 base64) → 32 bytes.
pub fn key32(text: &str) -> Option<[u8; 32]> {
    let t = text.trim();
    if t.len() == 64 {
        return unhex32(t, "the key").ok();
    }
    monetize_embed::signing::base64_decode(t)?.try_into().ok()
}

/// 64 hex → 32 bytes, refused by `what`'s name.
pub fn unhex32(s: &str, what: &str) -> Result<[u8; 32], String> {
    let s = s.trim();
    if s.len() != 64 || !s.is_ascii() {
        return Err(format!("{what} is not 64 hex"));
    }
    let v: Result<Vec<u8>, _> = (0..64)
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
        .collect();
    v.map_err(|_| format!("{what} is not 64 hex"))?
        .try_into()
        .map_err(|_| format!("{what} is not 32 bytes"))
}

/// Write `bytes` to `path` atomically (temp + fsync + rename), owner-only when `secret`.
#[cfg(feature = "pump")]
pub(crate) fn write_atomic(
    path: &std::path::Path,
    bytes: &[u8],
    secret: bool,
) -> std::io::Result<()> {
    use std::io::Write;
    let dir = path.parent().unwrap_or(std::path::Path::new("."));
    std::fs::create_dir_all(dir)?;
    let tmp = path.with_extension("tmp");
    let mut opts = std::fs::OpenOptions::new();
    opts.write(true).create(true).truncate(true);
    #[cfg(unix)]
    if secret {
        use std::os::unix::fs::OpenOptionsExt;
        opts.mode(0o600);
    }
    #[cfg(not(unix))]
    let _ = secret;
    let mut f = opts.open(&tmp)?;
    f.write_all(bytes)?;
    f.sync_all()?;
    std::fs::rename(&tmp, path)
}

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

    #[test]
    fn a_key_is_64_hex_or_44_base64_and_anything_else_is_none() {
        let k = [0x11u8; 32];
        assert_eq!(key32(&hex(&k)), Some(k));
        assert_eq!(key32(&format!("  {}\n", hex(&k))), Some(k), "trimmed");
        assert_eq!(key32(&monetize_embed::signing::base64_encode(&k)), Some(k));
        assert_eq!(key32("11"), None);
        assert_eq!(key32(&"zz".repeat(32)), None);
        assert!(unhex32(&"1".repeat(63), "the client key")
            .unwrap_err()
            .contains("the client key is not 64 hex"));
        assert!(
            unhex32(&"é".repeat(32), "k").is_err(),
            "a non-ASCII text never panics on a char boundary"
        );
        assert!(now_unix_ms() > 1_700_000_000_000);
    }
}