auth.validate-registration
Validate a sign-up form (email, password, name) in one call, with a message per field, in the browser and the API alike.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
One call checks a whole sign-up form and answers in the shape an API's `validation_failed` error and a form both want:
validateRegistration(" Ada@Example.com ", "short", "Ada Lovelace", passwordPolicy("nist-800-63b-4-single-factor"))
# {valid: false, email: "Ada@example.com", name: "Ada Lovelace",
# fields: {"password": "Use at least 15 characters."}}
For example
validateRegistration( Ada@Example.COM , correct horse battery staple, Ada Lovelace , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, blo…)→ valid true, email Ada@example.com, name Ada Lovelace, fields … a good form: the email normalised and the name trimmed for storagevalidateRegistration(ada@example.com, short, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true)→ valid false, email ada@example.com, name Ada Lovelace, fields … a short passwordvalidateRegistration(ada@example.com, password, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true)→ valid false, email ada@example.com, name Ada Lovelace, fields … several password failures are joined into one message
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.
export function validateRegistration(email: string, password: string, name: string, policy: PasswordPolicy): RegistrationCheck
| string | as typed; normalised with auth.normalise-email | |
| password | string | as typed; checked with auth.password-policy against the email and name |
| name | string | as typed; trimmed, 1 to 100 characters, no control characters |
| policy | PasswordPolicy | usually passwordPolicy("nist-800-63b-4-single-factor") |
| returns | RegistrationCheck | valid with the values to store, or a message for each field that needs fixing |
The type it declares, generated into your project
/** A sign-up form's verdict, shaped for an API's validation error and a form's field messages. */
export interface RegistrationCheck {
readonly valid: boolean;
/** normalised, when the email is valid */
readonly email: string | null;
/** trimmed, when the name is valid */
readonly name: string | null;
/** field name to message, only for fields that need fixing; empty when valid */
readonly fields: Readonly<Record<string, string>>;
}
Your code names it in one line, in the file that uses it
import { validateRegistration } from "#fune/auth.validate-registration@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { normaliseEmail } from "./auth_normalise_email.ts"; ← from auth.normalise-email ^1.0.0 · built alongside by fune
import { checkPassword } from "./auth_password_policy_check_password.ts";
import { type PasswordPolicy } from "./auth_password_policy_types.ts";
import { type RegistrationCheck } from "./auth_validate_registration_types.ts";
const MAX_NAME = 100;
function isAsciiSpace(ch: string): boolean {
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
}
function trimAscii(text: string): string {
let start = 0;
let end = text.length;
while (start < end && isAsciiSpace(text[start])) start++;
while (end > start && isAsciiSpace(text[end - 1])) end--;
return text.slice(start, end);
}
/**
* A sign-up form's email, password and name checked together: the values to
* store when valid, and a message for each field that needs fixing.
*/
export function validateRegistration(email: string, password: string, name: string, policy: PasswordPolicy): RegistrationCheck {
// A non-string (a malformed JSON body) is an empty field, not an exception.
const emailText = typeof email === "string" ? email : "";
const passwordText = typeof password === "string" ? password : "";
const nameText = typeof name === "string" ? name : "";
const fields: Record<string, string> = {};
const normalised = normaliseEmail(emailText);
if (normalised === null) {
fields.email = trimAscii(emailText) === "" ? "Enter your email address." : "Enter a valid email address, like name@example.com.";
}
const trimmedName = trimAscii(nameText);
const nameChars = Array.from(trimmedName);
let nameError: string | null = null;
if (nameChars.length === 0) nameError = "Enter your name.";
else if (nameChars.length > MAX_NAME) nameError = `Use no more than ${MAX_NAME} characters for your name.`;
else if (nameChars.some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) nameError = "Your name cannot contain control characters.";
const check = checkPassword(passwordText, normalised, nameError === null ? trimmedName : null, policy);
if (passwordText === "") {
fields.password = "Enter a password.";
} else if (!check.valid) {
fields.password = check.failures.map((f) => f.message).join(" ");
}
if (nameError !== null) fields.name = nameError;
return {
valid: Object.keys(fields).length === 0,
email: normalised,
name: nameError === null ? trimmedName : null,
fields,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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. Or pin a range in fune.project and build in one step:
fune add auth.validate-registration
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./auth.validate-registration-1.0.0-typescript.fune, or fetch it from a terminal with fune pull auth.validate-registration@1.0.0:typescript.
The whole function, every language, is one file too: auth.validate-registration-1.0.0.fune, 20,018 bytes, sha256 61f73a1f82c59d4baea20cec7e33b7e61e88780f6af7fc6438ddca4bad4208e4. 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-registration
after — your function gets the result and the arguments, and returns the final result.
// fune: after auth.validate-registration
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-registration
// fune: replace auth.password-policy in auth.validate-registration
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-registration --steps.
// fune: step auth.validate-registration 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 normalised and the name trimmed for storage | Ada@Example.COM , correct horse battery staple, Ada Lovelace , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, blo… | → | valid true, email Ada@example.com, name Ada Lovelace, fields … |
| a short password | ada@example.com, short, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email ada@example.com, name Ada Lovelace, fields … |
| several password failures are joined into one message | ada@example.com, password, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email ada@example.com, name Ada Lovelace, fields … |
| the password contains the (normalised) email's local part and the trimmed name | Ada@example.com, ada-lovelace-rules-ok, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal … | → | valid false, email Ada@example.com, name Ada Lovelace, fields … |
| every field empty | , , , name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email —, name —, fields … |
| an email that is only spaces counts as empty | , correct horse battery staple, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email —, name Ada, fields … |
| an email that is not an address | ada@localhost, correct horse battery staple, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email —, name Ada, fields … |
| a name of 101 characters | ada@example.com, correct horse battery staple, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, name nist-800-63b-4-single-fa… | → | valid false, email ada@example.com, name —, fields … |
| a name of exactly 100 characters, counted in code points (emoji are one each) | ada@example.com, correct horse battery staple, 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀… | → | valid true, email ada@example.com, name 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀�… |
| a control character inside the name | ada@example.com, correct horse battery staple, Ada Lovelace, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block pe… | → | valid false, email ada@example.com, name —, fields … |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| names in any script are fine | li@example.cn, correct horse battery staple, 李小龍, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid true, email li@example.cn, name 李小龍, fields … |
| the multi-factor policy accepts 8 characters | ada@example.com, tr0ub4dor, Ada Lovelace, name nist-800-63b-4-multi-factor, min length 8, max length 128, min character classes 0, block common true, block personal true | → | valid true, email ada@example.com, name Ada Lovelace, fields … |
| a password that is not a string (a malformed JSON body) is treated as empty | ada@example.com, 12,345, Ada, name nist-800-63b-4-single-factor, min length 15, max length 128, min character classes 0, block common true, block personal true | → | valid false, email ada@example.com, name Ada, fields … |
| a nonsensical policy is the caller's error | ada@example.com, x, Ada, name nist-800-63b-4-single-factor, min length 0, max length 128, min character classes 0, block common true, block personal true | → | error: policy minLength must be a whole number of at least 1 |
More from the author
The browser runs it on submit to show messages beside the fields; the API runs the very same function and returns `fields` in its 400 response, so the two can never disagree about what is acceptable. When `valid` is true, store `email` and `name` from the result (normalised and trimmed), never the raw input.
**email** goes through `auth.normalise-email` (trimmed, domain lower-cased). Empty after trimming: "Enter your email address."; otherwise not an address `validation.email` accepts: "Enter a valid email address, like name@example.com."
**password** goes through `auth.password-policy`'s `checkPassword`, with the normalised email and trimmed name as the personal words to refuse. Empty: "Enter a password."; otherwise every failure's message, joined with a space in the policy's fixed order, so the field shows the whole story ("Use at least 15 characters. This password is too common. Choose something harder to guess.").
**name** is trimmed of ASCII whitespace and must then be 1 to 100 characters (code points), with no control characters (U+0000 to U+001F and U+007F): "Enter your name.", "Use no more than 100 characters for your name." or "Your name cannot contain control characters.". Any script is welcome; names are not otherwise judged (see "Falsehoods Programmers Believe About Names").
`fields` lists only the fields that need fixing, keyed `email`, `password`, `name`, in that order. 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. A nonsensical policy throws, as in `checkPassword`.
Whether the email is already taken is not a rule a pure function can know; the API checks its database after this passes (409 `email_taken`).
Files
| Path | Bytes |
|---|---|
| README.md | 2,172 |
| impl/python.py | 2,060 |
| impl/rust.rs | 3,208 |
| impl/typescript.ts | 2,435 |
| vectors.json | 6,412 |