crates/monetize-estate-side/src/sign_in.rs
//! **A resend sign-in link, minted and mailed by the estate itself** (Gunnar's S10, owner
//! 2026-10-11; Holger's A16; SHARED since P4c, A28 M11): a sold estate's buyer lost or let run
//! out the one link the ready mail carried. monetize queues a signed `sign_in_again` order for
//! the estate's main naming whose passkey, which address and which console host — never a
//! secret. The main verifies it, mints a fresh passkey invite on that account (only while it
//! holds no passkey) and mails the link itself. Pure (std only, no I/O): the product mints and
//! mails; this is the order's checks, the lifetime and the words. Gunnar's gunnar-repair
//! `sign_in` is the same code until T16 (H20).
//!
//! The link has the shape of the first one (monetize's `first_login_link`):
//! `https://{host}/login?invite={secret}&user={account}`. Pure: the order's checks, the
//! invite's lifetime, the link and the mail's words.
/// The shortest invite a resend mints.
pub const MIN_TTL_SECS: u64 = 3_600;
/// The longest: an invite lives 30 days at most.
pub const MAX_TTL_SECS: u64 = 30 * 86_400;
/// An order that names no lifetime.
pub const DEFAULT_TTL_SECS: u64 = 7 * 86_400;
/// **Can this box mail this resend?** `account` goes into the link, `to` into a mail header,
/// `host` into the link's authority: none may carry anything but itself (no second header,
/// no other path, no query of its own).
pub fn check(account: &str, to: &str, host: &str) -> Result<(), String> {
let name = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.');
if account.is_empty() || account.len() > 128 || !account.chars().all(name) {
return Err(format!(
"the account {account:?} is not a name a link can carry"
));
}
let mut parts = to.split('@');
let (local, domain) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));
let plain = |c: char| {
c.is_ascii_graphic()
&& !matches!(
c,
'<' | '>' | ',' | ';' | '"' | '\\' | '(' | ')' | '[' | ']' | ':'
)
};
if parts.next().is_some()
|| local.is_empty()
|| domain.is_empty()
|| !domain.contains('.')
|| to.len() > 254
|| !to.chars().all(plain)
{
return Err(format!("{to:?} is not one mail address"));
}
let label = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '.');
if host.is_empty()
|| host.len() > 253
|| !host.chars().all(label)
|| host.starts_with('.')
|| host.ends_with('.')
{
return Err(format!("{host:?} is not a host name"));
}
Ok(())
}
/// The invite's lifetime: what the order asked, within [`MIN_TTL_SECS`]..=[`MAX_TTL_SECS`];
/// [`DEFAULT_TTL_SECS`] when it asked none.
pub fn ttl(asked_secs: u64) -> u64 {
if asked_secs == 0 {
DEFAULT_TTL_SECS
} else {
asked_secs.clamp(MIN_TTL_SECS, MAX_TTL_SECS)
}
}
/// The link, the same shape as the first one.
pub fn link(host: &str, secret: &str, account: &str) -> String {
format!("https://{host}/login?invite={secret}&user={account}")
}
/// A lifetime in words: whole days as days, else hours.
pub fn ttl_words(secs: u64) -> String {
match secs {
s if s >= 86_400 && s % 86_400 == 0 => {
format!("{} day{}", s / 86_400, if s == 86_400 { "" } else { "s" })
}
s => format!(
"{} hour{}",
(s / 3_600).max(1),
if s / 3_600 <= 1 { "" } else { "s" }
),
}
}
/// **The mail the estate sends**: to the order's address, the link in its body and nowhere
/// else. Its `Debug` withholds the body (it carries the secret).
#[derive(Clone, PartialEq, Eq)]
pub struct SignInMail {
pub to: String,
pub subject: String,
pub body: String,
}
impl std::fmt::Debug for SignInMail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SignInMail {{ to: {:?}, subject: {:?}, body: <withheld> }}",
self.to, self.subject
)
}
}
/// The mail for `account` on `host`, its invite `secret` living `ttl_secs`.
pub fn mail(host: &str, account: &str, to: &str, secret: &str, ttl_secs: u64) -> SignInMail {
let ttl = ttl_words(ttl_secs);
let link = link(host, secret, account);
let body = format!(
"You asked for a new sign-in link for {host}. It registers your passkey on {account}, and it works once, for {ttl}:\n\n {link}\n\n\
After that, sign in at https://{host} with your passkey. If you did not ask for this, you can ignore this mail: the link is only for {account}, who has no passkey yet.\n"
);
SignInMail {
to: to.to_string(),
subject: format!("Your new sign-in link for https://{host}"),
body,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_link_has_the_first_links_shape_and_the_mail_carries_it_once() {
assert_eq!(
link("acme.holger.mock.invalid", "s3cret", "acme"),
"https://acme.holger.mock.invalid/login?invite=s3cret&user=acme"
);
let m = mail(
"acme.holger.mock.invalid",
"acme",
"buyer@mock.invalid",
"s3cret",
7 * 86_400,
);
assert_eq!(m.to, "buyer@mock.invalid");
assert_eq!(
m.body
.matches("https://acme.holger.mock.invalid/login?invite=s3cret&user=acme")
.count(),
1,
"{}",
m.body
);
assert!(m.body.contains("7 days") && !m.subject.contains("s3cret"));
assert!(
!format!("{m:?}").contains("s3cret"),
"Debug withholds the secret"
);
}
#[test]
fn the_lifetime_is_what_was_asked_within_an_hour_and_thirty_days() {
assert_eq!(
(ttl(0), ttl(60), ttl(86_400), ttl(365 * 86_400)),
(DEFAULT_TTL_SECS, MIN_TTL_SECS, 86_400, MAX_TTL_SECS)
);
assert_eq!(
(
ttl_words(86_400),
ttl_words(3 * 86_400),
ttl_words(3_600),
ttl_words(7_200)
),
(
"1 day".into(),
"3 days".into(),
"1 hour".into(),
"2 hours".into()
)
);
}
#[test]
fn nothing_but_a_name_an_address_and_a_host_passes() {
assert!(check("acme", "buyer@mock.invalid", "acme.holger.mock.invalid").is_ok());
assert!(check(
"first.admin-1",
"a.b+c@sub.mock.invalid",
"acme.mock.invalid"
)
.is_ok());
for (a, t, h) in [
("", "b@e.mock.invalid", "h.mock.invalid"),
("al ice", "b@e.mock.invalid", "h.mock.invalid"),
("a&user=x", "b@e.mock.invalid", "h.mock.invalid"),
(
"a",
"b@e.mock.invalid\r\nBcc: x@evil.mock.invalid",
"h.mock.invalid",
),
("a", "b@@e.mock.invalid", "h.mock.invalid"),
("a", "Bob <b@e.mock.invalid>", "h.mock.invalid"),
("a", "b@localhost", "h.mock.invalid"),
("a", "b@e.mock.invalid", ""),
("a", "b@e.mock.invalid", "evil.mock.invalid/x?"),
("a", "b@e.mock.invalid", "user@evil.mock.invalid"),
] {
assert!(check(a, t, h).is_err(), "{a:?} {t:?} {h:?}");
}
}
}