feat(fof-layer4): Tauri commands + Settings "Rotate my vouch key" UI
Three new Tauri commands surface Layer 4 to the frontend:
- rotate_v_me() -> { newEpoch }: generates next V_me epoch +
republishes bio. Old epoch retained in vouch_keys_own; existing
vouchees receive the new key on their next bio-scan.
- cascade_revoke_v_me_epoch(retired_epoch, reason_code) ->
{ postsRevoked }: bulk per-post revocation across every author
post that sealed slots under (self, retired_epoch). Useful as a
follow-up after rotate_v_me when the author wants to actively
cut off comment access on old posts.
- key_burn_post_slot(post_id_hex, slot_index, new_v_x_hex): the
leaked-V_me primitive. Re-seals one slot under a different V_x.
Frontend (Settings → Vouches):
- New "Rotate my vouch key" button + status pill below the Given /
Received lists.
- Confirmation prompt explains the grandfather-by-default semantics:
"old posts remain readable to anyone who held the old key — cascade-
revoke separately if you want to cut off old-content access."
- Wired once per settings-tab activation.
Cascade-revoke and key-burn surfaces aren't visualized yet (require
per-post selection UI); the Tauri commands are available for follow-up
UI work or scripting via the desktop dev console.
Workspace builds clean; 148 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fdbf97f2d7
commit
ce710a6596
3 changed files with 89 additions and 1 deletions
|
|
@ -1228,6 +1228,57 @@ async fn read_fof_closed_body(
|
||||||
node.read_fof_closed_body(&pid).await.map_err(|e| e.to_string())
|
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<VmeRotatedDto, String> {
|
||||||
|
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<CascadeRevokeResultDto, String> {
|
||||||
|
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]
|
#[tauri::command]
|
||||||
async fn list_follows(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
async fn list_follows(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
||||||
let node = get_node(&state).await;
|
let node = get_node(&state).await;
|
||||||
|
|
@ -3251,6 +3302,9 @@ pub fn run() {
|
||||||
revoke_fof_commenter,
|
revoke_fof_commenter,
|
||||||
create_post_fof_closed,
|
create_post_fof_closed,
|
||||||
read_fof_closed_body,
|
read_fof_closed_body,
|
||||||
|
rotate_v_me,
|
||||||
|
cascade_revoke_v_me_epoch,
|
||||||
|
key_burn_post_slot,
|
||||||
list_circles,
|
list_circles,
|
||||||
create_circle,
|
create_circle,
|
||||||
delete_circle,
|
delete_circle,
|
||||||
|
|
|
||||||
|
|
@ -3154,7 +3154,16 @@ document.querySelectorAll('.tab').forEach(tab => {
|
||||||
loadMessages(true); loadDmRecipientOptions();
|
loadMessages(true); loadDmRecipientOptions();
|
||||||
clearNotifications('msg-');
|
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() {
|
async function loadVouches() {
|
||||||
const givenEl = document.getElementById('vouches-given-list');
|
const givenEl = document.getElementById('vouches-given-list');
|
||||||
const recvEl = document.getElementById('vouches-received-list');
|
const recvEl = document.getElementById('vouches-received-list');
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,11 @@
|
||||||
<div id="vouches-received-list"></div>
|
<div id="vouches-received-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:0.75rem;padding-top:0.75rem;border-top:1px solid var(--border)">
|
||||||
|
<p class="empty-hint" style="margin-bottom:0.4rem">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.</p>
|
||||||
|
<button id="rotate-v-me-btn" class="btn btn-ghost btn-sm">Rotate my vouch key</button>
|
||||||
|
<span id="rotate-v-me-status" class="empty-hint" style="margin-left:0.5rem;font-size:0.8rem"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-card" style="text-align:center">
|
<div class="section-card" style="text-align:center">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue