diff --git a/crates/tauri-app/src/lib.rs b/crates/tauri-app/src/lib.rs index a1b373a..6246241 100644 --- a/crates/tauri-app/src/lib.rs +++ b/crates/tauri-app/src/lib.rs @@ -1228,6 +1228,57 @@ async fn read_fof_closed_body( node.read_fof_closed_body(&pid).await.map_err(|e| e.to_string()) } +// FoF Layer 4: V_me lifecycle + cascade + key-burn. + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct VmeRotatedDto { + new_epoch: u32, +} + +#[tauri::command] +async fn rotate_v_me(state: State<'_, AppNode>) -> Result { + let node = get_node(&state).await; + let new_epoch = node.rotate_v_me().await.map_err(|e| e.to_string())?; + Ok(VmeRotatedDto { new_epoch }) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CascadeRevokeResultDto { + posts_revoked: usize, +} + +#[tauri::command] +async fn cascade_revoke_v_me_epoch( + state: State<'_, AppNode>, + retired_epoch: u32, + reason_code: u8, +) -> Result { + let node = get_node(&state).await; + let n = node.cascade_revoke_v_me_epoch(retired_epoch, reason_code) + .await + .map_err(|e| e.to_string())?; + Ok(CascadeRevokeResultDto { posts_revoked: n }) +} + +#[tauri::command] +async fn key_burn_post_slot( + state: State<'_, AppNode>, + post_id_hex: String, + slot_index: u32, + new_v_x_hex: String, +) -> Result<(), String> { + let node = get_node(&state).await; + let pid = parse_node_id(&post_id_hex)?; + let new_v_x_bytes = hex::decode(&new_v_x_hex) + .map_err(|e| format!("invalid new_v_x hex: {}", e))?; + let new_v_x: [u8; 32] = new_v_x_bytes.as_slice().try_into() + .map_err(|_| "new_v_x must be 32 bytes".to_string())?; + node.key_burn_post_slot(pid, slot_index, &new_v_x).await + .map_err(|e| e.to_string()) +} + #[tauri::command] async fn list_follows(state: State<'_, AppNode>) -> Result, String> { let node = get_node(&state).await; @@ -3251,6 +3302,9 @@ pub fn run() { revoke_fof_commenter, create_post_fof_closed, read_fof_closed_body, + rotate_v_me, + cascade_revoke_v_me_epoch, + key_burn_post_slot, list_circles, create_circle, delete_circle, diff --git a/frontend/app.js b/frontend/app.js index 5194405..1840b8e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3154,7 +3154,16 @@ document.querySelectorAll('.tab').forEach(tab => { loadMessages(true); loadDmRecipientOptions(); clearNotifications('msg-'); } - if (target === 'settings') { loadIdentities(); loadPersonas(); loadRedundancy(); loadPublicVisible(); loadCacheSizeSetting(); loadUpdateSettings(); loadIgnored(); loadVouches(); } + if (target === 'settings') { + loadIdentities(); loadPersonas(); loadRedundancy(); loadPublicVisible(); + loadCacheSizeSetting(); loadUpdateSettings(); loadIgnored(); loadVouches(); + // FoF Layer 4: rotate button wires once per settings open. + const rotateBtn = document.getElementById('rotate-v-me-btn'); + if (rotateBtn && !rotateBtn._wired) { + rotateBtn.addEventListener('click', rotateVMe); + rotateBtn._wired = true; + } + } }); }); }); @@ -3710,6 +3719,26 @@ async function loadIgnored() { } } +async function rotateVMe() { + const btn = document.getElementById('rotate-v-me-btn'); + const status = document.getElementById('rotate-v-me-status'); + if (!btn) return; + if (!confirm('Rotate your vouch key? A new key will be issued to all current vouchees via your next bio post. Old posts remain readable to anyone who held the old key — cascade-revoke separately if you want to cut off old-content access.')) return; + btn.disabled = true; + status.textContent = 'Rotating…'; + try { + const { newEpoch } = await invoke('rotate_v_me'); + status.textContent = `Rotated → epoch ${newEpoch}`; + toast(`Vouch key rotated to epoch ${newEpoch}`); + loadVouches(); + } catch (e) { + status.textContent = ''; + toast('Error: ' + e); + } finally { + btn.disabled = false; + } +} + async function loadVouches() { const givenEl = document.getElementById('vouches-given-list'); const recvEl = document.getElementById('vouches-received-list'); diff --git a/frontend/index.html b/frontend/index.html index 82e2b31..e415fd8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -213,6 +213,11 @@
+
+

Rotate your vouch key (V_me) to issue a fresh key to all your current vouchees. Old posts remain readable by anyone who held the old key — use "Cascade revoke" afterward to actively cut off comment access to your old posts.

+ + +