From bf81792c4993c3fcb9cacfa64b9f2b8e141d8811 Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:33:38 -0500 Subject: [PATCH] the proposal board: a Space's pinned playlist takes signed proposals, and an accept republishes it --- crates/persona-state/src/lib.rs | 1 + crates/persona-state/src/proposals.rs | 613 ++++++++++ crates/proposal-contract/Cargo.lock | 1449 +++++++++++++++++++++++ crates/proposal-contract/Cargo.toml | 28 + crates/proposal-contract/src/lib.rs | 118 ++ src-tauri/assets/proposal_contract.wasm | Bin 0 -> 369313 bytes src-tauri/src/lib.rs | 4 + src-tauri/src/playlists.rs | 2 +- src-tauri/src/proposals.rs | 646 ++++++++++ src-tauri/src/shared_playlists.rs | 86 +- src-tauri/src/spaces.rs | 2 +- src/components/Icon.svelte | 1 + src/lib/api.js | 3 + src/lib/community.svelte.js | 42 +- src/views/Community.svelte | 222 +++- 15 files changed, 3207 insertions(+), 10 deletions(-) create mode 100644 crates/persona-state/src/proposals.rs create mode 100644 crates/proposal-contract/Cargo.lock create mode 100644 crates/proposal-contract/Cargo.toml create mode 100644 crates/proposal-contract/src/lib.rs create mode 100644 src-tauri/assets/proposal_contract.wasm create mode 100644 src-tauri/src/proposals.rs diff --git a/crates/persona-state/src/lib.rs b/crates/persona-state/src/lib.rs index 2073bfc..7278f82 100644 --- a/crates/persona-state/src/lib.rs +++ b/crates/persona-state/src/lib.rs @@ -20,6 +20,7 @@ pub mod friend; pub mod playlist; pub mod inbox; pub mod space; +pub mod proposals; pub const SCHEMA_VERSION: u32 = 1; pub const SIGN_DOMAIN: &str = "soulstream.profile/v1"; diff --git a/crates/persona-state/src/proposals.rs b/crates/persona-state/src/proposals.rs new file mode 100644 index 0000000..d9068ce --- /dev/null +++ b/crates/persona-state/src/proposals.rs @@ -0,0 +1,613 @@ +//! The collaborative-playlist proposal board (F7 → the app). One board per +//! shared playlist, keyed by (owner, playlist uuid); a MULTI-WRITER contract +//! in the inbox mold — anyone may offer, `merge_board` is the bouncer. +//! +//! Three record families: +//! - **AuthorityCheckpoint** — the owner's signed contributor roster with a +//! monotonic epoch, co-located with the board. Highest (epoch, hash) wins; +//! an epoch that drops an author IS revocation, and that author's pending +//! proposals leave the board deterministically. +//! - **Proposal** — immutable, contributor-signed, bound to an EXACT base +//! revision (the F3 content hash) and valid only while its author is on +//! the CURRENT roster. Bounded ops, bounded size, per-author and total +//! caps (newest per author kept). +//! - **Decision** — owner-signed accept/reject with a monotonic sequence: +//! the audit trail. A decided proposal leaves the pending set and no stale +//! replica can bring it back. The canonical playlist never lives here — an +//! accepted proposal becomes an ordinary owner-signed revision + pointer +//! bump outside. +//! +//! BYTE-COMPATIBILITY IS THE CONTRACT: these shapes are bincode-identical to +//! the research crate the bundled `proposal_contract.wasm` was proven with +//! (research/freenet-f7); the entry types are `playlist::EntryV1` itself. + +use serde::{Deserialize, Serialize}; + +use crate::playlist as f3; + +pub const SCHEMA_VERSION: u32 = 1; +pub const PARAMS_FAMILY: &[u8] = b"soulstream.playlist-props.v1"; +/// Proposals and decisions sign under the playlist domain — the delegate +/// allowlist has no separate board entry, and a board IS playlist business. +pub const SIGN_DOMAIN: &str = f3::SIGN_DOMAIN; + +pub const MAX_STATE_BYTES: usize = 1024 * 1024; +pub const MAX_CONTRIBUTORS: usize = 64; +pub const MAX_OPS_PER_PROPOSAL: usize = 32; +pub const MAX_PROPOSAL_BYTES: usize = 64 * 1024; +pub const MAX_PROPOSALS: usize = 128; +pub const PER_AUTHOR_PENDING_CAP: usize = 8; +pub const MAX_DECISIONS: usize = 256; +pub const MAX_REASON_BYTES: usize = 128; + +fn verify(public: &[u8; 32], body: &[u8], sig: &[u8]) -> bool { + crate::verify_sig_in(SIGN_DOMAIN, public, &sign_payload(body), sig) +} + +/// What the delegate is handed for any board record: blake3 of its sig body. +pub fn sign_payload(body: &[u8]) -> Vec { + blake3::hash(body).as_bytes().to_vec() +} + +// ---------------------------------------------------------------- records + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthorityCheckpoint { + pub epoch: u64, + pub contributors: Vec<[u8; 32]>, + pub sig: Vec, // owner +} + +#[derive(Serialize)] +struct CheckpointSigBody<'a> { + epoch: u64, + contributors: &'a [[u8; 32]], +} + +pub fn checkpoint_sig_body(c: &AuthorityCheckpoint) -> Vec { + bincode::serialize(&CheckpointSigBody { epoch: c.epoch, contributors: &c.contributors }).expect("serializes") +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProposalOp { + Add { position: u32, entry: f3::EntryV1 }, + Remove { entry_id: [u8; 16] }, + Move { entry_id: [u8; 16], to_position: u32 }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Proposal { + pub proposal_id: [u8; 16], + pub author: [u8; 32], + pub authority_epoch: u64, + pub base_revision_hash: [u8; 32], + pub ops: Vec, + pub note: String, + pub created_at: u64, + pub sig: Vec, +} + +#[derive(Serialize)] +struct ProposalSigBody<'a> { + proposal_id: &'a [u8; 16], + author: &'a [u8; 32], + authority_epoch: u64, + base_revision_hash: &'a [u8; 32], + ops: &'a [ProposalOp], + note: &'a str, + created_at: u64, +} + +pub fn proposal_sig_body(p: &Proposal) -> Vec { + bincode::serialize(&ProposalSigBody { + proposal_id: &p.proposal_id, + author: &p.author, + authority_epoch: p.authority_epoch, + base_revision_hash: &p.base_revision_hash, + ops: &p.ops, + note: &p.note, + created_at: p.created_at, + }) + .expect("serializes") +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Decision { + pub proposal_id: [u8; 16], + pub accept: bool, + pub seq: u64, + pub reason: String, + /// When accepted: the revision the owner published applying it. + pub result_revision_hash: Option<[u8; 32]>, + pub sig: Vec, // owner +} + +#[derive(Serialize)] +struct DecisionSigBody<'a> { + proposal_id: &'a [u8; 16], + accept: bool, + seq: u64, + reason: &'a str, + result_revision_hash: &'a Option<[u8; 32]>, +} + +pub fn decision_sig_body(d: &Decision) -> Vec { + bincode::serialize(&DecisionSigBody { + proposal_id: &d.proposal_id, + accept: d.accept, + seq: d.seq, + reason: &d.reason, + result_revision_hash: &d.result_revision_hash, + }) + .expect("serializes") +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct BoardState { + pub schema_version: u32, + pub owner: [u8; 32], + pub playlist_uuid: [u8; 16], + pub authority: Option, + pub proposals: Vec, + pub decisions: Vec, +} + +pub fn make_params(owner: &[u8; 32], uuid: &[u8; 16]) -> Vec { + let mut p = Vec::with_capacity(PARAMS_FAMILY.len() + 48); + p.extend_from_slice(PARAMS_FAMILY); + p.extend_from_slice(owner); + p.extend_from_slice(uuid); + p +} + +pub fn params_parts(params: &[u8]) -> Option<([u8; 32], [u8; 16])> { + let t = PARAMS_FAMILY.len(); + if params.len() != t + 48 || ¶ms[..t] != PARAMS_FAMILY { + return None; + } + Some((params[t..t + 32].try_into().unwrap(), params[t + 32..t + 48].try_into().unwrap())) +} + +// ---------------------------------------------------------------- merge + +pub fn checkpoint_valid(owner: &[u8; 32], c: &AuthorityCheckpoint) -> bool { + c.contributors.len() <= MAX_CONTRIBUTORS && verify(owner, &checkpoint_sig_body(c), &c.sig) +} + +pub fn proposal_valid(p: &Proposal) -> bool { + if p.ops.is_empty() || p.ops.len() > MAX_OPS_PER_PROPOSAL { + return false; + } + if p.note.len() > f3::MAX_DESC_BYTES { + return false; + } + if bincode::serialized_size(p).map(|n| n as usize > MAX_PROPOSAL_BYTES).unwrap_or(true) { + return false; + } + for op in &p.ops { + if let ProposalOp::Add { entry, .. } = op { + for s in [&entry.recording.title, &entry.recording.album] { + if s.len() > f3::MAX_FIELD_BYTES { + return false; + } + } + if entry.note.as_deref().map(str::len).unwrap_or(0) > f3::MAX_NOTE_BYTES { + return false; + } + if entry.slsk_hint.as_deref().map(str::len).unwrap_or(0) > f3::MAX_FIELD_BYTES { + return false; + } + } + } + verify(&p.author, &proposal_sig_body(p), &p.sig) +} + +pub fn decision_valid(owner: &[u8; 32], d: &Decision) -> bool { + d.reason.len() <= MAX_REASON_BYTES && verify(owner, &decision_sig_body(d), &d.sig) +} + +/// Deterministic multi-writer merge — the bouncer. Every delivery order of +/// the same candidates yields the same bytes. +pub fn merge_board(owner: &[u8; 32], uuid: &[u8; 16], candidates: &[&[u8]]) -> BoardState { + use std::collections::{BTreeMap, BTreeSet}; + + let mut parsed: Vec = Vec::new(); + for bytes in candidates { + if bytes.is_empty() || bytes.len() > MAX_STATE_BYTES { + continue; + } + let Ok(st) = bincode::deserialize::(bytes) else { continue }; + if st.schema_version != SCHEMA_VERSION || &st.owner != owner || &st.playlist_uuid != uuid { + continue; + } + parsed.push(st); + } + + // 1. The winning authority checkpoint: highest (epoch, blake3) valid one. + let mut best_auth: Option<(u64, [u8; 32], AuthorityCheckpoint)> = None; + for st in &parsed { + if let Some(c) = &st.authority { + if checkpoint_valid(owner, c) { + let rank = (c.epoch, *blake3::hash(&checkpoint_sig_body(c)).as_bytes()); + let better = match &best_auth { + None => true, + Some((e, h, _)) => rank > (*e, *h), + }; + if better { + best_auth = Some((rank.0, rank.1, c.clone())); + } + } + } + } + let authority = best_auth.map(|(_, _, c)| c); + + // 2. Decisions: union by proposal id, highest (seq, hash) per proposal; + // the oldest age out past the cap — the window is honest. + let mut dec_by_id: BTreeMap<[u8; 16], (u64, [u8; 32], Decision)> = BTreeMap::new(); + for st in &parsed { + for d in &st.decisions { + if !decision_valid(owner, d) { + continue; + } + let rank = (d.seq, *blake3::hash(&decision_sig_body(d)).as_bytes()); + match dec_by_id.get(&d.proposal_id) { + Some((s, h, _)) if (*s, *h) >= rank => {} + _ => { + dec_by_id.insert(d.proposal_id, (rank.0, rank.1, d.clone())); + } + } + } + } + let mut decisions: Vec = dec_by_id.into_values().map(|(_, _, d)| d).collect(); + decisions.sort_by_key(|d| (d.seq, d.proposal_id)); + if decisions.len() > MAX_DECISIONS { + let cut = decisions.len() - MAX_DECISIONS; + decisions.drain(0..cut); + } + + // 3. Proposals: union by id (a collision keeps the smaller hash), gated + // on the CURRENT roster, undecided only, then the caps (newest per + // author). + let decided: BTreeSet<[u8; 16]> = decisions.iter().map(|d| d.proposal_id).collect(); + let roster: Vec<[u8; 32]> = authority.as_ref().map(|a| a.contributors.clone()).unwrap_or_default(); + let mut prop_by_id: BTreeMap<[u8; 16], ([u8; 32], Proposal)> = BTreeMap::new(); + for st in &parsed { + for p in &st.proposals { + if decided.contains(&p.proposal_id) { + continue; + } + if !roster.contains(&p.author) { + continue; + } + if !proposal_valid(p) { + continue; + } + let h = *blake3::hash(&bincode::serialize(p).unwrap()).as_bytes(); + match prop_by_id.get(&p.proposal_id) { + Some((eh, _)) if eh <= &h => {} + _ => { + prop_by_id.insert(p.proposal_id, (h, p.clone())); + } + } + } + } + let mut all: Vec = prop_by_id.into_values().map(|(_, p)| p).collect(); + all.sort_by_key(|p| (p.created_at, p.proposal_id)); + let mut per_author: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); + for p in all { + per_author.entry(p.author).or_default().push(p); + } + let mut proposals: Vec = Vec::new(); + for (_, mut list) in per_author { + let keep_from = list.len().saturating_sub(PER_AUTHOR_PENDING_CAP); + proposals.extend(list.drain(keep_from..)); + } + proposals.sort_by_key(|p| (p.created_at, p.proposal_id)); + proposals.truncate(MAX_PROPOSALS); + + BoardState { schema_version: SCHEMA_VERSION, owner: *owner, playlist_uuid: *uuid, authority, proposals, decisions } +} + +// ---------------------------------------------------------------- apply + +#[derive(Debug, PartialEq, Eq)] +pub enum ApplyError { + StaleBase { expected: [u8; 32], got: [u8; 32] }, + RemoveMissing([u8; 16]), + MoveMissing([u8; 16]), +} + +impl core::fmt::Display for ApplyError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ApplyError::StaleBase { .. } => write!( + f, + "the proposal targets a revision that is no longer current; ask for a re-submit against the current one" + ), + ApplyError::RemoveMissing(_) => write!(f, "a remove targets an entry not in the base revision"), + ApplyError::MoveMissing(_) => write!(f, "a move targets an entry not in the base revision"), + } + } +} + +/// Pure application of a proposal's ops to the CURRENT canonical manifest. +/// A stale base is refused outright: exact-base binding is the whole +/// concurrency model — the owner rebases by asking for a re-submit, never +/// by guessing intent. +pub fn apply_proposal( + current: &f3::ManifestV1, + current_bytes_hash: &[u8; 32], + p: &Proposal, +) -> Result, ApplyError> { + if &p.base_revision_hash != current_bytes_hash { + return Err(ApplyError::StaleBase { expected: *current_bytes_hash, got: p.base_revision_hash }); + } + let mut entries = current.entries.clone(); + for op in &p.ops { + match op { + ProposalOp::Add { position, entry } => { + let at = (*position as usize).min(entries.len()); + entries.insert(at, entry.clone()); + } + ProposalOp::Remove { entry_id } => { + let Some(i) = entries.iter().position(|e| &e.entry_id == entry_id) else { + return Err(ApplyError::RemoveMissing(*entry_id)); + }; + entries.remove(i); + } + ProposalOp::Move { entry_id, to_position } => { + let Some(i) = entries.iter().position(|e| &e.entry_id == entry_id) else { + return Err(ApplyError::MoveMissing(*entry_id)); + }; + let e = entries.remove(i); + let at = (*to_position as usize).min(entries.len()); + entries.insert(at, e); + } + } + } + for (i, e) in entries.iter_mut().enumerate() { + e.position = i as u32; + } + Ok(entries) +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signer, SigningKey}; + + fn kp(seed: u8) -> SigningKey { + SigningKey::from_bytes(&[seed; 32]) + } + fn sign(key: &SigningKey, body: &[u8]) -> Vec { + key.sign(&crate::signable(SIGN_DOMAIN, &sign_payload(body))).to_bytes().to_vec() + } + fn entry(i: u32, title: &str) -> f3::EntryV1 { + let mut id = [0u8; 16]; + id[..4].copy_from_slice(&i.to_le_bytes()); + id[15] = 0xEE; + f3::EntryV1 { + entry_id: id, + position: i, + recording: f3::RecordingRef { + musicbrainz_recording_id: None, + isrcs: vec![], + deezer_id: Some(format!("dz{i}")), + apple_music_id: None, + title: title.into(), + artists: vec!["Artist".into()], + album: "Album".into(), + duration_ms: Some(180_000), + release_year: Some(2000), + }, + note: None, + slsk_hint: None, + } + } + fn checkpoint(owner: &SigningKey, epoch: u64, contributors: &[&SigningKey]) -> AuthorityCheckpoint { + let mut c = AuthorityCheckpoint { + epoch, + contributors: contributors.iter().map(|k| k.verifying_key().to_bytes()).collect(), + sig: vec![], + }; + c.sig = sign(owner, &checkpoint_sig_body(&c)); + c + } + fn proposal(author: &SigningKey, id: u8, epoch: u64, base: [u8; 32], created: u64) -> Proposal { + let mut p = Proposal { + proposal_id: [id; 16], + author: author.verifying_key().to_bytes(), + authority_epoch: epoch, + base_revision_hash: base, + ops: vec![ProposalOp::Add { position: 0, entry: entry(90 + id as u32, "Proposed") }], + note: "please add".into(), + created_at: created, + sig: vec![], + }; + p.sig = sign(author, &proposal_sig_body(&p)); + p + } + fn decision(owner: &SigningKey, pid: [u8; 16], accept: bool, seq: u64) -> Decision { + let mut d = Decision { + proposal_id: pid, + accept, + seq, + reason: if accept { "applied".into() } else { "no".into() }, + result_revision_hash: accept.then_some([7u8; 32]), + sig: vec![], + }; + d.sig = sign(owner, &decision_sig_body(&d)); + d + } + fn state(owner: &SigningKey, auth: Option, proposals: Vec, decisions: Vec) -> Vec { + bincode::serialize(&BoardState { + schema_version: SCHEMA_VERSION, + owner: owner.verifying_key().to_bytes(), + playlist_uuid: [7u8; 16], + authority: auth, + proposals, + decisions, + }) + .unwrap() + } + fn permute(items: &[usize], f: &mut dyn FnMut(&[usize])) { + fn go(cur: &mut Vec, rest: &mut Vec, f: &mut dyn FnMut(&[usize])) { + if rest.is_empty() { + f(cur); + return; + } + for i in 0..rest.len() { + let x = rest.remove(i); + cur.push(x); + go(cur, rest, f); + cur.pop(); + rest.insert(i, x); + } + } + go(&mut Vec::new(), &mut items.to_vec(), f); + } + + #[test] + fn merge_converges_over_permutations_and_is_idempotent() { + let owner = kp(1); + let opub = owner.verifying_key().to_bytes(); + let b = kp(2); + let c = kp(3); + let base = [5u8; 32]; + let cp1 = checkpoint(&owner, 1, &[&b, &c]); + let cp2 = checkpoint(&owner, 2, &[&b]); + let p_b = proposal(&b, 10, 1, base, 100); + let p_c = proposal(&c, 11, 1, base, 101); + let d_b = decision(&owner, [10u8; 16], true, 1); + let cands = vec![ + state(&owner, Some(cp1.clone()), vec![p_b.clone(), p_c.clone()], vec![]), + state(&owner, Some(cp2.clone()), vec![], vec![]), + state(&owner, Some(cp1.clone()), vec![p_c.clone()], vec![d_b.clone()]), + state(&owner, None, vec![p_b.clone()], vec![d_b.clone()]), + ]; + let uuid = [7u8; 16]; + let reference = { + let refs: Vec<&[u8]> = cands.iter().map(|v| v.as_slice()).collect(); + merge_board(&opub, &uuid, &refs) + }; + permute(&[0, 1, 2, 3], &mut |order| { + let refs: Vec<&[u8]> = order.iter().map(|&i| cands[i].as_slice()).collect(); + assert_eq!(merge_board(&opub, &uuid, &refs), reference, "diverged for {order:?}"); + }); + assert_eq!(reference.authority.as_ref().unwrap().epoch, 2); + assert!(reference.proposals.is_empty()); + assert_eq!(reference.decisions.len(), 1); + let rb = bincode::serialize(&reference).unwrap(); + assert_eq!(merge_board(&opub, &uuid, &[&rb, &rb]), reference); + } + + #[test] + fn revoked_author_drops_and_never_re_enters() { + let owner = kp(1); + let opub = owner.verifying_key().to_bytes(); + let b = kp(2); + let c = kp(3); + let uuid = [7u8; 16]; + let cp1 = checkpoint(&owner, 1, &[&b, &c]); + let cp2 = checkpoint(&owner, 2, &[&b]); + let p_c_old = proposal(&c, 20, 1, [5u8; 32], 100); + let with_c = state(&owner, Some(cp1), vec![p_c_old.clone()], vec![]); + let revoked = state(&owner, Some(cp2), vec![], vec![]); + let m = merge_board(&opub, &uuid, &[&with_c, &revoked]); + assert!(m.proposals.is_empty()); + let mb = bincode::serialize(&m).unwrap(); + let offer = state(&owner, None, vec![proposal(&c, 21, 2, [5u8; 32], 200)], vec![]); + assert!(merge_board(&opub, &uuid, &[&mb, &offer]).proposals.is_empty()); + let replay = state(&owner, Some(checkpoint(&owner, 1, &[&b, &c])), vec![p_c_old], vec![]); + let m3 = merge_board(&opub, &uuid, &[&mb, &replay]); + assert_eq!(m3.authority.as_ref().unwrap().epoch, 2); + assert!(m3.proposals.is_empty()); + } + + #[test] + fn forged_oversized_and_non_owner_decisions_never_enter() { + let owner = kp(1); + let opub = owner.verifying_key().to_bytes(); + let b = kp(2); + let mallory = kp(66); + let uuid = [7u8; 16]; + let cp = checkpoint(&owner, 1, &[&b]); + let mut forged = proposal(&mallory, 30, 1, [5u8; 32], 100); + forged.author = b.verifying_key().to_bytes(); + let mut big = Proposal { + proposal_id: [31u8; 16], + author: b.verifying_key().to_bytes(), + authority_epoch: 1, + base_revision_hash: [5u8; 32], + ops: (0..(MAX_OPS_PER_PROPOSAL as u32 + 1)).map(|i| ProposalOp::Add { position: i, entry: entry(i, "x") }).collect(), + note: String::new(), + created_at: 101, + sig: vec![], + }; + big.sig = sign(&b, &proposal_sig_body(&big)); + let good = proposal(&b, 32, 1, [5u8; 32], 102); + let fake_dec = decision(&mallory, [32u8; 16], true, 1); + let st = state(&owner, Some(cp), vec![forged, big, good], vec![fake_dec]); + let m = merge_board(&opub, &uuid, &[&st]); + assert_eq!(m.proposals.len(), 1); + assert_eq!(m.proposals[0].proposal_id, [32u8; 16]); + assert!(m.decisions.is_empty()); + } + + #[test] + fn flood_and_sybil_bounded_newest_kept() { + let owner = kp(1); + let opub = owner.verifying_key().to_bytes(); + let b = kp(2); + let uuid = [7u8; 16]; + let cp = checkpoint(&owner, 1, &[&b]); + let mut props: Vec = (0..20).map(|i| proposal(&b, 100 + i, 1, [5u8; 32], 1000 + i as u64)).collect(); + for s in 0..50u8 { + props.push(proposal(&kp(150 + s), 200 + s, 1, [5u8; 32], 5000)); + } + let st = state(&owner, Some(cp), props, vec![]); + let m = merge_board(&opub, &uuid, &[&st]); + assert_eq!(m.proposals.len(), PER_AUTHOR_PENDING_CAP); + assert!(m.proposals.iter().all(|p| p.created_at >= 1000 + 12)); + } + + #[test] + fn apply_composes_and_refuses_stale_or_missing() { + let b = kp(2); + let current = f3::ManifestV1 { + schema_version: f3::SCHEMA_VERSION, + owner: [1u8; 32], + playlist_uuid: [7u8; 16], + revision_seq: 1, + prev_revision_hash: None, + name: "n".into(), + description: String::new(), + artwork: None, + entries: vec![entry(0, "First"), entry(1, "Middle"), entry(2, "Last")], + sig: vec![], + }; + let hash = [9u8; 32]; + let mut p = Proposal { + proposal_id: [1u8; 16], + author: b.verifying_key().to_bytes(), + authority_epoch: 1, + base_revision_hash: hash, + ops: vec![ + ProposalOp::Add { position: 99, entry: entry(50, "Appended") }, + ProposalOp::Move { entry_id: entry(1, "").entry_id, to_position: 0 }, + ProposalOp::Remove { entry_id: entry(0, "").entry_id }, + ], + note: String::new(), + created_at: 1, + sig: vec![], + }; + p.sig = sign(&b, &proposal_sig_body(&p)); + let out = apply_proposal(¤t, &hash, &p).unwrap(); + let titles: Vec<&str> = out.iter().map(|e| e.recording.title.as_str()).collect(); + assert_eq!(titles, ["Middle", "Last", "Appended"]); + assert!(out.iter().enumerate().all(|(i, e)| e.position == i as u32)); + assert!(matches!(apply_proposal(¤t, &[8u8; 32], &p), Err(ApplyError::StaleBase { .. }))); + p.ops = vec![ProposalOp::Remove { entry_id: [0xAB; 16] }]; + assert!(matches!(apply_proposal(¤t, &hash, &p), Err(ApplyError::RemoveMissing(_)))); + } +} diff --git a/crates/proposal-contract/Cargo.lock b/crates/proposal-contract/Cargo.lock new file mode 100644 index 0000000..cd6f987 --- /dev/null +++ b/crates/proposal-contract/Cargo.lock @@ -0,0 +1,1449 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "freenet-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26e5c6bee9ffa0e2683bc8b41a7bd9f1e57e7fd49ac4f6ae433e5a82e9006df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "freenet-stdlib" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62aeba7634d1ae5c2d02f78db33ee40442511858275be5be2099d24a13b2559" +dependencies = [ + "bincode", + "blake3", + "bs58", + "byteorder", + "bytes", + "chrono", + "flatbuffers", + "freenet-macros", + "futures", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "persona-state" +version = "1.6.0-alpha" +dependencies = [ + "bincode", + "blake3", + "chacha20poly1305", + "ed25519-dalek", + "serde", + "x25519-dalek", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.1", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "soulstream-proposal-contract" +version = "0.1.0" +dependencies = [ + "bincode", + "blake3", + "freenet-stdlib", + "persona-state", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/proposal-contract/Cargo.toml b/crates/proposal-contract/Cargo.toml new file mode 100644 index 0000000..1a2d415 --- /dev/null +++ b/crates/proposal-contract/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "soulstream-proposal-contract" +version = "0.1.0" +edition = "2021" +description = "The SoulStream proposal-board contract — a thin shell over persona_state::proposals::merge_board. Built to wasm32-unknown-unknown and bundled into the app; its code hash is a commitment (see src-tauri/assets)." + +[lib] +crate-type = ["cdylib"] + +[dependencies] +freenet-stdlib = { version = "=0.8.5" } +persona-state = { path = "../persona-state" } +bincode = "1" +blake3 = "1" + +[features] +default = ["freenet-main-contract", "contract"] +freenet-main-contract = [] +contract = ["freenet-stdlib/contract"] +trace = ["freenet-stdlib/trace"] + +[profile.release] +strip = true +lto = true + +# Its own workspace: this crate targets wasm32 and must never join the +# app's host build. +[workspace] diff --git a/crates/proposal-contract/src/lib.rs b/crates/proposal-contract/src/lib.rs new file mode 100644 index 0000000..183b68e --- /dev/null +++ b/crates/proposal-contract/src/lib.rs @@ -0,0 +1,118 @@ +//! The SoulStream proposal-board contract. Multi-writer: the owner offers +//! checkpoints and decisions, contributors offer proposals; `persona_state::proposals::merge_board` +//! is the bouncer (owner signature, the current roster, decided-tombstones, +//! the caps). Deterministic by construction, so every delivery order +//! converges; a state that does not merge to itself is refused. +//! +//! Build: `cargo build --release --target wasm32-unknown-unknown` in this +//! directory; copy `target/wasm32-unknown-unknown/release/ +//! soulstream_proposal_contract.wasm` to `src-tauri/assets/proposal_contract.wasm` +//! and re-pin the code hash in `src-tauri/src/proposals.rs`. + +use freenet_stdlib::prelude::*; +use persona_state::proposals as t; + +pub struct Contract; + +fn parts(parameters: &Parameters<'static>) -> Result<([u8; 32], [u8; 16]), ContractError> { + t::params_parts(parameters.as_ref()) + .ok_or_else(|| ContractError::Other("params are not family+owner+uuid".into())) +} + +fn collect<'a>(state: &'a State<'static>, data: &'a [UpdateData<'static>]) -> (Vec<&'a [u8]>, bool) { + let mut candidates: Vec<&[u8]> = Vec::new(); + if !state.as_ref().is_empty() { + candidates.push(state.as_ref()); + } + let mut any_parse = false; + let mut push = |b: &'a [u8]| { + if !b.is_empty() { + any_parse |= bincode::deserialize::(b).is_ok(); + candidates.push(b); + } + }; + for ud in data { + match ud { + UpdateData::State(s) => push(s.as_ref()), + UpdateData::Delta(d) => push(d.as_ref()), + UpdateData::StateAndDelta { state, delta } => { + push(state.as_ref()); + push(delta.as_ref()); + } + _ => {} + } + } + (candidates, any_parse) +} + +#[contract] +impl ContractInterface for Contract { + fn validate_state( + parameters: Parameters<'static>, + state: State<'static>, + _related: RelatedContracts<'static>, + ) -> Result { + let (owner, uuid) = parts(¶meters)?; + let bytes = state.as_ref(); + if bytes.is_empty() { + return Ok(ValidateResult::Valid); + } + if bytes.len() > t::MAX_STATE_BYTES { + return Err(ContractError::Other(format!( + "board state {} bytes over cap {}", + bytes.len(), + t::MAX_STATE_BYTES + ))); + } + let merged = t::merge_board(&owner, &uuid, &[bytes]); + let canon = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?; + if canon != bytes { + return Err(ContractError::Other( + "state is not canonical (unsigned, off-roster, decided, or over a cap)".into(), + )); + } + Ok(ValidateResult::Valid) + } + + fn update_state( + parameters: Parameters<'static>, + state: State<'static>, + data: Vec>, + ) -> Result, ContractError> { + let (owner, uuid) = parts(¶meters)?; + let (candidates, any_parse) = collect(&state, &data); + if !any_parse && !data.is_empty() { + return Err(ContractError::InvalidUpdate); + } + let merged = t::merge_board(&owner, &uuid, &candidates); + let bytes = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?; + Ok(UpdateModification::valid(State::from(bytes))) + } + + fn summarize_state( + parameters: Parameters<'static>, + state: State<'static>, + ) -> Result, ContractError> { + let _ = parts(¶meters)?; + if state.as_ref().is_empty() { + return Ok(StateSummary::from(Vec::new())); + } + Ok(StateSummary::from(blake3::hash(state.as_ref()).as_bytes().to_vec())) + } + + fn get_state_delta( + parameters: Parameters<'static>, + state: State<'static>, + summary: StateSummary<'static>, + ) -> Result, ContractError> { + let _ = parts(¶meters)?; + if state.as_ref().is_empty() { + return Ok(StateDelta::from(Vec::new())); + } + if summary.as_ref() == blake3::hash(state.as_ref()).as_bytes() { + Ok(StateDelta::from(Vec::new())) + } else { + Ok(StateDelta::from(state.as_ref().to_vec())) + } + } +} diff --git a/src-tauri/assets/proposal_contract.wasm b/src-tauri/assets/proposal_contract.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ae4a77fc2735018626ba20019385c56864180467 GIT binary patch literal 369313 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

