Skip to content

KellyDC/genauid

Repository files navigation

genauid

GenauID is a high-performance, environment-agnostic utility for generating time-based, lexicographically sortable, and cryptographically random unique identifiers. It works in Node.js, browsers, Cloudflare Workers, Deno, and Bun.

Inspired by the stern and precise First-Class Mage Genau from Frieren: Beyond Journey's End, this package is designed for systems where order, efficiency, and unwavering reliability are non-negotiable.

Key Features

  • Sortable by creation time — lexicographic order equals chronological order, enabling efficient B-tree index range scans.
  • Cryptographically random — uses globalThis.crypto.getRandomValues() with bias elimination, never Math.random().
  • Environment-agnostic — no Node.js built-ins required; works in browsers, Cloudflare Workers, Deno, and Bun out of the box.
  • Dual CJS + ESM output — ships dist/index.cjs and dist/index.mjs with full tree-shaking support ("sideEffects": false).
  • Highly customisable — length, character set, timestamp width, and separator are all configurable.
  • Human-readable slugsslugify() converts any string to a URL-safe slug, with optional random or timestamp suffix for collision avoidance.
  • Validation — built-in validator checks format, character set, and optionally enforces a maximum age.
  • Full TypeScript types.d.ts / .d.mts declarations generated from TypeScript source.
  • 100% test coverage — 92 tests covering edge cases, security, and performance.

Requirements

  • Node.js ≥ 15, or any runtime that exposes the Web Crypto API (globalThis.crypto): modern browsers, Cloudflare Workers, Deno, Bun.

Installation

npm install genauid

Quick Start

import { generate, slugify, validate, CHARSETS } from 'genauid';

// Generate a time-based sortable ID (26 chars, BASE32 charset by default)
const id = generate();
console.log(id); // e.g. '01J3RVMQ8Z4KXNTBPD6S7WHMF'

// Slugify a string (plain)
const slug = slugify('Hello World');
console.log(slug); // 'hello-world'

// Slugify with a timestamp suffix for collision-safe unique slugs
const uniqueSlug = slugify('Hello World', { suffix: 'timestamp' });
console.log(uniqueSlug); // e.g. 'hello-world-01j3rvmq8z-k4xntbpd'

// Validate a previously generated ID
const result = validate(id);
console.log(result.valid);     // true
console.log(result.timestamp); // Date object: the generation time

API

generate(options?): string

Generates a time-based, cryptographically random, lexicographically sortable ID.

The ID starts with a fixed-width timestamp prefix (chronologically sortable) followed by a cryptographically random suffix.

Options

Option Type Default Description
length number 26 Total length of the generated ID (10–128).
tsLength number 10 Number of leading characters encoding the timestamp.
charset string CHARSETS.BASE32 Character set to draw characters from.
separator string '' Optional separator inserted between timestamp and random parts.

Examples

// Default — 26-char BASE32 ID
generate();
// '01J3RVMQ8Z4KXNTBPD6S7WHMF'

// Custom length (32 chars)
generate({ length: 32 });

// Custom charset
generate({ charset: CHARSETS.ALPHANUMERIC, length: 32 });

// With separator (makes the timestamp boundary visible)
generate({ tsLength: 10, length: 27, separator: '-' });
// '01J3RVMQ8Z-4KXNTBPD6S7WH'

// HEX IDs for legacy systems
generate({ charset: CHARSETS.HEX, length: 32 });

slugify(str, options?): string

Converts a string into a URL-friendly slug. Optionally appends a random or timestamp-based suffix to guarantee uniqueness.

Options

Option Type Default Description
charset string CHARSETS.SLUG Character set to allow (lowercase alphanumeric by default).
separator string '-' Separator used between words and suffix parts.
suffix 'none'|'random'|'timestamp' 'none' Suffix mode: 'none' = plain slug; 'random' = append random string; 'timestamp' = append encoded timestamp + random string.
randomLength number 8 Characters for the random part (used when suffix is 'random' or 'timestamp').
tsLength number 10 Characters for the timestamp part (used when suffix is 'timestamp').

Examples

// Plain slug
slugify('Hello World!');
// 'hello-world'

// Diacritics / accents are removed
slugify('Café au lait');
// 'cafe-au-lait'

// Custom separator
slugify('Café au lait', { separator: '_' });
// 'cafe_au_lait'

// Append a random suffix (collision-resistant)
slugify('Hello World', { suffix: 'random' });
// 'hello-world-a3b8x2k4'

// Append a timestamp + random suffix (sortable and collision-safe)
slugify('Hello World', { suffix: 'timestamp' });
// 'hello-world-01j3rvmq8z-k4xntbpd'

// Custom suffix length
slugify('Hello World', { suffix: 'random', randomLength: 12 });
// 'hello-world-a3b8x2k4j9p2'

validate(id, options?): ValidationResult

Validates a previously generated ID.

Checks performed:

  1. Input is a non-empty string.
  2. Length matches expectation (when length is provided).
  3. All characters belong to the expected charset.
  4. The embedded timestamp is not in the future (within tolerated clock skew).
  5. The embedded timestamp is not before year 2000.
  6. The ID has not exceeded maxAgeMs (when provided).

Options

Option Type Default Description
charset string CHARSETS.BASE32 Expected character set.
length number Expected total length (optional).
tsLength number 10 Expected timestamp prefix length.
separator string '' Expected separator used during generation.
maxAgeMs number Reject IDs older than this many milliseconds (optional).
clockSkewMs number 5000 Tolerated future clock skew in milliseconds.

Return value: ValidationResult

{
  valid: boolean;      // true if all checks pass
  errors: string[];    // list of failure reasons (empty when valid)
  timestamp: Date | null; // extracted Date from the embedded timestamp
}

