uniku 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -24,17 +24,18 @@ console.log(first < second && second < third) // true
24
24
 
25
25
  ## At a Glance
26
26
 
27
- | | uniku | uuid | nanoid | ulid | cuid2 |
28
- |--------------------|:-----:|:----:|:------:|:----:|:-----:|
29
- | UUID v4 | ✅ | ✅ | ❌ | ❌ | ❌ |
30
- | UUID v7 | ✅ | ✅ | ❌ | ❌ | ❌ |
31
- | ULID | ✅ | ❌ | ❌ | ✅ | ❌ |
32
- | CUID2 | ✅ | ❌ | ❌ | ❌ | ✅ |
33
- | Nanoid | ✅ | ❌ | ✅ | ❌ | ❌ |
34
- | Tree-shakeable | ✅ | ❌ | ✅ | ✅ | ✅ |
35
- | ESM-only | ✅ | ❌ | ✅ | ✅ | ✅ |
36
- | Edge/Workers | ✅ | ⚠️ | ✅ | ⚠️ | ✅ |
37
- | Byte ↔ String | ✅ | ✅ | - | ✅ | - |
27
+ | | uniku | uuid | nanoid | ulid | cuid2 | ksuid |
28
+ |--------------------|:-----:|:----:|:------:|:----:|:-----:|:-----:|
29
+ | UUID v4 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
30
+ | UUID v7 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
31
+ | ULID | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
32
+ | CUID2 | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
33
+ | Nanoid | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
34
+ | KSUID | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
35
+ | Tree-shakeable | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
36
+ | ESM-only | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
37
+ | Edge/Workers | ✅ | ⚠️ | ✅ | ⚠️ | ✅ | ✅ |
38
+ | Byte ↔ String | ✅ | ✅ | - | ✅ | - | ✅ |
38
39
 
39
40
  > **Note**: Byte ↔ String conversion doesn't make sense for nanoid and cuid2, since they are string-native formats with no canonical binary representation.
40
41
 
@@ -60,18 +61,96 @@ Uses `globalThis.crypto` (Web Crypto API) — no Node.js-specific APIs.
60
61
  | UUID v4 | **1.5× faster** |
61
62
  | Nanoid | **1.1× faster** |
62
63
  | UUID v7 | npm 1.6× faster* |
64
+ | KSUID | npm 1.8× faster* |
63
65
 
64
66
  <sub>*UUID v7 tradeoff: uniku prioritizes strict monotonic sequencing for database use cases.</sub>
65
67
 
66
68
  ## Which ID Should I Use?
67
69
 
70
+ ### Quick Recommendations
71
+
68
72
  | Use Case | Recommended | Why |
69
73
  |----------|-------------|-----|
70
74
  | Database primary keys | **UUID v7** or **ULID** | Time-ordered for index efficiency |
71
75
  | URL shorteners | **Nanoid** | Compact, URL-safe characters |
72
76
  | Prevent enumeration | **CUID2** | Non-sequential, secure |
73
77
  | Maximum compatibility | **UUID v4** | Universal standard |
74
- | Distributed systems | **ULID** | Sortable + high entropy |
78
+ | Distributed systems (high entropy) | **KSUID** | 128-bit payload, second precision |
79
+
80
+ ### Detailed Guide
81
+
82
+ **UUID v4** — Use when you need maximum compatibility with existing systems.
83
+ - 36 characters, 122-bit entropy
84
+ - Universally supported by databases (native `UUID` type)
85
+ - Not time-ordered (random distribution)
86
+ - Best for: Legacy systems, cross-platform compatibility
87
+
88
+ **UUID v7** — Use for database primary keys when you need RFC compliance.
89
+ - 36 characters, time-ordered with millisecond precision
90
+ - Native database support as `UUID` type
91
+ - Strictly monotonic within same millisecond
92
+ - Best for: PostgreSQL/MySQL primary keys, API identifiers
93
+
94
+ **ULID** — Use for database primary keys when you want URL-safe IDs.
95
+ - 26 characters, time-ordered with millisecond precision
96
+ - URL-safe (no hyphens), case-insensitive
97
+ - 80-bit random entropy per millisecond
98
+ - Best for: APIs, URLs, distributed systems needing sortability
99
+
100
+ **KSUID** — Use when you need highest collision resistance.
101
+ - 27 characters, time-ordered with second precision
102
+ - URL-safe, 128-bit random entropy (highest of all formats)
103
+ - Best for: Very high-volume distributed systems, event sourcing
104
+
105
+ **Nanoid** — Use for short, URL-friendly identifiers.
106
+ - 21 characters (configurable), 126-bit entropy
107
+ - Customizable alphabet and length
108
+ - Not time-ordered
109
+ - Best for: URL shorteners, session tokens, invite codes
110
+
111
+ **CUID2** — Use when ID unpredictability matters.
112
+ - 24 characters (configurable), hash-based
113
+ - Non-sequential, prevents enumeration attacks
114
+ - Not time-ordered
115
+ - Best for: User-facing IDs where security matters
116
+
117
+ ### Binary Serialization: When to Use `toBytes()`
118
+
119
+ Formats with binary representations (UUID, ULID, KSUID) support `toBytes()` and `fromBytes()` for efficient storage:
120
+
121
+ ```ts
122
+ // Store as BINARY(16) instead of VARCHAR(36)
123
+ const bytes = uuidv7.toBytes(id) // 16 bytes
124
+ db.insert({ id: bytes })
125
+
126
+ // Retrieve and convert back
127
+ const id = uuidv7.fromBytes(row.id)
128
+ ```
129
+
130
+ **Benefits of binary storage:**
131
+ - **Up to 50% smaller**: E.g., 16 bytes vs 36 characters for UUID
132
+ - **Faster indexing**: Binary comparison is faster than string comparison
133
+ - **Reduced I/O**: Less data transferred between app and database
134
+
135
+ **When to use binary:**
136
+ - High-volume tables (millions of rows)
137
+ - Performance-critical queries on ID columns
138
+ - Storage cost is a concern
139
+
140
+ **When to keep strings:**
141
+ - Debugging/logging convenience
142
+ - API responses (humans read them)
143
+ - Small tables where savings are negligible
144
+
145
+ | Format | String Length | Binary Size | Savings |
146
+ |--------|--------------|-------------|---------|
147
+ | UUID v4/v7 | 36 chars | 16 bytes | 56% |
148
+ | ULID | 26 chars | 16 bytes | 38% |
149
+ | KSUID | 27 chars | 20 bytes | 26% |
150
+ | Nanoid | 21 chars | N/A | - |
151
+ | CUID2 | 24 chars | N/A | - |
152
+
153
+ > **Note**: Nanoid and CUID2 don't have binary representations because they're string-native formats with no canonical byte encoding.
75
154
 
76
155
  ## Installation
77
156
 
@@ -101,6 +180,7 @@ deno install npm:uniku
101
180
  | `uniku/ulid` | ~1.5 KB |
102
181
  | `uniku/cuid2` | ~1.1 KB* |
103
182
  | `uniku/nanoid` | ~967 B |
183
+ | `uniku/ksuid` | ~1.1 KB |
104
184
 
105
185
  ## Quick Start
106
186
 
@@ -178,6 +258,22 @@ const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
178
258
  // => "4f90d13a42bc"
179
259
  ```
180
260
 
261
+ ### KSUID (time-ordered, high entropy)
262
+
263
+ ```ts
264
+ import { ksuid } from 'uniku/ksuid'
265
+
266
+ const id = ksuid()
267
+ // => "2QnJjKLvpSfpZqGiPPxVwWLMy2p"
268
+
269
+ // Convert to/from bytes
270
+ const bytes = ksuid.toBytes(id)
271
+ const str = ksuid.fromBytes(bytes)
272
+
273
+ // Extract timestamp (milliseconds)
274
+ const ts = ksuid.timestamp(id)
275
+ ```
276
+
181
277
  ## Migrating to uniku
182
278
 
183
279
  ### From `uuid`
@@ -208,26 +304,45 @@ const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
208
304
  + import { cuid2 } from 'uniku/cuid2'
209
305
  ```
