Ahead of the release
npm install @evanion/token gives you 0.1.0. These pages document main, which has changes that release does not.
Entropy and collisions
length counts the check character, so usable entropy is
(length - 1) * log2(n). At the defaults — length: 8, n: 32 — that is 35
bits, and entropyBits reports it:
const token = createToken();
token.entropyBits; // 35
createToken({ length: 12 }).entropyBits; // 55
createToken({ length: 13, chunkSize: 13 }).entropyBits; // 60Read it as a collision budget
35 bits is 34,359,738,368 values. The birthday bound, not the size of the space, is what decides when two codes collide:
| Issued codes | Chance at least two collide, at 35 bits |
|---|---|
| 10,000 | about 0.15% |
| 100,000 | about 13% |
| 218,000 | about 50% |
| 1,000,000 | effectively certain |
The 50% point is roughly 1.18 × sqrt(2^bits). Five more characters adds 25
more bits, which moves it from 218,000 to about 1.3 billion:
length | bits | 50% collision at |
|---|---|---|
| 6 | 25 | 6,800 |
| 8 | 35 | 218,000 |
| 10 | 45 | 7,000,000 |
| 13 | 60 | 1,300,000,000 |
Pick length from the row above your expected volume, with room for the volume
you will have in three years.
Uniqueness is a database constraint
generate never retries and never checks for collisions. It cannot: the package
has no idea what you have issued.
So put a unique index on the column, and handle the conflict:
async function issue(): Promise<string> {
for (let attempt = 0; attempt < 5; attempt++) {
const { value, body } = token.generate({ prefix: 'ORD' });
try {
await db.orders.insert({ code: body });
return value;
} catch (error) {
if (!isUniqueViolation(error)) throw error;
}
}
throw new Error('could not allocate an order code');
}Index on body, not on value. body is the unchunked, prefix-free,
case-folded form, and it is what validate hands back for a lookup — a caller
who typed the code without its separators has to find the same row.
The retry loop belongs here because the database is the only participant that knows what has been issued.
This is not a guess-resistance budget
35 bits is not enough to stop an attacker enumerating codes. If the code is a capability — a password reset link, a download token, anything where knowing the string grants access — this is the wrong package. Use a 128-bit random value and do not make it human-readable, because nobody types it.
What this package is for is a code a person handles: an order reference, a gift card, a support ticket, a table number. Those are looked up, not authorised, and the thing you are defending against is a transcription error.
If a human-readable code does gate something, rate-limit the lookup and bind it to a second factor the guesser does not have — the email address it was issued to, for instance.
Shortening the code instead
Two ways to spend fewer characters for the same entropy, neither free:
Raise n. 64 characters is 6 bits each instead of 5, but the alphabet rules
forbid uppercase, which is where the extra 32 would have to come from.
Drop the check character. length: 8 with no check would be 40 bits rather than
35, and a mistyped code would then cost a database lookup and an unhelpful “not
found”. There is no option to do it.