Functional Weave
Code in TypeScript

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

  • validateLogin( 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 lookup
  • validateLogin( bob@EXAMPLE.org , hunter2hunter2hunter2) → valid true, email bob@example.org, fields … a pasted address with a tab and a trailing newline
  • validateLogin(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.

export function validateLogin(email: string, password: string): LoginCheck
emailstringas typed; normalised with auth.normalise-email
passwordstringas typed; only checked for being there, never against a policy
returnsLoginCheckvalid 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. */
export interface LoginCheck {
  readonly valid: boolean;
  /** normalised, when the email is an address */
  readonly email: 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 { validateLogin } from "#fune/auth.validate-login@^1";
impl/typescript.ts · 30 lines · open · raw

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 { type LoginCheck } from "./auth_validate_login_types.ts";

function isAsciiSpace(ch: string): boolean {
  return ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
}

function isBlank(text: string): boolean {
  return Array.from(text).every(isAsciiSpace);
}

/**
 * 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.
 */
export function validateLogin(email: string, password: string): LoginCheck {
  // 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 fields: Record<string, string> = {};

  const normalised = normaliseEmail(emailText);
  if (normalised === null) {
    fields.email = isBlank(emailText) ? "Enter your email address." : "Enter a valid email address, like name@example.com.";
  }
  if (passwordText === "") fields.password = "Enter your password.";

  return { valid: Object.keys(fields).length === 0, email: normalised, 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 1 dependency, 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-login
Download for TypeScript auth.validate-login-1.0.0-typescript.fune · 7,945 bytes sha256 0419b9f6357434e059e7e2c20f624cbc0caa5ca905f655525b006a5aad45c40d

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./auth.validate-login-1.0.0-typescript.fune, or fetch it from a terminal with fune pull auth.validate-login@1.0.0:typescript.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,840
impl/python.py1,157
impl/rust.rs1,638
impl/typescript.ts1,291
vectors.json2,254