feat(fof-layer1): Tauri commands + frontend UI for vouches

Node helpers (crates/core/src/node.rs):
- vouch_for_peer(target): derives target X25519 pub from NodeId,
  inserts into own_vouch_targets, republishes bio post so the new
  VouchGrantBatch propagates to the receiver via CDN.
- revoke_vouch_and_rotate(target): per Scott's design, revocation IS
  the rotation primitive. Marks target current=0, generates new V_me
  epoch in vouch_keys_own (prior retained, Layer 4 receiver-chain),
  republishes bio. Revoked persona retains old V_me → grandfathered
  access to old content; locked out of new content sealed under V_new.
- list_vouches_given / list_vouches_received: enriched with display
  names via resolve_display_name.

Tauri commands (crates/tauri-app/src/lib.rs):
- vouch_for_peer, revoke_vouch_for_peer (single-action commands)
- list_vouches_given, list_vouches_received (DTOs with camelCase)
- All registered in the generate_handler! list.

Frontend (frontend/index.html + app.js):
- New "Vouches" section in Settings tab with side-by-side Given /
  Received lists. Per-row Revoke button on Given entries with confirm
  prompt explaining the rotation semantics.
- Bio modal gains Vouch / Revoke Vouch button next to Follow/Unfollow.
  State derived from list_vouches_given on modal open.
- loadVouches wired into the settings-tab activation handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-13 06:47:18 -04:00
parent d1afcec26a
commit 34c5b60686
4 changed files with 291 additions and 1 deletions

View file

@ -1089,6 +1089,62 @@ async fn list_ignored_peers(state: State<'_, AppNode>) -> Result<Vec<IgnoredPeer
Ok(out)
}
// --- FoF Layer 1: Vouches ---
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct VouchGivenDto {
node_id: String,
display_name: String,
granted_at_ms: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct VouchReceivedDto {
node_id: String,
display_name: String,
epoch: u32,
received_at_ms: u64,
}
#[tauri::command]
async fn vouch_for_peer(state: State<'_, AppNode>, node_id_hex: String) -> Result<(), String> {
let node = get_node(&state).await;
let nid = parse_node_id(&node_id_hex)?;
node.vouch_for_peer(&nid).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn revoke_vouch_for_peer(state: State<'_, AppNode>, node_id_hex: String) -> Result<(), String> {
let node = get_node(&state).await;
let nid = parse_node_id(&node_id_hex)?;
node.revoke_vouch_and_rotate(&nid).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn list_vouches_given(state: State<'_, AppNode>) -> Result<Vec<VouchGivenDto>, String> {
let node = get_node(&state).await;
let rows = node.list_vouches_given().await.map_err(|e| e.to_string())?;
Ok(rows.into_iter().map(|(nid, name, at)| VouchGivenDto {
node_id: hex::encode(nid),
display_name: name,
granted_at_ms: at,
}).collect())
}
#[tauri::command]
async fn list_vouches_received(state: State<'_, AppNode>) -> Result<Vec<VouchReceivedDto>, String> {
let node = get_node(&state).await;
let rows = node.list_vouches_received().await.map_err(|e| e.to_string())?;
Ok(rows.into_iter().map(|(nid, name, epoch, at)| VouchReceivedDto {
node_id: hex::encode(nid),
display_name: name,
epoch,
received_at_ms: at,
}).collect())
}
#[tauri::command]
async fn list_follows(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
let node = get_node(&state).await;
@ -3103,6 +3159,10 @@ pub fn run() {
ignore_peer,
unignore_peer,
list_ignored_peers,
vouch_for_peer,
revoke_vouch_for_peer,
list_vouches_given,
list_vouches_received,
list_circles,
create_circle,
delete_circle,