Skip to Content
TokenAPI Reference

Unreleased changes

These pages document main, which has changes 0.1.2 does not. npm install @evanion/token gives you 0.1.2, documented here. Values on these pages are produced by running the current source on every build.

API reference

@evanion/token exports createToken, the types it takes and returns, five constants and three error classes, one heading each below. It needs Node 20 or newer and ships as ESM, which require() loads on Node 20.19, 22.12 or newer.

createToken

function

Validates the options and returns a frozen object carrying them and the two operations bound to them.

throws {InvalidAlphabetError} when the dictionary holds a character of CONFUSABLE_CHARACTERS or an uppercase one, or has a size that does not divide 256. · throws {InvalidShapeError} when length, chunkSize and separator cannot describe a code. · throws {InvalidDictionaryError} from @evanion/luhn, when the dictionary cannot carry a check character.

Full documentation

Everything is checked here, once. Nothing is checked at use, so an accepted instance cannot produce a code its own validate rejects.

function (?: TokenOptions): Token;

What the tests state

6 behaviours

exposes the configuration it was given
reports usable entropy excluding the check character
is frozen
is 32 distinct lowercase code points
excludes every confusable character
divides 256, so byte % n is unbiased

exposes the configuration it was given

Reading the case from the package.

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

const = ({ : 16, : 4 }); .()..; // -> 19 .; // -> 75

Token

interface

A configuration with generate and validate bound to it.

Token is the frozen object createToken returns.

MemberWhat it is
generatemints a code; never retries and never checks for collision
validatereports whether a string is a well-formed code
dictionarythe alphabet, as configured
nthe number of characters in the dictionary
length, chunkSize, separatorthe shape, as configured
entropyBits(length - 1) * log2(n), the random bits in one code
interface Token { readonly : string; readonly : number; readonly : number; readonly : number; readonly : string; readonly : number; (?: GenerateOptions): GenerateResult; (: string): ; }
const = ({ : '0123456789abcdef', : 16 }); .; // -> 16 .; // -> 60 .()..; // -> 19

TokenOptions

interface

The shape and alphabet createToken builds a token from. Every field is optional, and createToken checks all four once, at construction.

OptionDefaultConstraint
lengthDEFAULT_LENGTHan integer of at least 2, the check character included
chunkSizeDEFAULT_CHUNK_SIZEan integer of at least 1 that divides length
separatorDEFAULT_SEPARATORnon-empty, and sharing no character with dictionary
dictionaryDEFAULT_DICTIONARYthe four constraints under InvalidAlphabetError
interface TokenOptions { ?: number; ?: number; ?: string; ?: string; }
const = ({ : 6, : 3, : ' ' }); const { } = .(); .; // -> 7 .(3); // -> ' ' .().; // -> true .(.(' ', '-')).; // -> false

GenerateOptions

interface

Options generate takes per call.

prefix is not checked against the dictionary and is not part of the check character.

interface GenerateOptions { ?: string; }
const = (); const { } = .({ : 'ORD' }); .(); // -> { valid: false, reason: 'outside-alphabet' } /** Validates a pickup code read back with or without its `ORD` prefix. */ function (: string) { return .(.(/^ord-?/i, '')); } ().; // -> true (.()).; // -> true (.('-', '')).; // -> true

GenerateResult

interface

A freshly minted code, in the two forms a caller needs.

value is the chunked code, preceded by prefix and separator when a prefix is given. body is what to store and index on.

interface GenerateResult { : string; : string; : string; : string | undefined; }
const = (); const = .({ : 'ORD' }); .; // -> 'ORD' ..; // -> 7 ..; // -> 13 ..(.); // -> true

ValidateResult

type

What validate returns. Narrow on valid: true carries the body to look up, and false carries the reason the code failed first.

validate strips the separator and folds case before it checks, so body is the same string whatever grouping or case the input used.

export type = ValidToken | InvalidToken;
const = (); const = new ([['a4kp9mx', 'order-2026-0042']]); function (: string): string { const = .(); if (!.) return `refused: ${.}`; return .(.) ?? 'no such order'; } ('A4KP9MXA'); // -> 'order-2026-0042' ('a4kp-9mx8'); // -> 'refused: check-failed' (.().); // -> 'no such order'

ValidToken

interface

A code that is well formed. It does not follow that the code exists.

