//! SHA-256 hashed answer chain. `parent_hashes` is DAG-shaped, but //! nothing produces more than one parent yet - the shape is reserved //! for follow-up questions posted back into a visitor's chain. #![cfg(feature = "ssr")] use sha2::{Digest, Sha256}; /// Hashes one chain node from its question, its parent hash(es), the /// submitted responses, and a timestamp. Parents are sorted first so the /// hash doesn't depend on the order multiple parents happened to arrive /// in. /// Where submitted chain nodes are indexed - hash → which question was /// answered. Small on purpose (no responses), and shared by every /// portal instance on the JetStream (hashes are globally unique, so /// cross-site collisions can't happen). This is what lets /// `requires_chain` pages verify a visitor's `?chain=` actually ends /// at the question they claim to have answered. pub const CHAIN_BUCKET: &str = "portal_chains"; #[derive(serde::Serialize, serde::Deserialize)] pub struct ChainNode { pub question_id: String, pub timestamp_ms: i64, } /// Indexes one submitted node. Best-effort by design (the caller logs /// and continues): the NATS event is the durable record, this is a /// lookup convenience. pub async fn record_node( js: &async_nats::jetstream::Context, chain_hash: &str, question_id: &str, timestamp_ms: i64, ) -> anyhow::Result<()> { let store = match js.get_key_value(CHAIN_BUCKET).await { Ok(store) => store, Err(_) => { js.create_key_value(async_nats::jetstream::kv::Config { bucket: CHAIN_BUCKET.to_string(), ..Default::default() }) .await? } }; let node = ChainNode { question_id: question_id.to_string(), timestamp_ms, }; store.put(chain_hash, serde_json::to_vec(&node)?.into()).await?; Ok(()) } /// Looks a chain hash up - `None` covers both "no such node" and /// "bucket not created yet" (no submissions anywhere), which read the /// same to a `requires_chain` check: the claimed lineage can't be /// verified, so the gate stays shut. pub async fn lookup_node( js: &async_nats::jetstream::Context, chain_hash: &str, ) -> Option { let store = js.get_key_value(CHAIN_BUCKET).await.ok()?; let bytes = store.get(chain_hash).await.ok()??; serde_json::from_slice(&bytes).ok() } pub fn hash_node( question_id: &str, parent_hashes: &[String], responses: &serde_json::Value, timestamp_ms: i64, ) -> String { let mut parents = parent_hashes.to_vec(); parents.sort(); let payload = serde_json::json!({ "questionId": question_id, "parentHashes": parents, "responses": responses, "timestamp": timestamp_ms, }); let data = serde_json::to_vec(&payload).expect("json values always serialize"); let mut hasher = Sha256::new(); hasher.update(&data); format!("{:x}", hasher.finalize()) }