210
306
 
307
+ ### From `@owpz/ksuid`
308
+
309
+ ```diff
310
+ - import { KSUID } from '@owpz/ksuid'
311
+ + import { ksuid } from 'uniku/ksuid'
312
+
313
+ - const id = KSUID.random().toString()
314
+ + const id = ksuid()
315
+
316
+ - const parsed = KSUID.parse(str)
317
+ - const timestamp = parsed.timestamp
318
+ + const timestamp = ksuid.timestamp(str)
319
+ ```
320
+
211
321
  ## API Reference
212
322
 
213
323
  ### `uuidv4` (from `uniku/uuid/v4`)
214
324
 
215
325
  ```ts
216
- uuidv4(options?: Version4Options): string
217
- uuidv4(options: Version4Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
326
+ uuidv4(options?: UuidV4Options): string
327
+ uuidv4(options: UuidV4Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
218
328
  uuidv4.toBytes(id: string): Uint8Array
219
329
  uuidv4.fromBytes(bytes: Uint8Array): string
330
+ uuidv4.isValid(id: unknown): id is string
331
+ uuidv4.NIL // "00000000-0000-0000-0000-000000000000"
332
+ uuidv4.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"
220
333
  ```
221
334
 
222
335
  ### `uuidv7` (from `uniku/uuid/v7`)
223
336
 
224
337
  ```ts
225
- uuidv7(options?: Version7Options): string
226
- uuidv7(options: Version7Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
338
+ uuidv7(options?: UuidV7Options): string
339
+ uuidv7(options: UuidV7Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
227
340
  uuidv7.toBytes(id: string): Uint8Array
228
341
  uuidv7.fromBytes(bytes: Uint8Array): string
229
342
  uuidv7.timestamp(id: string): number
230
- uuidv7.isValid(id: string): boolean
343
+ uuidv7.isValid(id: unknown): id is string
344
+ uuidv7.NIL // "00000000-0000-0000-0000-000000000000"
345
+ uuidv7.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"
231
346
  ```
232
347
 
233
348
  ### `ulid` (from `uniku/ulid`)
@@ -238,7 +353,9 @@ ulid(options: UlidOptions | undefined, buf: Uint8Array, offset?: number): Uint8A
238
353
  ulid.toBytes(id: string): Uint8Array
239
354
  ulid.fromBytes(bytes: Uint8Array): string
240
355
  ulid.timestamp(id: string): number
241
- ulid.isValid(id: string): boolean
356
+ ulid.isValid(id: unknown): id is string
357
+ ulid.NIL // "00000000000000000000000000"
358
+ ulid.MAX // "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
242
359
  ```
243
360
 
244
361
  ### `cuid2` (from `uniku/cuid2`)
@@ -255,12 +372,49 @@ nanoid(): string
255
372
  nanoid(size: number): string
256
373
  nanoid(options: NanoidOptions): string
257
374
  nanoid.isValid(id: unknown): id is string
375
+
376
+ // Constant
377
+ URL_ALPHABET // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
378
+ ```
379
+
380
+ ### `ksuid` (from `uniku/ksuid`)
381
+
382
+ ```ts
383
+ ksuid(options?: KsuidOptions): string
384
+ ksuid(options: KsuidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
385
+ ksuid.toBytes(id: string): Uint8Array
386
+ ksuid.fromBytes(bytes: Uint8Array): string
387
+ ksuid.timestamp(id: string): number
388
+ ksuid.isValid(id: unknown): id is string
389
+ ksuid.NIL // "000000000000000000000000000"
390
+ ksuid.MAX // "aWgEPTl1tmebfsQzFP4bxwgy80V"
258
391
  ```
259
392
 
260
393
  ## Documentation
261
394
 
