Unlinking a device is one flow in the core: link::unlink

The desktop's sync_unlink and the ffi's unlink were both written out in full: try
the revoke, clear the link either way, and log the outcome. link::unlink(db, held)
now does that. Each client reads its link with state::credentials (with its seal)
before the await and passes it in.

DRY pass #2, batch 1, F3 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:16:59 -04:00
co-authored by Claude Opus 5.5
parent 91c47245ab
commit 1f2ddd70d3
3 changed files with 31 additions and 27 deletions
+24 -2
View File
@@ -1,4 +1,4 @@
//! Linking a device to a server.
//! Linking a device to a server, and unlinking it.
//!
//! One flow for every client. The desktop and Android each wrote it out, the same
//! steps in the same order; what differs between them is only how the token is
@@ -6,9 +6,10 @@
use rusqlite::Connection;
use super::client::{self, Identity};
use super::client::{self, Identity, RevokeOutcome};
use super::compat::Compatibility;
use super::state::{self, TokenSeal};
use crate::local::Db;
/// How a device proves whose it is.
pub enum Credential<'a> {
@@ -88,3 +89,24 @@ pub fn store(
log::info!("linked to {base_url}");
Ok(())
}
/// Stop syncing, and retire this device's token on the server.
///
/// `held` is the link as `state::credentials` read it, which the caller reads and
/// releases before this awaits: a std MutexGuard isn't Send, and holding the store
/// through a round-trip would freeze every note operation. None skips the revoke.
///
/// The local half is unconditional. Someone unlinking because the device is being
/// sold or handed on must not be held to it by a server that is offline or gone,
/// so the revoke is tried first, its outcome returned for the UI to report
/// honestly, and the link cleared either way.
pub async fn unlink(db: &Db, held: Option<(String, String)>) -> Result<RevokeOutcome, String> {
let revoked = match &held {
Some((base_url, token)) => client::revoke_self(base_url, token).await,
None => RevokeOutcome::Skipped,
};
let conn = db.conn()?;
state::clear_link(&conn).map_err(|e| e.to_string())?;
log::info!("unlinked from server (server-side token: {revoked:?})");
Ok(revoked)
}