@marianmeres/uid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +111 -0
- package/API.md +425 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/alphabet.d.ts +25 -0
- package/dist/alphabet.js +40 -0
- package/dist/base56.d.ts +34 -0
- package/dist/base56.js +69 -0
- package/dist/counter.d.ts +55 -0
- package/dist/counter.js +87 -0
- package/dist/mod.d.ts +20 -0
- package/dist/mod.js +20 -0
- package/dist/random.d.ts +49 -0
- package/dist/random.js +102 -0
- package/dist/reversible.d.ts +41 -0
- package/dist/reversible.js +93 -0
- package/dist/rhr.d.ts +23 -0
- package/dist/rhr.js +28 -0
- package/dist/uid.d.ts +93 -0
- package/dist/uid.js +93 -0
- package/dist/uuid.d.ts +33 -0
- package/dist/uuid.js +74 -0
- package/package.json +42 -0
package/dist/random.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto-backed randomness primitives shared by every strategy.
|
|
3
|
+
*
|
|
4
|
+
* All randomness goes through the Web Crypto API (`crypto.getRandomValues`),
|
|
5
|
+
* which is available in every modern runtime (browsers, Deno, Node 19+, Bun)
|
|
6
|
+
* without any external dependency. This is good-quality randomness, but the
|
|
7
|
+
* library is NOT intended for cryptographic secrets (tokens, keys, etc.).
|
|
8
|
+
*/
|
|
9
|
+
/** Default length for random, fixed-length strategies (nanoid, base*, hex, …). */
|
|
10
|
+
export const DEFAULT_LENGTH = 21;
|
|
11
|
+
/**
|
|
12
|
+
* Named alphabets used by the built-in strategies.
|
|
13
|
+
*
|
|
14
|
+
* The "no-ambiguous" alphabets (`base56`, `base58`, `base32`) drop visually
|
|
15
|
+
* confusable characters so the output is safe to read aloud / retype.
|
|
16
|
+
*/
|
|
17
|
+
export const ALPHABETS = {
|
|
18
|
+
// lowercase hex
|
|
19
|
+
hex: "0123456789abcdef",
|
|
20
|
+
// Crockford base32 (no I, L, O, U) — power of two, sorts case-insensitively
|
|
21
|
+
base32: "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
|
|
22
|
+
// lowercase base36
|
|
23
|
+
base36: "0123456789abcdefghijklmnopqrstuvwxyz",
|
|
24
|
+
// no 0 O o 1 l I — safe to read aloud / retype
|
|
25
|
+
base56: "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz",
|
|
26
|
+
// Bitcoin base58 (no 0 O I l)
|
|
27
|
+
base58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
|
|
28
|
+
// full base62
|
|
29
|
+
base62: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
30
|
+
// digits only — handy for OTPs / numeric coupon codes
|
|
31
|
+
numeric: "0123456789",
|
|
32
|
+
// alias of base62
|
|
33
|
+
alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
34
|
+
// URL-safe 64-char set (A–Z a–z 0–9 _ -)
|
|
35
|
+
nanoid: "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",
|
|
36
|
+
};
|
|
37
|
+
/** Throws `TypeError` unless `n` is an integer `>= 1`. */
|
|
38
|
+
export function assertPositiveInt(n, label) {
|
|
39
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
|
|
40
|
+
throw new TypeError(`"${label}" must be a positive integer, got ${String(n)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Throws `TypeError` unless `n` is an integer `>= 0`. */
|
|
44
|
+
export function assertNonNegativeInt(n, label) {
|
|
45
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
|
|
46
|
+
throw new TypeError(`"${label}" must be a non-negative integer, got ${String(n)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns `size` cryptographically-strong random bytes.
|
|
51
|
+
*
|
|
52
|
+
* Transparently chunks calls larger than the 65536-byte `getRandomValues`
|
|
53
|
+
* limit so any `size` is supported.
|
|
54
|
+
*/
|
|
55
|
+
export function randomBytes(size) {
|
|
56
|
+
assertPositiveInt(size, "size");
|
|
57
|
+
const bytes = new Uint8Array(size);
|
|
58
|
+
const MAX = 65536; // crypto.getRandomValues hard limit per call
|
|
59
|
+
for (let offset = 0; offset < size; offset += MAX) {
|
|
60
|
+
crypto.getRandomValues(bytes.subarray(offset, Math.min(offset + MAX, size)));
|
|
61
|
+
}
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generates a random string of `length` characters from `alphabet`.
|
|
66
|
+
*
|
|
67
|
+
* Uses rejection sampling (the nanoid algorithm) so the output is unbiased
|
|
68
|
+
* even when `alphabet.length` is not a power of two — no modulo skew.
|
|
69
|
+
*
|
|
70
|
+
* @param length Number of characters to produce (positive integer).
|
|
71
|
+
* @param alphabet Characters to choose from (1–256 chars). Defaults to the
|
|
72
|
+
* URL-safe nanoid alphabet.
|
|
73
|
+
*/
|
|
74
|
+
export function randomString(length, alphabet = ALPHABETS.nanoid) {
|
|
75
|
+
assertPositiveInt(length, "length");
|
|
76
|
+
if (typeof alphabet !== "string") {
|
|
77
|
+
throw new TypeError(`"alphabet" must be a string, got ${typeof alphabet}`);
|
|
78
|
+
}
|
|
79
|
+
const alen = alphabet.length;
|
|
80
|
+
if (alen < 1 || alen > 256) {
|
|
81
|
+
throw new RangeError(`"alphabet" length must be between 1 and 256, got ${alen}`);
|
|
82
|
+
}
|
|
83
|
+
// degenerate single-char alphabet: every position is that char
|
|
84
|
+
if (alen === 1)
|
|
85
|
+
return alphabet.repeat(length);
|
|
86
|
+
// smallest 2^k - 1 mask that can index the whole alphabet
|
|
87
|
+
const mask = (2 << Math.floor(Math.log2(alen - 1))) - 1;
|
|
88
|
+
// over-fetch factor (1.6) keeps the rejection loop fast on average
|
|
89
|
+
const step = Math.ceil((1.6 * mask * length) / alen);
|
|
90
|
+
let id = "";
|
|
91
|
+
while (true) {
|
|
92
|
+
const bytes = randomBytes(step);
|
|
93
|
+
for (let i = 0; i < step; i++) {
|
|
94
|
+
const idx = bytes[i] & mask;
|
|
95
|
+
if (idx < alen) {
|
|
96
|
+
id += alphabet[idx];
|
|
97
|
+
if (id.length === length)
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reversible integer ↔ short-string codec (a small "sqids/hashids-lite").
|
|
3
|
+
*
|
|
4
|
+
* Encodes a non-negative integer into a short opaque string and back. With a
|
|
5
|
+
* `salt`, the alphabet is deterministically shuffled so sequential ids do not
|
|
6
|
+
* produce obviously sequential output — handy for exposing database row ids in
|
|
7
|
+
* URLs without leaking the row count.
|
|
8
|
+
*
|
|
9
|
+
* NOT a security mechanism: like hashids, the mapping is reversible by anyone
|
|
10
|
+
* who knows (or guesses) the salt and alphabet. Do not use it to protect data.
|
|
11
|
+
*/
|
|
12
|
+
/** Options for {@link encodeInt}. */
|
|
13
|
+
export interface EncodeIntOptions {
|
|
14
|
+
/** Alphabet of unique characters to encode into (default: base62). */
|
|
15
|
+
alphabet?: string;
|
|
16
|
+
/** Salt that deterministically shuffles the alphabet (default: none). */
|
|
17
|
+
salt?: string;
|
|
18
|
+
/** Left-pad the result to at least this length (default: `0`). */
|
|
19
|
+
minLength?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link decodeInt}. */
|
|
22
|
+
export interface DecodeIntOptions {
|
|
23
|
+
/** Must match the alphabet used to encode (default: base62). */
|
|
24
|
+
alphabet?: string;
|
|
25
|
+
/** Must match the salt used to encode (default: none). */
|
|
26
|
+
salt?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Encodes a non-negative integer into a short string.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* encodeInt(12345); // base62, e.g. "3D7"
|
|
33
|
+
* encodeInt(12345, { salt: "secret" }); // shuffled, e.g. "Yq9"
|
|
34
|
+
* encodeInt(7, { minLength: 5 }); // "00007"-style padding
|
|
35
|
+
*/
|
|
36
|
+
export declare function encodeInt(value: number, options?: EncodeIntOptions): string;
|
|
37
|
+
/**
|
|
38
|
+
* Decodes a string produced by {@link encodeInt}. The `alphabet` and `salt`
|
|
39
|
+
* must match those used to encode.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decodeInt(str: string, options?: DecodeIntOptions): number;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reversible integer ↔ short-string codec (a small "sqids/hashids-lite").
|
|
3
|
+
*
|
|
4
|
+
* Encodes a non-negative integer into a short opaque string and back. With a
|
|
5
|
+
* `salt`, the alphabet is deterministically shuffled so sequential ids do not
|
|
6
|
+
* produce obviously sequential output — handy for exposing database row ids in
|
|
7
|
+
* URLs without leaking the row count.
|
|
8
|
+
*
|
|
9
|
+
* NOT a security mechanism: like hashids, the mapping is reversible by anyone
|
|
10
|
+
* who knows (or guesses) the salt and alphabet. Do not use it to protect data.
|
|
11
|
+
*/
|
|
12
|
+
import { ALPHABETS } from "./random.js";
|
|
13
|
+
/**
|
|
14
|
+
* Deterministically shuffles `alphabet` based on `salt` (the hashids
|
|
15
|
+
* "consistent shuffle" — stable across runtimes, no RNG involved).
|
|
16
|
+
*/
|
|
17
|
+
function consistentShuffle(alphabet, salt) {
|
|
18
|
+
if (!salt.length)
|
|
19
|
+
return alphabet;
|
|
20
|
+
const a = alphabet.split("");
|
|
21
|
+
for (let i = a.length - 1, v = 0, p = 0; i > 0; i--, v++) {
|
|
22
|
+
v %= salt.length;
|
|
23
|
+
const int = salt.charCodeAt(v);
|
|
24
|
+
p += int;
|
|
25
|
+
const j = (int + v + p) % i;
|
|
26
|
+
const tmp = a[i];
|
|
27
|
+
a[i] = a[j];
|
|
28
|
+
a[j] = tmp;
|
|
29
|
+
}
|
|
30
|
+
return a.join("");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The codec requires an alphabet of at least 2 *unique* characters — duplicates
|
|
34
|
+
* would make `indexOf`-based decoding ambiguous and silently corrupt the
|
|
35
|
+
* round-trip, so we reject them up front rather than return wrong values.
|
|
36
|
+
*/
|
|
37
|
+
function assertReversibleAlphabet(alphabet, fn) {
|
|
38
|
+
if (alphabet.length < 2) {
|
|
39
|
+
throw new RangeError(`${fn}: "alphabet" needs at least 2 characters`);
|
|
40
|
+
}
|
|
41
|
+
if (new Set(alphabet).size !== alphabet.length) {
|
|
42
|
+
throw new RangeError(`${fn}: "alphabet" must have unique characters`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Encodes a non-negative integer into a short string.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* encodeInt(12345); // base62, e.g. "3D7"
|
|
50
|
+
* encodeInt(12345, { salt: "secret" }); // shuffled, e.g. "Yq9"
|
|
51
|
+
* encodeInt(7, { minLength: 5 }); // "00007"-style padding
|
|
52
|
+
*/
|
|
53
|
+
export function encodeInt(value, options = {}) {
|
|
54
|
+
const { alphabet = ALPHABETS.base62, salt = "", minLength = 0 } = options;
|
|
55
|
+
if (!Number.isInteger(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
|
56
|
+
throw new RangeError(`encodeInt: value must be a safe non-negative integer, got ${String(value)}`);
|
|
57
|
+
}
|
|
58
|
+
assertReversibleAlphabet(alphabet, "encodeInt");
|
|
59
|
+
const a = consistentShuffle(alphabet, salt);
|
|
60
|
+
const base = a.length;
|
|
61
|
+
let n = value;
|
|
62
|
+
let out = "";
|
|
63
|
+
do {
|
|
64
|
+
out = a[n % base] + out;
|
|
65
|
+
n = Math.floor(n / base);
|
|
66
|
+
} while (n > 0);
|
|
67
|
+
// pad with the alphabet's zero char; harmless because leading "zeros"
|
|
68
|
+
// decode back to 0 in positional notation.
|
|
69
|
+
while (out.length < minLength)
|
|
70
|
+
out = a[0] + out;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decodes a string produced by {@link encodeInt}. The `alphabet` and `salt`
|
|
75
|
+
* must match those used to encode.
|
|
76
|
+
*/
|
|
77
|
+
export function decodeInt(str, options = {}) {
|
|
78
|
+
const { alphabet = ALPHABETS.base62, salt = "" } = options;
|
|
79
|
+
assertReversibleAlphabet(alphabet, "decodeInt");
|
|
80
|
+
const a = consistentShuffle(alphabet, salt);
|
|
81
|
+
const base = a.length;
|
|
82
|
+
let n = 0;
|
|
83
|
+
for (const ch of str) {
|
|
84
|
+
const idx = a.indexOf(ch);
|
|
85
|
+
if (idx < 0)
|
|
86
|
+
throw new Error(`decodeInt: invalid character "${ch}"`);
|
|
87
|
+
n = n * base + idx;
|
|
88
|
+
if (n > Number.MAX_SAFE_INTEGER) {
|
|
89
|
+
throw new RangeError(`decodeInt: decoded value exceeds safe integer range`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return n;
|
|
93
|
+
}
|
package/dist/rhr.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in `rhr` strategy — human-readable ids built on
|
|
3
|
+
* `@marianmeres/random-human-readable`.
|
|
4
|
+
*
|
|
5
|
+
* Kept out of the core entry point because its word lists add weight to browser
|
|
6
|
+
* bundles. Import this module to use it:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import "@marianmeres/uid/rhr"; // registers the strategy
|
|
10
|
+
* import { uid } from "@marianmeres/uid";
|
|
11
|
+
* uid("rhr"); // "happy-blue-otter-canyon"
|
|
12
|
+
*
|
|
13
|
+
* // …or call it directly (tree-shakeable, no registration needed):
|
|
14
|
+
* import { rhr } from "@marianmeres/uid/rhr";
|
|
15
|
+
* rhr({ nounsCount: 1 });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { type RhrOptions } from "./uid.js";
|
|
19
|
+
/**
|
|
20
|
+
* Generates a random human-readable id (e.g. `"happy-blue-otter-canyon"`).
|
|
21
|
+
* Joined with `-` by default; pass any `getRandomHumanReadable` options.
|
|
22
|
+
*/
|
|
23
|
+
export declare function rhr(options?: RhrOptions): string;
|
package/dist/rhr.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in `rhr` strategy — human-readable ids built on
|
|
3
|
+
* `@marianmeres/random-human-readable`.
|
|
4
|
+
*
|
|
5
|
+
* Kept out of the core entry point because its word lists add weight to browser
|
|
6
|
+
* bundles. Import this module to use it:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import "@marianmeres/uid/rhr"; // registers the strategy
|
|
10
|
+
* import { uid } from "@marianmeres/uid";
|
|
11
|
+
* uid("rhr"); // "happy-blue-otter-canyon"
|
|
12
|
+
*
|
|
13
|
+
* // …or call it directly (tree-shakeable, no registration needed):
|
|
14
|
+
* import { rhr } from "@marianmeres/uid/rhr";
|
|
15
|
+
* rhr({ nounsCount: 1 });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { getRandomHumanReadable } from "@marianmeres/random-human-readable";
|
|
19
|
+
import { registerStrategy } from "./uid.js";
|
|
20
|
+
/**
|
|
21
|
+
* Generates a random human-readable id (e.g. `"happy-blue-otter-canyon"`).
|
|
22
|
+
* Joined with `-` by default; pass any `getRandomHumanReadable` options.
|
|
23
|
+
*/
|
|
24
|
+
export function rhr(options = {}) {
|
|
25
|
+
return getRandomHumanReadable({ joinWith: "-", ...options });
|
|
26
|
+
}
|
|
27
|
+
// Self-register so `uid("rhr", …)` works once this module is imported.
|
|
28
|
+
registerStrategy("rhr", (o = {}) => rhr(o));
|
package/dist/uid.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified `uid(strategy, options)` dispatcher plus a small strategy
|
|
3
|
+
* registry. Every built-in strategy is also available as a standalone named
|
|
4
|
+
* export (see ./mod.ts) for tree-shaking and ergonomics; the dispatcher is the
|
|
5
|
+
* convenience entry point.
|
|
6
|
+
*/
|
|
7
|
+
import { type CounterOptions } from "./counter.js";
|
|
8
|
+
/** Options for the `uuid` / `uuidv4` strategies (none). */
|
|
9
|
+
export interface UuidOptions {
|
|
10
|
+
}
|
|
11
|
+
/** Options for the `uuidv7` strategy. */
|
|
12
|
+
export interface Uuidv7Options {
|
|
13
|
+
/** Unix epoch milliseconds to embed (default: now). */
|
|
14
|
+
timestamp?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Options for the `ulid` strategy. */
|
|
17
|
+
export interface UlidOptions {
|
|
18
|
+
/** Unix epoch milliseconds to embed (default: now). */
|
|
19
|
+
timestamp?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Options for the `base56` strategy. */
|
|
22
|
+
export interface Base56Options {
|
|
23
|
+
/** Length of the random string (ignored when `uuid` is given). */
|
|
24
|
+
length?: number;
|
|
25
|
+
/** If given, reversibly encode this UUID instead of generating randomly. */
|
|
26
|
+
uuid?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Options for the `nanoid` strategy. */
|
|
29
|
+
export interface NanoidOptions {
|
|
30
|
+
/** Number of characters (default: 21). */
|
|
31
|
+
length?: number;
|
|
32
|
+
/** Override the alphabet (default: URL-safe 64-char set). */
|
|
33
|
+
alphabet?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Options for the alphabet strategies (`hex`, `base32`, `numeric`, …). */
|
|
36
|
+
export interface AlphabetOptions {
|
|
37
|
+
/** Number of characters (default: 21). */
|
|
38
|
+
length?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Options for the `custom` strategy. */
|
|
41
|
+
export interface CustomOptions {
|
|
42
|
+
/** Alphabet to draw from (required, 1–256 chars). */
|
|
43
|
+
alphabet: string;
|
|
44
|
+
/** Number of characters (default: 21). */
|
|
45
|
+
length?: number;
|
|
46
|
+
}
|
|
47
|
+
/** Options for the `reversible` strategy. */
|
|
48
|
+
export interface ReversibleOptions {
|
|
49
|
+
/** The non-negative integer to encode (required). */
|
|
50
|
+
value: number;
|
|
51
|
+
/** Alphabet of unique characters (default: base62). */
|
|
52
|
+
alphabet?: string;
|
|
53
|
+
/** Salt that shuffles the alphabet (default: none). */
|
|
54
|
+
salt?: string;
|
|
55
|
+
/** Left-pad the result to at least this length. */
|
|
56
|
+
minLength?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Options for the `rhr` (random-human-readable) strategy. The strategy is
|
|
60
|
+
* opt-in: `import "@marianmeres/uid/rhr"` once to register it. Mirrors the
|
|
61
|
+
* subset of `@marianmeres/random-human-readable` options that yield a string.
|
|
62
|
+
*/
|
|
63
|
+
export interface RhrOptions {
|
|
64
|
+
adjCount?: number;
|
|
65
|
+
colorsCount?: number;
|
|
66
|
+
nounsCount?: number;
|
|
67
|
+
syllablesCount?: number;
|
|
68
|
+
digitsCount?: number;
|
|
69
|
+
specialCharsCount?: number;
|
|
70
|
+
randomizeCase?: boolean;
|
|
71
|
+
joinWith?: string;
|
|
72
|
+
}
|
|
73
|
+
/** A strategy implementation: takes its options object, returns a string. */
|
|
74
|
+
export type StrategyFn = (options?: Record<string, unknown>) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Registers (or overrides) a strategy under `name`, making it callable via
|
|
77
|
+
* `uid(name, options)`. Used internally for the built-ins and by the opt-in
|
|
78
|
+
* `./rhr` subpath; also available for your own custom strategies.
|
|
79
|
+
*/
|
|
80
|
+
export declare function registerStrategy(name: string, fn: StrategyFn): void;
|
|
81
|
+
/** Returns the names of all currently registered strategies. */
|
|
82
|
+
export declare function listStrategies(): string[];
|
|
83
|
+
export declare function uid(strategy?: "uuid" | "uuidv4", options?: UuidOptions): string;
|
|
84
|
+
export declare function uid(strategy: "uuidv7", options?: Uuidv7Options): string;
|
|
85
|
+
export declare function uid(strategy: "ulid", options?: UlidOptions): string;
|
|
86
|
+
export declare function uid(strategy: "base56", options?: Base56Options): string;
|
|
87
|
+
export declare function uid(strategy: "nanoid", options?: NanoidOptions): string;
|
|
88
|
+
export declare function uid(strategy: "hex" | "base32" | "base36" | "base58" | "base62" | "numeric", options?: AlphabetOptions): string;
|
|
89
|
+
export declare function uid(strategy: "custom", options: CustomOptions): string;
|
|
90
|
+
export declare function uid(strategy: "counter", options?: CounterOptions): string;
|
|
91
|
+
export declare function uid(strategy: "reversible", options: ReversibleOptions): string;
|
|
92
|
+
export declare function uid(strategy: "rhr", options?: RhrOptions): string;
|
|
93
|
+
export declare function uid(strategy: string, options?: Record<string, unknown>): string;
|
package/dist/uid.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified `uid(strategy, options)` dispatcher plus a small strategy
|
|
3
|
+
* registry. Every built-in strategy is also available as a standalone named
|
|
4
|
+
* export (see ./mod.ts) for tree-shaking and ergonomics; the dispatcher is the
|
|
5
|
+
* convenience entry point.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_LENGTH, randomString } from "./random.js";
|
|
8
|
+
import { ulid, uuid, uuidv7 } from "./uuid.js";
|
|
9
|
+
import { base56, uuidToBase56 } from "./base56.js";
|
|
10
|
+
import { base32, base36, base58, base62, hex, nanoid, numeric } from "./alphabet.js";
|
|
11
|
+
import { counter } from "./counter.js";
|
|
12
|
+
import { encodeInt } from "./reversible.js";
|
|
13
|
+
const _registry = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Registers (or overrides) a strategy under `name`, making it callable via
|
|
16
|
+
* `uid(name, options)`. Used internally for the built-ins and by the opt-in
|
|
17
|
+
* `./rhr` subpath; also available for your own custom strategies.
|
|
18
|
+
*/
|
|
19
|
+
export function registerStrategy(name, fn) {
|
|
20
|
+
if (typeof name !== "string" || !name) {
|
|
21
|
+
throw new TypeError(`registerStrategy: "name" must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
if (typeof fn !== "function") {
|
|
24
|
+
throw new TypeError(`registerStrategy: "fn" must be a function`);
|
|
25
|
+
}
|
|
26
|
+
_registry.set(name, fn);
|
|
27
|
+
}
|
|
28
|
+
/** Returns the names of all currently registered strategies. */
|
|
29
|
+
export function listStrategies() {
|
|
30
|
+
return [..._registry.keys()];
|
|
31
|
+
}
|
|
32
|
+
// Register the zero-dependency built-ins.
|
|
33
|
+
registerStrategy("uuid", () => uuid());
|
|
34
|
+
registerStrategy("uuidv4", () => uuid());
|
|
35
|
+
registerStrategy("uuidv7", (o = {}) => uuidv7(o.timestamp));
|
|
36
|
+
registerStrategy("ulid", (o = {}) => ulid(o.timestamp));
|
|
37
|
+
registerStrategy("base56", (o = {}) => {
|
|
38
|
+
const { uuid: u, length } = o;
|
|
39
|
+
return u != null ? uuidToBase56(u) : base56(length);
|
|
40
|
+
});
|
|
41
|
+
registerStrategy("nanoid", (o = {}) => {
|
|
42
|
+
const { length, alphabet } = o;
|
|
43
|
+
return nanoid(length, alphabet);
|
|
44
|
+
});
|
|
45
|
+
registerStrategy("hex", (o = {}) => hex(o.length));
|
|
46
|
+
registerStrategy("base32", (o = {}) => base32(o.length));
|
|
47
|
+
registerStrategy("base36", (o = {}) => base36(o.length));
|
|
48
|
+
registerStrategy("base58", (o = {}) => base58(o.length));
|
|
49
|
+
registerStrategy("base62", (o = {}) => base62(o.length));
|
|
50
|
+
registerStrategy("numeric", (o = {}) => numeric(o.length));
|
|
51
|
+
registerStrategy("custom", (o = {}) => {
|
|
52
|
+
const { alphabet, length } = o;
|
|
53
|
+
if (typeof alphabet !== "string") {
|
|
54
|
+
throw new TypeError(`uid("custom"): "alphabet" option is required`);
|
|
55
|
+
}
|
|
56
|
+
return randomString(length ?? DEFAULT_LENGTH, alphabet);
|
|
57
|
+
});
|
|
58
|
+
registerStrategy("counter", (o = {}) => counter(o));
|
|
59
|
+
registerStrategy("reversible", (o = {}) => {
|
|
60
|
+
const { value, ...rest } = o;
|
|
61
|
+
if (typeof value !== "number") {
|
|
62
|
+
throw new TypeError(`uid("reversible"): "value" option is required`);
|
|
63
|
+
}
|
|
64
|
+
return encodeInt(value, rest);
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Generates a unique id using the named `strategy`.
|
|
68
|
+
*
|
|
69
|
+
* Defaults to `uuid` (v4) when called with no arguments. The `options` shape
|
|
70
|
+
* depends on the strategy — see the per-strategy option interfaces. The `rhr`
|
|
71
|
+
* strategy must be enabled first via `import "@marianmeres/uid/rhr"`.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* uid(); // uuid v4
|
|
75
|
+
* uid("uuidv7"); // sortable uuid
|
|
76
|
+
* uid("nanoid", { length: 12 }); // "V1StGXR8_Z5j"
|
|
77
|
+
* uid("numeric", { length: 6 }); // "048217"
|
|
78
|
+
* uid("counter", { prefix: "n", pad: 3 }); // "n000"
|
|
79
|
+
*/
|
|
80
|
+
export function uid(strategy = "uuid",
|
|
81
|
+
// deno-lint-ignore no-explicit-any -- implementation signature for overloads
|
|
82
|
+
options = {}) {
|
|
83
|
+
const fn = _registry.get(strategy);
|
|
84
|
+
if (!fn) {
|
|
85
|
+
if (strategy === "rhr") {
|
|
86
|
+
throw new Error(`The "rhr" strategy is opt-in. Enable it once with:\n` +
|
|
87
|
+
` import "@marianmeres/uid/rhr";\n` +
|
|
88
|
+
`(or use the named export: import { rhr } from "@marianmeres/uid/rhr")`);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Unknown uid strategy "${strategy}". Available: ${listStrategies().join(", ")}`);
|
|
91
|
+
}
|
|
92
|
+
return fn(options);
|
|
93
|
+
}
|
package/dist/uuid.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID and UUID-shaped, time-sortable strategies.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generates a random RFC 9562 UUID v4 via the native `crypto.randomUUID()`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* uuid(); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
|
|
9
|
+
*/
|
|
10
|
+
export declare function uuid(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Generates a UUID v7 (RFC 9562): a 48-bit Unix-millisecond timestamp prefix
|
|
13
|
+
* followed by random bits. The result is a valid UUID *and* lexicographically
|
|
14
|
+
* sortable by creation time — ideal for database primary keys.
|
|
15
|
+
*
|
|
16
|
+
* @param timestamp Unix epoch milliseconds to embed (default: `Date.now()`).
|
|
17
|
+
* Useful for tests or back-dating; must be in `[0, 2^48)`.
|
|
18
|
+
* @example
|
|
19
|
+
* uuidv7(); // "0192f6c4-1d2e-7a3b-8c4d-5e6f7a8b9c0d"
|
|
20
|
+
*/
|
|
21
|
+
export declare function uuidv7(timestamp?: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* Generates a ULID: a 26-character Crockford-base32 string with a 48-bit
|
|
24
|
+
* millisecond timestamp prefix (10 chars) and 80 bits of randomness (16 chars).
|
|
25
|
+
*
|
|
26
|
+
* Like UUID v7 it sorts by creation time, but it is shorter, case-insensitive,
|
|
27
|
+
* and dash-free — not a valid UUID string.
|
|
28
|
+
*
|
|
29
|
+
* @param timestamp Unix epoch milliseconds to embed (default: `Date.now()`).
|
|
30
|
+
* @example
|
|
31
|
+
* ulid(); // "01J9Z7Q8K3M4N5P6R7S8T9V0W1"
|
|
32
|
+
*/
|
|
33
|
+
export declare function ulid(timestamp?: number): string;
|
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID and UUID-shaped, time-sortable strategies.
|
|
3
|
+
*/
|
|
4
|
+
import { ALPHABETS, randomBytes, randomString } from "./random.js";
|
|
5
|
+
/** Formats 16 bytes as a canonical lowercase UUID string. */
|
|
6
|
+
function bytesToUuid(bytes) {
|
|
7
|
+
let h = "";
|
|
8
|
+
for (let i = 0; i < 16; i++)
|
|
9
|
+
h += bytes[i].toString(16).padStart(2, "0");
|
|
10
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generates a random RFC 9562 UUID v4 via the native `crypto.randomUUID()`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* uuid(); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
|
|
17
|
+
*/
|
|
18
|
+
export function uuid() {
|
|
19
|
+
return crypto.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates a UUID v7 (RFC 9562): a 48-bit Unix-millisecond timestamp prefix
|
|
23
|
+
* followed by random bits. The result is a valid UUID *and* lexicographically
|
|
24
|
+
* sortable by creation time — ideal for database primary keys.
|
|
25
|
+
*
|
|
26
|
+
* @param timestamp Unix epoch milliseconds to embed (default: `Date.now()`).
|
|
27
|
+
* Useful for tests or back-dating; must be in `[0, 2^48)`.
|
|
28
|
+
* @example
|
|
29
|
+
* uuidv7(); // "0192f6c4-1d2e-7a3b-8c4d-5e6f7a8b9c0d"
|
|
30
|
+
*/
|
|
31
|
+
export function uuidv7(timestamp = Date.now()) {
|
|
32
|
+
const ts = Math.floor(timestamp);
|
|
33
|
+
if (!Number.isFinite(ts) || ts < 0 || ts > 0xffffffffffff) {
|
|
34
|
+
throw new RangeError(`uuidv7: timestamp must be in [0, 2^48), got ${String(timestamp)}`);
|
|
35
|
+
}
|
|
36
|
+
const bytes = randomBytes(16);
|
|
37
|
+
// 48-bit big-endian timestamp in the first 6 bytes (division avoids the
|
|
38
|
+
// 32-bit truncation of bitwise shifts).
|
|
39
|
+
bytes[0] = Math.floor(ts / 2 ** 40) & 0xff;
|
|
40
|
+
bytes[1] = Math.floor(ts / 2 ** 32) & 0xff;
|
|
41
|
+
bytes[2] = Math.floor(ts / 2 ** 24) & 0xff;
|
|
42
|
+
bytes[3] = Math.floor(ts / 2 ** 16) & 0xff;
|
|
43
|
+
bytes[4] = Math.floor(ts / 2 ** 8) & 0xff;
|
|
44
|
+
bytes[5] = ts & 0xff;
|
|
45
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70; // version 7
|
|
46
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx
|
|
47
|
+
return bytesToUuid(bytes);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generates a ULID: a 26-character Crockford-base32 string with a 48-bit
|
|
51
|
+
* millisecond timestamp prefix (10 chars) and 80 bits of randomness (16 chars).
|
|
52
|
+
*
|
|
53
|
+
* Like UUID v7 it sorts by creation time, but it is shorter, case-insensitive,
|
|
54
|
+
* and dash-free — not a valid UUID string.
|
|
55
|
+
*
|
|
56
|
+
* @param timestamp Unix epoch milliseconds to embed (default: `Date.now()`).
|
|
57
|
+
* @example
|
|
58
|
+
* ulid(); // "01J9Z7Q8K3M4N5P6R7S8T9V0W1"
|
|
59
|
+
*/
|
|
60
|
+
export function ulid(timestamp = Date.now()) {
|
|
61
|
+
const ts = Math.floor(timestamp);
|
|
62
|
+
if (!Number.isFinite(ts) || ts < 0 || ts > 0xffffffffffff) {
|
|
63
|
+
throw new RangeError(`ulid: timestamp must be in [0, 2^48), got ${String(timestamp)}`);
|
|
64
|
+
}
|
|
65
|
+
const enc = ALPHABETS.base32;
|
|
66
|
+
let time = ts;
|
|
67
|
+
let timeChars = "";
|
|
68
|
+
for (let i = 0; i < 10; i++) {
|
|
69
|
+
timeChars = enc[time % 32] + timeChars;
|
|
70
|
+
time = Math.floor(time / 32);
|
|
71
|
+
}
|
|
72
|
+
// base32 is a power of two, so randomString is unbiased and rejection-free
|
|
73
|
+
return timeChars + randomString(16, enc);
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marianmeres/uid",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/mod.js",
|
|
6
|
+
"types": "dist/mod.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/mod.d.ts",
|
|
10
|
+
"import": "./dist/mod.js"
|
|
11
|
+
},
|
|
12
|
+
"./rhr": {
|
|
13
|
+
"types": "./dist/rhr.d.ts",
|
|
14
|
+
"import": "./dist/rhr.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"API.md",
|
|
22
|
+
"AGENTS.md"
|
|
23
|
+
],
|
|
24
|
+
"author": "Marian Meres",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@marianmeres/random-human-readable": "^1.10"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@marianmeres/random-human-readable": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/marianmeres/uid.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/marianmeres/uid/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|