feat: dial hints on the account-bound operations for self-resolving hosts

This commit is contained in:
randogoth 2026-09-29 12:48:40 +03:00
parent 5559fcce31
commit 28f13641a3
3 changed files with 45 additions and 23 deletions

View file

@ -340,7 +340,7 @@ fn restore(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<
fn rotate(key: &PathBuf, store: &Store, timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let rotated = client::rotate(store, &account, timeout)?;
let rotated = client::rotate(store, &account, None, timeout)?;
let addr = store.account()?.expect("rotate checked for an account");
println!("rotated {}", addr.short());
println!("new key: {}", b32(&rotated.new_pk));
@ -509,7 +509,7 @@ fn accept(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<(
}
store.accept(&address, &identity)?;
println!("accepted {address}; its token travels in your next message to them");
match client::push_tokens(store, &account, timeout)? {
match client::push_tokens(store, &account, None, timeout)? {
Pushed::NotRegistered => {
warn("not registered; the set will be pushed with your first fetch")
}
@ -525,7 +525,7 @@ fn block(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<()
return Err(anyhow!(format!("{address} was never accepted")));
}
println!("blocked {address}; their mail lands in requests from their next message on");
match client::push_tokens(store, &account, timeout)? {
match client::push_tokens(store, &account, None, timeout)? {
Pushed::NotRegistered => {
warn("not registered; the set will be pushed with your first fetch")
}
@ -711,7 +711,7 @@ fn resolve_id_prefixes(store: &Store, ids: &[String]) -> Result<Vec<[u8; ID_LEN]
fn delete(key: &PathBuf, store: &Store, ids: &[String], timeout: u64) -> Result<()> {
let account = load_account(key, store)?;
let full = resolve_id_prefixes(store, ids)?;
let removed = client::delete(store, &account, &full, timeout)?;
let removed = client::delete(store, &account, &full, None, timeout)?;
println!("removed {removed} message(s) from the server");
Ok(())
}

View file

@ -217,6 +217,7 @@ fn valid_username(name: &str) -> Result<(), Error> {
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::b32;
#[test]
fn dial_hint_routes_without_renaming() {
@ -227,14 +228,10 @@ mod tests {
// proof-of-possession and display all read `host`.
assert_eq!(addr.dial.as_deref(), Some("192.0.2.10"));
assert_eq!(addr.host, "example.org");
assert_eq!(addr.short(), "alice@example.org:1961");
// The default port is elided in the short form, as everywhere.
assert_eq!(addr.short(), "alice@example.org");
assert_eq!(Address::parse("alice@example.org").unwrap().dial, None);
}
}
mod tests {
use super::*;
use crate::crypto::b32;
#[test]
fn short_form_and_default_port() {

View file

@ -56,6 +56,20 @@ impl Session {
/// Opens a session, enforcing sec 4's rule about unpinned servers. TCP pins
/// the server's Noise static key; on RNS the destination hash is the pin
/// (RNS.md sec 13.4).
/// The account address with the host's dial hint applied, when the caller
/// resolved DNS itself: account-bound operations re-read the account, so
/// the hint rides along this way rather than in the stored row.
#[cfg(feature = "store")]
fn account_with_dial(store: &Store, dial: Option<&str>) -> Result<Address, Error> {
let addr = store
.account()?
.ok_or(Error::NotRegistered)?;
Ok(match dial {
Some(dial) => addr.with_dial(dial),
None => addr,
})
}
#[cfg(feature = "store")]
pub fn connect(
store: &Store,
@ -286,9 +300,16 @@ pub enum Pushed {
/// An accept or a block only takes effect once the server holds the changed
/// set, so it is pushed now rather than at the next fetch.
#[cfg(feature = "store")]
pub fn push_tokens(store: &Store, account: &Account, timeout: u64) -> Result<Pushed, Error> {
let Some(addr) = store.account()? else {
return Ok(Pushed::NotRegistered);
pub fn push_tokens(
store: &Store,
account: &Account,
dial: Option<&str>,
timeout: u64,
) -> Result<Pushed, Error> {
let addr = match account_with_dial(store, dial) {
Ok(addr) => addr,
Err(Error::NotRegistered) => return Ok(Pushed::NotRegistered),
Err(e) => return Err(e),
};
let mut session = connect(store, &addr, true, timeout)?;
let held = authenticate(session.transport(), &addr.user, account, store)?;
@ -346,10 +367,13 @@ pub struct Rotated {
/// is untouched; only the index moves, and the superseded key stays
/// derivable from it (sec 2).
#[cfg(feature = "store")]
pub fn rotate(store: &Store, account: &Account, timeout: u64) -> Result<Rotated, Error> {
let Some(addr) = store.account()? else {
return Err(Error::NotRegistered);
};
pub fn rotate(
store: &Store,
account: &Account,
dial: Option<&str>,
timeout: u64,
) -> Result<Rotated, Error> {
let addr = account_with_dial(store, dial)?;
if account.index() as usize >= MAX_CHAIN {
return Err(Error::ChainLimit {
index: account.index() + 1,
@ -564,6 +588,9 @@ pub struct FetchOptions<'a> {
pub reset: bool,
/// Network timeout in seconds.
pub timeout: u64,
/// Dial this address instead of the account host, for hosts that
/// resolve DNS themselves; the host keeps its identity roles.
pub dial: Option<&'a str>,
/// Checked between pages and before each envelope; when raised,
/// `fetch_with` stops and returns the partial summary with `cancelled`
/// set. What was processed is accounted for locally — cursor moved or
@ -579,6 +606,7 @@ impl FetchOptions<'_> {
keep,
reset,
timeout,
dial: None,
cancel: None,
}
}
@ -607,9 +635,7 @@ pub fn fetch_with(
account: &Account,
opts: FetchOptions<'_>,
) -> Result<Fetched, Error> {
let Some(addr) = store.account()? else {
return Err(Error::NotRegistered);
};
let addr = account_with_dial(store, opts.dial)?;
if opts.reset {
store.set_cursor(0, &[0u8; ID_LEN])?;
}
@ -761,11 +787,10 @@ pub fn delete(
store: &Store,
account: &Account,
ids: &[[u8; ID_LEN]],
dial: Option<&str>,
timeout: u64,
) -> Result<u16, Error> {
let Some(addr) = store.account()? else {
return Err(Error::NotRegistered);
};
let addr = account_with_dial(store, dial)?;
let mut session = connect(store, &addr, true, timeout)?;
let transport = session.transport();
authenticate(transport, &addr.user, account, store)?;