auth.validate-login
Check a login form (email, password): the normalised email to look up, or a message for each field to fix.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 11 tests, run in TypeScript, Python and Rust.
What it does
Checks a login form before the API looks anything up, and answers in the shape an API's `validation_failed` error and a form both want:
validateLogin(" Ada@Example.COM ", "correct horse battery staple")
# {valid: true, email: "Ada@example.com", fields: {}}
validateLogin("ada@localhost", "")
# {valid: false, email: null,
# fields: {"email": "Enter a valid email address, like name@example.com.",
# "password": "Enter your password."}}
For example
validate_login( Ada@Example.COM , correct horse battery staple)→ valid true, email Ada@example.com, fields … a good form: the email is trimmed and its domain lower-cased for the lookupvalidate_login( bob@EXAMPLE.org , hunter2hunter2hunter2)→ valid true, email bob@example.org, fields … a pasted address with a tab and a trailing newlinevalidate_login(ada@example.com, x)→ valid true, email ada@example.com, fields … a short password is not judged at login: it was set under older rules, and verifyPassword decides
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn validate_login(email: &str, password: &str) -> LoginCheck
| string | as typed; normalised with auth.normalise-email | |
| password | string | as typed; only checked for being there, never against a policy |
| returns | LoginCheck | valid with the email to look up, or a message for each field that needs fixing |
The type it declares, generated into your project
/// A login form's verdict, shaped for an API's validation error and a form's field messages.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoginCheck {
pub valid: bool,
/// normalised, when the email is an address
pub email: Option<String>,
/// field name to message, only for fields that need fixing; empty when valid
pub fields: Vec<(String, String)>,
}
Your code names it in one line, in the file that uses it
fune!(auth.validate-login@^1); // then call validate_login(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::auth_normalise_email::normalise_email; ← from auth.normalise-email ^1.0.0 · built alongside by fune
fn is_ascii_space(ch: char) -> bool {
matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{0C}' | '\u{0B}')
}
/// A login form checked for shape only: the email to look up when valid, and
/// a message for each field that needs fixing. The password is never judged
/// against a policy here, so a password set under older rules still logs in.
pub fn validate_login(email: &str, password: &str) -> LoginCheck {
let mut fields: Vec<(String, String)> = Vec::new();
let normalised = normalise_email(email);
if normalised.is_none() {
let message = if email.trim_matches(is_ascii_space).is_empty() {
"Enter your email address."
} else {
"Enter a valid email address, like name@example.com."
};
fields.push(("email".to_string(), message.to_string()));
}
if password.is_empty() {
fields.push(("password".to_string(), "Enter your password.".to_string()));
}
LoginCheck { valid: fields.is_empty(), email: normalised, fields }
}
pub fn login_check_to_value(check: &LoginCheck) -> Value {
Value::obj(vec![
("valid", Value::Bool(check.valid)),
("email", check.email.as_deref().map(Value::str).unwrap_or(Value::Null)),
(
"fields",
Value::Obj(check.fields.iter().map(|(k, v)| (k.clone(), Value::str(v))).collect()),
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
// A non-string is an empty field, as in TypeScript and Python.
login_check_to_value(&validate_login(args[0].as_str(), args[1].as_str()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add auth.validate-login
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./auth.validate-login-1.0.0-rust.fune, or fetch it from a terminal with fune pull auth.validate-login@1.0.0:rust.
The whole function, every language, is one file too: auth.validate-login-1.0.0.fune, 10,888 bytes, sha256 fe4473f438dfa5de4b3d85efea93e2d2b0863864953844544b1801e656e6120f. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before auth.validate-login
after — your function gets the result and the arguments, and returns the final result.
// fune: after auth.validate-login
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace auth.normalise-email in auth.validate-login
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show auth.validate-login --steps.
// fune: step auth.validate-login after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a good form: the email is trimmed and its domain lower-cased for the lookup | Ada@Example.COM , correct horse battery staple | → | valid true, email Ada@example.com, fields … |
| a pasted address with a tab and a trailing newline | bob@EXAMPLE.org , hunter2hunter2hunter2 | → | valid true, email bob@example.org, fields … |
| a short password is not judged at login: it was set under older rules, and verifyPassword decides | ada@example.com, x | → | valid true, email ada@example.com, fields … |
| a password of spaces is not empty: passwords are never trimmed | ada@example.com, | → | valid true, email ada@example.com, fields … |
| an empty password | ada@example.com, | → | valid false, email ada@example.com, fields … |
| both fields empty, email first | , | → | valid false, email —, fields … |
| an email of only whitespace counts as empty | , correct horse battery staple | → | valid false, email —, fields … |
| an email that is not an address | ada@localhost, correct horse battery staple | → | valid false, email —, fields … |
| no @ at all | ada.example.com, correct horse battery staple | → | valid false, email —, fields … |
| values that are not strings (a malformed JSON body) are treated as empty | 42, — | → | valid false, email —, fields … |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| the local part keeps its case: only the domain is folded | ADA@EXAMPLE.COM, correct horse battery staple | → | valid true, email ADA@example.com, fields … |
More from the author
When `valid` is true, look the account up by `email` from the result: it has been through `auth.normalise-email` (trimmed, domain lower-cased), exactly as `auth.validate-registration` stored it, so `Ada@EXAMPLE.com` finds the account `Ada@example.com` made. Throttle on that normalised email too (`auth.login-throttle`), so changing the domain's case does not dodge the count.
**email**: empty after trimming ASCII whitespace is "Enter your email address."; anything else `auth.normalise-email` refuses is "Enter a valid email address, like name@example.com.", the same messages sign-up uses.
**password**: only its presence is checked, "Enter your password." when it is empty. It is deliberately not trimmed (a space can be part of a password) and never checked against a password policy: a password set under an older, weaker policy must still log in, and a login form that says "use at least 15 characters" tells an attacker what the policy is while telling the user nothing useful. Whether it is right is `auth.password-hash`'s `verifyPassword`, after the throttle allows the attempt.
`fields` lists only the fields that need fixing, keyed `email` then `password`. A value that is not a string (a JSON body with a number where the password goes) is treated as empty, so a malformed request gets field messages rather than an exception. It never throws.
Files
| Path | Bytes |
|---|---|
| README.md | 1,840 |
| impl/python.py | 1,157 |
| impl/rust.rs | 1,638 |
| impl/typescript.ts | 1,291 |
| vectors.json | 2,254 |