server/lib/src/sign_in_again.rs
//! **A sold Holger's resend sign-in link** (A16, Gunnar's S10: gunnar-server `postman` and
//! `repair::sign_in_again`, moved as they are). A buyer who lost the one link the ready mail
//! carried asks the shop again; monetize queues a signed `sign_in_again` order for this
//! estate's main, never holding the link's secret. This main mints a fresh first-login invite
//! for the buyer (only while they hold no passkey) and mails the link itself, through the relay
//! the shop fed the estate's main and twin (`secret.smtp_url` / `secret.smtp_from`, monetize
//! `estate_order::relay_feed`). Nothing fed is the ordinary state: such a main refuses a resend
//! by name BEFORE it mints anything.
//!
//! A box laid behind its front (the lean estate, A19 row 13: no public IPv4) mails through the
//! front's egress door it was fed (`crate::egress::front_door`): lettre speaks no proxy, so it
//! dials a loopback forward (Skidbladnir `nordisk_pairing::egress::Forward`) carried through
//! the door, with the relay's own TLS name. The sender is the ONE shared one, monetize's
//! `monetize-mail` (P4c, A28 M11); Gunnar's postman joins it in T16.
use std::collections::BTreeMap;
use std::sync::{Arc, RwLock};
use holger_fleet::order::SignInAgain;
use holger_fleet::sign_in::SignInMail;
use crate::lock::RwLockOrRecover as _;
/// The feed keys the shop names the sold estate's relay under (monetize `estate_order::relay_feed`):
/// the shared sender's own.
pub use monetize_mail::{FEED_SMTP_FROM, FEED_SMTP_URL};
/// **How this box mails**: one plain-text mail to one address.
pub trait Postman: Send + Sync {
fn post(&self, mail: &SignInMail) -> Result<(), String>;
/// **Can it mail now?** Asked BEFORE anything is minted: `Err` says why in words.
fn ready(&self) -> Result<(), String> {
Ok(())
}
}
/// **A configured relay**: the ONE shared sender, monetize's [`monetize_mail::Mailer`] (P4c, A28
/// M11): TLS always (STARTTLS required on 587, implicit on 465), proper MIME so the link stays
/// one line, the sender's own EHLO name, 30 s. Its `Debug` withholds the password.
#[derive(Debug)]
pub struct Relay(monetize_mail::Mailer);
impl Relay {
/// A relay from its URL (`smtp://` or `smtps://`, `user:token@host[:port]`) and the address
/// mail is sent AS.
pub fn from_url(url: &str, from: &str) -> Result<Relay, String> {
monetize_mail::Mailer::new(url, from).map(Relay)
}
/// The relay's host, port and whether it speaks TLS from the first byte.
pub fn endpoint(&self) -> (&str, u16, bool) {
let r = self.0.relay();
(
&r.host,
r.port,
r.security == monetize_mail::Security::ImplicitTls,
)
}
}
impl Postman for Relay {
/// One mail. Holger's offline gate first; a box behind its front (the lean estate, A19 row
/// 13) leaves through the front's egress door it was fed (`crate::egress::front_door`).
fn post(&self, mail: &SignInMail) -> Result<(), String> {
crate::egress::check("a resend sign-in link's mail", &self.0.relay().host)
.map_err(|e| e.to_string())?;
let door = crate::egress::front_door();
self.0
.notice_via(
door.as_deref(),
&mail.to,
"sign-in link",
&mail.subject,
&mail.body,
)
.map(|_| ())
}
}
/// **The relay the installer FED this box**, read at each mail from the feed in force, so a
/// feed that arrives (or changes) after the start is the one used. Nothing fed: mail is off, by
/// name, and nothing is dialled (`monetize_mail::Mailer::from_feed`).
pub struct FedRelay<F: Fn() -> BTreeMap<String, String> + Send + Sync> {
pub fed: F,
}
impl<F: Fn() -> BTreeMap<String, String> + Send + Sync> FedRelay<F> {
fn relay(&self) -> Result<Relay, String> {
let fed = (self.fed)();
// A relay on a private CA (the bench's) names its root (`secret.smtp_ca`, MAIL-RELAY).
let get = |k: &str| fed.get(k).map(|v| v.trim()).filter(|v| !v.is_empty());
if let (Some(url), Some(from), Some(ca)) = (
get(FEED_SMTP_URL),
get(FEED_SMTP_FROM),
get(crate::refeed::FEED_SMTP_CA),
) {
return monetize_mail::Mailer::with_root(url, from, Some(ca.as_bytes())).map(Relay);
}
monetize_mail::Mailer::from_feed(&fed).map(Relay)
}
}
impl<F: Fn() -> BTreeMap<String, String> + Send + Sync> Postman for FedRelay<F> {
fn post(&self, mail: &SignInMail) -> Result<(), String> {
self.relay()?.post(mail)
}
fn ready(&self) -> Result<(), String> {
self.relay().map(|_| ())
}
}
/// The postman this process mails with (the fed relay once the pull channel starts; a test's
/// own). `None`: this box mails nothing.
static POSTMAN: RwLock<Option<Arc<dyn Postman>>> = RwLock::new(None);
/// Mail through `postman` from now on.
pub fn set_postman(postman: Arc<dyn Postman>) {
*POSTMAN.write_or_recover() = Some(postman);
}
/// The postman in force.
pub fn postman() -> Option<Arc<dyn Postman>> {
POSTMAN.read_or_recover().clone()
}
/// **Run one `sign_in_again`** (gunnar-server `repair::sign_in_again`): the order's words
/// checked, the relay ready BEFORE anything is minted, a fresh invite for the buyer, the mail
/// sent — and an invite whose mail did not go withdrawn. Answers the ack's words.
pub fn resend(
store: &crate::passkey::PasskeyDoor,
auth: &crate::auth::AuthConfig,
s: &SignInAgain,
postman: Option<&dyn Postman>,
) -> Result<String, String> {
let account = s.account.as_str();
holger_fleet::sign_in::check(account, &s.to, &s.host)
.map_err(|why| format!("sign_in_again: {why}"))?;
let postman = postman.ok_or_else(|| format!("this box has no mail relay ({FEED_SMTP_URL}), so it mints no sign-in link for {account} it could not mail"))?;
postman.ready().map_err(|why| format!("this box cannot mail ({why}), so it mints no sign-in link for {account} it could not mail"))?;
let ttl = holger_fleet::sign_in::ttl(s.ttl_secs);
let (secret, id) = store
.mint_sign_in_again(account, ttl, auth)
.map_err(|(_, why)| why)?;
let mail = holger_fleet::sign_in::mail(&s.host, account, &s.to, &secret, ttl);
drop(secret);
if let Err(why) = postman.post(&mail) {
if let Err((_, e)) = store.withdraw_invite(&id) {
log::warn!("sign_in_again {account}: the unmailed invite could not be withdrawn: {e}");
}
return Err(format!("the sign-in link for {account} could not be mailed to {} ({why}); the invite is withdrawn — ask again", s.to));
}
log::warn!("sign_in_again: a new sign-in link for {account} was minted here and mailed to the order's address ({} s)", ttl);
Ok(format!(
"a new sign-in link for {account} ({}) was mailed to {}",
holger_fleet::sign_in::ttl_words(ttl),
s.to
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct Kept(Mutex<Vec<SignInMail>>, bool);
impl Postman for Kept {
fn post(&self, mail: &SignInMail) -> Result<(), String> {
if self.1 {
return Err("the relay said no".into());
}
self.0.lock().unwrap().push(mail.clone());
Ok(())
}
}
fn door_at(
dir: &std::path::Path,
host: &str,
auth: &crate::auth::AuthConfig,
) -> crate::passkey::SharedPasskeyDoor {
let mut auth = auth.clone();
auth.methods.push(crate::auth::AuthMethodConfig::Passkey {
rp_id: host.into(),
rp_origin: format!("https://{host}"),
rp_name: "holger".into(),
session_key_file: None,
bootstrap_until_first_admin: true,
});
crate::passkey::PasskeyDoor::open(dir, &auth, Arc::new(crate::audit::MemoryAuditLog::new()))
.unwrap()
.unwrap()
}
fn estate() -> (
crate::passkey::SharedPasskeyDoor,
tempfile::TempDir,
crate::auth::AuthConfig,
) {
let dir = tempfile::tempdir().unwrap();
let mut auth = crate::auth::AuthConfig {
methods: vec![crate::auth::AuthMethodConfig::StaticToken {
subject: "console".into(),
token: Some("c".repeat(40)),
token_env: None,
token_file: None,
}],
roles: std::collections::HashMap::from([(
"console".to_string(),
crate::auth::Role::Admin,
)]),
group_roles: std::collections::HashMap::from([
("holger-admins".to_string(), crate::auth::Role::Admin),
("holger-writers".to_string(), crate::auth::Role::Writer),
]),
..Default::default()
};
assert!(!crate::auth::estate_duties(&mut auth).is_empty());
let s = door_at(dir.path(), "acme.holger.mock.invalid", &auth);
let fed = crate::passkey::FirstAdminInvite {
sha256_hex: "ab".repeat(32),
ttl_secs: 3600,
display: "acme".into(),
};
s.seed_first_admin(&fed, &auth).unwrap();
(s, dir, auth)
}
fn order() -> SignInAgain {
SignInAgain {
account: "acme".into(),
to: "buyer@mock.invalid".into(),
host: "acme.holger.mock.invalid".into(),
ttl_secs: 86_400,
}
}
/// ★ **The buyer's new link is minted here and mailed here, once**, the first link's shape.
#[test]
fn a_resend_is_minted_and_mailed_once_with_the_first_links_shape() {
let (s, _d, auth) = estate();
let kept = Kept::default();
let said = resend(&s, &auth, &order(), Some(&kept)).unwrap();
assert!(
said.contains("was mailed to buyer@mock.invalid") && said.contains("1 day"),
"{said}"
);
let mails = kept.0.lock().unwrap();
assert_eq!(mails.len(), 1);
assert_eq!(mails[0].to, "buyer@mock.invalid");
assert_eq!(
mails[0]
.body
.matches("https://acme.holger.mock.invalid/login?invite=")
.count(),
1,
"{:?}",
mails[0]
);
assert!(mails[0].body.contains("&user=acme"));
}
/// ★ **FIRST-LINK-1: an operator site's first admin** (holger.rs, its first admin
/// `ignalina`, seeded from the installer's feed as a buyer's is) gets a new first sign-in
/// link from the ONE box's signed order: the host is the bare apex, and the link signs in
/// the account the site was fed.
#[test]
fn an_operator_sites_first_admin_is_sent_a_new_first_link_at_the_apex() {
let dir = tempfile::tempdir().unwrap();
let (_, _, auth) = estate();
let s = door_at(dir.path(), "holger.rs", &auth);
let fed = crate::passkey::FirstAdminInvite {
sha256_hex: "cd".repeat(32),
ttl_secs: 3600,
display: "ignalina".into(),
};
s.seed_first_admin(&fed, &auth).unwrap();
let kept = Kept::default();
let order = SignInAgain {
account: "ignalina".into(),
to: "first-admin@mock.invalid".into(),
host: "holger.rs".into(),
ttl_secs: 86_400,
};
let said = resend(&s, &auth, &order, Some(&kept)).unwrap();
assert!(
said.contains("ignalina") && said.contains("was mailed to first-admin@mock.invalid"),
"{said}"
);
let mails = kept.0.lock().unwrap();
assert_eq!(mails.len(), 1);
assert_eq!(
mails[0]
.body
.matches("https://holger.rs/login?invite=")
.count(),
1,
"{:?}",
mails[0]
);
assert!(mails[0].body.contains("&user=ignalina"), "{:?}", mails[0]);
}
/// ★ **HOL-FIRST-ADMIN (holger.rs 2026-10-30): a first-login record taken in before the
/// identity move names nobody** (`{"sha256","at","seeded"}`: no `display`, no `ttl_secs`;
/// the account lived in the old `invites.json`, which the move did not carry over). Its main
/// refused the ONE box's `sign_in_again` for `ignalina` ("is no account the shop named on
/// this box"). The next start of the main, with the shop's feed, makes the record name the
/// fed first admin (Gunnar's main makes the operator's principal from the same feed), and
/// the ONE box's order is minted and mailed.
#[test]
fn a_first_login_record_from_before_the_identity_move_names_the_fed_first_admin_at_the_next_start(
) {
let dir = tempfile::tempdir().unwrap();
let (_, _, auth) = estate();
let marker = dir.path().join(crate::auth::FIRST_ADMIN_MARKER_REL);
std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
std::fs::write(
&marker,
format!(
r#"{{"sha256":"{}","at":1792713392,"seeded":true}}"#,
"cd".repeat(32)
),
)
.unwrap();
let s = door_at(dir.path(), "holger.rs", &auth);
let order = SignInAgain {
account: "ignalina".into(),
to: "first-admin@mock.invalid".into(),
host: "holger.rs".into(),
ttl_secs: 86_400,
};
let before = resend(&s, &auth, &order, Some(&Kept::default())).unwrap_err();
assert!(
before.contains("is no account the shop named"),
"the record names nobody yet: {before}"
);
let fed = crate::passkey::FirstAdminInvite {
sha256_hex: "cd".repeat(32),
ttl_secs: 604_800,
display: "ignalina".into(),
};
let said = s.seed_first_admin(&fed, &auth).unwrap();
assert!(
said.contains("not opened again") && said.contains("ignalina"),
"{said}"
);
let kept = Kept::default();
let said = resend(&s, &auth, &order, Some(&kept)).unwrap();
assert!(
said.contains("was mailed to first-admin@mock.invalid"),
"{said}"
);
assert!(kept.0.lock().unwrap()[0].body.contains("&user=ignalina"));
let record: serde_json::Value =
serde_json::from_slice(&std::fs::read(&marker).unwrap()).unwrap();
assert_eq!(
(
record["sha256"].as_str(),
record["at"].as_i64(),
record["seeded"].as_bool()
),
(Some("cd".repeat(32).as_str()), Some(1792713392), Some(true)),
"the rest of the record is kept: {record}"
);
assert!(
record.get("used_by").is_none_or(serde_json::Value::is_null),
"{record}"
);
// A record that names its first admin is never renamed by a later feed.
let other = crate::passkey::FirstAdminInvite {
display: "somebody-else".into(),
..fed
};
assert!(!s
.seed_first_admin(&other, &auth)
.unwrap()
.contains("somebody-else"));
let record: serde_json::Value =
serde_json::from_slice(&std::fs::read(&marker).unwrap()).unwrap();
assert_eq!(record["display"].as_str(), Some("ignalina"));
}
/// ★ **No relay: refused by name before anything is minted; a mail that did not go
/// withdraws its invite.**
#[test]
fn no_relay_mints_nothing_and_an_unmailed_invite_is_withdrawn() {
let (s, _d, auth) = estate();
let before = s.invites_json(&auth)["invites"].as_array().unwrap().len();
let none = resend(&s, &auth, &order(), None).unwrap_err();
assert!(none.contains(FEED_SMTP_URL), "{none}");
let unfed = FedRelay { fed: BTreeMap::new };
let refused = resend(&s, &auth, &order(), Some(&unfed)).unwrap_err();
assert!(refused.contains("cannot mail"), "{refused}");
assert_eq!(
s.invites_json(&auth)["invites"].as_array().unwrap().len(),
before,
"nothing minted"
);
let failing = Kept(Mutex::default(), true);
let e = resend(&s, &auth, &order(), Some(&failing)).unwrap_err();
assert!(e.contains("withdrawn"), "{e}");
let invites = s.invites_json(&auth);
let last = invites["invites"]
.as_array()
.unwrap()
.last()
.unwrap()
.clone();
assert_eq!(
(last["created_by"].as_str(), last["revoked"].as_bool()),
(Some(crate::passkey::SIGN_IN_AGAIN_BY), Some(true))
);
}
/// The fed relay is read as a submission URL (the shared sender's table); its token never
/// shows; half a relay is not ready.
#[test]
fn a_fed_relay_url_is_read_as_a_submission_url_and_its_token_never_shows() {
let s = Relay::from_url(
"smtp://estate%40acme.mock.invalid:s3cret%2Ftok@smtp.mock.invalid:2525",
"Holger <estate@acme.mock.invalid>",
)
.unwrap();
assert_eq!(s.endpoint(), ("smtp.mock.invalid", 2525, false));
assert!(!format!("{s:?}").contains("s3cret"), "{s:?}");
assert_eq!(
Relay::from_url("smtps://u:p@relay.mock.invalid", "u@relay.mock.invalid")
.unwrap()
.endpoint(),
("relay.mock.invalid", 465, true)
);
assert_eq!(
Relay::from_url("smtp://u:p@relay.mock.invalid:465", "u@relay.mock.invalid")
.unwrap()
.endpoint(),
("relay.mock.invalid", 465, true),
"465 is TLS from the first byte"
);
assert!(
Relay::from_url("smtp://u@relay.mock.invalid", "u@relay.mock.invalid")
.unwrap_err()
.contains("no password")
);
assert!(
Relay::from_url("http://u:p@relay.mock.invalid", "u@relay.mock.invalid")
.unwrap_err()
.contains("scheme")
);
let fed = FedRelay {
fed: || {
BTreeMap::from([(
FEED_SMTP_URL.to_string(),
"smtp://u:p@relay.mock.invalid".to_string(),
)])
},
};
assert!(
fed.ready().unwrap_err().contains(FEED_SMTP_FROM),
"half a relay is not ready"
);
}
/// ★ **Mail is OFF until a relay is fed** (owner 2026-10-25, the airgap default): with the
/// sender linked (`metered`) and a door fed, a box fed no relay refuses the mail by name and
/// opens no socket, to the door or anywhere.
#[test]
fn a_box_fed_no_relay_opens_no_socket_and_refuses_by_name() {
let door = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
door.set_nonblocking(true).unwrap();
let at = door.local_addr().unwrap();
let fed = FedRelay {
fed: move || {
BTreeMap::from([(
crate::egress::FEED_EGRESS.to_string(),
format!("http://{at}"),
)])
},
};
let mail = SignInMail {
to: "buyer@mock.invalid".into(),
subject: "s".into(),
body: "b".into(),
};
let e = fed.post(&mail).unwrap_err();
assert!(
e.contains("mail is off") && e.contains(FEED_SMTP_URL),
"{e}"
);
assert!(fed.ready().is_err());
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(
matches!(door.accept(), Err(ref w) if w.kind() == std::io::ErrorKind::WouldBlock),
"nothing dialled the door"
);
}
}