Test / test (push) Successful in 23s
The questions/ tree is the router now: ids derive from file paths
(index.yaml names its directory; explicit id still wins for legacy
content), actions and requires_chain accept relative refs, nested
non-index files infer followup, and _section.yaml applies qualifies/
requires_chain/responsible to everything under its directory. Dynamic
[name].yaml pages serve any /dir/<value> with the segment substituted
into {name} resource-key placeholders; submissions index their chain
node in a portal_chains KV so requires_chain pages can verify a
visitor's ?chain= lineage actually ends at the required question.
Loading uses one recursive git-trees call; question_lint walks
subdirectories the same way. Implements docs/design/filesystem-routes.md.
Also: the YES hero now starts at HTML parse time via an inline module
script (yes.js moved to public/ for a stable /yes.js the wasm binding
raw_module-imports too - snippet paths are per-build-hashed), with
hydration adopting the running instance; and both gesture containers
reserve their box in CSS so mounting doesn't shift content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
2.9 KiB
Rust
85 lines
2.9 KiB
Rust
//! 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<ChainNode> {
|
|
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())
|
|
}
|