{
  "id": "auth",
  "title": "User accounts and authentication",
  "summary": "Sign-up and login forms, password policy and hashing, login lockout, and signed access tokens (JWT) read from the Authorization header.",
  "synonyms": [
    "authentication",
    "auth",
    "login",
    "log in",
    "sign in",
    "signup",
    "sign up",
    "registration",
    "user accounts",
    "passwords",
    "password reset",
    "jwt",
    "access tokens",
    "bearer tokens",
    "api keys",
    "brute force protection"
  ],
  "apps": [
    "login system",
    "user account system",
    "sign-up and sign-in pages",
    "authentication service",
    "API with token auth",
    "membership site"
  ],
  "language": "typescript",
  "includes": [
    "validation-basics"
  ],
  "includedBy": [],
  "caveats": [],
  "sections": [
    {
      "key": "forms",
      "title": "Sign-up and login forms",
      "purpose": "Check what people type into the account forms, the same way in the browser and the API.",
      "from": "auth",
      "members": [
        {
          "id": "auth.validate-registration",
          "tier": "core",
          "why": "Validates a sign-up form (email, password, name) in one call, with a message per field.",
          "section": "forms",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Validate a sign-up form (email, password, name) in one call, with a message per field, in the browser and the API alike.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateRegistration"
          ],
          "signatures": [
            "export function validateRegistration(email: string, password: string, name: string, policy: PasswordPolicy): RegistrationCheck"
          ],
          "reference": "import { validateRegistration } from \"#fune/auth.validate-registration@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.validate-registration"
        },
        {
          "id": "auth.validate-login",
          "tier": "core",
          "why": "Checks a login form and gives the normalised email to look the account up by.",
          "section": "forms",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Check a login form (email, password): the normalised email to look up, or a message for each field to fix.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateLogin"
          ],
          "signatures": [
            "export function validateLogin(email: string, password: string): LoginCheck"
          ],
          "reference": "import { validateLogin } from \"#fune/auth.validate-login@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.validate-login"
        },
        {
          "id": "auth.normalise-email",
          "tier": "core",
          "why": "Trims an email and lower-cases its domain, so one address is one account.",
          "section": "forms",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Trim an email address and lower-case its domain for storage and lookup, or null if it is not a plausible address.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseEmail"
          ],
          "signatures": [
            "export function normaliseEmail(value: string): string | null"
          ],
          "reference": "import { normaliseEmail } from \"#fune/auth.normalise-email@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.normalise-email"
        },
        {
          "id": "auth.validate-password-change",
          "tier": "optional",
          "why": "Checks a change-password form: current password given and correct, new one meeting the policy and different.",
          "section": "forms",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Check a change-password form: current password given and correct, new one meeting the policy and different.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validatePasswordChange"
          ],
          "signatures": [
            "export function validatePasswordChange(currentPassword: string, newPassword: string, currentPasswordMatches: boolean, email: string, name: string, policy: PasswordPolicy): PasswordChangeCheck"
          ],
          "reference": "import { validatePasswordChange } from \"#fune/auth.validate-password-change@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.validate-password-change"
        }
      ],
      "gaps": []
    },
    {
      "key": "passwords",
      "title": "Passwords",
      "purpose": "Decide what passwords are acceptable and store them safely.",
      "from": "auth",
      "members": [
        {
          "id": "auth.password-policy",
          "tier": "core",
          "why": "Checks a new password against NIST SP 800-63B-4 by default: length, common passwords, name and email.",
          "section": "passwords",
          "from": "auth",
          "version": "1.0.1",
          "summary": "Check a new password against a named policy (NIST SP 800-63B-4 by default): length, common passwords, name and email.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "passwordPolicy",
            "checkPassword"
          ],
          "signatures": [
            "export function passwordPolicy(name: string): PasswordPolicy",
            "export function checkPassword(password: string, email: string | null, name: string | null, policy: PasswordPolicy): PasswordCheck"
          ],
          "reference": "import { passwordPolicy, checkPassword } from \"#fune/auth.password-policy@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.password-policy"
        },
        {
          "id": "auth.password-hash",
          "tier": "core",
          "why": "Hashes passwords as PBKDF2-SHA256 for storage, verifies in constant time, and flags hashes to upgrade.",
          "section": "passwords",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Hash a password for storage as pbkdf2_sha256$iterations$salt$hash, verify one in constant time, and spot weak hashes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "hashPassword",
            "verifyPassword",
            "passwordNeedsRehash"
          ],
          "signatures": [
            "export function hashPassword(password: string, salt: readonly number[], iterations: number): string",
            "export function verifyPassword(password: string, stored: string): boolean",
            "export function passwordNeedsRehash(stored: string, iterations: number): boolean"
          ],
          "reference": "import { hashPassword, verifyPassword, passwordNeedsRehash } from \"#fune/auth.password-hash@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.password-hash"
        },
        {
          "id": "crypto.pbkdf2-sha256",
          "tier": "optional",
          "why": "The PBKDF2 key derivation underneath, if you need it directly.",
          "section": "passwords",
          "from": "auth",
          "version": "1.0.0",
          "summary": "PBKDF2 with HMAC-SHA256 (RFC 8018): derive a key from a password and salt, slowly, fast enough for a login request.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "pbkdf2Sha256"
          ],
          "signatures": [
            "export function pbkdf2Sha256(password: readonly number[], salt: readonly number[], iterations: number, keyLength: number): readonly number[]"
          ],
          "reference": "import { pbkdf2Sha256 } from \"#fune/crypto.pbkdf2-sha256@^1\";",
          "url": "http://www.functionalweave.com/functions/crypto.pbkdf2-sha256"
        },
        {
          "id": "crypto.constant-time-equal",
          "tier": "optional",
          "why": "Compares MACs and hashes in constant time, for your own token or signature checks.",
          "section": "passwords",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Compare two byte strings in time that does not depend on where they differ, for checking MACs and hashes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "constantTimeEqual"
          ],
          "signatures": [
            "export function constantTimeEqual(a: readonly number[], b: readonly number[]): boolean"
          ],
          "reference": "import { constantTimeEqual } from \"#fune/crypto.constant-time-equal@^1\";",
          "url": "http://www.functionalweave.com/functions/crypto.constant-time-equal"
        }
      ],
      "gaps": [
        {
          "key": "breached-passwords",
          "text": "Checking passwords against a breach corpus (Have I Been Pwned's range API) is network work; the policy's common-password list is built in.",
          "section": "passwords",
          "from": "auth"
        }
      ]
    },
    {
      "key": "login",
      "title": "Login protection",
      "purpose": "Slow down password guessing.",
      "from": "auth",
      "members": [
        {
          "id": "auth.login-throttle",
          "tier": "core",
          "why": "Allows a login attempt or locks the account out from its recent failures, with the seconds until retry.",
          "section": "login",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Allow a login attempt or lock the account out, from its recent failed attempts, with the seconds until it may retry.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "loginThrottle"
          ],
          "signatures": [
            "export function loginThrottle(failures: readonly number[], now: number, policy: ThrottlePolicy): ThrottleDecision"
          ],
          "reference": "import { loginThrottle } from \"#fune/auth.login-throttle@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.login-throttle"
        },
        {
          "id": "http.retry-after",
          "tier": "optional",
          "why": "Reads a Retry-After header, for a client that backs off when locked out or rate limited.",
          "section": "login",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Seconds to wait from an HTTP Retry-After header (delay-seconds or an HTTP date), or null if missing or malformed.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "retryAfterSeconds"
          ],
          "signatures": [
            "export function retryAfterSeconds(header: string | null, now: number): number | null"
          ],
          "reference": "import { retryAfterSeconds } from \"#fune/http.retry-after@^1\";",
          "url": "http://www.functionalweave.com/functions/http.retry-after"
        }
      ],
      "gaps": [
        {
          "key": "ip-rate-limiting",
          "text": "Rate limiting by IP address across accounts, with its shared counter store (Redis, a database).",
          "section": "login",
          "from": "auth"
        }
      ]
    },
    {
      "key": "tokens",
      "title": "Tokens",
      "purpose": "Issue and check signed tokens for an API or a single-page app.",
      "from": "auth",
      "members": [
        {
          "id": "auth.access-token",
          "tier": "core",
          "why": "Issues HS256 access tokens, reads them from the Authorization header, and refuses revoked or superseded ones.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Issue an API's HS256 access token, read one from an Authorization header, and refuse revoked or superseded ones.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "issueAccessToken",
            "readAccessToken",
            "confirmAccessToken"
          ],
          "signatures": [
            "export function issueAccessToken(subject: string, name: string, email: string, tokenVersion: number, jti: string, now: number, ttlSeconds: number, secret: readonly number[]): AccessToken",
            "export function readAccessToken(authorization: string | null, secret: readonly number[], now: number, leewaySeconds: number): AccessCheck",
            "export function confirmAccessToken(check: AccessCheck, currentTokenVersion: number | null, revoked: boolean): AccessCheck"
          ],
          "reference": "import { issueAccessToken, readAccessToken, confirmAccessToken } from \"#fune/auth.access-token@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.access-token"
        },
        {
          "id": "auth.bearer-token",
          "tier": "core",
          "why": "Takes the token out of an \"Authorization: Bearer ...\" header, or null.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "The token from an HTTP Authorization header of the form \"Bearer <token>\" (RFC 6750), or null if it has none.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseBearerToken"
          ],
          "signatures": [
            "export function parseBearerToken(authorization: string | null): string | null"
          ],
          "reference": "import { parseBearerToken } from \"#fune/auth.bearer-token@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.bearer-token"
        },
        {
          "id": "auth.jwt",
          "tier": "optional",
          "why": "Signs, verifies and decodes HS256 JWTs with exp/nbf/iat checks, for tokens of your own shape.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Sign, verify and decode HS256 JSON Web Tokens (RFC 7519): signature, algorithm and exp/nbf/iat checked with leeway.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "signJwt",
            "verifyJwt",
            "decodeJwt"
          ],
          "signatures": [
            "export function signJwt(claims: Readonly<Record<string, unknown>>, secret: readonly number[]): string",
            "export function verifyJwt(token: string, secret: readonly number[], now: number, leewaySeconds: number): JwtVerification",
            "export function decodeJwt(token: string): Readonly<Record<string, unknown>> | null"
          ],
          "reference": "import { signJwt, verifyJwt, decodeJwt } from \"#fune/auth.jwt@^1\";",
          "url": "http://www.functionalweave.com/functions/auth.jwt"
        },
        {
          "id": "crypto.hmac-sha256",
          "tier": "optional",
          "why": "HMAC-SHA256, for signing webhooks, links or cookies.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "HMAC-SHA256 of a message under a secret key (RFC 2104, RFC 4231), in pure code that also runs in the browser.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "hmacSha256"
          ],
          "signatures": [
            "export function hmacSha256(key: readonly number[], message: readonly number[]): readonly number[]"
          ],
          "reference": "import { hmacSha256 } from \"#fune/crypto.hmac-sha256@^1\";",
          "url": "http://www.functionalweave.com/functions/crypto.hmac-sha256"
        },
        {
          "id": "crypto.sha256",
          "tier": "optional",
          "why": "SHA-256 digests, for storing API keys or reset tokens hashed.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "The SHA-256 digest of a byte string (FIPS 180-4), in pure code that also runs in the browser.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "sha256"
          ],
          "signatures": [
            "export function sha256(bytes: readonly number[]): readonly number[]"
          ],
          "reference": "import { sha256 } from \"#fune/crypto.sha256@^1\";",
          "url": "http://www.functionalweave.com/functions/crypto.sha256"
        },
        {
          "id": "encoding.base64",
          "tier": "optional",
          "why": "Base64 and base64url, for encoding tokens and keys.",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Bytes to base64 and base64url text and back (RFC 4648), with strict decoding, identically in every language.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "base64Encode",
            "base64Decode",
            "base64UrlEncode",
            "base64UrlDecode"
          ],
          "signatures": [
            "export function base64Encode(bytes: readonly number[]): string",
            "export function base64Decode(text: string): readonly number[]",
            "export function base64UrlEncode(bytes: readonly number[]): string",
            "export function base64UrlDecode(text: string): readonly number[]"
          ],
          "reference": "import { base64Encode, base64Decode, base64UrlEncode, base64UrlDecode } from \"#fune/encoding.base64@^1\";",
          "url": "http://www.functionalweave.com/functions/encoding.base64"
        },
        {
          "id": "time.countdown",
          "tier": "optional",
          "why": "Seconds left until a token expires and a timer text, for \"session expires in 4:05\".",
          "section": "tokens",
          "from": "auth",
          "version": "1.0.0",
          "summary": "Seconds left until a Unix time, whether it has passed, and a timer text like 4:05, for tokens or retry waits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "countdown"
          ],
          "signatures": [
            "export function countdown(until: number, now: number): Countdown"
          ],
          "reference": "import { countdown } from \"#fune/time.countdown@^1\";",
          "url": "http://www.functionalweave.com/functions/time.countdown"
        }
      ],
      "gaps": [
        {
          "key": "sessions",
          "text": "Server-side sessions and cookies: storing them, Secure/HttpOnly/SameSite flags, CSRF tokens and expiry are yours.",
          "section": "tokens",
          "from": "auth"
        },
        {
          "key": "asymmetric-jwt",
          "text": "RS256/ES256 tokens and JWKS key sets (for third-party identity providers): auth.jwt is HS256 only.",
          "section": "tokens",
          "from": "auth"
        }
      ]
    },
    {
      "key": "forms-ui",
      "title": "Forms in React",
      "purpose": "Accessible account pages in a React front end (TypeScript only).",
      "from": "auth",
      "members": [
        {
          "id": "react.form.password-field",
          "tier": "optional",
          "why": "A password input with Show/Hide and a live checklist of the password policy.",
          "section": "forms-ui",
          "from": "auth",
          "version": "1.0.1",
          "summary": "A password input with a Show/Hide button and an optional live checklist of a password policy (GOV.UK password input).",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript"
          ],
          "available": true,
          "framework": {
            "name": "react",
            "range": "^19"
          },
          "group": true,
          "functions": [
            "passwordChecklist",
            "PasswordField"
          ],
          "signatures": [
            "export function passwordChecklist(policy: PasswordPolicy, check: PasswordCheck): readonly PasswordChecklistItem[]",
            "export function PasswordField(props: PasswordFieldProps): JSX.Element"
          ],
          "reference": "import { passwordChecklist, PasswordField } from \"#fune/react.form.password-field@^1\";",
          "url": "http://www.functionalweave.com/functions/react.form.password-field"
        },
        {
          "id": "react.form.text-field",
          "tier": "optional",
          "why": "Labelled email and name inputs with hints and errors.",
          "section": "forms-ui",
          "from": "auth",
          "version": "1.0.1",
          "summary": "A labelled single-line text input with hint, error message, autocomplete, prefix and suffix (GOV.UK text input).",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript"
          ],
          "available": true,
          "framework": {
            "name": "react",
            "range": "^19"
          },
          "group": false,
          "functions": [
            "TextField"
          ],
          "signatures": [
            "export function TextField(props: TextFieldProps): JSX.Element"
          ],
          "reference": "import { TextField } from \"#fune/react.form.text-field@^1\";",
          "url": "http://www.functionalweave.com/functions/react.form.text-field"
        },
        {
          "id": "react.form.error-summary",
          "tier": "optional",
          "why": "The \"There is a problem\" box linking to each field in error.",
          "section": "forms-ui",
          "from": "auth",
          "version": "1.0.1",
          "summary": "A \"There is a problem\" box linking to each field in error, focused on arrival, built from a validator's fields (GOV.UK).",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript"
          ],
          "available": true,
          "framework": {
            "name": "react",
            "range": "^19"
          },
          "group": true,
          "functions": [
            "errorSummaryItems",
            "ErrorSummary"
          ],
          "signatures": [
            "export function errorSummaryItems(fields: Readonly<Record<string, string>>, order: readonly string[], targets: Readonly<Record<string, string>>): readonly ErrorSummaryItem[]",
            "export function ErrorSummary(props: ErrorSummaryProps): JSX.Element"
          ],
          "reference": "import { errorSummaryItems, ErrorSummary } from \"#fune/react.form.error-summary@^1\";",
          "url": "http://www.functionalweave.com/functions/react.form.error-summary"
        },
        {
          "id": "form.state",
          "tier": "optional",
          "why": "Form values, touched fields and validator errors as a reducer for useReducer.",
          "section": "forms-ui",
          "from": "auth",
          "version": "1.0.1",
          "summary": "A form's state as a pure reducer: values, touched fields, validator errors and the submit, for useReducer.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript"
          ],
          "available": true,
          "group": true,
          "functions": [
            "initialFormState",
            "formReducer",
            "visibleErrors"
          ],
          "signatures": [
            "export function initialFormState(values: Readonly<Record<string, string>>): FormState",
            "export function formReducer(state: FormState, action: FormAction): FormState",
            "export function visibleErrors(state: FormState): Readonly<Record<string, string>>"
          ],
          "reference": "import { initialFormState, formReducer, visibleErrors } from \"#fune/form.state@^1\";",
          "url": "http://www.functionalweave.com/functions/form.state"
        }
      ],
      "gaps": []
    },
    {
      "key": "contact",
      "title": "Contact details",
      "purpose": "Check and normalise a person's contact details before storing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.email",
          "tier": "core",
          "why": "Is an email address plausible, by a documented subset of RFC 5322.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Is this a plausible email address? A documented, pragmatic subset of RFC 5322, not the full grammar.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isEmail"
          ],
          "signatures": [
            "export function isEmail(value: string): boolean"
          ],
          "reference": "import { isEmail } from \"#fune/validation.email@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.email"
        },
        {
          "id": "validation.phone-e164",
          "tier": "core",
          "why": "Normalises a phone number to +44… E.164 for a default country.",
          "section": "contact",
          "from": "validation-basics",
          "version": "2.0.0",
          "summary": "Normalise a phone number to E.164 (+447700900123) using a default country's trunk and dialling prefixes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validatePhoneE164"
          ],
          "signatures": [
            "export function validatePhoneE164(value: string, defaultCountry: string): PhoneE164"
          ],
          "reference": "import { validatePhoneE164 } from \"#fune/validation.phone-e164@^2\";",
          "url": "http://www.functionalweave.com/functions/validation.phone-e164"
        },
        {
          "id": "validation.uk-postcode",
          "tier": "core",
          "why": "Validates a UK postcode and puts it in canonical form.",
          "section": "contact",
          "from": "validation-basics",
          "version": "2.0.0",
          "summary": "Validate a UK postcode and normalise it to canonical upper case with one space before the inward code.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkPostcode"
          ],
          "signatures": [
            "export function validateUkPostcode(value: string): UkPostcode"
          ],
          "reference": "import { validateUkPostcode } from \"#fune/validation.uk-postcode@^2\";",
          "url": "http://www.functionalweave.com/functions/validation.uk-postcode"
        },
        {
          "id": "text.normalise-name",
          "tier": "optional",
          "why": "Tidies a typed name: spacing and title case with Mc, O' and hyphen rules.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Tidy a personal name: trim, collapse whitespace and title-case, with Mc, Mac, O', hyphen and van/de rules.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseName"
          ],
          "signatures": [
            "export function normaliseName(value: string): string"
          ],
          "reference": "import { normaliseName } from \"#fune/text.normalise-name@^1\";",
          "url": "http://www.functionalweave.com/functions/text.normalise-name"
        }
      ],
      "gaps": []
    },
    {
      "key": "bank",
      "title": "Bank details",
      "purpose": "Check payee and customer bank details before money moves.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.iban",
          "tier": "core",
          "why": "Checks an IBAN's mod-97 checksum and its country's length.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check an IBAN against the ISO 13616 mod-97-10 checksum and the registered length for its country.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isIban"
          ],
          "signatures": [
            "export function isIban(value: string): boolean"
          ],
          "reference": "import { isIban } from \"#fune/validation.iban@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.iban"
        },
        {
          "id": "validation.bic",
          "tier": "optional",
          "why": "Checks a SWIFT/BIC code's format and splits it.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateBic"
          ],
          "signatures": [
            "export function validateBic(value: string): BicCheck"
          ],
          "reference": "import { validateBic } from \"#fune/validation.bic@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.bic"
        },
        {
          "id": "validation.uk-sort-code-account",
          "tier": "optional",
          "why": "Checks a UK sort code and account number by the Vocalink modulus rules, against a table you supply.",
          "section": "bank",
          "from": "validation-basics",
          "version": "3.0.0",
          "summary": "Check a UK sort code and account number with the Vocalink/Pay.UK modulus rules, against a table you supply.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkSortCodeAccount"
          ],
          "signatures": [
            "export function validateUkSortCodeAccount(sortCode: string, accountNumber: string, table: UkModulusTable): UkSortCodeAccount"
          ],
          "reference": "import { validateUkSortCodeAccount } from \"#fune/validation.uk-sort-code-account@^3\";",
          "url": "http://www.functionalweave.com/functions/validation.uk-sort-code-account"
        },
        {
          "id": "validation.uk-modulus-table",
          "tier": "optional",
          "why": "Parses Vocalink's VALACDOS and SCSUBTAB files into that table.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Parse Vocalink's VALACDOS.txt and SCSUBTAB.txt into the table UK sort code modulus checking needs.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseUkModulusTable"
          ],
          "signatures": [
            "export function parseUkModulusTable(valacdos: string, scsubtab: string): UkModulusTable"
          ],
          "reference": "import { parseUkModulusTable } from \"#fune/validation.uk-modulus-table@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.uk-modulus-table"
        }
      ],
      "gaps": []
    },
    {
      "key": "check-digits",
      "title": "Identifiers and check digits",
      "purpose": "Catch mistyped reference numbers by their check digits.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.luhn",
          "tier": "optional",
          "why": "Luhn mod-10 check, for card numbers and many reference numbers.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a digit string against the Luhn mod-10 checksum used by payment cards and many identifiers.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isLuhn"
          ],
          "signatures": [
            "export function isLuhn(value: string): boolean"
          ],
          "reference": "import { isLuhn } from \"#fune/validation.luhn@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.luhn"
        },
        {
          "id": "validation.uk-company-number",
          "tier": "optional",
          "why": "Checks a Companies House number's format and prefix.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "2.0.1",
          "summary": "Check a Companies House company number's format and prefix (SC, NI, OC, LP...) and zero-pad it to eight characters.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkCompanyNumber"
          ],
          "signatures": [
            "export function validateUkCompanyNumber(value: string): UkCompanyNumber"
          ],
          "reference": "import { validateUkCompanyNumber } from \"#fune/validation.uk-company-number@^2\";",
          "url": "http://www.functionalweave.com/functions/validation.uk-company-number"
        },
        {
          "id": "validation.lei",
          "tier": "optional",
          "why": "Checks a Legal Entity Identifier's check digits.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a 20-character Legal Entity Identifier against its ISO 17442 / ISO 7064 MOD 97-10 check digits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateLei"
          ],
          "signatures": [
            "export function validateLei(value: string): LeiCheck"
          ],
          "reference": "import { validateLei } from \"#fune/validation.lei@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.lei"
        },
        {
          "id": "validation.gtin",
          "tier": "optional",
          "why": "Checks an EAN/UPC/GTIN barcode number.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.1.0",
          "summary": "Check an EAN-8, UPC-A, EAN-13 or GTIN-14 barcode number against the GS1 mod-10 check digit.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateGtin"
          ],
          "signatures": [
            "export function validateGtin(value: string): GtinCheck"
          ],
          "reference": "import { validateGtin } from \"#fune/validation.gtin@^1\";",
          "url": "http://www.functionalweave.com/functions/validation.gtin"
        }
      ],
      "gaps": []
    },
    {
      "key": "masking",
      "title": "Displaying sensitive values",
      "purpose": "Show stored identifiers without exposing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "text.mask",
          "tier": "optional",
          "why": "Masks all but the last digits of a card, account or phone number.",
          "section": "masking",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Mask all but the last n characters of a card, account or phone number, optionally keeping separators.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "mask"
          ],
          "signatures": [
            "export function mask(value: string, visible: number, maskChar: string, keepSeparators: boolean): string"
          ],
          "reference": "import { mask } from \"#fune/text.mask@^1\";",
          "url": "http://www.functionalweave.com/functions/text.mask"
        }
      ],
      "gaps": []
    }
  ],
  "gaps": [
    {
      "key": "breached-passwords",
      "text": "Checking passwords against a breach corpus (Have I Been Pwned's range API) is network work; the policy's common-password list is built in.",
      "section": "passwords",
      "from": "auth"
    },
    {
      "key": "ip-rate-limiting",
      "text": "Rate limiting by IP address across accounts, with its shared counter store (Redis, a database).",
      "section": "login",
      "from": "auth"
    },
    {
      "key": "sessions",
      "text": "Server-side sessions and cookies: storing them, Secure/HttpOnly/SameSite flags, CSRF tokens and expiry are yours.",
      "section": "tokens",
      "from": "auth"
    },
    {
      "key": "asymmetric-jwt",
      "text": "RS256/ES256 tokens and JWKS key sets (for third-party identity providers): auth.jwt is HS256 only.",
      "section": "tokens",
      "from": "auth"
    },
    {
      "key": "email-verification",
      "text": "Sending verification and password-reset emails, and storing their one-time tokens with expiry.",
      "section": null,
      "from": "auth"
    },
    {
      "key": "oauth-social-login",
      "text": "OAuth 2.0 / OpenID Connect sign-in with Google, Microsoft, GitHub and the like.",
      "section": null,
      "from": "auth"
    },
    {
      "key": "mfa",
      "text": "Second factors: TOTP authenticator codes, SMS codes, passkeys/WebAuthn and recovery codes.",
      "section": null,
      "from": "auth"
    },
    {
      "key": "roles-permissions",
      "text": "Roles, permissions and organisation membership: who may do what is yours to model.",
      "section": null,
      "from": "auth"
    },
    {
      "key": "user-storage",
      "text": "The users table, unique email index, migrations and account deletion.",
      "section": null,
      "from": "auth"
    },
    {
      "key": "address-lookup",
      "text": "Turning a postcode into a list of addresses (Royal Mail PAF or a lookup API) is network work and not in caps.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "email-deliverability",
      "text": "Whether a mailbox exists (MX lookups, a confirmation email): validation.email checks the syntax only.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "vocalink-data",
      "text": "The Vocalink modulus tables themselves: download them from Pay.UK under its terms; caps parses them but does not ship them.",
      "section": null,
      "from": "validation-basics"
    }
  ],
  "counts": {
    "members": 34,
    "core": 12,
    "optional": 22,
    "unreviewed": 0,
    "deprecated": [],
    "unavailable": [],
    "missing": [],
    "gaps": 12
  },
  "warnings": [],
  "url": "http://www.functionalweave.com/suites/auth",
  "owner": null
}