n)5G&xRYOMGC_-C$-t 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 z i64 { .unwrap_or(0) } -fn changed(app: &tauri::AppHandle) { +pub(crate) fn changed(app: &tauri::AppHandle) { let _ = app.emit("soulseek:playlists-changed", ()); } diff --git a/src-tauri/src/proposals.rs b/src-tauri/src/proposals.rs new file mode 100644 index 0000000..d22e85b --- /dev/null +++ b/src-tauri/src/proposals.rs @@ -0,0 +1,646 @@ +//! The proposal board (F7 → the app). A Space's pinned playlist gets a +//! multi-writer board beside it, keyed by (playlist owner, uuid). Members +//! propose bounded edits — add, remove, move — against the EXACT revision +//! they were looking at; the playlist's owner accepts or rejects, and every +//! decision is a signed line in the audit trail. Accepting applies the ops +//! to the owner's local playlist and publishes it as the next revision; the +//! decision then names that revision's hash. The canonical playlist never +//! lives in here. +//! +//! The roster is the Space's: when the playlist owner opens the board and +//! the contributors differ from the Space's members, a new epoch is signed. +//! Dropping a member is revocation — their pending proposals leave the board +//! on every node, deterministically. +//! +//! The contract is a bundled commitment (`assets/proposal_contract.wasm`, +//! built from crates/proposal-contract; its code hash is pinned by a test). + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use freenet_adapter::FreenetAdapter; +use library::{NewPlaylistItem, PlaylistItem}; +use persona_state::playlist as pl; +use persona_state::proposals as pr; +use serde::{Deserialize, Serialize}; +use tauri::Manager; + +use crate::persona; +use crate::pipeline::LibraryState; +use crate::playlists; +use crate::shared_playlists::{self, Published}; +use crate::slsk_link; +use crate::spaces; +use crate::state::AppState; + +const BOARD_WASM: &[u8] = include_bytes!("../assets/proposal_contract.wasm"); + +#[derive(Default, Clone, Serialize, Deserialize)] +struct BoardRecord { + #[serde(default)] + last_epoch: u64, + #[serde(default)] + last_seq: u64, +} + +#[derive(Default, Serialize, Deserialize)] +struct Ledger { + #[serde(default)] + boards: BTreeMap, +} + +// ---------------------------------------------------------------- views + +#[derive(Debug, Serialize)] +pub struct BoardEntry { + pub entry_id: String, + pub position: u32, + pub title: String, + pub artist: String, + pub album: String, + pub duration_secs: Option, +} + +#[derive(Debug, Serialize)] +pub struct OpView { + /// "add" | "remove" | "move" + pub kind: String, + pub position: Option, + pub entry_id: Option, + pub title: String, + pub artist: String, + /// False when a remove/move names an entry the current revision no + /// longer has (a stale proposal). + pub known: bool, +} + +#[derive(Debug, Serialize)] +pub struct ProposalView { + pub id: String, + pub author: String, + pub username: Option, + pub is_me: bool, + pub created_at: u64, + pub note: String, + pub base: String, + /// Its base is not the current revision: the owner can only reject it + /// and ask for a re-submit. + pub stale: bool, + pub ops: Vec, +} + +#[derive(Debug, Serialize)] +pub struct DecisionView { + pub proposal_id: String, + pub accept: bool, + pub seq: u64, + pub reason: String, + pub result_hash: Option, +} + +#[derive(Debug, Serialize)] +pub struct ContributorView { + pub key: String, + pub username: Option, + pub is_me: bool, +} + +#[derive(Debug, Serialize)] +pub struct BoardView { + pub owner: String, + pub uuid: String, + pub name: String, + pub revision_seq: u64, + /// The current revision's content hash (b58) — what a proposal binds to. + pub base: String, + pub withdrawn: bool, + pub entries: Vec, + /// The board contract exists on the network. + pub reachable: bool, + pub epoch: u64, + pub contributors: Vec, + pub i_am_owner: bool, + pub can_propose: bool, + pub proposals: Vec, + /// Newest first. + pub decisions: Vec, + pub pending_mine: u32, +} + +/// One op as the composer sends it. +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum OpInput { + Add { + position: u32, + title: String, + artist: String, + #[serde(default)] + album: String, + #[serde(default)] + duration_secs: Option, + #[serde(default)] + track_id: Option, + /// The composer's handle for this row ("new:3"), so a later remove + /// or move in the same proposal can name a track it just added. + #[serde(default)] + token: Option, + }, + Remove { + entry_id: String, + }, + Move { + entry_id: String, + to_position: u32, + }, +} + +// ---------------------------------------------------------------- helpers + +fn now_secs() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) +} +fn rand16() -> [u8; 16] { + use rand::RngCore; + let mut n = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut n); + n +} +fn b58(bytes: &[u8]) -> String { + bs58::encode(bytes).into_string() +} +fn key_of(s: &str) -> Result<[u8; 32], String> { + let v = bs58::decode(s.trim()).into_vec().map_err(|_| "not a base58 key")?; + v.try_into().map_err(|_| "a key is 32 bytes".to_string()) +} +fn id_of(s: &str) -> Result<[u8; 16], String> { + let v = bs58::decode(s.trim()).into_vec().map_err(|_| "not a base58 id")?; + v.try_into().map_err(|_| "an id is 16 bytes".to_string()) +} +fn cap(s: &str, max: usize) -> String { + if s.len() <= max { + return s.to_string(); + } + let mut end = max; + while !s.is_char_boundary(end) { + end -= 1; + } + s[..end].to_string() +} +fn board_id(owner: &[u8; 32], uuid: &[u8; 16]) -> String { + format!("{}/{}", b58(owner), b58(uuid)) +} + +fn path(app: &tauri::AppHandle) -> Result { + Ok(app.path().app_data_dir().map_err(|e| e.to_string())?.join("freenet").join("boards.json")) +} +fn load(app: &tauri::AppHandle) -> Ledger { + path(app) + .ok() + .and_then(|p| std::fs::read(p).ok()) + .and_then(|b| serde_json::from_slice(&b).ok()) + .unwrap_or_default() +} +fn save(app: &tauri::AppHandle, l: &Ledger) -> Result<(), String> { + let p = path(app)?; + if let Some(dir) = p.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + std::fs::write(&p, serde_json::to_vec_pretty(l).map_err(|e| e.to_string())?).map_err(|e| e.to_string()) +} + +async fn my_key(app: &tauri::AppHandle) -> Result<[u8; 32], String> { + let adapter = app.state::().freenet.clone(); + let info = persona::current(&adapter).await?.ok_or("create a SoulStream identity first")?; + key_of(&info.public_key) +} +async fn sign(app: &tauri::AppHandle, payload: Vec) -> Result, String> { + let adapter = app.state::().freenet.clone(); + persona::sign(&adapter, pr::SIGN_DOMAIN, payload).await +} + +async fn fetch_board(app: &tauri::AppHandle, owner: &[u8; 32], uuid: &[u8; 16]) -> Result<(pr::BoardState, bool), String> { + let adapter = app.state::().freenet.clone(); + let ids = FreenetAdapter::contract_ids(BOARD_WASM.to_vec(), &pr::make_params(owner, uuid)); + match adapter.contract_get(&ids.instance, false).await { + Ok(bytes) => Ok((pr::merge_board(owner, uuid, &[&bytes]), true)), + Err(e) if e.to_string().contains("not found") => Ok((pr::BoardState::default(), false)), + Err(e) => Err(e.to_string()), + } +} + +async fn offer_board(app: &tauri::AppHandle, owner: &[u8; 32], uuid: &[u8; 16], state: &pr::BoardState, first: bool) -> Result<(), String> { + let adapter = app.state::().freenet.clone(); + let bytes = bincode::serialize(state).map_err(|e| e.to_string())?; + let params = pr::make_params(owner, uuid); + if first { + adapter.contract_put(BOARD_WASM.to_vec(), ¶ms, bytes, false).await.map(|_| ()) + } else { + adapter.get_then_update(BOARD_WASM.to_vec(), ¶ms, bytes).await.map(|_| ()) + } + .map_err(|e| e.to_string()) +} + +fn partial(owner: &[u8; 32], uuid: &[u8; 16], authority: Option, proposals: Vec, decisions: Vec) -> pr::BoardState { + pr::BoardState { schema_version: pr::SCHEMA_VERSION, owner: *owner, playlist_uuid: *uuid, authority, proposals, decisions } +} + +/// What a Space pins, plus its roster — the board's owner and contributors. +struct Pinned { + owner: [u8; 32], + uuid: [u8; 16], + members: Vec<[u8; 32]>, +} + +async fn pinned_of(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &str) -> Result { + let owner = key_of(space_owner_b58)?; + let space_id = id_of(space_b58)?; + let (state, _) = spaces::fetch_state(app, &owner, &space_id).await?; + let c = state.checkpoint.ok_or("the Space has not reached this node yet")?; + let p = c.pinned.ok_or("this Space has no pinned playlist — a board lives beside one")?; + Ok(Pinned { owner: p.owner, uuid: p.uuid, members: c.members.iter().map(|m| m.key).collect() }) +} + +type Current = (pl::ManifestV1, [u8; 32]); + +fn view_of(app: &tauri::AppHandle, pin: &Pinned, current: Option<&Current>, board: &pr::BoardState, me: &[u8; 32], reachable: bool) -> BoardView { + let entries: Vec = current + .map(|(m, _)| { + m.entries + .iter() + .map(|e| BoardEntry { + entry_id: b58(&e.entry_id), + position: e.position, + title: e.recording.title.clone(), + artist: e.recording.artists.join(", "), + album: e.recording.album.clone(), + duration_secs: e.recording.duration_ms.map(|ms| ms / 1000), + }) + .collect() + }) + .unwrap_or_default(); + // A remove or move may name a row the same proposal added a step earlier. + let named = |p: &pr::Proposal, id: &[u8; 16]| -> Option<(String, String)> { + current + .and_then(|(m, _)| m.entries.iter().find(|e| &e.entry_id == id)) + .or_else(|| { + p.ops.iter().find_map(|op| match op { + pr::ProposalOp::Add { entry, .. } if &entry.entry_id == id => Some(entry), + _ => None, + }) + }) + .map(|e| (e.recording.title.clone(), e.recording.artists.join(", "))) + }; + let roster: Vec<[u8; 32]> = board.authority.as_ref().map(|a| a.contributors.clone()).unwrap_or_default(); + let proposals = board + .proposals + .iter() + .map(|p| ProposalView { + id: b58(&p.proposal_id), + author: b58(&p.author), + username: slsk_link::username_for_key(app, &b58(&p.author)), + is_me: &p.author == me, + created_at: p.created_at, + note: p.note.clone(), + base: b58(&p.base_revision_hash), + stale: current.map(|(_, h)| h != &p.base_revision_hash).unwrap_or(true), + ops: p + .ops + .iter() + .map(|op| match op { + pr::ProposalOp::Add { position, entry } => OpView { + kind: "add".into(), + position: Some(*position), + entry_id: None, + title: entry.recording.title.clone(), + artist: entry.recording.artists.join(", "), + known: true, + }, + pr::ProposalOp::Remove { entry_id } => { + let n = named(p, entry_id); + OpView { + kind: "remove".into(), + position: None, + entry_id: Some(b58(entry_id)), + title: n.as_ref().map(|x| x.0.clone()).unwrap_or_default(), + artist: n.as_ref().map(|x| x.1.clone()).unwrap_or_default(), + known: n.is_some(), + } + } + pr::ProposalOp::Move { entry_id, to_position } => { + let n = named(p, entry_id); + OpView { + kind: "move".into(), + position: Some(*to_position), + entry_id: Some(b58(entry_id)), + title: n.as_ref().map(|x| x.0.clone()).unwrap_or_default(), + artist: n.as_ref().map(|x| x.1.clone()).unwrap_or_default(), + known: n.is_some(), + } + } + }) + .collect(), + }) + .collect(); + let decisions = board + .decisions + .iter() + .rev() + .map(|d| DecisionView { + proposal_id: b58(&d.proposal_id), + accept: d.accept, + seq: d.seq, + reason: d.reason.clone(), + result_hash: d.result_revision_hash.as_ref().map(|h| b58(h)), + }) + .collect(); + BoardView { + owner: b58(&pin.owner), + uuid: b58(&pin.uuid), + name: current.map(|(m, _)| m.name.clone()).unwrap_or_default(), + revision_seq: current.map(|(m, _)| m.revision_seq).unwrap_or(0), + base: current.map(|(_, h)| b58(h)).unwrap_or_default(), + withdrawn: current.is_none(), + entries, + reachable, + epoch: board.authority.as_ref().map(|a| a.epoch).unwrap_or(0), + contributors: roster + .iter() + .map(|k| ContributorView { key: b58(k), username: slsk_link::username_for_key(app, &b58(k)), is_me: k == me }) + .collect(), + i_am_owner: &pin.owner == me, + can_propose: reachable && current.is_some() && roster.contains(me), + proposals, + decisions, + pending_mine: board.proposals.iter().filter(|p| &p.author == me).count() as u32, + } +} + +// ---------------------------------------------------------------- actions + +/// Read the board beside a Space's pinned playlist. The playlist's owner +/// also brings the roster in line with the Space's members here — a new +/// signed epoch whenever they differ. +pub async fn open(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &str) -> Result { + let me = my_key(app).await?; + let pin = pinned_of(app, space_owner_b58, space_b58).await?; + let current = shared_playlists::current_revision(app, &pin.owner, &pin.uuid).await?; + let (mut board, mut exists) = fetch_board(app, &pin.owner, &pin.uuid).await?; + if pin.owner == me { + let want: Vec<[u8; 32]> = pin.members.iter().copied().take(pr::MAX_CONTRIBUTORS).collect(); + let have: Vec<[u8; 32]> = board.authority.as_ref().map(|a| a.contributors.clone()).unwrap_or_default(); + let same = want.iter().collect::>() == have.iter().collect::>(); + if !same { + let bid = board_id(&pin.owner, &pin.uuid); + let mut l = load(app); + let rec = l.boards.get(&bid).cloned().unwrap_or_default(); + let epoch = board.authority.as_ref().map(|a| a.epoch).unwrap_or(0).max(rec.last_epoch) + 1; + let mut c = pr::AuthorityCheckpoint { epoch, contributors: want, sig: vec![] }; + c.sig = sign(app, pr::sign_payload(&pr::checkpoint_sig_body(&c))).await?; + let offer = partial(&pin.owner, &pin.uuid, Some(c), vec![], vec![]); + offer_board(app, &pin.owner, &pin.uuid, &offer, !exists).await?; + l.boards.insert(bid, BoardRecord { last_epoch: epoch, ..rec }); + save(app, &l)?; + let a = bincode::serialize(&board).map_err(|e| e.to_string())?; + let b = bincode::serialize(&offer).map_err(|e| e.to_string())?; + board = pr::merge_board(&pin.owner, &pin.uuid, &[&a, &b]); + exists = true; + log::info!("proposals: roster epoch {} for board {}", epoch, b58(&pin.uuid)); + } + } + Ok(view_of(app, &pin, current.as_ref(), &board, &me, exists)) +} + +/// A member proposes: the ops are dry-run against the exact base first, so +/// a broken composition never leaves this device. +pub async fn propose(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &str, base_b58: &str, ops: Vec, note: &str) -> Result { + let me = my_key(app).await?; + let pin = pinned_of(app, space_owner_b58, space_b58).await?; + if ops.is_empty() { + return Err("nothing proposed yet".into()); + } + if ops.len() > pr::MAX_OPS_PER_PROPOSAL { + return Err(format!("over {} changes in one proposal", pr::MAX_OPS_PER_PROPOSAL)); + } + let (manifest, hash) = shared_playlists::current_revision(app, &pin.owner, &pin.uuid) + .await? + .ok_or("the owner withdrew this playlist")?; + if b58(&hash) != base_b58.trim() { + return Err("the playlist moved to a newer revision while you were composing — reload the board and try again".into()); + } + let (board, exists) = fetch_board(app, &pin.owner, &pin.uuid).await?; + if !exists { + return Err("the owner has not opened this board yet".into()); + } + let auth = board.authority.as_ref().ok_or("the board has no roster yet")?; + if !auth.contributors.contains(&me) { + return Err("you are not on this board's roster — the owner refreshes it from the Space's members".into()); + } + let mut fresh: BTreeMap = BTreeMap::new(); + let resolve = |fresh: &BTreeMap, s: &str| -> Result<[u8; 16], String> { + match fresh.get(s.trim()) { + Some(id) => Ok(*id), + None => id_of(s), + } + }; + let mut pops = Vec::with_capacity(ops.len()); + for op in ops { + pops.push(match op { + OpInput::Add { position, title, artist, album, duration_secs, track_id, token } => { + let title = cap(title.trim(), pl::MAX_FIELD_BYTES); + let artist = cap(artist.trim(), pl::MAX_FIELD_BYTES); + if title.is_empty() { + return Err("an added track needs a title".into()); + } + let entry_id = rand16(); + if let Some(t) = token { + fresh.insert(t, entry_id); + } + pr::ProposalOp::Add { + position, + entry: pl::EntryV1 { + entry_id, + position, + recording: pl::RecordingRef { + musicbrainz_recording_id: None, + isrcs: vec![], + deezer_id: track_id.filter(|t| !t.is_empty()), + apple_music_id: None, + title: title.clone(), + artists: vec![artist.clone()], + album: cap(album.trim(), pl::MAX_FIELD_BYTES), + duration_ms: duration_secs.map(|s| s.saturating_mul(1000)), + release_year: None, + }, + note: None, + slsk_hint: Some(cap(&format!("{artist} {title}"), pl::MAX_FIELD_BYTES)), + }, + } + } + OpInput::Remove { entry_id } => pr::ProposalOp::Remove { entry_id: resolve(&fresh, &entry_id)? }, + OpInput::Move { entry_id, to_position } => pr::ProposalOp::Move { entry_id: resolve(&fresh, &entry_id)?, to_position }, + }); + } + let mut p = pr::Proposal { + proposal_id: rand16(), + author: me, + authority_epoch: auth.epoch, + base_revision_hash: hash, + ops: pops, + note: cap(note.trim(), pl::MAX_DESC_BYTES), + created_at: now_secs(), + sig: vec![], + }; + pr::apply_proposal(&manifest, &hash, &p).map_err(|e| e.to_string())?; + p.sig = sign(app, pr::sign_payload(&pr::proposal_sig_body(&p))).await?; + if !pr::proposal_valid(&p) { + return Err("the proposal did not validate (a size or field cap)".into()); + } + offer_board(app, &pin.owner, &pin.uuid, &partial(&pin.owner, &pin.uuid, None, vec![p], vec![]), false).await?; + log::info!("proposals: offered a proposal to board {}", b58(&pin.uuid)); + open(app, space_owner_b58, space_b58).await +} + +/// The owner decides. Accepting brings the local playlist to the proposed +/// order, publishes it as the next revision, and the decision names that +/// revision; rejecting records the reason. Either way it is signed. +pub async fn decide(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &str, proposal_b58: &str, accept: bool, reason: &str) -> Result { + let me = my_key(app).await?; + let pin = pinned_of(app, space_owner_b58, space_b58).await?; + if pin.owner != me { + return Err("only the playlist's owner decides".into()); + } + let pid = id_of(proposal_b58)?; + let (board, exists) = fetch_board(app, &pin.owner, &pin.uuid).await?; + if !exists { + return Err("the board has not reached this node".into()); + } + let p = board.proposals.iter().find(|p| p.proposal_id == pid).cloned().ok_or("that proposal is no longer pending")?; + let bid = board_id(&pin.owner, &pin.uuid); + let mut l = load(app); + let rec = l.boards.get(&bid).cloned().unwrap_or_default(); + let seq = board.decisions.iter().map(|d| d.seq).max().unwrap_or(0).max(rec.last_seq) + 1; + let mut reason = cap(reason.trim(), pr::MAX_REASON_BYTES); + let result = if accept { + let (manifest, hash) = shared_playlists::current_revision(app, &pin.owner, &pin.uuid) + .await? + .ok_or("the playlist is withdrawn — publish it again before accepting")?; + let entries = pr::apply_proposal(&manifest, &hash, &p).map_err(|e| e.to_string())?; + let published = apply_locally(app, &pin.uuid, &manifest, entries).await?; + if reason.is_empty() { + reason = format!("applied as revision {}", published.revision_seq); + } + Some(key_of(published.current_hash.as_deref().unwrap_or_default())?) + } else { + if reason.is_empty() { + reason = "declined".into(); + } + None + }; + let mut d = pr::Decision { proposal_id: pid, accept, seq, reason, result_revision_hash: result, sig: vec![] }; + d.sig = sign(app, pr::sign_payload(&pr::decision_sig_body(&d))).await?; + offer_board(app, &pin.owner, &pin.uuid, &partial(&pin.owner, &pin.uuid, None, vec![], vec![d]), false).await?; + l.boards.insert(bid, BoardRecord { last_seq: seq, ..rec }); + save(app, &l)?; + log::info!("proposals: decision {} ({}) on board {}", seq, if accept { "accept" } else { "reject" }, b58(&pin.uuid)); + open(app, space_owner_b58, space_b58).await +} + +/// Bring the owner's local playlist to `entries`, then publish it as the +/// next revision. Refused when the local copy has drifted from the base +/// revision — the exact-base rule holds on this side too. +async fn apply_locally(app: &tauri::AppHandle, uuid: &[u8; 16], base: &pl::ManifestV1, entries: Vec) -> Result { + let published = shared_playlists::published_for_uuid(app, &b58(uuid)).ok_or("this device did not publish that playlist")?; + let playlist_id = published.playlist_id; + let lib = app.state::(); + let l = lib.library.clone(); + let items: Vec = tauri::async_runtime::spawn_blocking(move || l.playlist_items(playlist_id).map_err(|e| e.to_string())) + .await + .map_err(|e| e.to_string())??; + let local_ids: Vec<[u8; 16]> = items.iter().map(|it| shared_playlists::entry_id_for(uuid, it.id)).collect(); + let base_ids: Vec<[u8; 16]> = base.entries.iter().map(|e| e.entry_id).collect(); + if local_ids != base_ids { + return Err(format!( + "your local copy of this playlist has changed since revision {} was published — publish it again, then ask for a re-submit", + base.revision_seq + )); + } + let uuid = *uuid; + let keep: BTreeSet<[u8; 16]> = entries.iter().map(|e| e.entry_id).collect(); + let remove: Vec = items.iter().zip(&local_ids).filter(|(_, id)| !keep.contains(*id)).map(|(it, _)| it.id).collect(); + let existing: BTreeSet<[u8; 16]> = local_ids.iter().copied().collect(); + let adds: Vec = entries + .iter() + .filter(|e| !existing.contains(&e.entry_id)) + .map(|e| NewPlaylistItem { + title: e.recording.title.clone(), + artist: e.recording.artists.join(", "), + album: Some(e.recording.album.clone()).filter(|a| !a.is_empty()), + duration_secs: e.recording.duration_ms.map(|ms| ms / 1000), + path: None, + art_path: None, + kind: String::new(), + source_line: e.slsk_hint.clone(), + start_ms: None, + note: e.note.clone(), + track_id: e.recording.deezer_id.clone(), + art_url: None, + }) + .collect(); + let before: BTreeSet = items.iter().map(|it| it.id).collect(); + let now = now_secs() as i64; + let l = lib.library.clone(); + let after: Vec = tauri::async_runtime::spawn_blocking(move || -> Result, String> { + if !remove.is_empty() { + l.remove_playlist_items(playlist_id, &remove, now).map_err(|e| e.to_string())?; + } + if !adds.is_empty() { + l.add_playlist_items(playlist_id, &adds, None, now).map_err(|e| e.to_string())?; + } + l.playlist_items(playlist_id).map_err(|e| e.to_string()) + }) + .await + .map_err(|e| e.to_string())??; + let mut fresh: VecDeque = after.iter().filter(|it| !before.contains(&it.id)).map(|it| it.id).collect(); + let by_entry: BTreeMap<[u8; 16], i64> = after.iter().map(|it| (shared_playlists::entry_id_for(&uuid, it.id), it.id)).collect(); + let mut ordered = Vec::with_capacity(entries.len()); + for e in &entries { + match by_entry.get(&e.entry_id) { + Some(id) => ordered.push(*id), + None => ordered.push(fresh.pop_front().ok_or("the added rows did not come back from the library")?), + } + } + let l = lib.library.clone(); + tauri::async_runtime::spawn_blocking(move || l.set_playlist_order(playlist_id, &ordered, now).map_err(|e| e.to_string())) + .await + .map_err(|e| e.to_string())??; + playlists::changed(app); + shared_playlists::publish(app, &lib, playlist_id).await +} + +// ---------------------------------------------------------------- commands + +#[tauri::command] +pub async fn space_board(app: tauri::AppHandle, owner: String, space: String) -> Result { + open(&app, &owner, &space).await +} +#[tauri::command] +pub async fn space_propose(app: tauri::AppHandle, owner: String, space: String, base: String, ops: Vec, note: String) -> Result { + propose(&app, &owner, &space, &base, ops, ¬e).await +} +#[tauri::command] +pub async fn space_decide(app: tauri::AppHandle, owner: String, space: String, proposal: String, accept: bool, reason: String) -> Result { + decide(&app, &owner, &space, &proposal, accept, &reason).await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contract_generation_is_pinned() { + let ids = FreenetAdapter::contract_ids(BOARD_WASM.to_vec(), &pr::make_params(&[7u8; 32], &[1u8; 16])); + assert_eq!(ids.code_hash, "3oBYSW1pP9GfX7v4Q1CtP2ikiKcc1yT48U7wxMnTTFho"); + } +} diff --git a/src-tauri/src/shared_playlists.rs b/src-tauri/src/shared_playlists.rs index 29954fd..3b37b14 100644 --- a/src-tauri/src/shared_playlists.rs +++ b/src-tauri/src/shared_playlists.rs @@ -98,6 +98,8 @@ struct ShareRef { #[derive(Debug, Serialize)] pub struct ShareEntry { + /// The occurrence id (b58) — what a proposal's remove or move names. + pub entry_id: String, pub position: u32, pub title: String, pub artist: String, @@ -245,12 +247,12 @@ pub async fn publish(app: &tauri::AppHandle, lib: &LibraryState, playlist_id: i6 .iter() .enumerate() .map(|(i, it)| pl::EntryV1 { - entry_id: rand16(), + entry_id: entry_id_for(&uuid, it.id), position: i as u32, recording: pl::RecordingRef { musicbrainz_recording_id: None, isrcs: vec![], - deezer_id: None, + deezer_id: it.track_id.clone().filter(|t| !t.is_empty()), apple_music_id: None, title: cap(&it.title, pl::MAX_FIELD_BYTES), artists: vec![cap(&it.artist, pl::MAX_FIELD_BYTES)], @@ -281,6 +283,7 @@ pub async fn publish(app: &tauri::AppHandle, lib: &LibraryState, playlist_id: i6 .contract_put(REV_WASM.to_vec(), &pl::make_rev_params(&owner, &hash), bytes.clone(), false) .await .map_err(|e| e.to_string())?; + cache_revision(app, &hash, &bytes); let pointer_seq = prev.as_ref().map(|p| p.pointer_seq + 1).unwrap_or(1); let mut p = pl::PointerState { schema_version: pl::SCHEMA_VERSION, @@ -385,10 +388,7 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R entries: vec![], }); }; - let rev_ids = FreenetAdapter::contract_ids(REV_WASM.to_vec(), &pl::make_rev_params(&owner, &r.content_hash)); - let rbytes = adapter.contract_get(&rev_ids.instance, false).await.map_err(|e| e.to_string())?; - let m = pl::validate_revision(&owner, &r.content_hash, &rbytes) - .map_err(|e| format!("revision failed verification: {e}"))?; + let (m, _) = revision_by_hash(app, &owner, &r.content_hash).await?; Ok(ShareView { owner_key: owner_b58.to_string(), playlist_uuid: uuid_b58.to_string(), @@ -401,6 +401,7 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R .entries .into_iter() .map(|e| ShareEntry { + entry_id: b58(&e.entry_id), position: e.position, title: e.recording.title, artist: e.recording.artists.join(", "), @@ -412,6 +413,79 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R }) } +/// The occurrence id a local row publishes under: derived from the playlist +/// uuid and the row id, so it survives re-publication — a proposal's remove +/// or move names the same row the owner has. +pub(crate) fn entry_id_for(uuid: &[u8; 16], item_id: i64) -> [u8; 16] { + let mut buf = Vec::with_capacity(48); + buf.extend_from_slice(b"soulstream.entry.v1"); + buf.extend_from_slice(uuid); + buf.extend_from_slice(&item_id.to_le_bytes()); + ps::content_hash(&buf)[..16].try_into().unwrap() +} + +fn revisions_dir(app: &tauri::AppHandle) -> Option { + app.path().app_data_dir().ok().map(|d| d.join("freenet").join("revisions")) +} + +/// Revisions are kept on disk by hash: the owner's accept path needs the +/// exact base bytes, and a network GET for them costs seconds. +fn cache_revision(app: &tauri::AppHandle, hash: &[u8; 32], bytes: &[u8]) { + if let Some(dir) = revisions_dir(app) { + let _ = std::fs::create_dir_all(&dir); + let _ = std::fs::write(dir.join(format!("{}.bin", b58(hash))), bytes); + } +} + +/// A revision by content hash — the local cache first, the network after — +/// verified against the owner's key either way. +pub(crate) async fn revision_by_hash( + app: &tauri::AppHandle, + owner: &[u8; 32], + hash: &[u8; 32], +) -> Result<(pl::ManifestV1, Vec), String> { + let cached = revisions_dir(app).and_then(|d| std::fs::read(d.join(format!("{}.bin", b58(hash)))).ok()); + let bytes = match cached { + Some(b) => b, + None => { + let adapter = app.state::().freenet.clone(); + let ids = FreenetAdapter::contract_ids(REV_WASM.to_vec(), &pl::make_rev_params(owner, hash)); + let b = adapter.contract_get(&ids.instance, false).await.map_err(|e| e.to_string())?; + cache_revision(app, hash, &b); + b + } + }; + let m = pl::validate_revision(owner, hash, &bytes).map_err(|e| format!("revision failed verification: {e}"))?; + Ok((m, bytes)) +} + +/// The current revision of (owner, uuid): the pointer, then the revision it +/// names. None when the pointer says withdrawn. +pub(crate) async fn current_revision( + app: &tauri::AppHandle, + owner: &[u8; 32], + uuid: &[u8; 16], +) -> Result, String> { + let adapter = app.state::().freenet.clone(); + let ptr_ids = FreenetAdapter::contract_ids(PTR_WASM.to_vec(), &pl::make_ptr_params(owner, uuid)); + let pbytes = adapter.contract_get(&ptr_ids.instance, false).await.map_err(|e| { + if e.to_string().contains("not found") { + "no such shared playlist on the network (or not reachable yet)".to_string() + } else { + e.to_string() + } + })?; + let p = pl::validate_pointer(owner, uuid, &pbytes).map_err(|e| format!("pointer failed verification: {e}"))?; + let Some(r) = p.current.filter(|_| !p.withdrawn) else { return Ok(None) }; + let (m, _) = revision_by_hash(app, owner, &r.content_hash).await?; + Ok(Some((m, r.content_hash))) +} + +/// The local playlist behind a published uuid, when this device published it. +pub(crate) fn published_for_uuid(app: &tauri::AppHandle, uuid_b58: &str) -> Option { + load(app).published.into_values().find(|p| p.uuid == uuid_b58) +} + /// A verified shared playlist staged through the app's own import path. pub async fn import( app: &tauri::AppHandle, diff --git a/src-tauri/src/spaces.rs b/src-tauri/src/spaces.rs index 10cde56..c420514 100644 --- a/src-tauri/src/spaces.rs +++ b/src-tauri/src/spaces.rs @@ -191,7 +191,7 @@ async fn sign(app: &tauri::AppHandle, payload: Vec) -> Result, Strin persona::sign(&adapter, sp::SIGN_DOMAIN, payload).await } -async fn fetch_state(app: &tauri::AppHandle, owner: &[u8; 32], space_id: &[u8; 16]) -> Result<(sp::SpaceState, bool), String> { +pub(crate) async fn fetch_state(app: &tauri::AppHandle, owner: &[u8; 32], space_id: &[u8; 16]) -> Result<(sp::SpaceState, bool), String> { let adapter = app.state::().freenet.clone(); let ids = FreenetAdapter::contract_ids(SPACE_WASM.to_vec(), &sp::make_params(owner, space_id)); match adapter.contract_get(&ids.instance, false).await { diff --git a/src/components/Icon.svelte b/src/components/Icon.svelte index 30dcfed..2df52c0 100644 --- a/src/components/Icon.svelte +++ b/src/components/Icon.svelte @@ -57,6 +57,7 @@ "chevron-left": '', "chevron-right": '', "chevron-down": '', + "chevron-up": '', // Lucide layers-2: its ink fills the viewBox symmetrically, so the // glyph centers correctly inside small pills (the 3-layer variant // rides high when trimmed). diff --git a/src/lib/api.js b/src/lib/api.js index 4ffce01..e6c91c6 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -182,6 +182,9 @@ export const spaceInvite = (owner, space, personaKey) => invoke("space_invite", export const spaceRemoveMember = (owner, space, personaKey) => invoke("space_remove_member", { owner, space, personaKey }); export const spaceSetPinned = (owner, space, playlistOwner, playlistUuid) => invoke("space_set_pinned", { owner, space, playlistOwner, playlistUuid }); export const spaceLeave = (owner, space) => invoke("space_leave", { owner, space }); +export const spaceBoard = (owner, space) => invoke("space_board", { owner, space }); +export const spacePropose = (owner, space, base, ops, note) => invoke("space_propose", { owner, space, base, ops, note }); +export const spaceDecide = (owner, space, proposal, accept, reason) => invoke("space_decide", { owner, space, proposal, accept, reason }); export const buddyList = () => invoke("buddy_list"); export const buddyAdd = (username) => invoke("buddy_add", { username }); export const buddyRemove = (username) => invoke("buddy_remove", { username }); diff --git a/src/lib/community.svelte.js b/src/lib/community.svelte.js index 5242e64..d23d87a 100644 --- a/src/lib/community.svelte.js +++ b/src/lib/community.svelte.js @@ -80,6 +80,9 @@ export const community = $state({ /** The open Space (space_open), or null. */ space: null, spaceError: "", + /** space_board: the proposal board beside the open Space's pinned playlist, or null. */ + board: null, + boardError: "", /** persona key → ProfileView | {none} | {error} | {pending}: other * people's signed profiles, read once per session when surfaced. */ remoteProfiles: {}, @@ -574,11 +577,48 @@ export const leaveSpace = (owner, space) => run("space:leave", async () => { await api.spaceLeave(owner, space); community.space = null; + community.board = null; await refreshSpaces(); }); +// ---- the proposal board --------------------------------------------------------- +/** Read the board beside the Space's pinned playlist; the playlist's owner also + * brings its roster in line with the members here. */ +export async function openBoard(owner, space) { + community.boardError = ""; + community.busy["board:" + space] = true; + try { + community.board = await api.spaceBoard(owner, space); + } catch (e) { + community.board = null; + community.boardError = String(e?.message ?? e); + } finally { + delete community.busy["board:" + space]; + } +} +async function boardRun(key, fn) { + community.busy[key] = true; + community.boardError = ""; + try { + return await fn(); + } catch (e) { + community.boardError = String(e?.message ?? e); + throw e; + } finally { + delete community.busy[key]; + } +} +export const proposeToBoard = (owner, space, base, ops, note) => + boardRun("board:propose", async () => { + community.board = await api.spacePropose(owner, space, base, ops, note); + }); +export const decideProposal = (owner, space, proposal, accept, reason) => + boardRun("board:decide", async () => { + community.board = await api.spaceDecide(owner, space, proposal, accept, reason); + }); + // Dev handle beside `__soulstream` (state.svelte.js): probes reach the store // through this, never through a bare import (which would be a second copy). if (import.meta.env.DEV) globalThis.__community = community; -if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace }; +if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, openBoard, proposeToBoard, decideProposal }; diff --git a/src/views/Community.svelte b/src/views/Community.svelte index 4cd8a82..4c33f9b 100644 --- a/src/views/Community.svelte +++ b/src/views/Community.svelte @@ -88,6 +88,9 @@ removeFromSpace, pinToSpace, leaveSpace, + openBoard, + proposeToBoard, + decideProposal, } from "../lib/community.svelte.js"; onMount(() => { @@ -495,6 +498,91 @@ untrack(() => sharePreview({ id: "pin:" + s0.pinned_uuid, owner_key: s0.pinned_owner, playlist_uuid: s0.pinned_uuid })); } }); + // ---- the proposal board (F7). The composer is a working copy of the pinned + // playlist that records each act as an op; the Rust side dry-runs the same + // ops against the exact base before anything is signed. + const bv = $derived(community.board); + const boardKey = $derived(sv ? `${sv.id}|${sv.pinned_uuid ?? ""}` : ""); + const boardBusy = $derived(!!(community.busy["board:propose"] || community.busy["board:decide"] || (sv && community.busy["board:" + sv.space_id]))); + let boardDraft = $state(null); + let boardQuery = $state(""); + let boardHits = $state([]); + let boardReason = $state({}); + let freshCount = 0; + let boardTimer; + $effect(() => { + const k = boardKey; + if (spaceTab !== "proposals" || !k || k.endsWith("|")) return; + untrack(() => { + boardDraft = null; + void openBoard(sv.owner, sv.space_id); + }); + }); + function applyOp(list, op) { + if (op.kind === "add") { + list.splice(Math.min(op.position, list.length), 0, { entry_id: op.token, title: op.title, artist: op.artist, album: op.album, duration_secs: op.duration_secs, fresh: true }); + return true; + } + const i = list.findIndex((e) => e.entry_id === op.entry_id); + if (i < 0) return false; + if (op.kind === "remove") { + list.splice(i, 1); + return true; + } + const [e] = list.splice(i, 1); + list.splice(Math.min(op.to_position, list.length), 0, e); + return true; + } + function startDraft() { + if (!bv) return; + boardDraft = { base: bv.base, list: bv.entries.map((e) => ({ ...e })), ops: [], note: "" }; + boardQuery = ""; + boardHits = []; + } + function draftApply(op) { + if (!boardDraft || boardDraft.ops.length >= 32) return; + if (applyOp(boardDraft.list, op)) boardDraft.ops.push(op); + } + function draftUndo() { + if (!boardDraft?.ops.length) return; + boardDraft.ops.pop(); + const list = bv.entries.map((e) => ({ ...e })); + for (const op of boardDraft.ops) applyOp(list, op); + boardDraft.list = list; + } + function draftAdd(h) { + draftApply({ kind: "add", position: boardDraft.list.length, title: h.title, artist: h.artist, album: h.album ?? "", duration_secs: h.duration_secs ?? null, token: `new:${++freshCount}` }); + boardQuery = ""; + boardHits = []; + } + function searchLibrary() { + clearTimeout(boardTimer); + const q = boardQuery.trim(); + if (q.length < 2) { + boardHits = []; + return; + } + boardTimer = setTimeout(async () => { + try { + boardHits = await api.libraryTrackSearch(q); + } catch { + boardHits = []; + } + }, 220); + } + async function sendDraft() { + if (!boardDraft?.ops.length || !sv) return; + const ops = boardDraft.ops.map((o) => (o.kind === "add" ? { kind: "add", position: o.position, title: o.title, artist: o.artist, album: o.album, duration_secs: o.duration_secs, token: o.token } : { ...o })); + try { + await proposeToBoard(sv.owner, sv.space_id, boardDraft.base, ops, boardDraft.note); + boardDraft = null; + } catch { + // boardError shows it + } + } + const decideOn = (p, accept) => + decideProposal(sv.owner, sv.space_id, p.id, accept, boardReason[p.id] || (!accept && p.stale ? "stale base — re-submit against the current revision" : "")).catch(() => {}); + const whoProposed = (p) => (p.is_me ? "you" : p.username ? display(p.username) : `SoulStream ID ${fpShort(p.author)}`); async function importPinned() { if (!sv?.pinned_uuid) return; const id = await shareImport({ id: "pin:" + sv.pinned_uuid, owner_key: sv.pinned_owner, playlist_uuid: sv.pinned_uuid, name: pinnedPreview?.name ?? "Pinned playlist" }); @@ -1106,7 +1194,7 @@ - Proposals · next + {#if community.spaceError}

{community.spaceError}

{/if} {#if spaceTab === "chat"} @@ -1158,6 +1246,102 @@ {/if}

Members are their SoulStream identities here — a joined Space is a linked identity. Removing someone is a new epoch{#if sv.private}, and in this private Space it also rotates the key: those who stay receive the new one sealed{/if}.

+ {:else if spaceTab === "proposals"} + {#if !sv.pinned_uuid} +
+

Proposals

+

A board lives beside a pinned playlist: members propose adds, removes and moves against the exact revision they see, and the playlist's owner decides. {#if spaceOwner}Pin one of your public playlists from the Chat tab and the board opens here.{:else}The owner has not pinned a playlist yet.{/if}

+
+ {:else if !bv && community.boardError} +
+

Proposals

+

{community.boardError}

+
+
+ {:else if !bv} +

Reading the board through your node…

+ {:else} +
+

{bv.name || "Pinned playlist"}{#if bv.withdrawn}withdrawn by its owner{:else}revision {bv.revision_seq}{/if}

+

Proposals bind to revision {bv.revision_seq} exactly · {bv.i_am_owner ? "you decide" : "the playlist's owner decides"} · every decision is signed · {bv.contributors.length} {bv.contributors.length === 1 ? "contributor" : "contributors"} on epoch {bv.epoch}{#if !bv.reachable} · the owner has not opened the board yet{/if}

+
+ {#if bv.can_propose && !boardDraft}{/if} + {#if !bv.can_propose && !bv.i_am_owner && bv.reachable && !bv.withdrawn}You are not on this board's roster yet — the owner refreshes it from the members when they open the board.{/if} + +
+ {#if community.boardError}

{community.boardError}

{/if} +
+ {#if boardDraft} +
+

Your proposal {boardDraft.ops.length} / 32 changes

+

Move rows, take some out, add from your library. The owner receives titles, artists and albums — never a file, never a path. Nothing here plays or downloads.

+
    + {#each boardDraft.list as e, i (e.entry_id)} +
  1. + {i + 1} + {e.title}{e.artist}{#if e.fresh} · added{/if} + + + + + +
  2. + {/each} + {#if !boardDraft.list.length}
  3. Empty — add something below.
  4. {/if} +
+
+ {#if boardHits.length} +
{#each boardHits.slice(0, 8) as h (h.path)}{/each}
+ {/if} +
+
+ + + +
+
+ {/if} +
+

Pending {bv.proposals.length}

+ {#if !bv.proposals.length}

Nothing pending. {#if bv.can_propose}Propose an add, a remove or a move against the current revision — up to 32 changes each, 8 pending per person.{/if}

{/if} + {#each bv.proposals as p (p.id)} +
+
+ +
{whoProposed(p)}{fmtWhen(p.created_at * 1000)} · {p.ops.length} {p.ops.length === 1 ? "change" : "changes"}
+ {#if p.stale}stale · an older revision{:else}current revision{/if} +
+ {#if p.note}

{p.note}

{/if} +
    + {#each p.ops as op, i (i)} +
  • + {op.kind === "add" ? "+" : op.kind === "remove" ? "−" : "↕"} + + {#if op.kind === "add"}add {op.title} — {op.artist} at #{op.position + 1} + {:else if op.kind === "remove"}remove {#if op.known}{op.title} — {op.artist}{:else}an entry from an older revision{/if} + {:else}move {#if op.known}{op.title}{:else}an entry from an older revision{/if} to #{op.position + 1}{/if} + +
  • + {/each} +
+ {#if bv.i_am_owner} +
+ {#if !p.stale}{/if} + + +
+ {/if} +
+ {/each} +
+
+

Decisions {bv.decisions.length}

+ {#if !bv.decisions.length}

None yet. Each decision is owner-signed with a sequence number; the last 256 are kept.

{/if} + {#each bv.decisions as d (d.seq)} +
#{d.seq} · {d.accept ? "accepted" : "rejected"}{d.reason}{#if d.result_hash}→ revision {fpShort(d.result_hash)}{/if}
+ {/each} +
+ {/if} {:else}

About

@@ -1861,6 +2045,42 @@ .sphead .nm { font-size: 20px; font-weight: 800; display: flex; align-items: center; gap: 10px; } .sphead .sub { font-size: 12px; color: var(--text-muted); margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; max-width: none; } .src.space { background: color-mix(in srgb, var(--space) 14%, transparent); color: #d9a6ff; } + /* the proposal board */ + .bbtns { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; } + .dlist { list-style: none; margin: 10px 0; padding: 0; display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow: auto; } + .dlist li { display: grid; grid-template-columns: 26px 1fr auto; gap: 8px; align-items: center; padding: 5px 8px; border-radius: 8px; font-size: 12.5px; } + .dlist li:hover { background: var(--surf-hi); } + .dlist li.fresh { background: var(--accent-soft); } + .dlist li.none { display: block; color: var(--text-muted); font-size: 11.5px; } + .dlist .n { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; } + .dlist .tx { display: flex; flex-direction: column; min-width: 0; } + .dlist .t { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .dlist .a { font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .dlist .b { display: flex; gap: 2px; } + .hits { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; } + .hit { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; text-align: left; font-size: 12.5px; color: var(--text-secondary); min-width: 0; } + .hit:hover { background: var(--surf-hi); color: #fff; } + .hit .t { font-weight: 700; white-space: nowrap; } + .hit .a { font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .prop { padding: 12px 12px 10px; border: 1px solid var(--hair); border-radius: 10px; margin-top: 10px; } + .prop.stale { opacity: 0.8; border-style: dashed; } + .prop .ph { display: flex; align-items: center; gap: 10px; } + .prop .who { display: flex; flex-direction: column; min-width: 0; } + .prop .who .nm { font-weight: 800; font-size: 13px; } + .prop .ph .chip { margin-left: auto; } + .prop .note { margin: 8px 0 0; font-size: 12.5px; color: var(--text-secondary); font-style: italic; } + .ops { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; } + .ops li { display: flex; gap: 8px; align-items: baseline; } + .ops li b { width: 14px; text-align: center; font-weight: 900; flex: none; } + .ops li.add b { color: #5ee89a; } + .ops li.remove b { color: var(--err); } + .ops li.move b { color: var(--accent-hover); } + .ops i { font-style: normal; font-weight: 700; } + .decide { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; } + .decide input { flex: 1; min-width: 160px; height: 30px; padding: 0 10px; border-radius: 8px; background: var(--surf-hi); border: none; color: var(--text-primary); font-size: 12px; outline: none; } + .dec { display: flex; gap: 10px; align-items: center; padding: 7px 0; border-top: 1px solid var(--hair); font-size: 12.5px; } + .dec:first-of-type { border-top: none; } + .dec .rs { flex: 1; } .sptabs { display: flex; gap: 2px; margin: 12px 0 14px; align-items: center; } .subtab { padding: 6px 13px; border-radius: 8px; font-size: 12px; font-weight: 800; color: var(--text-secondary); display: inline-flex; gap: 6px; align-items: center; } .subtab.on { background: var(--surf-hi); color: #fff; }