ValidToken is the valid: true half of ValidateResult.

interface ValidToken { : true; : string; }

InvalidToken

interface

A code that is not well formed, and the first constraint it failed.

InvalidToken is the valid: false half of ValidateResult.

interface InvalidToken { : false; : ; }

ValidateFailureReason

type

Why a code is not well formed.

validate checks in the order the union lists and reports the first failure. A prefix is not part of a code: validate reads a leading ORD- as three more characters, and its o is outside-alphabet.

export type = 'outside-alphabet' | 'wrong-length' | 'check-failed';
const = (); .('b0zg-7kqb'); // -> { valid: true, body: 'b0zg7kq' } .('b0zg-7kq8'); // -> { valid: false, reason: 'check-failed' } .('bozg-7kqb'); // -> { valid: false, reason: 'outside-alphabet' } .('b0zg-7kq'); // -> { valid: false, reason: 'wrong-length' }

DEFAULT_DICTIONARY

constant

32 characters: the lowercase alphanumerics without i, l, o and w.

Full documentation

The size is the constraint, and it comes first. 32 is even and free of case pairs (a letter alongside its uppercase form), so @evanion/luhn can compute a check character over it, and it divides 256, so byte % 32 draws every character with equal probability. Any other count either biases the draw or cannot carry a check character.

That fixes how many characters leave: four. Three pick themselves, because they are the pairs a person actually confuses — i and l read as 1, and o reads as 0. In each pair the letter goes and the digit stays, because a code is as often typed as it is spoken, and a digit reads unambiguously on a keypad.

w is the fourth, and it is the weakest of the four. Dropping only the three leaves 33, which is odd and does not divide 256, so a fourth has to go whatever the reason. w is the candidate with an argument attached: it is the one English letter whose name is polysyllabic and contains another letter’s name, so “double-u” dictated down a phone can be written back as u. Nobody mistakes w for another character on a screen.

export declare const = "0123456789abcdefghjkmnpqrstuvxyz";
import { , } from '@evanion/token'; ; // -> '0123456789abcdefghjkmnpqrstuvxyz' .; // -> 32 ; // -> 'ilow'

CONFUSABLE_CHARACTERS

constant

Characters excluded from every token alphabet, matched without regard to case.

Full documentation

1 and 0 survive their groups because a code is as often typed as it is spoken, and a digit reads unambiguously on a keypad.

CONFUSABLE_CHARACTERS is 'ilow'. createToken refuses a dictionary holding any of the four, in either case.

export declare const = "ilow";

DEFAULT_LENGTH

constant

Total characters in a token, the check character included.

export declare const = 8;

DEFAULT_CHUNK_SIZE

constant

Characters between separators. Divides DEFAULT_LENGTH exactly.

export declare const = 4;

DEFAULT_SEPARATOR

constant

Placed between chunks, and between a prefix and the code.

export declare const = "-";

TokenError

error

Base class for every error this library throws. All of them are raised by createToken: generate and validate are total, so an accepted configuration cannot fail at use.

Full documentation

A dictionary that fails one of Luhn’s own constraints throws InvalidDictionaryError from @evanion/luhn instead, which does not extend this class. Catch the two by name, or catch Error to cover both.

InvalidAlphabetError and InvalidShapeError extend TokenError.

export declare class extends { constructor(: string); }

What the tests state

1 behaviour

is an Error

is an Error

Reading the case from the package.

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { } from '@evanion/luhn'; import { , } from '@evanion/token'; /** The name of the error `createToken` throws for `dictionary`. */ function (: string): string { try { ({ }); } catch () { if ( instanceof ) return .; if ( instanceof ) return .; throw ; } return 'accepted'; } ('0123456789abcdefghijklmnopqrstuvwxyz'); // -> 'InvalidAlphabetError' ('0123456789abcdefghjkmnpqrstuvxy'); // -> 'InvalidDictionaryError' ('0123456789abcdef'); // -> 'accepted'

InvalidAlphabetError

error

Thrown by createToken for the dictionary constraints token owns: no character of CONFUSABLE_CHARACTERS, no uppercase character, which validate folds away before it looks one up, and a size that divides 256, so that byte % n draws every character equally often.

Full documentation

The remaining constraints — even size, no repeats, no case pairs — are Luhn’s, and @evanion/luhn reports them as its own InvalidDictionaryError.

