spaces: a member wears a key minted for that room alone, so a roster stops naming personas

This commit is contained in:
KeelrLeek committed 2026-09-03 13:06:32 -05:00
1 parent b3c121fa94
commit 67e52697b2
6 files changed
+462 -58

No files matched your search

+33 -9
View File
@@ -271,7 +271,24 @@ async fn pinned_of(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &st
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 {
#[allow(clippy::too_many_arguments)]
fn view_of(
app: &tauri::AppHandle,
pin: &Pinned,
current: Option<&Current>,
board: &pr::BoardState,
me: &[u8; 32],
worn: &[u8; 32],
space: (&str, &str),
reachable: bool,
) -> BoardView {
// A room key has a name only on the device that seated it — the Space's
// owner. Everyone else sees the key, which is what unlinkable means.
let name_of = |k: &[u8; 32]| -> Option<String> {
let b = b58(k);
let persona = spaces::persona_behind(app, space.0, space.1, &b).unwrap_or(b);
slsk_link::username_for_key(app, &persona)
};
let entries: Vec<BoardEntry> = current
.map(|(m, _)| {
m.entries
@@ -306,8 +323,8 @@ fn view_of(app: &tauri::AppHandle, pin: &Pinned, current: Option<&Current>, boar
.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,
username: name_of(&p.author),
is_me: &p.author == worn,
created_at: p.created_at,
note: p.note.clone(),
base: b58(&p.base_revision_hash),
@@ -374,10 +391,10 @@ fn view_of(app: &tauri::AppHandle, pin: &Pinned, current: Option<&Current>, boar
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 })
.map(|k| ContributorView { key: b58(k), username: name_of(k), is_me: k == worn })
.collect(),
i_am_owner: &pin.owner == me,
can_propose: reachable && current.is_some() && roster.contains(me),
can_propose: reachable && current.is_some() && roster.contains(worn),
proposals,
decisions,
pending_mine: board.proposals.iter().filter(|p| &p.author == me).count() as u32,
@@ -417,7 +434,8 @@ pub async fn open(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &str
log::info!("proposals: roster epoch {} for board {}", epoch, b58(&pin.uuid));
}
}
Ok(view_of(app, &pin, current.as_ref(), &board, &me, exists))
let (worn, _) = spaces::worn_identity(app, space_owner_b58, space_b58, &me);
Ok(view_of(app, &pin, current.as_ref(), &board, &me, &worn, (space_owner_b58, space_b58), exists))
}
/// A member proposes: the ops are dry-run against the exact base first, so
@@ -441,8 +459,11 @@ pub async fn propose(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &
if !exists {
return Err("the owner has not opened this board yet".into());
}
// The board's roster is the Space's roster, so we appear on it as the key
// we wear there — a room-local one unless the Space is ours.
let (worn, room) = spaces::worn_identity(app, space_owner_b58, space_b58, &me);
let auth = board.authority.as_ref().ok_or("the board has no roster yet")?;
if !auth.contributors.contains(&me) {
if !auth.contributors.contains(&worn) {
return Err("you are not on this board's roster — the owner refreshes it from the Space's members".into());
}
let mut fresh: BTreeMap<String, [u8; 16]> = BTreeMap::new();
@@ -492,7 +513,7 @@ pub async fn propose(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &
}
let mut p = pr::Proposal {
proposal_id: rand16(),
author: me,
author: worn,
authority_epoch: auth.epoch,
base_revision_hash: hash,
ops: pops,
@@ -501,7 +522,10 @@ pub async fn propose(app: &tauri::AppHandle, space_owner_b58: &str, space_b58: &
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?;
p.sig = match room {
Some(secret) => persona_state::sign_local(pr::SIGN_DOMAIN, &secret, &pr::proposal_sig_body(&p)),
None => 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());
}