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.
The alphabet
To mint codes from your own set of characters, pass a dictionary string to createToken. However, not every string of characters makes for a reliable token. To prevent user frustration and mathematical bias, @evanion/token enforces strict rules on any custom alphabet.
The Problem: Confusable Characters and Mathematical Bias
When a code is read aloud or typed from a printed receipt, certain characters are easily confused. A 1 can be mistaken for an l or an i, and a 0 for an o. If your token dictionary includes these, your error-detection system’s effectiveness drops significantly because the user’s “typo” might be a character that is actually valid in your dictionary.
Furthermore, the way random characters are drawn from a dictionary can introduce “bias.” If the dictionary size doesn’t divide evenly into the range of the random number generator, some characters will appear more often than others, making the codes slightly more predictable.
The Default Dictionary
By default, @evanion/token uses DEFAULT_DICTIONARY, which consists of lowercase alphanumerics minus the confusable characters: i, l, o, and w.
import { , } from '@evanion/token';
; // -> '0123456789abcdefghjkmnpqrstuvxyz'
.; // -> 32
; // -> 'ilow'Why these four?
i,l, andoare visually similar to1and0.wis phonetically similar tou(“double-u”), which leads to transcription errors when codes are dictated over the phone.
This leaves exactly 32 characters. This is an ideal number because it is even (required for the Luhn algorithm) and it divides 256 evenly (preventing random bias).
Dictionary Constraints
When you provide a custom dictionary to createToken, the library validates it against several constraints. If any are broken, it throws either an InvalidAlphabetError or an InvalidDictionaryError.
| Constraint | Enforced by | reason | Why it matters |
|---|---|---|---|
| No Confusables | @evanion/token | confusable | Prevents 1/l/i and 0/o and w/u confusion. |
| Lowercase Only | @evanion/token | unfolded | validate folds input to lowercase; uppercase in the dictionary would cause mismatches. |
| Divides 256 | @evanion/token | non-uniform | Ensures every character is equally likely to be drawn. |
| Min Length 2 | @evanion/luhn | too-short | a 1-character dictionary makes every code valid. |
| Even Size | @evanion/luhn | odd-length | The Luhn doubling step requires an even-sized alphabet. |
| No Repeats | @evanion/luhn | duplicate | Each character must have a unique index for the checksum. |
| No Case Pairs | @evanion/luhn | case-pairs | Prevents a letter and its uppercase version from sharing an index. |
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'); // -> undefinedWhy the Dictionary Size Must Divide 256
The generate method draws characters using the formula byte % n, where the byte is a random value from 0 to 255.
If n (the dictionary size) is 32, the division is exact: . Every character appears exactly 8 times in the range.
If n were 36, with a remainder of . The first four characters in the dictionary would appear 8 times, while the others appear only 7 times. This 14.3% bias makes the codes mathematically non-uniform. To avoid “rejection sampling” (which takes variable time), @evanion/token simply requires that n divides 256.
Handling Alphabet Errors
Because @evanion/token relies on @evanion/luhn for its checksum logic, you may encounter errors from both packages.
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'Pro Tip: If you use a strict package manager like pnpm, ensure you add @evanion/luhn to your own package.json at the exact version pinned by @evanion/token. This ensures instanceof checks against InvalidDictionaryError work correctly across different versions of the library.
Hexadecimal Gift Cards
Hexadecimal (0123456789abcdef) is a perfect alternative dictionary. It is even, lowercase, free of confusables, and 16 divides 256.
const = ({ : '0123456789abcdef', : 16 });
.; // -> 16
.; // -> 60
.()..; // -> 19Note that since a hex character carries only 4 bits of entropy (compared to 5 bits for the default 32-character set), you will need a longer code to achieve the same level of collision resistance.