├── .gitignore ├── .npmignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── npm-publish-github-packages.yml ├── .npmrc ├── index.d.ts ├── package.json ├── README.md ├── index.js └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package 3 | *.tgz 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | *.test.js 3 | *.log 4 | *.tgz 5 | bun.lock 6 | bun.lockb 7 | package -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: adaptive 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @adaptive:registry=https://npm.pkg.github.com/adaptive 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** Generate a random ID string. */ 2 | export function generateId(length?: number): string; 3 | 4 | /** Generate a ULID (26-character lexicographically sortable identifier). */ 5 | export function generateUlid(ts?: number): string; 6 | 7 | /** Decode the timestamp portion of a ULID back into a Date. */ 8 | export function decodeUlidTimestamp(id?: string): Date | null; 9 | 10 | export default generateId; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "07:24" 8 | open-pull-requests-limit: 10 9 | target-branch: master 10 | assignees: 11 | - adaptive 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | time: "07:24" 17 | open-pull-requests-limit: 10 18 | target-branch: master 19 | assignees: 20 | - adaptive -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-gpr: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-node@v6 16 | with: 17 | node-version: 25 18 | registry-url: https://npm.pkg.github.com/ 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adaptive/makeid", 3 | "version": "1.0.0", 4 | "description": "generate random ids, ulids", 5 | "type": "module", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./index.d.ts", 11 | "import": "./index.js" 12 | } 13 | }, 14 | "publishConfig": { 15 | "registry": "https://npm.pkg.github.com/", 16 | "tag": "latest" 17 | }, 18 | "scripts": { 19 | "pretest": "npm pack && tar -xvzf *.tgz", 20 | "test": "bun test" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/adaptive/makeid.git" 25 | }, 26 | "keywords": [ 27 | "id", 28 | "javascript", 29 | "random", 30 | "ulid" 31 | ], 32 | "author": "Hugo Romano", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/adaptive/makeid/issues" 36 | }, 37 | "homepage": "https://github.com/adaptive/makeid#readme" 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @adaptive/makeid 2 | 3 | Generate short, unambiguous IDs using uppercase letters and digits (omits visually confusing characters). Also ships a ULID helper. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm install @adaptive/makeid 9 | ``` 10 | 11 | ## Quick start 12 | 13 | ```js 14 | import makeid, { decodeUlidTimestamp, generateId, generateUlid } from "@adaptive/makeid"; 15 | 16 | const id = makeid(); // e.g. "9X0V2G" (default length 6) 17 | const shorter = generateId(4); // e.g. "T72P" 18 | const sortable = generateUlid(); // 26-char, time-sortable ID 19 | const createdAt = decodeUlidTimestamp(sortable); // Date | null 20 | ``` 21 | 22 | ## API surface 23 | 24 | `makeid(length?: number): string` 25 | - Default length is 6; negative values are coerced via `Math.abs(length)`. 26 | - Alphabet omits ambiguous characters such as O/0. 27 | 28 | `generateUlid(ts?: number): string` 29 | - 26-character ULID; `ts` defaults to `Date.now()`. 30 | - Lexicographically sortable by timestamp; throws if `crypto.getRandomValues` is unavailable. 31 | 32 | `decodeUlidTimestamp(id?: string): Date | null` 33 | - Extracts the embedded timestamp (first 10 chars) as a `Date`. 34 | - Returns `null` for missing/invalid input. 35 | 36 | ## Examples 37 | 38 | ### Generate IDs of varying lengths 39 | 40 | ```js 41 | makeid(); // "A1B2C3" 42 | makeid(1); // "R" 43 | makeid(10); // "5JK8W2H9PD" 44 | makeid(-4); // "7T8C" (negative length handled) 45 | ``` 46 | 47 | ### ULIDs and decoding timestamps 48 | 49 | ```js 50 | const id = generateUlid(1700000000000); 51 | // "01HF3S9X5QZ9M8JW4YV3Y7HB5C" 52 | 53 | const date = decodeUlidTimestamp(id); 54 | // Date corresponding to 1700000000000 55 | ``` 56 | 57 | ## Development 58 | 59 | - Tests: `bun test` 60 | - Package is published as ESM. 61 | - ULID generation uses `crypto.getRandomValues`; ensure a crypto implementation is available in your runtime. 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { webcrypto } from "node:crypto"; 2 | 3 | // Alphabet for ULID generation 4 | const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; 5 | 6 | // Omit potentially confusing characters 7 | const nonConfusing = "ABCDEFGHIJKLMNPQRSTUVWXYZ0123456789"; 8 | 9 | /** 10 | * Generate a random ID. 11 | * @param {number} [length=6] positive number of characters in the generated ID 12 | * @returns {string} 13 | */ 14 | export const generateId = (length = 6) => { 15 | const normalizedLength = Math.abs(length); 16 | let text = ""; 17 | for (let i = 0; i < normalizedLength; i++) { 18 | text += nonConfusing.charAt(Math.floor(Math.random() * nonConfusing.length)); 19 | } 20 | return text; 21 | } 22 | 23 | /** 24 | * Generate a ULID (Universally Unique Lexicographically Sortable Identifier). 25 | * @param {number} [ts=Date.now()] timestamp in milliseconds 26 | * @returns {string} ULID string 27 | */ 28 | 29 | export const generateUlid = (ts = Date.now()) => { 30 | const cryptoApi = globalThis.crypto ?? webcrypto; 31 | if (!cryptoApi?.getRandomValues) { 32 | throw new Error("crypto.getRandomValues is not available in this environment"); 33 | } 34 | 35 | let id = ""; 36 | 37 | // Timestamp: most significant digit first so lexical order matches time 38 | for (let i = 9; i >= 0; i -= 1) { 39 | id += ALPHABET[Math.floor(ts / 32 ** i) % 32]; 40 | } 41 | 42 | // 16 random bytes, mask to 5 bits 43 | const buf = cryptoApi.getRandomValues(new Uint8Array(16)); 44 | for (const b of buf) { 45 | id += ALPHABET[b & 31]; 46 | } 47 | 48 | return id; 49 | } 50 | 51 | /** 52 | * Decode the timestamp component of a ULID. 53 | * @param {string} id ULID string 54 | * @returns {Date|null} Date for the embedded timestamp, or null on invalid input 55 | */ 56 | export const decodeUlidTimestamp = (id) => { 57 | if (!id || id.length < 10) return null; 58 | 59 | let ts = 0; 60 | 61 | for (let i = 0; i < 10; i += 1) { 62 | const value = ALPHABET.indexOf(id[i]); 63 | if (value < 0) return null; 64 | ts = ts * 32 + value; 65 | } 66 | 67 | if (!Number.isFinite(ts)) return null; 68 | return new Date(ts); 69 | } 70 | 71 | export default generateId; 72 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | import makeid, { decodeUlidTimestamp,generateUlid, generateId} from "./package/index.js"; 2 | 3 | test("import test", () => { 4 | expect(makeid()).toHaveLength(6); 5 | expect(generateId()).toHaveLength(6); 6 | expect(generateUlid()).toHaveLength(26); 7 | }); 8 | 9 | test("generate an ID with default length", () => { 10 | expect(makeid()).toHaveLength(6); 11 | }); 12 | 13 | test("generate an ID with length of -1", () => { 14 | expect(makeid(-1)).toHaveLength(1); 15 | }); 16 | 17 | test("generate an ID with length of 0", () => { 18 | expect(makeid(0)).toHaveLength(0); 19 | }); 20 | 21 | test("generate an ID with length of 1", () => { 22 | expect(makeid(1)).toHaveLength(1); 23 | }); 24 | 25 | test("generate an ID with length of 3", () => { 26 | expect(makeid(3)).toHaveLength(3); 27 | }); 28 | 29 | test("generate an ID with length of 6", () => { 30 | expect(makeid(6)).toHaveLength(6); 31 | }); 32 | 33 | test("generate an ID with length of 32", () => { 34 | expect(makeid(32)).toHaveLength(32); 35 | }); 36 | 37 | test("generate an ID with length of 100", () => { 38 | expect(makeid(100)).toHaveLength(100); 39 | }); 40 | 41 | test("generate an ID with length of -1000", () => { 42 | expect(makeid(-1000)).toHaveLength(1000); 43 | }); 44 | 45 | test("generate an ID with length of 10000", () => { 46 | expect(makeid(10000)).toHaveLength(10000); 47 | }); 48 | 49 | test("generate an ID with length of 100000", () => { 50 | expect(makeid(100000)).toHaveLength(100000); 51 | }); 52 | 53 | test("generate a ULID with default timestamp", () => { 54 | const id = generateUlid(); 55 | expect(id).toHaveLength(26); 56 | expect(id).toMatch(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/); 57 | }); 58 | 59 | test("generate a ULID with specific timestamp", () => { 60 | const ts = 1893455999000; // Specific timestamp 61 | const id = generateUlid(ts); 62 | expect(id).toHaveLength(26); 63 | expect(id).toMatch(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/); 64 | }); 65 | 66 | test("ULIDs are sortable by timestamp", () => { 67 | const older = generateUlid(1893455998999); 68 | const newer = generateUlid(1893455999000); 69 | expect(older < newer).toBe(true); 70 | }); 71 | 72 | test("generate multiple ULIDs and ensure uniqueness", () => { 73 | const ulidSet = new Set(); 74 | const count = 1000; 75 | for (let i = 0; i < count; i++) { 76 | ulidSet.add(generateUlid()); 77 | } 78 | expect(ulidSet.size).toBe(count); 79 | }); 80 | 81 | test("decode ULID timestamp returns matching Date", () => { 82 | const ts = 1893455999000; 83 | const id = generateUlid(ts); 84 | const decoded = decodeUlidTimestamp(id); 85 | expect(decoded?.getTime()).toBe(ts); 86 | }); 87 | 88 | test("decode ULID timestamp returns null on invalid input", () => { 89 | expect(decodeUlidTimestamp("")).toBeNull(); 90 | expect(decodeUlidTimestamp("INVALID-ULID-STRING")).toBeNull(); 91 | }); 92 | --------------------------------------------------------------------------------