One class carrying a reason rather than one class per constraint, so a caller that only wants to know whether an alphabet is acceptable writes one catch arm.

PropertyWhat it is
reasonan InvalidDictionaryReason
dictionarythe dictionary as supplied
offendingthe characters named; empty for non-uniform, which is about the size
ConstraintThrown by
no confusable charactersInvalidAlphabetError, reason: 'confusable'
lowercaseInvalidAlphabetError, reason: 'unfolded'
256 % n === 0InvalidAlphabetError, reason: 'non-uniform'
even size, no repeats, no case pairsInvalidDictionaryError from @evanion/luhn
export declare class extends { readonly : ; readonly : string; readonly : readonly string[]; constructor(: , : string, ?: readonly string[]); }

What the tests state

5 behaviours

is a TokenError carrying a name matching the class
carries the reason, the dictionary and the offending code points
defaults offending to empty, for the constraints no character owns
names the offending code points in the message
counts the dictionary by code point in the non-uniform message

is a TokenError carrying a name matching the class

Reading the case from the package.

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , } from '@evanion/token'; /** The alphabet error `createToken` throws for `dictionary`, if it throws one. */ function ( : string, ): | undefined { try { ({ }); } catch () { if ( instanceof ) return ; throw ; } return ; } const = '0123456789abcdefghijklmnopqrstuvwxyz'; ()?.; // -> 'confusable' ()?.; // -> ['i', 'l', 'o', 'w'] ('0123456789abcdefghjkmnpqrstuvx')?.; // -> 'non-uniform' ('0123456789ABCDEFGHJKMNPQRSTUVXYZ')?.; // -> 'unfolded' ('0123456789abcdef'); // -> undefined

InvalidDictionaryReason

type

Which of token’s own dictionary constraints the alphabet failed, as InvalidAlphabetError carries it. InvalidDictionaryError is @evanion/luhn’s class and carries Luhn’s reasons, not these.

reasonThe dictionary
confusableholds a character of CONFUSABLE_CHARACTERS
unfoldedholds an uppercase character
non-uniformhas a size that does not divide 256
export type = 'confusable' | 'unfolded' | 'non-uniform';

InvalidShapeError

error

Thrown by createToken when length, chunkSize or separator cannot describe a token.

Full documentation

It carries all three rather than only the offending one: the constraints are relations between them, so the value that has to change is not always the value the reason names.

PropertyWhat it is
reasonan InvalidShapeReason
length, chunkSize, separatorall three as resolved, defaults included
export declare class extends { readonly : ; readonly : number; readonly : number; readonly : string; constructor(: , : TokenShape); }

What the tests state

3 behaviours

is a TokenError carrying a name matching the class
carries all three shape values, not only the one it names
describes each reason distinctly

is a TokenError carrying a name matching the class

Reading the case from the package.

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , } from '@evanion/token'; import type { TokenOptions } from '@evanion/token'; /** The shape error `createToken` throws for `options`, if it throws one. */ function (: TokenOptions): | undefined { try { (); } catch () { if ( instanceof ) return ; throw ; } return ; } ({ : 10, : 5 }); // -> undefined ({ : 10, : 4 })?.; // -> 'chunk-size-indivisible' ({ : 10, : 4 })?.; // -> 'Token chunkSize must divide length, or the last chunk is shorter than the rest; 4 does not divide 10.' ({ : 12, : 4 }); // -> undefined ({ : '0123456789abcde-' })?.; // -> 'separator-in-dictionary' ({ : 'a' })?.; // -> 'separator-in-dictionary' ({ : 'a' })?.; // -> 8

InvalidShapeReason

type

Which of the three shape options createToken rejected, and why.

reasonThe shape
lengthlength is not an integer, or is below 2
chunk-sizechunkSize is not an integer, or is below 1
chunk-size-indivisiblechunkSize does not divide length
separator-emptyseparator is the empty string
separator-in-dictionaryseparator shares a character with dictionary
export type = 'length' | 'chunk-size' | 'chunk-size-indivisible' | 'separator-empty' | 'separator-in-dictionary';

TokenShape

interface

The three options that decide how a token is laid out, as resolved.

InvalidShapeError takes a TokenShape and copies its three fields onto itself.

interface TokenShape { : number; : number; : string; }
Last updated on