@smart-science/sid
v0.3.0
Published
SID: generator, parser, formatter, and checksum verifier
Maintainers
Readme
SID (smart-id)
Fast 20-character identifiers with two check characters: generate, parse, format, and verify. For TypeScript and JavaScript, with no dependencies.
Installation
# bun
bun add @smart-science/sid
# npm
npm install @smart-science/sidUsage
Create an ID: generate() and generateFormatted()
import { generate, generateFormatted } from '@smart-science/sid';
const id = generate(); // '0123456789ABCDEFGHWJ'
const pretty = generateFormatted(); // '0123-4567-89AB-CDEF-GHWJ'generate()returns a new random 20-character ID, typedSID.generateFormatted()returns the same kind of ID grouped for reading, typedFormattedSID.
Both throw a TypeError if the runtime has no Web Crypto (which every supported runtime has).
Derive an ID from bytes: fromBytes(bytes)
Returns the same ID for the same bytes, for example to derive an ID from an existing key. Hash the input yourself and pass the digest:
import { fromBytes } from '@smart-science/sid';
// Node.js / Bun
import { createHash } from 'node:crypto';
const id = fromBytes(createHash('sha256').update('10.1000/xyz123').digest());
// browsers
const data = new TextEncoder().encode('10.1000/xyz123');
const id = fromBytes(new Uint8Array(await crypto.subtle.digest('SHA-256', data)));- Only the first 90 bits (12 bytes) are used; a full 32-byte SHA-256 digest can be passed directly.
- Returns
nullfor fewer than 12 bytes or input that is not aUint8Array(a Node.jsBufferis accepted). - The output is not random: anyone with the same input gets the same ID.
Read an ID: parse(input)
Use parse() whenever an ID comes from outside. It accepts both forms, cleans up the input, and returns the canonical 20-character ID:
import { parse } from '@smart-science/sid';
parse('0123-4567-89AB-CDEF-GHWJ'); // { ok: true, data: '0123456789ABCDEFGHWJ' }
parse(' oi23-4567-89ab-cdef-ghwj '); // { ok: true, data: '0123456789ABCDEFGHWJ' } (see "Self-repairing input")
parse('0123-4567-89AB-CDEF-GHWK'); // { ok: false, code: 'CHECKSUM_MISMATCH', error: '...' }The result is either { ok: true, data } or { ok: false, code, error }. Check ok first:
const res = parse(input);
if (res.ok) {
console.log(res.data); // res.data is type SID
} else {
console.error(res.code); // e.g. 'CHECKSUM_MISMATCH'; see "Error codes"
}Always store and compare the canonical res.data, never the raw input.
Display an ID: format(input)
Works like parse(), but returns the hyphenated form XXXX-XXXX-XXXX-XXXX-XXXX, typed FormattedSID:
import { format } from '@smart-science/sid';
format('0123456789ABCDEFGHWJ'); // { ok: true, data: '0123-4567-89AB-CDEF-GHWJ' }
format('0123456789abcdefghwj'); // { ok: true, data: '0123-4567-89AB-CDEF-GHWJ' }
format('not an id'); // { ok: false, code: 'INVALID_LENGTH', error: '...' }Just check: verify(input)
Returns true or false. It accepts the same forgiving input as parse():
import { verify } from '@smart-science/sid';
verify('0123-4567-89ab-cdef-ghwj'); // true
verify('0123-4567-89AB-CDEF-GHWK'); // false (wrong check characters)Use parse() instead if you want to keep the ID, because verify() doesn't return the cleaned-up form.
Strict checks: isSID(input) and isFormattedSID(input)
Return true only when the input is already exactly in canonical form: uppercase, no extra spaces, no repaired characters. They are useful for checking data you store yourself:
import { isFormattedSID, isSID } from '@smart-science/sid';
isSID('0123456789ABCDEFGHWJ'); // true
isSID('0123456789abcdefghwj'); // false (valid, but not canonical: use parse())
isFormattedSID('0123-4567-89AB-CDEF-GHWJ'); // true
isFormattedSID('0123456789ABCDEFGHWJ'); // false (not hyphenated)In TypeScript, a true result also narrows the value's type to SID or FormattedSID.
import type { FormattedSID, SID } from '@smart-science/sid';
declare const input: unknown;
if (isSID(input)) {
const typed: SID = input; // typed SID
} else if (isFormattedSID(input)) {
const typed: FormattedSID = input; // typed FormattedSID
}All functions that take input accept any value, including null, numbers, and objects. They never throw; invalid input is simply rejected.
Error codes
parse() and format() report why an input was rejected. Branch on code. error is a human-readable message whose wording may change.
{
ok: false;
code: SidErrorCode;
error: string;
}| code | Meaning |
|---|---|
| NOT_A_STRING | The input is not a string |
| INVALID_LENGTH | Too short or too long for an ID |
| INVALID_FORMAT | The right length for the hyphenated form, but the hyphens are in the wrong places |
| INVALID_CHARACTER | Contains a character that can't appear in an ID; error names it |
| CHECKSUM_MISMATCH | All characters are allowed, but the check characters don't match: most likely a typo |
TypeScript types
import type { FormattedSID, SID, SidErrorCode, SidResult } from '@smart-science/sid';SID and FormattedSID are strings at runtime. In TypeScript they are kept apart from plain string; an unchecked string can't be passed where a typed ID is expected:
function load(id: SID) { /* ... */ }
declare const input: string;
load(input); // compile error: plain string is not a SID
const res = parse(input);
if (res.ok) load(res.data); // OK: validated by parse()
if (isSID(input)) load(input); // OK: validated by isSID()
load(input as SID); // OK: a cast, unsafe if uncheckedHow IDs work
Characters
An ID has 20 characters drawn from 32 symbols (Crockford's Base32): digits 0–9 and letters A–Z without I, L, O, and U. Left out because easily confused with 1 and 0 (and U to avoid accidental words). The 32 symbols are exported in order as CROCKFORD_ALPHABET, for example to build input masks.
First 18 characters are random, which gives about 1.24 × 10²⁷ possible IDs. Two randomly generated IDs are practically never the same. The last two characters are check characters: each of the first 18 characters is multiplied by its position (1 to 18), the products are summed, and the sum modulo 1024 is written as two characters.
Self-repairing input
- lowercase letters are accepted:
abc→ABC IandLconverted to1, andOto0- surrounding spaces are ignored
- the hyphenated and plain forms are both accepted
What the check characters catch
- Any single wrong character is always detected, including in the check characters.
- Any two characters swapped (
…AB…typed as…BA…) is always detected, at any distance and at any of the 20 positions. - Random input passes with a probability of 1 in 1024.
- Not detected: some combinations of several errors (about 0.32% of inputs with two wrong characters). The check guards against typos. It is not a guarantee that random input can never pass.
What an ID does not do
- It contains no date and has no order.
- It is not secret and proves nothing about who created it.
Runtime support
| Runtime | Versions |
|---|---|
| Node.js | 22.12 and later (import and require()) |
| Bun | 1.3 and later |
| Deno | Current, via npm:@smart-science/sid |
| Browsers | Current evergreen browsers |
Changelog
See CHANGELOG.md.