262
395
  For advanced usage, examples, and contributing guidelines, see the [full documentation on GitHub](https://github.com/jkomyno/uniku).
263
396
 
397
+ ## Related Projects
398
+
399
+ Third-party libraries that inspired this project:
400
+
401
+ - [uuid](https://github.com/uuidjs/uuid): the most popular UUID library for JavaScript
402
+ - [ulid](https://github.com/ulid/javascript): the reference ULID implementation for JavaScript
403
+ - [@paralleldrive/cuid2](https://github.com/paralleldrive/cuid2): secure, collision-resistant IDs
404
+ - [@owpz/ksuid](https://github.com/owpz/ksuid): K-Sortable Unique Identifier
405
+ - [nanoid](https://github.com/ai/nanoid): tiny, URL-friendly unique string ID generator
406
+
407
+ Other:
408
+
409
+ - [pnpm-monorepo-template](https://github.com/jkomyno/pnpm-monorepo-template): the template I used to create this library
410
+
411
+ ## Author
412
+
413
+ Hi, I'm Alberto Schiabel, aka @jkomyno. You can follow me on:
414
+
415
+ - Github: [@jkomyno](https://github.com/jkomyno)
416
+ - X: [@jkomyno](https://x.com/jkomyno)
417
+
264
418
  ## License
265
419
 
266
420
  MIT — see [LICENSE](https://github.com/jkomyno/uniku/blob/main/LICENSE)
@@ -0,0 +1,2 @@
1
+ function e(e){for(let t=e.length-1;t>=0;--t){if(e[t]<255)return e[t]+=1,!0;e[t]=0}return!1}function t(e,t,n){e[t]=n/1099511627776&255,e[t+1]=n/4294967296&255,e[t+2]=n/16777216&255,e[t+3]=n/65536&255,e[t+4]=n/256&255,e[t+5]=n&255}function n(e,t,n){e[t]=n>>>24&255,e[t+1]=n>>>16&255,e[t+2]=n>>>8&255,e[t+3]=n&255}export{n,t as r,e as t};
2
+ //# sourceMappingURL=bytes-CNiRs9Me.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes-CNiRs9Me.mjs","names":[],"sources":["../src/common/bytes.ts"],"sourcesContent":["/**\n * Common byte manipulation utilities shared across ID generators.\n */\n\n/**\n * Increment a byte array by 1 in-place, propagating carry from LSB to MSB.\n * Mutates the input array directly - no allocation.\n *\n * Used by ULID and KSUID for monotonic ordering within the same time unit.\n *\n * @returns true if increment succeeded, false if all bytes overflowed to 0\n */\nexport function incrementBytesInPlace(bytes: Uint8Array): boolean {\n for (let i = bytes.length - 1; i >= 0; i -= 1) {\n if (bytes[i] < 255) {\n bytes[i] += 1\n return true\n }\n bytes[i] = 0\n }\n // All bytes overflowed to 0 - astronomically unlikely (1 in 2^80 for ULID, 1 in 2^128 for KSUID)\n return false\n}\n\n/**\n * Write a 48-bit timestamp as big-endian bytes.\n * Used by UUID v7 and ULID for millisecond-precision timestamps.\n *\n * @param buf - Target buffer\n * @param offset - Starting byte offset\n * @param msecs - Milliseconds since Unix epoch (must fit in 48 bits)\n */\nexport function writeTimestamp48(buf: Uint8Array, offset: number, msecs: number): void {\n buf[offset] = (msecs / 0x10000000000) & 0xff\n buf[offset + 1] = (msecs / 0x100000000) & 0xff\n buf[offset + 2] = (msecs / 0x1000000) & 0xff\n buf[offset + 3] = (msecs / 0x10000) & 0xff\n buf[offset + 4] = (msecs / 0x100) & 0xff\n buf[offset + 5] = msecs & 0xff\n}\n\n/**\n * Write a 32-bit timestamp as big-endian bytes.\n * Used by KSUID for second-precision timestamps.\n *\n * @param buf - Target buffer\n * @param offset - Starting byte offset\n * @param secs - Seconds (must fit in 32 bits)\n */\nexport function writeTimestamp32(buf: Uint8Array, offset: number, secs: number): void {\n buf[offset] = (secs >>> 24) & 0xff\n buf[offset + 1] = (secs >>> 16) & 0xff\n buf[offset + 2] = (secs >>> 8) & 0xff\n buf[offset + 3] = secs & 0xff\n}\n"],"mappings":"AAYA,SAAgB,EAAsB,EAA4B,CAChE,IAAK,IAAI,EAAI,EAAM,OAAS,EAAG,GAAK,EAAG,IAAQ,CAC7C,GAAI,EAAM,GAAK,IAEb,MADA,GAAM,IAAM,EACL,GAET,EAAM,GAAK,EAGb,MAAO,GAWT,SAAgB,EAAiB,EAAiB,EAAgB,EAAqB,CACrF,EAAI,GAAW,EAAQ,cAAiB,IACxC,EAAI,EAAS,GAAM,EAAQ,WAAe,IAC1C,EAAI,EAAS,GAAM,EAAQ,SAAa,IACxC,EAAI,EAAS,GAAM,EAAQ,MAAW,IACtC,EAAI,EAAS,GAAM,EAAQ,IAAS,IACpC,EAAI,EAAS,GAAK,EAAQ,IAW5B,SAAgB,EAAiB,EAAiB,EAAgB,EAAoB,CACpF,EAAI,GAAW,IAAS,GAAM,IAC9B,EAAI,EAAS,GAAM,IAAS,GAAM,IAClC,EAAI,EAAS,GAAM,IAAS,EAAK,IACjC,EAAI,EAAS,GAAK,EAAO"}
@@ -0,0 +1,34 @@
1
+ //#region src/ksuid/ksuid.d.ts
2
+ type KsuidOptions = {
3
+ /**
4
+ * 16 bytes of random data to use for KSUID payload.
5
+ */
6
+ random?: Uint8Array;
7
+ /**
8
+ * Timestamp in seconds since Unix epoch.
9
+ * Defaults to Math.floor(Date.now() / 1000).
10
+ * KSUID natively uses second precision.
11
+ */
12
+ secs?: number;
13
+ };
14
+ type Ksuid = {
15
+ (): string;
16
+ <TBuf extends Uint8Array = Uint8Array>(options: KsuidOptions | undefined, buf: TBuf, offset?: number): TBuf;
17
+ (options?: KsuidOptions, buf?: undefined, offset?: number): string;
18
+ toBytes(id: string): Uint8Array;
19
+ fromBytes(bytes: Uint8Array): string;
20
+ timestamp(id: string): number;
21
+ isValid(id: unknown): id is string;
22
+ /** The nil KSUID (all zeros) */
23
+ NIL: string;
24
+ /** The max KSUID (maximum valid value) */
25
+ MAX: string;
26
+ };
27
+ /**
28
+ * Generate a KSUID string or write the bytes into a buffer.
29
+ * Also includes helpers to convert to and from byte arrays.
30
+ */
31
+ declare const ksuid: Ksuid;
32
+ //#endregion
33
+ export { Ksuid, KsuidOptions, ksuid };
34
+ //# sourceMappingURL=ksuid.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ksuid.d.mts","names":["ksuid: Ksuid"],"sources":["../../src/ksuid/ksuid.ts"],"sourcesContent":[],"mappings":";KA2BY,YAAA;EAAZ;AAaA;;EAE6B,MAAA,CAAA,EAXlB,UAWkB;EAAqB;;;;;EAG/B,IAAA,CAAA,EAAA,MAAA;CAAA;AA6JNA,KAlKD,KAAA,GAkKQ;;gBAhKJ,aAAa,qBAAqB,+BAA+B,wBAAwB;aAC5F;uBACU;mBACJ;;;;;;;;;;;;cA6JNA,OAAO"}
@@ -0,0 +1,2 @@
1
+ import{n as e,t}from"../bytes-CNiRs9Me.mjs";import{t as n}from"../random-E8GC9ETc.mjs";const r=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,i=62n,a=new Uint8Array(128);a.fill(255);for(let e=0;e<62;e+=1)a[r.charCodeAt(e)]=e;function o(e){if(e.length<20)throw Error(`KSUID bytes must be at least 20 bytes, got ${e.length}`);let t=0n;for(let n=0;n<20;n+=1)t=t<<8n|BigInt(e[n]);let n=``;for(let e=0;e<27;e+=1)n=r[Number(t%i)]+n,t/=i;return n}function s(e){if(e.length!==27)throw Error(`KSUID string must be 27 characters, got ${e.length}`);let t=0n;for(let n=0;n<27;n+=1){let r=e.charCodeAt(n);if(r>=128)throw Error(`Invalid KSUID character: ${e[n]}`);let o=a[r];if(o===255)throw Error(`Invalid KSUID character: ${e[n]}`);t=t*i+BigInt(o)}let n=new Uint8Array(20);for(let e=19;e>=0;--e)n[e]=Number(t&255n),t>>=8n;return n}const c=14e8,l=/^[0-9A-Za-z]{27}$/,u={secs:-1/0,lastPayload:new Uint8Array(16)};function d(t,n,r,i=0){if(!r)r=new Uint8Array(20),i=0;else if(i<0||i+20>r.length)throw RangeError(`KSUID byte range ${i}:${i+20-1} is out of buffer bounds`);e(r,i,t);for(let e=0;e<16;e+=1)r[i+4+e]=n[e];return r}function f(e,r,i=0){let a,s,l=Date.now(),f=Math.floor(l/1e3)-c;if(e)if(a=e.secs===void 0?f:e.secs-c,e.random){if(e.random.length<16)throw Error(`Random bytes length must be >= 16 for KSUID`);s=e.random}else s=n();else a=f,a>u.secs?(s=n(),u.secs=a,u.lastPayload.set(s.subarray(0,16))):(t(u.lastPayload),s=u.lastPayload);return r?(d(a,s,r,i),r):o(d(a,s))}function p(e){return s(e)}function m(e){return o(e)}function h(e){let t=s(e);return(((t[0]<<24|t[1]<<16|t[2]<<8|t[3])>>>0)+c)*1e3}function g(e){return typeof e==`string`&&e.length===27&&l.test(e)}const _=Object.assign(f,{toBytes:p,fromBytes:m,timestamp:h,isValid:g,NIL:`000000000000000000000000000`,MAX:`aWgEPTl1tmebfsQzFP4bxwgy80V`});export{_ as ksuid};
2
+ //# sourceMappingURL=ksuid.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ksuid.mjs","names":["KSUID_BYTES","KSUID_STRING_LEN","state: KsuidState","secs: number","payload: Uint8Array","ksuid: Ksuid"],"sources":["../../src/ksuid/base62.ts","../../src/ksuid/ksuid.ts"],"sourcesContent":["/**\n * Base62 encoding/decoding for KSUID.\n * Alphabet: 0-9A-Za-z (standard Base62 ordering)\n *\n * KSUID binary format: 20 bytes (160 bits)\n * - 4 bytes: timestamp (seconds since KSUID epoch)\n * - 16 bytes: payload (cryptographically random)\n *\n * String format: 27 characters of Base62\n */\n\n// Base62 alphabet: digits (0-9), uppercase (A-Z), lowercase (a-z)\nconst ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst BASE = 62n\nconst KSUID_BYTES = 20\nconst KSUID_STRING_LEN = 27\n\n// Pre-computed decode table indexed by ASCII code (0-127)\n// Uses Uint8Array for cache-efficient lookups via charCodeAt\n// Note: Base62 is case-sensitive - 'A' (value 10) and 'a' (value 36) are different\nconst DECODING = new Uint8Array(128)\nDECODING.fill(255) // 255 = invalid marker\n\nfor (let i = 0; i < ALPHABET.length; i += 1) {\n DECODING[ALPHABET.charCodeAt(i)] = i\n}\n\n/**\n * Encode a 20-byte KSUID to a 27-character Base62 string.\n *\n * Algorithm: Convert the 160-bit number to base 62 using BigInt.\n * V8 has highly optimized BigInt operations for this size.\n */\nexport function encodeBase62(bytes: Uint8Array): string {\n if (bytes.length < KSUID_BYTES) {\n throw new Error(`KSUID bytes must be at least ${KSUID_BYTES} bytes, got ${bytes.length}`)\n }\n\n // Convert bytes to BigInt (big-endian)\n let num = 0n\n for (let i = 0; i < KSUID_BYTES; i += 1) {\n num = (num << 8n) | BigInt(bytes[i])\n }\n\n // Convert to Base62 string (build from right to left)\n // Direct string concatenation is faster than array + join in V8\n let result = ''\n for (let i = 0; i < KSUID_STRING_LEN; i += 1) {\n result = ALPHABET[Number(num % BASE)] + result\n num = num / BASE\n }\n\n return result\n}\n\n/**\n * Decode a 27-character Base62 string to a 20-byte KSUID.\n *\n * Algorithm: Convert Base62 string to BigInt, then to bytes.\n * V8 has highly optimized BigInt operations.\n */\nexport function decodeBase62(str: string): Uint8Array {\n if (str.length !== KSUID_STRING_LEN) {\n throw new Error(`KSUID string must be ${KSUID_STRING_LEN} characters, got ${str.length}`)\n }\n\n // Convert Base62 string to BigInt\n let num = 0n\n for (let i = 0; i < KSUID_STRING_LEN; i += 1) {\n const code = str.charCodeAt(i)\n if (code >= 128) {\n throw new Error(`Invalid KSUID character: ${str[i]}`)\n }\n const value = DECODING[code]\n if (value === 255) {\n throw new Error(`Invalid KSUID character: ${str[i]}`)\n }\n num = num * BASE + BigInt(value)\n }\n\n // Convert BigInt to bytes (big-endian)\n const bytes = new Uint8Array(KSUID_BYTES)\n for (let i = KSUID_BYTES - 1; i >= 0; i -= 1) {\n bytes[i] = Number(num & 0xffn)\n num = num >> 8n\n }\n\n return bytes\n}\n","import { incrementBytesInPlace, writeTimestamp32 } from '../common/bytes'\nimport { rng } from '../common/random'\nimport { decodeBase62, encodeBase62 } from './base62'\n\n/**\n * KSUID (K-Sortable Unique Identifier)\n *\n * A 160-bit identifier consisting of:\n * - 4 bytes: timestamp (seconds since KSUID epoch: May 13, 2014)\n * - 16 bytes: cryptographically random payload\n *\n * Encoded as a 27-character Base62 string.\n */\n\n// KSUID epoch: May 13, 2014 00:00:00 UTC (Unix timestamp in seconds)\nconst KSUID_EPOCH = 1400000000\n\nconst KSUID_BYTES = 20\nconst KSUID_STRING_LEN = 27\nconst TIMESTAMP_BYTES = 4\nconst PAYLOAD_BYTES = 16\n\n// Validation regex: 27 alphanumeric characters\n// Note: Both cases are valid Base62 characters, but they decode to different values\n// (e.g., 'A' = 10, 'a' = 36). The regex validates format, not semantic equivalence.\nconst KSUID_REGEX = /^[0-9A-Za-z]{27}$/\n\nexport type KsuidOptions = {\n /**\n * 16 bytes of random data to use for KSUID payload.\n */\n random?: Uint8Array\n /**\n * Timestamp in seconds since Unix epoch.\n * Defaults to Math.floor(Date.now() / 1000).\n * KSUID natively uses second precision.\n */\n secs?: number\n}\n\nexport type Ksuid = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: KsuidOptions | undefined, buf: TBuf, offset?: number): TBuf\n (options?: KsuidOptions, buf?: undefined, offset?: number): string\n toBytes(id: string): Uint8Array\n fromBytes(bytes: Uint8Array): string\n timestamp(id: string): number\n isValid(id: unknown): id is string\n /** The nil KSUID (all zeros) */\n NIL: string\n /** The max KSUID (maximum valid value) */\n MAX: string\n}\n\ntype KsuidState = {\n secs: number\n lastPayload: Uint8Array\n}\n\n/**\n * Module-level state for maintaining monotonic ordering within the same second.\n *\n * IMPORTANT: This state persists across all ksuid() calls in the module's lifetime.\n * - In serverless/edge functions with warm starts, state persists between invocations.\n * - For isolated state, pass explicit `secs` and `random` via options.\n * - Tests should mock Date.now() or provide explicit options for deterministic behavior.\n */\nconst state: KsuidState = {\n secs: -Infinity,\n lastPayload: new Uint8Array(PAYLOAD_BYTES),\n}\n\n/**\n * Write KSUID bytes to a buffer.\n */\nfunction ksuidBytes(secs: number, payload: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {\n if (!buf) {\n buf = new Uint8Array(KSUID_BYTES)\n offset = 0\n } else if (offset < 0 || offset + KSUID_BYTES > buf.length) {\n throw new RangeError(`KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`)\n }\n\n // Timestamp (32-bit big-endian seconds since KSUID epoch) -> bytes 0-3\n writeTimestamp32(buf, offset, secs)\n\n // Payload (128 bits) -> bytes 4-19\n for (let i = 0; i < PAYLOAD_BYTES; i += 1) {\n buf[offset + TIMESTAMP_BYTES + i] = payload[i]\n }\n\n return buf\n}\n\n/*\n * Overload: no buffer => return a KSUID string.\n */\nfunction ksuidFn(options?: KsuidOptions, buf?: undefined, offset?: number): string\n/*\n * Overload: caller provides a buffer slice to fill with KSUID bytes.\n */\nfunction ksuidFn<TBuf extends Uint8Array = Uint8Array>(\n options: KsuidOptions | undefined,\n buf: TBuf,\n offset?: number,\n): TBuf\nfunction ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, buf?: TBuf, offset = 0): string | TBuf {\n let secs: number\n let payload: Uint8Array\n\n /**\n * Note: by default, Cloudflare Workers \"freezes\" time during request handling to prevent\n * side-channel attacks. This means that Date.now() will return the same value for the entire\n * duration of a request.\n * Implications:\n * - all KSUIDs generated within a single request will have the same timestamp.\n * - the monotonic ordering will rely entirely on incrementing the payload portion.\n */\n const now = Date.now()\n const defaultSecs = Math.floor(now / 1000) - KSUID_EPOCH\n\n if (options) {\n // Explicit options provided - use them directly without monotonic state\n secs = options.secs !== undefined ? options.secs - KSUID_EPOCH : defaultSecs\n\n if (options.random) {\n if (options.random.length < PAYLOAD_BYTES) {\n throw new Error(`Random bytes length must be >= ${PAYLOAD_BYTES} for KSUID`)\n }\n payload = options.random\n } else {\n payload = rng()\n }\n } else {\n secs = defaultSecs\n\n if (secs > state.secs) {\n // New second: generate fresh random payload\n payload = rng()\n state.secs = secs\n state.lastPayload.set(payload.subarray(0, PAYLOAD_BYTES))\n } else {\n // Same second: increment payload for monotonic ordering\n incrementBytesInPlace(state.lastPayload)\n payload = state.lastPayload\n }\n }\n\n if (buf) {\n ksuidBytes(secs, payload, buf, offset)\n return buf\n }\n\n // String mode: create bytes then encode to Base62\n const bytes = ksuidBytes(secs, payload)\n return encodeBase62(bytes)\n}\n\n/**\n * Convert a KSUID string to 20 bytes.\n *\n * Note: Base62 is case-sensitive. 'A' (value 10) and 'a' (value 36) decode\n * to different byte values. This function accepts both cases as valid input.\n */\nfunction toBytes(id: string): Uint8Array {\n return decodeBase62(id)\n}\n\n/**\n * Convert 20 bytes to a KSUID string.\n */\nfunction fromBytes(bytes: Uint8Array): string {\n return encodeBase62(bytes)\n}\n\n/**\n * Extract the timestamp from a KSUID string.\n * Returns Unix timestamp in milliseconds for API consistency with ulid/uuidv7.\n * Note: KSUID only has second precision, so the returned value will always end in 000.\n */\nfunction timestamp(id: string): number {\n const bytes = decodeBase62(id)\n // First 4 bytes are big-endian timestamp (seconds since KSUID epoch)\n const secs = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0\n // Add KSUID epoch and convert to milliseconds\n return (secs + KSUID_EPOCH) * 1000\n}\n\n/**\n * Validate a KSUID string format.\n *\n * Note: Both uppercase and lowercase letters are valid Base62 characters,\n * but they represent different values (e.g., 'A' = 10, 'a' = 36).\n */\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && id.length === KSUID_STRING_LEN && KSUID_REGEX.test(id)\n}\n\n/**\n * Generate a KSUID string or write the bytes into a buffer.\n * Also includes helpers to convert to and from byte arrays.\n */\nexport const ksuid: Ksuid = Object.assign(ksuidFn, {\n toBytes,\n fromBytes,\n timestamp,\n isValid,\n NIL: '000000000000000000000000000',\n MAX: 'aWgEPTl1tmebfsQzFP4bxwgy80V',\n})\n"],"mappings":"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"}
@@ -33,6 +33,49 @@ type Nanoid = {
33
33
  };
34
34
  /**
35
35
  * Generate a URL-friendly unique string ID.
36
+ *
37
+ * Nanoid is a tiny, secure, URL-friendly unique string ID generator.
38
+ * It uses a URL-safe alphabet (A-Za-z0-9_-) and generates 21-character
39
+ * IDs by default with 126 bits of entropy.
40
+ *
41
+ * Unlike UUID v7 or ULID, nanoid is NOT time-ordered. Use it for:
42
+ * - URL shorteners
43
+ * - Session tokens
44
+ * - Invite codes
45
+ * - Any case where you need short, random IDs
46
+ *
47
+ * @example Basic usage
48
+ * ```ts
49
+ * import { nanoid } from 'uniku/nanoid'
50
+ *
51
+ * const id = nanoid()
52
+ * // => "V1StGXR8_Z5jdHi6B-myT"
53
+ * ```
54
+ *
55
+ * @example Custom size
56
+ * ```ts
57
+ * const shortId = nanoid(10)
58
+ * // => "IRFa-VaY2b"
59
+ * ```
60
+ *
61
+ * @example Custom alphabet (hex)
62
+ * ```ts
63
+ * const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
64
+ * // => "4f90d13a42bc"
65
+ * ```
66
+ *
67
+ * @example Validation
68
+ * ```ts
69
+ * const maybeId: unknown = getUserInput()
70
+ * if (nanoid.isValid(maybeId)) {
71
+ * // TypeScript knows maybeId is string
72
+ * console.log(maybeId.length)
73
+ * }
74
+ * ```
75
+ *
76
+ * @throws {RangeError} Size must be between 0 and 2048
77
+ * @throws {Error} Alphabet must contain 2-256 unique printable ASCII characters
78
+ * @throws {Error} Insufficient random bytes for requested size
36
79
  */
37
80
  declare const nanoid: Nanoid;
38
81
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"nanoid.d.mts","names":["nanoid: Nanoid"],"sources":["../../src/nanoid/nanoid.ts"],"sourcesContent":[],"mappings":";;AACa,cAAA,YAAA,GAAA,kEAAA;AAwBD,KAAA,aAAA,GAAA;EAkBZ;AAmJA;;;;WA/JW;;;;;;;;;;;KAYC,MAAA;;;;;;YAMA;;;;;;;;;;cA6ICA,QAAQ"}
1
+ {"version":3,"file":"nanoid.d.mts","names":["nanoid: Nanoid"],"sources":["../../src/nanoid/nanoid.ts"],"sourcesContent":[],"mappings":";;AACa,cAAA,YAAA,GAAA,kEAAA;AAwBD,KAAA,aAAA,GAAA;EAkBZ;AA8LA;;;;WA1MW;;;;;;;;;;;KAYC,MAAA;;;;;;YAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwLCA,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"nanoid.mjs","names":["pool: Uint8Array | undefined","id","randomBytes: Uint8Array | undefined","mask","bytes: Uint8Array","nanoid: Nanoid"],"sources":["../../src/nanoid/nanoid.ts"],"sourcesContent":["/** Default URL-safe alphabet (64 characters): A-Z, a-z, 0-9, underscore, hyphen */\nexport const URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'\n\nconst DEFAULT_SIZE = 21\nconst MAX_SIZE = 2048\nconst NANOID_REGEX = /^[A-Za-z0-9_-]+$/\n\n// Simple random pool for ultra-fast path (npm nanoid style - no thread-safety overhead)\nconst POOL_SIZE_MULTIPLIER = 128\nlet pool: Uint8Array | undefined\nlet poolOffset = 0\n\nfunction fillPool(bytes: number): void {\n const size = bytes * POOL_SIZE_MULTIPLIER\n if (!pool || pool.length < size) {\n pool = new Uint8Array(size)\n crypto.getRandomValues(pool)\n poolOffset = 0\n } else if (poolOffset + bytes > pool.length) {\n crypto.getRandomValues(pool)\n poolOffset = 0\n }\n poolOffset += bytes\n}\n\nexport type NanoidOptions = {\n /**\n * Random bytes for deterministic output (testing).\n * For power-of-2 alphabets (2, 4, 8, 16, 32, 64, 128, 256): exactly `size` bytes needed.\n * For other alphabets: ~size * 2 bytes needed (rejection sampling).\n */\n random?: Uint8Array\n /**\n * Custom alphabet to use. Default: URL-safe A-Za-z0-9_-\n * Must be 2-256 printable ASCII characters (32-126) with no duplicates.\n */\n alphabet?: string\n /**\n * Length of generated ID. Default: 21. Maximum: 2048.\n */\n size?: number\n}\n\nexport type Nanoid = {\n /** Generate nanoid with default settings */\n (): string\n /** Generate nanoid with custom size */\n (size: number): string\n /** Generate nanoid with options */\n (options: NanoidOptions): string\n /**\n * Validate a nanoid string against the default URL-safe alphabet.\n * Note: Does not validate IDs generated with custom alphabets.\n */\n isValid(id: unknown): id is string\n}\n\n/**\n * Validate alphabet: 2-256 printable ASCII chars, no duplicates\n */\nfunction validateAlphabet(alphabet: string): void {\n if (alphabet.length < 2) {\n throw new Error('Alphabet must contain at least 2 characters')\n }\n if (alphabet.length > 256) {\n throw new Error('Alphabet must not exceed 256 characters')\n }\n const seen = new Set<string>()\n for (const char of alphabet) {\n const code = char.charCodeAt(0)\n if (code < 32 || code > 126) {\n throw new Error('Alphabet must contain only printable ASCII characters (32-126)')\n }\n if (seen.has(char)) {\n throw new Error(`Duplicate character in alphabet: \"${char}\"`)\n }\n seen.add(char)\n }\n}\n\n/**\n * Validate size parameter\n */\nfunction validateSize(size: number): void {\n if (!Number.isInteger(size) || size < 0) {\n throw new RangeError('Size must be a non-negative integer')\n }\n if (size > MAX_SIZE) {\n throw new RangeError(`Size must not exceed ${MAX_SIZE}`)\n }\n}\n\n// Overloads\nfunction nanoidFn(): string\nfunction nanoidFn(size: number): string\nfunction nanoidFn(options: NanoidOptions): string\nfunction nanoidFn(sizeOrOptions?: number | NanoidOptions): string {\n // ULTRA-FAST PATH: No arguments = default nanoid\n // Uses simple pooled random bytes (npm nanoid style) for best performance\n if (sizeOrOptions === undefined) {\n fillPool(DEFAULT_SIZE)\n let id = ''\n for (let i = poolOffset - DEFAULT_SIZE; i < poolOffset; i++) {\n id += URL_ALPHABET[pool![i] & 63]\n }\n return id\n }\n\n let size = DEFAULT_SIZE\n let alphabet = URL_ALPHABET\n let randomBytes: Uint8Array | undefined\n\n if (typeof sizeOrOptions === 'number') {\n size = sizeOrOptions\n } else {\n size = sizeOrOptions.size ?? DEFAULT_SIZE\n alphabet = sizeOrOptions.alphabet ?? URL_ALPHABET\n randomBytes = sizeOrOptions.random\n if (sizeOrOptions.alphabet !== undefined) {\n validateAlphabet(alphabet)\n }\n }\n\n validateSize(size)\n\n if (size === 0) return ''\n\n const alphabetLen = alphabet.length\n\n // FAST PATH: Power-of-2 alphabet (includes default 64-char)\n // No rejection needed - each byte maps directly to a character\n if ((alphabetLen & (alphabetLen - 1)) === 0) {\n const mask = alphabetLen - 1\n if (randomBytes && randomBytes.length < size) {\n throw new Error(`Insufficient random bytes: need ${size}, have ${randomBytes.length}`)\n }\n const bytes = randomBytes?.subarray(0, size) ?? globalThis.crypto.getRandomValues(new Uint8Array(size))\n let id = ''\n for (let i = 0; i < size; i++) {\n id += alphabet[bytes[i] & mask]\n }\n return id\n }\n\n // SLOW PATH: Rejection sampling for non-power-of-2 alphabets\n // Calculate mask: smallest power-of-2 minus 1 that covers alphabet size\n const mask = (2 << (31 - Math.clz32((alphabetLen - 1) | 1))) - 1\n // Calculate step: random bytes per batch (1.6x accounts for rejection)\n const step = Math.ceil((1.6 * mask * size) / alphabetLen)\n\n let id = ''\n let randomOffset = 0\n\n while (id.length < size) {\n let bytes: Uint8Array\n if (randomBytes) {\n if (randomBytes.length - randomOffset < step) {\n throw new Error(\n `Insufficient random bytes: need at least ${step} more, have ${randomBytes.length - randomOffset}`,\n )\n }\n bytes = randomBytes.subarray(randomOffset, randomOffset + step)\n randomOffset += step\n } else {\n bytes = globalThis.crypto.getRandomValues(new Uint8Array(step))\n }\n\n for (let i = 0; i < bytes.length && id.length < size; i++) {\n const index = bytes[i] & mask\n if (index < alphabetLen) {\n id += alphabet[index]\n }\n // Otherwise reject and continue (no modulo bias)\n }\n }\n\n return id\n}\n\n/**\n * Validate a nanoid string against the default URL-safe alphabet.\n * Note: Does not validate IDs generated with custom alphabets.\n */\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && id.length > 0 && NANOID_REGEX.test(id)\n}\n\n/**\n * Generate a URL-friendly unique string ID.\n */\nexport const nanoid: Nanoid = Object.assign(nanoidFn, {\n isValid,\n})\n"],"mappings":"AACA,MAAa,EAAe,mEAGtB,EAAW,KACX,EAAe,mBAIrB,IAAIA,EACA,EAAa,EAEjB,SAAS,EAAS,EAAqB,CACrC,IAAM,EAAO,EAAQ,IACjB,CAAC,GAAQ,EAAK,OAAS,GACzB,EAAO,IAAI,WAAW,EAAK,CAC3B,OAAO,gBAAgB,EAAK,CAC5B,EAAa,GACJ,EAAa,EAAQ,EAAK,SACnC,OAAO,gBAAgB,EAAK,CAC5B,EAAa,GAEf,GAAc,EAsChB,SAAS,EAAiB,EAAwB,CAChD,GAAI,EAAS,OAAS,EACpB,MAAU,MAAM,8CAA8C,CAEhE,GAAI,EAAS,OAAS,IACpB,MAAU,MAAM,0CAA0C,CAE5D,IAAM,EAAO,IAAI,IACjB,IAAK,IAAM,KAAQ,EAAU,CAC3B,IAAM,EAAO,EAAK,WAAW,EAAE,CAC/B,GAAI,EAAO,IAAM,EAAO,IACtB,MAAU,MAAM,iEAAiE,CAEnF,GAAI,EAAK,IAAI,EAAK,CAChB,MAAU,MAAM,qCAAqC,EAAK,GAAG,CAE/D,EAAK,IAAI,EAAK,EAOlB,SAAS,EAAa,EAAoB,CACxC,GAAI,CAAC,OAAO,UAAU,EAAK,EAAI,EAAO,EACpC,MAAU,WAAW,sCAAsC,CAE7D,GAAI,EAAO,EACT,MAAU,WAAW,wBAAwB,IAAW,CAQ5D,SAAS,EAAS,EAAgD,CAGhE,GAAI,IAAkB,IAAA,GAAW,CAC/B,EAAS,GAAa,CACtB,IAAIC,EAAK,GACT,IAAK,IAAI,EAAI,EAAa,GAAc,EAAI,EAAY,IACtD,GAAM,EAAa,EAAM,GAAK,IAEhC,OAAOA,EAGT,IAAI,EAAO,GACP,EAAW,EACXC,EAeJ,GAbI,OAAO,GAAkB,SAC3B,EAAO,GAEP,EAAO,EAAc,MAAQ,GAC7B,EAAW,EAAc,UAAY,EACrC,EAAc,EAAc,OACxB,EAAc,WAAa,IAAA,IAC7B,EAAiB,EAAS,EAI9B,EAAa,EAAK,CAEd,IAAS,EAAG,MAAO,GAEvB,IAAM,EAAc,EAAS,OAI7B,GAAA,EAAK,EAAe,EAAc,GAAW,CAC3C,IAAMC,EAAO,EAAc,EAC3B,GAAI,GAAe,EAAY,OAAS,EACtC,MAAU,MAAM,mCAAmC,EAAK,SAAS,EAAY,SAAS,CAExF,IAAM,EAAQ,GAAa,SAAS,EAAG,EAAK,EAAI,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAK,CAAC,CACnGF,EAAK,GACT,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,IACxB,GAAM,EAAS,EAAM,GAAKE,GAE5B,OAAOF,EAKT,IAAM,GAAQ,GAAM,GAAK,KAAK,MAAO,EAAc,EAAK,EAAE,EAAK,EAEzD,EAAO,KAAK,KAAM,IAAM,EAAO,EAAQ,EAAY,CAErD,EAAK,GACL,EAAe,EAEnB,KAAO,EAAG,OAAS,GAAM,CACvB,IAAIG,EACJ,GAAI,EAAa,CACf,GAAI,EAAY,OAAS,EAAe,EACtC,MAAU,MACR,4CAA4C,EAAK,cAAc,EAAY,OAAS,IACrF,CAEH,EAAQ,EAAY,SAAS,EAAc,EAAe,EAAK,CAC/D,GAAgB,OAEhB,EAAQ,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAK,CAAC,CAGjE,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,QAAU,EAAG,OAAS,EAAM,IAAK,CACzD,IAAM,EAAQ,EAAM,GAAK,EACrB,EAAQ,IACV,GAAM,EAAS,KAMrB,OAAO,EAOT,SAAS,EAAQ,EAA2B,CAC1C,OAAO,OAAO,GAAO,UAAY,EAAG,OAAS,GAAK,EAAa,KAAK,EAAG,CAMzE,MAAaC,EAAiB,OAAO,OAAO,EAAU,CACpD,UACD,CAAC"}
1
+ {"version":3,"file":"nanoid.mjs","names":["pool: Uint8Array | undefined","id","randomBytes: Uint8Array | undefined","mask","bytes: Uint8Array","nanoid: Nanoid"],"sources":["../../src/nanoid/nanoid.ts"],"sourcesContent":["/** Default URL-safe alphabet (64 characters): A-Z, a-z, 0-9, underscore, hyphen */\nexport const URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'\n\nconst DEFAULT_SIZE = 21\nconst MAX_SIZE = 2048\nconst NANOID_REGEX = /^[A-Za-z0-9_-]+$/\n\n// Simple random pool for ultra-fast path (npm nanoid style - no thread-safety overhead)\nconst POOL_SIZE_MULTIPLIER = 128\nlet pool: Uint8Array | undefined\nlet poolOffset = 0\n\nfunction fillPool(bytes: number): void {\n const size = bytes * POOL_SIZE_MULTIPLIER\n if (!pool || pool.length < size) {\n pool = new Uint8Array(size)\n crypto.getRandomValues(pool)\n poolOffset = 0\n } else if (poolOffset + bytes > pool.length) {\n crypto.getRandomValues(pool)\n poolOffset = 0\n }\n poolOffset += bytes\n}\n\nexport type NanoidOptions = {\n /**\n * Random bytes for deterministic output (testing).\n * For power-of-2 alphabets (2, 4, 8, 16, 32, 64, 128, 256): exactly `size` bytes needed.\n * For other alphabets: ~size * 2 bytes needed (rejection sampling).\n */\n random?: Uint8Array\n /**\n * Custom alphabet to use. Default: URL-safe A-Za-z0-9_-\n * Must be 2-256 printable ASCII characters (32-126) with no duplicates.\n */\n alphabet?: string\n /**\n * Length of generated ID. Default: 21. Maximum: 2048.\n */\n size?: number\n}\n\nexport type Nanoid = {\n /** Generate nanoid with default settings */\n (): string\n /** Generate nanoid with custom size */\n (size: number): string\n /** Generate nanoid with options */\n (options: NanoidOptions): string\n /**\n * Validate a nanoid string against the default URL-safe alphabet.\n * Note: Does not validate IDs generated with custom alphabets.\n */\n isValid(id: unknown): id is string\n}\n\n/**\n * Validate alphabet: 2-256 printable ASCII chars, no duplicates\n */\nfunction validateAlphabet(alphabet: string): void {\n if (alphabet.length < 2) {\n throw new Error('Alphabet must contain at least 2 characters')\n }\n if (alphabet.length > 256) {\n throw new Error('Alphabet must not exceed 256 characters')\n }\n const seen = new Set<string>()\n for (const char of alphabet) {\n const code = char.charCodeAt(0)\n if (code < 32 || code > 126) {\n throw new Error('Alphabet must contain only printable ASCII characters (32-126)')\n }\n if (seen.has(char)) {\n throw new Error(`Duplicate character in alphabet: \"${char}\"`)\n }\n seen.add(char)\n }\n}\n\n/**\n * Validate size parameter\n */\nfunction validateSize(size: number): void {\n if (!Number.isInteger(size) || size < 0) {\n throw new RangeError('Size must be a non-negative integer')\n }\n if (size > MAX_SIZE) {\n throw new RangeError(`Size must not exceed ${MAX_SIZE}`)\n }\n}\n\n// Overloads\nfunction nanoidFn(): string\nfunction nanoidFn(size: number): string\nfunction nanoidFn(options: NanoidOptions): string\nfunction nanoidFn(sizeOrOptions?: number | NanoidOptions): string {\n // ULTRA-FAST PATH: No arguments = default nanoid\n // Uses simple pooled random bytes (npm nanoid style) for best performance\n if (sizeOrOptions === undefined) {\n fillPool(DEFAULT_SIZE)\n let id = ''\n for (let i = poolOffset - DEFAULT_SIZE; i < poolOffset; i++) {\n id += URL_ALPHABET[pool![i] & 63]\n }\n return id\n }\n\n let size = DEFAULT_SIZE\n let alphabet = URL_ALPHABET\n let randomBytes: Uint8Array | undefined\n\n if (typeof sizeOrOptions === 'number') {\n size = sizeOrOptions\n } else {\n size = sizeOrOptions.size ?? DEFAULT_SIZE\n alphabet = sizeOrOptions.alphabet ?? URL_ALPHABET\n randomBytes = sizeOrOptions.random\n if (sizeOrOptions.alphabet !== undefined) {\n validateAlphabet(alphabet)\n }\n }\n\n validateSize(size)\n\n if (size === 0) return ''\n\n const alphabetLen = alphabet.length\n\n // FAST PATH: Power-of-2 alphabet (includes default 64-char)\n // No rejection needed - each byte maps directly to a character\n if ((alphabetLen & (alphabetLen - 1)) === 0) {\n const mask = alphabetLen - 1\n if (randomBytes && randomBytes.length < size) {\n throw new Error(`Insufficient random bytes: need ${size}, have ${randomBytes.length}`)\n }\n const bytes = randomBytes?.subarray(0, size) ?? globalThis.crypto.getRandomValues(new Uint8Array(size))\n let id = ''\n for (let i = 0; i < size; i++) {\n id += alphabet[bytes[i] & mask]\n }\n return id\n }\n\n // SLOW PATH: Rejection sampling for non-power-of-2 alphabets\n // Calculate mask: smallest power-of-2 minus 1 that covers alphabet size\n const mask = (2 << (31 - Math.clz32((alphabetLen - 1) | 1))) - 1\n // Calculate step: random bytes per batch (1.6x accounts for rejection)\n const step = Math.ceil((1.6 * mask * size) / alphabetLen)\n\n let id = ''\n let randomOffset = 0\n\n while (id.length < size) {\n let bytes: Uint8Array\n if (randomBytes) {\n if (randomBytes.length - randomOffset < step) {\n throw new Error(\n `Insufficient random bytes: need at least ${step} more, have ${randomBytes.length - randomOffset}`,\n )\n }\n bytes = randomBytes.subarray(randomOffset, randomOffset + step)\n randomOffset += step\n } else {\n bytes = globalThis.crypto.getRandomValues(new Uint8Array(step))\n }\n\n for (let i = 0; i < bytes.length && id.length < size; i++) {\n const index = bytes[i] & mask\n if (index < alphabetLen) {\n id += alphabet[index]\n }\n // Otherwise reject and continue (no modulo bias)\n }\n }\n\n return id\n}\n\n/**\n * Validate a nanoid string against the default URL-safe alphabet.\n * Note: Does not validate IDs generated with custom alphabets.\n */\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && id.length > 0 && NANOID_REGEX.test(id)\n}\n\n/**\n * Generate a URL-friendly unique string ID.\n *\n * Nanoid is a tiny, secure, URL-friendly unique string ID generator.\n * It uses a URL-safe alphabet (A-Za-z0-9_-) and generates 21-character\n * IDs by default with 126 bits of entropy.\n *\n * Unlike UUID v7 or ULID, nanoid is NOT time-ordered. Use it for:\n * - URL shorteners\n * - Session tokens\n * - Invite codes\n * - Any case where you need short, random IDs\n *\n * @example Basic usage\n * ```ts\n * import { nanoid } from 'uniku/nanoid'\n *\n * const id = nanoid()\n * // => \"V1StGXR8_Z5jdHi6B-myT\"\n * ```\n *\n * @example Custom size\n * ```ts\n * const shortId = nanoid(10)\n * // => \"IRFa-VaY2b\"\n * ```\n *\n * @example Custom alphabet (hex)\n * ```ts\n * const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })\n * // => \"4f90d13a42bc\"\n * ```\n *\n * @example Validation\n * ```ts\n * const maybeId: unknown = getUserInput()\n * if (nanoid.isValid(maybeId)) {\n * // TypeScript knows maybeId is string\n * console.log(maybeId.length)\n * }\n * ```\n *\n * @throws {RangeError} Size must be between 0 and 2048\n * @throws {Error} Alphabet must contain 2-256 unique printable ASCII characters\n * @throws {Error} Insufficient random bytes for requested size\n */\nexport const nanoid: Nanoid = Object.assign(nanoidFn, {\n isValid,\n})\n"],"mappings":"AACA,MAAa,EAAe,mEAGtB,EAAW,KACX,EAAe,mBAIrB,IAAIA,EACA,EAAa,EAEjB,SAAS,EAAS,EAAqB,CACrC,IAAM,EAAO,EAAQ,IACjB,CAAC,GAAQ,EAAK,OAAS,GACzB,EAAO,IAAI,WAAW,EAAK,CAC3B,OAAO,gBAAgB,EAAK,CAC5B,EAAa,GACJ,EAAa,EAAQ,EAAK,SACnC,OAAO,gBAAgB,EAAK,CAC5B,EAAa,GAEf,GAAc,EAsChB,SAAS,EAAiB,EAAwB,CAChD,GAAI,EAAS,OAAS,EACpB,MAAU,MAAM,8CAA8C,CAEhE,GAAI,EAAS,OAAS,IACpB,MAAU,MAAM,0CAA0C,CAE5D,IAAM,EAAO,IAAI,IACjB,IAAK,IAAM,KAAQ,EAAU,CAC3B,IAAM,EAAO,EAAK,WAAW,EAAE,CAC/B,GAAI,EAAO,IAAM,EAAO,IACtB,MAAU,MAAM,iEAAiE,CAEnF,GAAI,EAAK,IAAI,EAAK,CAChB,MAAU,MAAM,qCAAqC,EAAK,GAAG,CAE/D,EAAK,IAAI,EAAK,EAOlB,SAAS,EAAa,EAAoB,CACxC,GAAI,CAAC,OAAO,UAAU,EAAK,EAAI,EAAO,EACpC,MAAU,WAAW,sCAAsC,CAE7D,GAAI,EAAO,EACT,MAAU,WAAW,wBAAwB,IAAW,CAQ5D,SAAS,EAAS,EAAgD,CAGhE,GAAI,IAAkB,IAAA,GAAW,CAC/B,EAAS,GAAa,CACtB,IAAIC,EAAK,GACT,IAAK,IAAI,EAAI,EAAa,GAAc,EAAI,EAAY,IACtD,GAAM,EAAa,EAAM,GAAK,IAEhC,OAAOA,EAGT,IAAI,EAAO,GACP,EAAW,EACXC,EAeJ,GAbI,OAAO,GAAkB,SAC3B,EAAO,GAEP,EAAO,EAAc,MAAQ,GAC7B,EAAW,EAAc,UAAY,EACrC,EAAc,EAAc,OACxB,EAAc,WAAa,IAAA,IAC7B,EAAiB,EAAS,EAI9B,EAAa,EAAK,CAEd,IAAS,EAAG,MAAO,GAEvB,IAAM,EAAc,EAAS,OAI7B,GAAA,EAAK,EAAe,EAAc,GAAW,CAC3C,IAAMC,EAAO,EAAc,EAC3B,GAAI,GAAe,EAAY,OAAS,EACtC,MAAU,MAAM,mCAAmC,EAAK,SAAS,EAAY,SAAS,CAExF,IAAM,EAAQ,GAAa,SAAS,EAAG,EAAK,EAAI,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAK,CAAC,CACnGF,EAAK,GACT,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,IACxB,GAAM,EAAS,EAAM,GAAKE,GAE5B,OAAOF,EAKT,IAAM,GAAQ,GAAM,GAAK,KAAK,MAAO,EAAc,EAAK,EAAE,EAAK,EAEzD,EAAO,KAAK,KAAM,IAAM,EAAO,EAAQ,EAAY,CAErD,EAAK,GACL,EAAe,EAEnB,KAAO,EAAG,OAAS,GAAM,CACvB,IAAIG,EACJ,GAAI,EAAa,CACf,GAAI,EAAY,OAAS,EAAe,EACtC,MAAU,MACR,4CAA4C,EAAK,cAAc,EAAY,OAAS,IACrF,CAEH,EAAQ,EAAY,SAAS,EAAc,EAAe,EAAK,CAC/D,GAAgB,OAEhB,EAAQ,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAK,CAAC,CAGjE,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,QAAU,EAAG,OAAS,EAAM,IAAK,CACzD,IAAM,EAAQ,EAAM,GAAK,EACrB,EAAQ,IACV,GAAM,EAAS,KAMrB,OAAO,EAOT,SAAS,EAAQ,EAA2B,CAC1C,OAAO,OAAO,GAAO,UAAY,EAAG,OAAS,GAAK,EAAa,KAAK,EAAG,CAiDzE,MAAaC,EAAiB,OAAO,OAAO,EAAU,CACpD,UACD,CAAC"}
@@ -1,2 +1,2 @@
1
1
  const e=new Uint8Array(256);let t=256;function n(){t>240&&(crypto.getRandomValues(e),t=0);let n=t;return t+=16,e.subarray(n,t)}export{n as t};
2
- //# sourceMappingURL=random-pEE8mU9d.mjs.map
2
+ //# sourceMappingURL=random-E8GC9ETc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"random-E8GC9ETc.mjs","names":[],"sources":["../src/common/random.ts"],"sourcesContent":["/**\n * Simple, fast random byte pool for ID generation.\n * Uses plain JavaScript for maximum performance - no Atomics overhead.\n * Thread safety is not needed for typical ID generation use cases.\n */\n\nconst POOL_SIZE = 256\nconst rnds8Pool = new Uint8Array(POOL_SIZE)\nlet poolPtr = POOL_SIZE // Start exhausted to trigger first fill\n\n/**\n * Generate 16 bytes of cryptographically strong random data.\n * Uses a pre-filled pool to minimize crypto API calls.\n */\nexport function rng(): Uint8Array {\n if (poolPtr > POOL_SIZE - 16) {\n crypto.getRandomValues(rnds8Pool)\n poolPtr = 0\n }\n // Return a view - consumed synchronously within the same call\n const start = poolPtr\n poolPtr += 16\n return rnds8Pool.subarray(start, poolPtr)\n}\n"],"mappings":"AAMA,MACM,EAAY,IAAI,WAAW,IAAU,CAC3C,IAAI,EAAU,IAMd,SAAgB,GAAkB,CAC5B,EAAU,MACZ,OAAO,gBAAgB,EAAU,CACjC,EAAU,GAGZ,IAAM,EAAQ,EAEd,MADA,IAAW,GACJ,EAAU,SAAS,EAAO,EAAQ"}