Examples

const id = generate();
const result = validate(id);
// { valid: true, errors: [], timestamp: Date }

// Validate with full options
validate(id, {
  charset: CHARSETS.BASE32,
  length: 26,
  maxAgeMs: 30_000, // reject IDs older than 30 seconds
});

// Validate a slug
validate(slug, {
  charset: CHARSETS.SLUG,
  separator: '-',
  tsLength: 10,
});

decodeTimestamp(encoded, charset): bigint

Decodes the timestamp prefix of an ID back into a BigInt millisecond value.

import { decodeTimestamp, CHARSETS } from 'genauid';

const tsPart = id.slice(0, 10);
const ms = decodeTimestamp(tsPart, CHARSETS.BASE32);
console.log(new Date(Number(ms))); // generation Date

CHARSETS

Built-in character sets:

Name Characters Notes
CHARSETS.BASE32 0–9 A–Z (Crockford variant) Default for generate(). Excludes I, L, O, U to avoid visual confusion.
CHARSETS.SLUG 0–9 a–z Default for slugify(). URL-safe, lowercase.
CHARSETS.ALPHANUMERIC 0–9 A–Z a–z Maximum density (62 symbols).
CHARSETS.HEX 0–9 a–f Hexadecimal — lowest density, widest compatibility.

CHARSET alias

CHARSET is a convenience alias for CHARSETS, enabling the single-word import pattern:

import { generate, slugify, CHARSET } from 'genauid';
generate({ charset: CHARSET.BASE32 });

You can also pass any custom string as a charset. Requirements:

  • Minimum 2 characters.
  • No duplicate characters.
  • Characters should be in ASCII-sorted order for guaranteed lexicographic sortability (the timestamp prefix relies on this).

Sortability guarantee

The timestamp prefix is encoded as a fixed-width base-N number where the digit symbols are in ascending ASCII order. This means:

encode(t1) < encode(t2)  ⟺  t1 < t2

All built-in charsets (BASE32, SLUG, ALPHANUMERIC, HEX) are in ascending ASCII order, so lexicographic sorting of generated IDs equals chronological sorting.

Custom charsets: ensure your charset characters are in ascending ASCII order if sortability is required.


Uniqueness

Each ID combines:

  • A millisecond-precision timestamp (common prefix within the same millisecond).
  • A cryptographically random suffix produced via crypto.randomBytes with modulo-bias elimination.

With a 16-character BASE32 random suffix the probability of a collision within the same millisecond is approximately $1 / 32^{16} \approx 10^{-24}$, making collisions practically impossible.


Security considerations

  • The random component is generated with globalThis.crypto.getRandomValues(), which is cryptographically secure across all supported runtimes.
  • Modulo bias is eliminated by rejection sampling.
  • The validation function uses a pure constant-time XOR comparison to prevent timing attacks (no longer depends on Node's timingSafeEqual).
  • IDs embed a timestamp and are therefore not suitable as opaque secrets (an attacker can determine when an ID was created). Use them as primary keys, not as authentication tokens.

Performance

Benchmarked on Node.js 24 (Apple M-class / modern x86):

Operation Throughput
generate() > 100,000 IDs/s
slugify() (plain) > 100,000 slugs/s
validate() > 500,000 validations/s

Run the built-in performance tests:

npm test

Running tests

npm test               # run tests with coverage report
npm run test:watch     # watch mode

Coverage thresholds (enforced):

  • Statements: 90%
  • Branches: 90%
  • Functions: 90%
  • Lines: 90%

Actual coverage of this release: 100% statements, 97.84% branches, 100% functions, 100% lines.

92 tests pass, ESLint clean, 100% statement/function/line coverage.

Here's the full security assessment:


Automated checks

Check Result
npm audit 0 vulnerabilities
eslint-plugin-security 0 warnings/errors
tsc --noEmit 0 type errors

Manual review — all clear

Area What was checked Status
Random generation globalThis.crypto.getRandomValues() only — no Math.random()
Modulo bias Rejection sampling eliminates skew
Timing attacks Pure constant-time XOR comparison — no Node.js dependency
RegExp injection escapeRegex() sanitises separator before new RegExp()
eval / new Function None present
Prototype pollution Options read with !== undefined guards, not in or bare access
Input bounds All lengths, charsets, and separators validated before use
NaN/Infinity inputs Fixed — maxAgeMs and clockSkewMs now reject non-finite/invalid values with a clear error

One inherent design note (not a vulnerability): IDs embed a millisecond timestamp, so anyone who holds an ID can determine when it was created — don't use them as secrets or authentication tokens, only as primary keys.


TypeScript

The package ships with .d.ts and .d.mts declaration files generated from TypeScript source (no hand-written stubs). An exports map ensures it works out of the box with all TypeScript moduleResolution modes — including node, node16, and bundler.

import { generate, slugify, validate, CHARSETS, CHARSET } from 'genauid';
import type { GenerateOptions, SlugOptions, ValidateOptions, ValidationResult } from 'genauid';

const id: string = generate({ length: 32, charset: CHARSETS.ALPHANUMERIC });
const slug: string = slugify('Hello World', { suffix: 'timestamp' });
const result: ValidationResult = validate(id);

Limitations

  • IDs are not RFC 4122 UUIDs. If strict UUID format compliance is required, use the uuid package instead.
  • The timestamp has millisecond precision. Multiple IDs generated within the same millisecond share the same timestamp prefix and are ordered randomly among themselves.
  • Maximum ID length is 128 characters.

License

MIT

About

A utility for generating time-based, lexicographically sortable, and cryptographically random unique identifiers.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors