uniku 0.0.5 → 0.0.7
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 +175 -57
- package/build/cuid2/cuid2.mjs +1 -132
- package/build/cuid2/cuid2.mjs.map +1 -1
- package/build/ksuid/ksuid.d.mts +30 -0
- package/build/ksuid/ksuid.d.mts.map +1 -0
- package/build/ksuid/ksuid.mjs +2 -0
- package/build/ksuid/ksuid.mjs.map +1 -0
- package/build/nanoid/nanoid.d.mts.map +1 -1
- package/build/nanoid/nanoid.mjs +1 -91
- package/build/nanoid/nanoid.mjs.map +1 -1
- package/build/random-CPlgK_62.mjs +2 -0
- package/build/{random-B5WWq7hM.mjs.map → random-CPlgK_62.mjs.map} +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/ulid/ulid.mjs +1 -191
- package/build/ulid/ulid.mjs.map +1 -1
- package/build/uuid/v4.mjs +1 -34
- package/build/uuid/v4.mjs.map +1 -1
- package/build/uuid/v7.d.mts.map +1 -1
- package/build/uuid/v7.mjs +1 -83
- package/build/uuid/v7.mjs.map +1 -1
- package/build/uuid-DcbjGX9x.mjs +2 -0
- package/build/{uuid-BEUMa9NG.mjs.map → uuid-DcbjGX9x.mjs.map} +1 -1
- package/package.json +12 -1
- package/build/random-B5WWq7hM.mjs +0 -21
- package/build/random-pool-CRCqpq0I.mjs +0 -100
- package/build/random-pool-CRCqpq0I.mjs.map +0 -1
- package/build/uuid-BEUMa9NG.mjs +0 -43
package/README.md
CHANGED
|
@@ -1,51 +1,110 @@
|
|
|
1
1
|
# uniku
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/uniku)
|
|
4
|
+
[](https://npmjs.com/package/uniku)
|
|
5
|
+
[](https://bundlephobia.com/package/uniku)
|
|
4
6
|
[](https://github.com/jkomyno/uniku/actions/workflows/ci.yaml)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
5
8
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://bundlephobia.com/package/uniku)
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
One library. Every ID format. Every runtime. Zero dependencies.
|
|
9
11
|
|
|
10
|
-
>
|
|
12
|
+
> **uniku** */uˈniːku/* — Maltese for "unique"
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
```ts
|
|
15
|
+
import { uuidv7 } from 'uniku/uuid/v7'
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Truly tree-shakeable** - Separate entry points mean you only bundle what you import. No barrel exports that defeat dead-code elimination.
|
|
17
|
-
- **Byte-level access** - Convert between UUID strings and `Uint8Array` with built-in `toBytes()` and `fromBytes()` helpers.
|
|
18
|
-
- **Modern standards** - Includes UUID v7 (RFC 9562) for time-ordered, sortable identifiers with monotonic sequencing.
|
|
19
|
-
- **Lightweight** - Designed for use in performance-sensitive contexts like ORMs.
|
|
17
|
+
const id = uuidv7()
|
|
18
|
+
// => "018e5e5c-7c8a-7000-8000-000000000000"
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
// Time-ordered: IDs sort by creation time
|
|
21
|
+
const [first, second, third] = [uuidv7(), uuidv7(), uuidv7()]
|
|
22
|
+
console.log(first < second && second < third) // true
|
|
23
|
+
```
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
## At a Glance
|
|
26
|
+
|
|
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 | ✅ | ✅ | - | ✅ | - | ✅ |
|
|
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.
|
|
41
|
+
|
|
42
|
+
### Works Everywhere
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<img src="https://img.shields.io/badge/Node.js-339933?logo=nodedotjs&logoColor=white" alt="Node.js">
|
|
46
|
+
<img src="https://img.shields.io/badge/Deno-000000?logo=deno&logoColor=white" alt="Deno">
|
|
47
|
+
<img src="https://img.shields.io/badge/Bun-000000?logo=bun&logoColor=white" alt="Bun">
|
|
48
|
+
<img src="https://img.shields.io/badge/Cloudflare_Workers-F38020?logo=cloudflare&logoColor=white" alt="Cloudflare Workers">
|
|
49
|
+
<img src="https://img.shields.io/badge/Vercel_Edge-000000?logo=vercel&logoColor=white" alt="Vercel Edge">
|
|
50
|
+
<img src="https://img.shields.io/badge/Browsers-4285F4?logo=googlechrome&logoColor=white" alt="Browsers">
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
Uses `globalThis.crypto` (Web Crypto API) — no Node.js-specific APIs.
|
|
54
|
+
|
|
55
|
+
## Performance
|
|
56
|
+
|
|
57
|
+
| Generator | uniku vs npm |
|
|
58
|
+
|-----------|-------------:|
|
|
59
|
+
| ULID | **77× faster** |
|
|
60
|
+
| CUID2 | **8× faster** |
|
|
61
|
+
| UUID v4 | **1.5× faster** |
|
|
62
|
+
| Nanoid | **1.1× faster** |
|
|
63
|
+
| UUID v7 | npm 1.6× faster* |
|
|
64
|
+
| KSUID | npm 1.8× faster* |
|
|
65
|
+
|
|
66
|
+
<sub>*UUID v7 tradeoff: uniku prioritizes strict monotonic sequencing for database use cases.</sub>
|
|
67
|
+
|
|
68
|
+
## Which ID Should I Use?
|
|
69
|
+
|
|
70
|
+
| Use Case | Recommended | Why |
|
|
71
|
+
|----------|-------------|-----|
|
|
72
|
+
| Database primary keys | **UUID v7** or **ULID** | Time-ordered for index efficiency |
|
|
73
|
+
| URL shorteners | **Nanoid** | Compact, URL-safe characters |
|
|
74
|
+
| Prevent enumeration | **CUID2** | Non-sequential, secure |
|
|
75
|
+
| Maximum compatibility | **UUID v4** | Universal standard |
|
|
76
|
+
| Distributed systems | **ULID** | Sortable + high entropy |
|
|
28
77
|
|
|
29
78
|
## Installation
|
|
30
79
|
|
|
31
|
-
With pnpm:
|
|
32
|
-
|
|
33
80
|
```bash
|
|
81
|
+
# pnpm (recommended)
|
|
34
82
|
pnpm add uniku
|
|
35
|
-
```
|
|
36
83
|
|
|
37
|
-
|
|
84
|
+
# bun
|
|
85
|
+
bun add uniku
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
87
|
+
# npm
|
|
88
|
+
npm install uniku
|
|
42
89
|
|
|
43
|
-
|
|
90
|
+
# yarn
|
|
91
|
+
yarn add uniku
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
|
|
93
|
+
# deno
|
|
94
|
+
deno install npm:uniku
|
|
47
95
|
```
|
|
48
96
|
|
|
97
|
+
### Bundle Impact
|
|
98
|
+
|
|
99
|
+
| Import | Minified + gzipped |
|
|
100
|
+
|--------|-------------------:|
|
|
101
|
+
| `uniku/uuid/v4` | ~930 B |
|
|
102
|
+
| `uniku/uuid/v7` | ~1.1 KB |
|
|
103
|
+
| `uniku/ulid` | ~1.5 KB |
|
|
104
|
+
| `uniku/cuid2` | ~1.1 KB* |
|
|
105
|
+
| `uniku/nanoid` | ~967 B |
|
|
106
|
+
| `uniku/ksuid` | ~1.1 KB |
|
|
107
|
+
|
|
49
108
|
## Quick Start
|
|
50
109
|
|
|
51
110
|
### UUID v4 (random)
|
|
@@ -100,7 +159,6 @@ const shortId = cuid2({ length: 10 })
|
|
|
100
159
|
// => "tz4a98xxat"
|
|
101
160
|
|
|
102
161
|
// Validation (type guard)
|
|
103
|
-
const maybeId: unknown = getUserInput()
|
|
104
162
|
if (cuid2.isValid(maybeId)) {
|
|
105
163
|
console.log(maybeId) // TypeScript knows this is a string
|
|
106
164
|
}
|
|
@@ -121,10 +179,66 @@ const shortId = nanoid(10)
|
|
|
121
179
|
// Custom alphabet and size via options
|
|
122
180
|
const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
|
|
123
181
|
// => "4f90d13a42bc"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### KSUID (time-ordered, high entropy)
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { ksuid } from 'uniku/ksuid'
|
|
188
|
+
|
|
189
|
+
const id = ksuid()
|
|
190
|
+
// => "2QnJjKLvpSfpZqGiPPxVwWLMy2p"
|
|
191
|
+
|
|
192
|
+
// Convert to/from bytes
|
|
193
|
+
const bytes = ksuid.toBytes(id)
|
|
194
|
+
const str = ksuid.fromBytes(bytes)
|
|
195
|
+
|
|
196
|
+
// Extract timestamp (milliseconds)
|
|
197
|
+
const ts = ksuid.timestamp(id)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Migrating to uniku
|
|
201
|
+
|
|
202
|
+
### From `uuid`
|
|
203
|
+
|
|
204
|
+
```diff
|
|
205
|
+
- import { v4 as uuidv4 } from 'uuid'
|
|
206
|
+
+ import { uuidv4 } from 'uniku/uuid/v4'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### From `nanoid`
|
|
124
210
|
|
|
125
|
-
|
|
126
|
-
nanoid
|
|
127
|
-
|
|
211
|
+
```diff
|
|
212
|
+
- import { nanoid } from 'nanoid'
|
|
213
|
+
+ import { nanoid } from 'uniku/nanoid'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### From `ulid`
|
|
217
|
+
|
|
218
|
+
```diff
|
|
219
|
+
- import { ulid } from 'ulid'
|
|
220
|
+
+ import { ulid } from 'uniku/ulid'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### From `@paralleldrive/cuid2`
|
|
224
|
+
|
|
225
|
+
```diff
|
|
226
|
+
- import { createId } from '@paralleldrive/cuid2'
|
|
227
|
+
+ import { cuid2 } from 'uniku/cuid2'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### From `@owpz/ksuid`
|
|
231
|
+
|
|
232
|
+
```diff
|
|
233
|
+
- import { KSUID } from '@owpz/ksuid'
|
|
234
|
+
+ import { ksuid } from 'uniku/ksuid'
|
|
235
|
+
|
|
236
|
+
- const id = KSUID.random().toString()
|
|
237
|
+
+ const id = ksuid()
|
|
238
|
+
|
|
239
|
+
- const parsed = KSUID.parse(str)
|
|
240
|
+
- const timestamp = parsed.timestamp
|
|
241
|
+
+ const timestamp = ksuid.timestamp(str)
|
|
128
242
|
```
|
|
129
243
|
|
|
130
244
|
## API Reference
|
|
@@ -134,47 +248,32 @@ nanoid.isValid(id)
|
|
|
134
248
|
```ts
|
|
135
249
|
uuidv4(options?: Version4Options): string
|
|
136
250
|
uuidv4(options: Version4Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
|
|
137
|
-
|
|
138
251
|
uuidv4.toBytes(id: string): Uint8Array
|
|
139
252
|
uuidv4.fromBytes(bytes: Uint8Array): string
|
|
140
253
|
```
|
|
141
254
|
|
|
142
|
-
**Options:**
|
|
143
|
-
- `random?: Uint8Array` - 16 bytes of random data
|
|
144
|
-
|
|
145
255
|
### `uuidv7` (from `uniku/uuid/v7`)
|
|
146
256
|
|
|
147
257
|
```ts
|
|
148
258
|
uuidv7(options?: Version7Options): string
|
|
149
259
|
uuidv7(options: Version7Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
|
|
150
|
-
|
|
151
260
|
uuidv7.toBytes(id: string): Uint8Array
|
|
152
261
|
uuidv7.fromBytes(bytes: Uint8Array): string
|
|
153
262
|
uuidv7.timestamp(id: string): number
|
|
154
263
|
uuidv7.isValid(id: string): boolean
|
|
155
264
|
```
|
|
156
265
|
|
|
157
|
-
**Options:**
|
|
158
|
-
- `msecs?: number` - Timestamp in milliseconds (defaults to `Date.now()`)
|
|
159
|
-
- `seq?: number` - Sequence number for monotonicity
|
|
160
|
-
- `random?: Uint8Array` - 16 bytes of random data
|
|
161
|
-
|
|
162
266
|
### `ulid` (from `uniku/ulid`)
|
|
163
267
|
|
|
164
268
|
```ts
|
|
165
269
|
ulid(options?: UlidOptions): string
|
|
166
270
|
ulid(options: UlidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
|
|
167
|
-
|
|
168
271
|
ulid.toBytes(id: string): Uint8Array
|
|
169
272
|
ulid.fromBytes(bytes: Uint8Array): string
|
|
170
273
|
ulid.timestamp(id: string): number
|
|
171
274
|
ulid.isValid(id: string): boolean
|
|
172
275
|
```
|
|
173
276
|
|
|
174
|
-
**Options:**
|
|
175
|
-
- `msecs?: number` - Timestamp in milliseconds (defaults to `Date.now()`)
|
|
176
|
-
- `random?: Uint8Array` - 16 bytes of random data (only first 10 bytes used)
|
|
177
|
-
|
|
178
277
|
### `cuid2` (from `uniku/cuid2`)
|
|
179
278
|
|
|
180
279
|
```ts
|
|
@@ -182,12 +281,6 @@ cuid2(options?: Cuid2Options): string
|
|
|
182
281
|
cuid2.isValid(id: unknown): id is string
|
|
183
282
|
```
|
|
184
283
|
|
|
185
|
-
**Options:**
|
|
186
|
-
- `length?: number` - ID length (2-32, default 24)
|
|
187
|
-
- `random?: Uint8Array` - Custom random bytes for testing
|
|
188
|
-
|
|
189
|
-
Note: Unlike UUID and ULID, CUID2 does not provide `toBytes`/`fromBytes` because it is a string-native format with no canonical binary representation.
|
|
190
|
-
|
|
191
284
|
### `nanoid` (from `uniku/nanoid`)
|
|
192
285
|
|
|
193
286
|
```ts
|
|
@@ -197,17 +290,42 @@ nanoid(options: NanoidOptions): string
|
|
|
197
290
|
nanoid.isValid(id: unknown): id is string
|
|
198
291
|
```
|
|
199
292
|
|
|
200
|
-
|
|
201
|
-
- `size?: number` - ID length (default 21, max 2048)
|
|
202
|
-
- `alphabet?: string` - Custom alphabet (2-256 printable ASCII characters)
|
|
203
|
-
- `random?: Uint8Array` - Custom random bytes for testing
|
|
293
|
+
### `ksuid` (from `uniku/ksuid`)
|
|
204
294
|
|
|
205
|
-
|
|
295
|
+
```ts
|
|
296
|
+
ksuid(options?: KsuidOptions): string
|
|
297
|
+
ksuid(options: KsuidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
|
|
298
|
+
ksuid.toBytes(id: string): Uint8Array
|
|
299
|
+
ksuid.fromBytes(bytes: Uint8Array): string
|
|
300
|
+
ksuid.timestamp(id: string): number
|
|
301
|
+
ksuid.isValid(id: string): boolean
|
|
302
|
+
```
|
|
206
303
|
|
|
207
304
|
## Documentation
|
|
208
305
|
|
|
209
306
|
For advanced usage, examples, and contributing guidelines, see the [full documentation on GitHub](https://github.com/jkomyno/uniku).
|
|
210
307
|
|
|
308
|
+
## Related Projects
|
|
309
|
+
|
|
310
|
+
Third-party libraries that inspired this project:
|
|
311
|
+
|
|
312
|
+
- [uuid](https://github.com/uuidjs/uuid): the most popular UUID library for JavaScript
|
|
313
|
+
- [ulid](https://github.com/ulid/javascript): the reference ULID implementation for JavaScript
|
|
314
|
+
- [@paralleldrive/cuid2](https://github.com/paralleldrive/cuid2): secure, collision-resistant IDs
|
|
315
|
+
- [@owpz/ksuid](https://github.com/owpz/ksuid): K-Sortable Unique Identifier
|
|
316
|
+
- [nanoid](https://github.com/ai/nanoid): tiny, URL-friendly unique string ID generator
|
|
317
|
+
|
|
318
|
+
Other:
|
|
319
|
+
|
|
320
|
+
- [pnpm-monorepo-template](https://github.com/jkomyno/pnpm-monorepo-template): the template I used to create this library
|
|
321
|
+
|
|
322
|
+
## Author
|
|
323
|
+
|
|
324
|
+
Hi, I'm Alberto Schiabel, aka @jkomyno. You can follow me on:
|
|
325
|
+
|
|
326
|
+
- Github: [@jkomyno](https://github.com/jkomyno)
|
|
327
|
+
- X: [@jkomyno](https://x.com/jkomyno)
|
|
328
|
+
|
|
211
329
|
## License
|
|
212
330
|
|
|
213
|
-
MIT
|
|
331
|
+
MIT — see [LICENSE](https://github.com/jkomyno/uniku/blob/main/LICENSE)
|
package/build/cuid2/cuid2.mjs
CHANGED
|
@@ -1,133 +1,2 @@
|
|
|
1
|
-
import { n
|
|
2
|
-
import { sha3_512 } from "@noble/hashes/sha3.js";
|
|
3
|
-
|
|
4
|
-
//#region src/cuid2/cuid2.ts
|
|
5
|
-
const DEFAULT_LENGTH = 24;
|
|
6
|
-
const MAX_LENGTH = 32;
|
|
7
|
-
const MIN_LENGTH = 2;
|
|
8
|
-
const INITIAL_COUNT_MAX = 476782367;
|
|
9
|
-
const CUID2_REGEX = /^[a-z][0-9a-z]+$/;
|
|
10
|
-
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
11
|
-
const LETTER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
12
|
-
const textEncoder = new TextEncoder();
|
|
13
|
-
let pool;
|
|
14
|
-
function ensurePool() {
|
|
15
|
-
if (!pool) pool = createPool(4);
|
|
16
|
-
return pool;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Module-level state for counter and fingerprint.
|
|
20
|
-
* Counter is initialized lazily on first call to prevent unnecessary crypto operations.
|
|
21
|
-
* Fingerprint is also generated lazily on first call.
|
|
22
|
-
*/
|
|
23
|
-
const state = {
|
|
24
|
-
counter: void 0,
|
|
25
|
-
fingerprint: void 0
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Initialize counter using crypto for consistency with other entropy sources.
|
|
29
|
-
*/
|
|
30
|
-
function initializeCounter() {
|
|
31
|
-
const buffer = getRandomBytes(ensurePool(), 4);
|
|
32
|
-
return ((buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3]) >>> 0) % (INITIAL_COUNT_MAX + 1);
|
|
33
|
-
}
|
|
34
|
-
function bufToBigInt(buf) {
|
|
35
|
-
let value = 0n;
|
|
36
|
-
for (const byte of buf) value = value * 256n + BigInt(byte);
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
function bigIntToBase36(value) {
|
|
40
|
-
if (value === 0n) return "0";
|
|
41
|
-
const chars = [];
|
|
42
|
-
while (value > 0n) {
|
|
43
|
-
chars.push(ALPHABET[Number(value % 36n)]);
|
|
44
|
-
value = value / 36n;
|
|
45
|
-
}
|
|
46
|
-
return chars.reverse().join("");
|
|
47
|
-
}
|
|
48
|
-
function randomLetter(random) {
|
|
49
|
-
return LETTER_ALPHABET[Math.floor(random() * 26)];
|
|
50
|
-
}
|
|
51
|
-
function createEntropy(length, random) {
|
|
52
|
-
const chars = new Array(length);
|
|
53
|
-
for (let i = 0; i < length; i++) chars[i] = ALPHABET[Math.floor(random() * 36)];
|
|
54
|
-
return chars.join("");
|
|
55
|
-
}
|
|
56
|
-
function hash(input) {
|
|
57
|
-
return sha3_512(textEncoder.encode(input));
|
|
58
|
-
}
|
|
59
|
-
const BIG_LENGTH = 32;
|
|
60
|
-
function createFingerprint() {
|
|
61
|
-
const random = getCryptoRandom;
|
|
62
|
-
return bigIntToBase36(bufToBigInt(hash(Object.keys(globalThis).toString() + createEntropy(BIG_LENGTH, random)))).slice(1, BIG_LENGTH + 1);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Get a random number in [0, 1) using CUID2's own random pool.
|
|
66
|
-
* Thread-safe when SharedArrayBuffer is available.
|
|
67
|
-
*/
|
|
68
|
-
function getCryptoRandom() {
|
|
69
|
-
const p = ensurePool();
|
|
70
|
-
const offset = getPooledBytes(p, 4);
|
|
71
|
-
const bytes = p.bytes;
|
|
72
|
-
return (bytes[offset] * 16777216 + bytes[offset + 1] * 65536 + bytes[offset + 2] * 256 + bytes[offset + 3]) / 4294967296;
|
|
73
|
-
}
|
|
74
|
-
function getRandomFn(random) {
|
|
75
|
-
if (random) {
|
|
76
|
-
if (random.length === 0) throw new Error("Random byte array cannot be empty");
|
|
77
|
-
let index = 0;
|
|
78
|
-
return () => {
|
|
79
|
-
const value = random[index % random.length] / 256;
|
|
80
|
-
index += 1;
|
|
81
|
-
return value;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return getCryptoRandom;
|
|
85
|
-
}
|
|
86
|
-
function cuid2Fn(options) {
|
|
87
|
-
const length = options?.length ?? DEFAULT_LENGTH;
|
|
88
|
-
if (length < MIN_LENGTH || length > MAX_LENGTH) throw new RangeError(`CUID2 length must be between ${MIN_LENGTH} and ${MAX_LENGTH}. Received: ${length}`);
|
|
89
|
-
const random = getRandomFn(options?.random);
|
|
90
|
-
if (state.counter === void 0) state.counter = initializeCounter();
|
|
91
|
-
if (state.fingerprint === void 0) state.fingerprint = createFingerprint();
|
|
92
|
-
const firstLetter = randomLetter(random);
|
|
93
|
-
const time = Date.now().toString(36);
|
|
94
|
-
state.counter += 1;
|
|
95
|
-
const count = state.counter.toString(36);
|
|
96
|
-
return firstLetter + bigIntToBase36(bufToBigInt(hash(time + createEntropy(length, random) + count + state.fingerprint))).slice(1, length);
|
|
97
|
-
}
|
|
98
|
-
function isValid(id) {
|
|
99
|
-
return typeof id === "string" && id.length >= MIN_LENGTH && id.length <= MAX_LENGTH && CUID2_REGEX.test(id);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Generate a CUID2 string.
|
|
103
|
-
*
|
|
104
|
-
* CUID2 is a secure, collision-resistant identifier that hashes multiple
|
|
105
|
-
* entropy sources using SHA3-512. Unlike time-ordered IDs (ULID, UUID v7),
|
|
106
|
-
* CUID2 prevents enumeration attacks by making IDs non-predictable.
|
|
107
|
-
*
|
|
108
|
-
* Note: CUID2 does not provide toBytes/fromBytes because it is a string-native
|
|
109
|
-
* format with no canonical binary representation (unlike UUID's 16-byte format).
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```ts
|
|
113
|
-
* import { cuid2 } from 'uniku/cuid2'
|
|
114
|
-
*
|
|
115
|
-
* const id = cuid2()
|
|
116
|
-
* // => "pfh0haxfpzowht3oi213cqos"
|
|
117
|
-
*
|
|
118
|
-
* // Custom length
|
|
119
|
-
* const shortId = cuid2({ length: 10 })
|
|
120
|
-
* // => "tz4a98xxat"
|
|
121
|
-
*
|
|
122
|
-
* // Validation (type guard)
|
|
123
|
-
* const maybeId: unknown = getUserInput()
|
|
124
|
-
* if (cuid2.isValid(maybeId)) {
|
|
125
|
-
* console.log(maybeId.length) // TypeScript knows maybeId is string
|
|
126
|
-
* }
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
const cuid2 = Object.assign(cuid2Fn, { isValid });
|
|
130
|
-
|
|
131
|
-
//#endregion
|
|
132
|
-
export { cuid2 };
|
|
1
|
+
import{sha3_512 as e}from"@noble/hashes/sha3.js";function t(e){let t=4+e*128,n,r=!1;if(typeof SharedArrayBuffer<`u`)try{n=new SharedArrayBuffer(t),r=!0}catch{n=new ArrayBuffer(t)}else n=new ArrayBuffer(t);let i=new Int32Array(n,0,1),a=new Uint8Array(n,4);return crypto.getRandomValues(a),{buffer:n,bytes:a,offset:i,useAtomics:r}}function n(e,t){if(e.useAtomics){let r=Atomics.add(e.offset,0,t);return r+t<=e.bytes.length?r:Atomics.compareExchange(e.offset,0,r+t,t)===r+t?(crypto.getRandomValues(e.bytes),0):n(e,t)}else{let n=e.offset[0];return n+t>e.bytes.length&&(crypto.getRandomValues(e.bytes),n=0),e.offset[0]=n+t,n}}function r(e,t){let r=n(e,t);return e.bytes.subarray(r,r+t)}const i=/^[a-z][0-9a-z]+$/,a=`0123456789abcdefghijklmnopqrstuvwxyz`,o=new TextEncoder;let s;function c(){return s||=t(4),s}const l={counter:void 0,fingerprint:void 0};function u(){let e=r(c(),4);return((e[0]<<24|e[1]<<16|e[2]<<8|e[3])>>>0)%476782368}function d(e){let t=0n;for(let n of e)t=t*256n+BigInt(n);return t}function f(e){if(e===0n)return`0`;let t=[];for(;e>0n;)t.push(a[Number(e%36n)]),e/=36n;return t.reverse().join(``)}function p(e){return`abcdefghijklmnopqrstuvwxyz`[Math.floor(e()*26)]}function m(e,t){let n=Array(e);for(let r=0;r<e;r++)n[r]=a[Math.floor(t()*36)];return n.join(``)}function h(t){return e(o.encode(t))}function g(){let e=_;return f(d(h(Object.keys(globalThis).toString()+m(32,e)))).slice(1,33)}function _(){let e=c(),t=n(e,4),r=e.bytes;return(r[t]*16777216+r[t+1]*65536+r[t+2]*256+r[t+3])/4294967296}function v(e){if(e){if(e.length===0)throw Error(`Random byte array cannot be empty`);let t=0;return()=>{let n=e[t%e.length]/256;return t+=1,n}}return _}function y(e){let t=e?.length??24;if(t<2||t>32)throw RangeError(`CUID2 length must be between 2 and 32. Received: ${t}`);let n=v(e?.random);l.counter===void 0&&(l.counter=u()),l.fingerprint===void 0&&(l.fingerprint=g());let r=p(n),i=Date.now().toString(36);l.counter+=1;let a=l.counter.toString(36);return r+f(d(h(i+m(t,n)+a+l.fingerprint))).slice(1,t)}function b(e){return typeof e==`string`&&e.length>=2&&e.length<=32&&i.test(e)}const x=Object.assign(y,{isValid:b});export{x as cuid2};
|
|
133
2
|
//# sourceMappingURL=cuid2.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cuid2.mjs","names":["pool: RandomPool | undefined","state: { counter: number | undefined; fingerprint: string | undefined }","chars: string[]","cuid2: Cuid2"],"sources":["../../src/cuid2/cuid2.ts"],"sourcesContent":["import { sha3_512 } from '@noble/hashes/sha3.js'\nimport { createPool, getPooledBytes, getRandomBytes, type RandomPool } from '../common/random-pool'\n\nexport type Cuid2Options = {\n /**\n * Length of the generated ID (2-32 characters).\n * Default: 24\n */\n length?: number\n /**\n * Custom random bytes for deterministic testing.\n * Must be at least 1 byte. For adequate entropy, use at least 16 bytes.\n * Note: The fingerprint always uses cryptographically secure random bytes,\n * regardless of this option.\n */\n random?: Uint8Array\n}\n\nexport type Cuid2 = {\n (options?: Cuid2Options): string\n isValid(id: unknown): id is string\n}\n\nconst DEFAULT_LENGTH = 24\nconst MAX_LENGTH = 32\nconst MIN_LENGTH = 2\n\n// Maximum initial counter value from cuid2 spec - provides ~29 bits of\n// initial entropy to prevent cross-process collisions at startup\nconst INITIAL_COUNT_MAX = 476782367\n\n// Validation regex: first char must be a-z, rest can be a-z or 0-9\nconst CUID2_REGEX = /^[a-z][0-9a-z]+$/\n\n// Base36 alphabet\nconst ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'\nconst LETTER_ALPHABET = 'abcdefghijklmnopqrstuvwxyz'\n\n// Reusable TextEncoder instance (stateless, safe to share)\nconst textEncoder = new TextEncoder()\n\n// CUID2's own pool - lazily initialized on first use\n// Uses 4 bytes per random() call\nlet pool: RandomPool | undefined\n\nfunction ensurePool(): RandomPool {\n if (!pool) pool = createPool(4)\n return pool\n}\n\n/**\n * Module-level state for counter and fingerprint.\n * Counter is initialized lazily on first call to prevent unnecessary crypto operations.\n * Fingerprint is also generated lazily on first call.\n */\nconst state: { counter: number | undefined; fingerprint: string | undefined } = {\n counter: undefined,\n fingerprint: undefined,\n}\n\n/**\n * Initialize counter using crypto for consistency with other entropy sources.\n */\nfunction initializeCounter(): number {\n const buffer = getRandomBytes(ensurePool(), 4)\n return (((buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]) >>> 0) % (INITIAL_COUNT_MAX + 1)\n}\n\n// --- Base36 utilities ---\n\nfunction bufToBigInt(buf: Uint8Array): bigint {\n let value = 0n\n for (const byte of buf) {\n value = value * 256n + BigInt(byte)\n }\n return value\n}\n\nfunction bigIntToBase36(value: bigint): string {\n if (value === 0n) return '0'\n const chars: string[] = []\n while (value > 0n) {\n chars.push(ALPHABET[Number(value % 36n)])\n value = value / 36n\n }\n return chars.reverse().join('')\n}\n\nfunction randomLetter(random: () => number): string {\n return LETTER_ALPHABET[Math.floor(random() * 26)]\n}\n\nfunction createEntropy(length: number, random: () => number): string {\n const chars = new Array<string>(length)\n for (let i = 0; i < length; i++) {\n chars[i] = ALPHABET[Math.floor(random() * 36)]\n }\n return chars.join('')\n}\n\n// --- SHA3 hash wrapper ---\n\nfunction hash(input: string): Uint8Array {\n return sha3_512(textEncoder.encode(input))\n}\n\n// --- Fingerprint generation ---\n\nconst BIG_LENGTH = 32\n\nfunction createFingerprint(): string {\n // Always use CSPRNG for fingerprint to ensure security regardless of custom random option\n const random = getCryptoRandom\n const globals = Object.keys(globalThis).toString()\n const sourceString = globals + createEntropy(BIG_LENGTH, random)\n const hashed = hash(sourceString)\n return bigIntToBase36(bufToBigInt(hashed)).slice(1, BIG_LENGTH + 1)\n}\n\n// --- Random function factory ---\n\n/**\n * Get a random number in [0, 1) using CUID2's own random pool.\n * Thread-safe when SharedArrayBuffer is available.\n */\nfunction getCryptoRandom(): number {\n const p = ensurePool()\n const offset = getPooledBytes(p, 4)\n const bytes = p.bytes\n const value =\n (bytes[offset] * 0x1000000 + bytes[offset + 1] * 0x10000 + bytes[offset + 2] * 0x100 + bytes[offset + 3]) /\n 0x100000000\n return value\n}\n\nfunction getRandomFn(random?: Uint8Array): () => number {\n if (random) {\n if (random.length === 0) {\n throw new Error('Random byte array cannot be empty')\n }\n let index = 0\n return () => {\n const value = random[index % random.length] / 256\n index += 1\n return value\n }\n }\n return getCryptoRandom\n}\n\n// --- Main generator ---\n\nfunction cuid2Fn(options?: Cuid2Options): string {\n const length = options?.length ?? DEFAULT_LENGTH\n\n if (length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(`CUID2 length must be between ${MIN_LENGTH} and ${MAX_LENGTH}. Received: ${length}`)\n }\n\n const random = getRandomFn(options?.random)\n\n // Initialize counter lazily on first call\n if (state.counter === undefined) {\n state.counter = initializeCounter()\n }\n\n // Initialize fingerprint lazily on first call (always uses CSPRNG)\n if (state.fingerprint === undefined) {\n state.fingerprint = createFingerprint()\n }\n\n const firstLetter = randomLetter(random)\n const time = Date.now().toString(36)\n state.counter += 1\n const count = state.counter.toString(36)\n const salt = createEntropy(length, random)\n\n const hashInput = time + salt + count + state.fingerprint\n const hashed = hash(hashInput)\n const base36Hash = bigIntToBase36(bufToBigInt(hashed))\n\n // Drop first char of hash to avoid histogram bias, prepend random letter\n return firstLetter + base36Hash.slice(1, length)\n}\n\n// --- Validation (type guard) ---\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && id.length >= MIN_LENGTH && id.length <= MAX_LENGTH && CUID2_REGEX.test(id)\n}\n\n/**\n * Generate a CUID2 string.\n *\n * CUID2 is a secure, collision-resistant identifier that hashes multiple\n * entropy sources using SHA3-512. Unlike time-ordered IDs (ULID, UUID v7),\n * CUID2 prevents enumeration attacks by making IDs non-predictable.\n *\n * Note: CUID2 does not provide toBytes/fromBytes because it is a string-native\n * format with no canonical binary representation (unlike UUID's 16-byte format).\n *\n * @example\n * ```ts\n * import { cuid2 } from 'uniku/cuid2'\n *\n * const id = cuid2()\n * // => \"pfh0haxfpzowht3oi213cqos\"\n *\n * // Custom length\n * const shortId = cuid2({ length: 10 })\n * // => \"tz4a98xxat\"\n *\n * // Validation (type guard)\n * const maybeId: unknown = getUserInput()\n * if (cuid2.isValid(maybeId)) {\n * console.log(maybeId.length) // TypeScript knows maybeId is string\n * }\n * ```\n */\nexport const cuid2: Cuid2 = Object.assign(cuid2Fn, {\n isValid,\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cuid2.mjs","names":["buffer: SharedArrayBuffer | ArrayBuffer","pool","pool: RandomPool | undefined","state: { counter: number | undefined; fingerprint: string | undefined }","chars: string[]","cuid2: Cuid2"],"sources":["../../src/common/random-pool.ts","../../src/cuid2/cuid2.ts"],"sourcesContent":["/**\n * Thread-safe random byte pool utilities using Atomics (when SharedArrayBuffer is available).\n * Falls back to regular pooling in environments without SharedArrayBuffer.\n *\n * This module provides STATELESS utilities - each consumer creates and manages its own pool.\n * Pass the pool state to each function (C-style) for explicit, predictable behavior.\n *\n * @example\n * ```ts\n * // Each module creates its own pool\n * let pool: RandomPool | undefined\n *\n * function getBytes(count: number): number {\n * if (!pool) pool = createPool(count)\n * return getPooledBytes(pool, count)\n * }\n * ```\n */\n\nconst POOL_SIZE_MULTIPLIER = 128\nconst POOL_HEADER_SIZE = 4 // 4 bytes for Int32 offset counter\n\n/**\n * Pool state - passed to all pool functions.\n * Each module creates and owns its own pool instance.\n */\nexport type RandomPool = {\n buffer: SharedArrayBuffer | ArrayBuffer\n bytes: Uint8Array\n offset: Int32Array\n useAtomics: boolean\n}\n\n/**\n * Create a new random pool. Uses SharedArrayBuffer + Atomics when available\n * for thread-safety in Web Worker scenarios.\n *\n * @param minSize - Minimum bytes needed per request (pool = minSize * 128)\n */\nexport function createPool(minSize: number): RandomPool {\n const poolSize = minSize * POOL_SIZE_MULTIPLIER\n const totalSize = POOL_HEADER_SIZE + poolSize\n\n let buffer: SharedArrayBuffer | ArrayBuffer\n let useAtomics = false\n\n // Try SharedArrayBuffer first (thread-safe with Atomics)\n if (typeof SharedArrayBuffer !== 'undefined') {\n try {\n buffer = new SharedArrayBuffer(totalSize)\n useAtomics = true\n } catch {\n // SharedArrayBuffer may be disabled (security restrictions)\n buffer = new ArrayBuffer(totalSize)\n }\n } else {\n buffer = new ArrayBuffer(totalSize)\n }\n\n // First 4 bytes = atomic offset counter, rest = random bytes\n const offset = new Int32Array(buffer, 0, 1)\n const bytes = new Uint8Array(buffer, POOL_HEADER_SIZE)\n crypto.getRandomValues(bytes)\n\n return { buffer, bytes, offset, useAtomics }\n}\n\n/**\n * Get pooled random bytes. Thread-safe when SharedArrayBuffer is available.\n * Returns the starting index in the pool for the requested bytes.\n *\n * @param pool - The pool state (from createPool)\n * @param count - Number of bytes needed\n * @returns Starting offset in pool.bytes\n *\n * @example\n * ```ts\n * const offset = getPooledBytes(pool, 21)\n * for (let i = offset; i < offset + 21; i++) {\n * // Use pool.bytes[i]\n * }\n * ```\n */\nexport function getPooledBytes(pool: RandomPool, count: number): number {\n if (pool.useAtomics) {\n // Thread-safe path using Atomics\n const startOffset = Atomics.add(pool.offset, 0, count)\n\n if (startOffset + count <= pool.bytes.length) {\n return startOffset\n }\n\n // Pool exhausted - need to refill\n // Use compareExchange to ensure only one thread refills\n const resetResult = Atomics.compareExchange(pool.offset, 0, startOffset + count, count)\n if (resetResult === startOffset + count) {\n // We won the race - refill the pool\n crypto.getRandomValues(pool.bytes)\n return 0\n }\n\n // Another thread is refilling or already refilled - retry\n return getPooledBytes(pool, count)\n } else {\n // Single-threaded path\n let currentOffset = pool.offset[0]\n\n if (currentOffset + count > pool.bytes.length) {\n crypto.getRandomValues(pool.bytes)\n currentOffset = 0\n }\n\n pool.offset[0] = currentOffset + count\n return currentOffset\n }\n}\n\n/**\n * Get a Uint8Array slice of random bytes from the pool.\n *\n * Note: Returns a subarray VIEW into the pool. If you need the bytes to persist\n * beyond the current synchronous operation, use getRandomBytesCopy() instead.\n *\n * @param pool - The pool state\n * @param count - Number of bytes needed\n */\nexport function getRandomBytes(pool: RandomPool, count: number): Uint8Array {\n const offset = getPooledBytes(pool, count)\n return pool.bytes.subarray(offset, offset + count)\n}\n\n/**\n * Get a COPY of random bytes from the pool.\n * Use this when bytes need to persist (e.g., returned to caller who may use them later).\n *\n * @param pool - The pool state\n * @param count - Number of bytes needed\n */\nexport function getRandomBytesCopy(pool: RandomPool, count: number): Uint8Array {\n const offset = getPooledBytes(pool, count)\n return pool.bytes.slice(offset, offset + count)\n}\n\n/**\n * Fill the provided buffer with random bytes from the pool.\n *\n * @param pool - The pool state\n * @param out - Buffer to fill\n */\nexport function fillRandomBytes(pool: RandomPool, out: Uint8Array): void {\n const offset = getPooledBytes(pool, out.length)\n out.set(pool.bytes.subarray(offset, offset + out.length))\n}\n","import { sha3_512 } from '@noble/hashes/sha3.js'\nimport { createPool, getPooledBytes, getRandomBytes, type RandomPool } from '../common/random-pool'\n\nexport type Cuid2Options = {\n /**\n * Length of the generated ID (2-32 characters).\n * Default: 24\n */\n length?: number\n /**\n * Custom random bytes for deterministic testing.\n * Must be at least 1 byte. For adequate entropy, use at least 16 bytes.\n * Note: The fingerprint always uses cryptographically secure random bytes,\n * regardless of this option.\n */\n random?: Uint8Array\n}\n\nexport type Cuid2 = {\n (options?: Cuid2Options): string\n isValid(id: unknown): id is string\n}\n\nconst DEFAULT_LENGTH = 24\nconst MAX_LENGTH = 32\nconst MIN_LENGTH = 2\n\n// Maximum initial counter value from cuid2 spec - provides ~29 bits of\n// initial entropy to prevent cross-process collisions at startup\nconst INITIAL_COUNT_MAX = 476782367\n\n// Validation regex: first char must be a-z, rest can be a-z or 0-9\nconst CUID2_REGEX = /^[a-z][0-9a-z]+$/\n\n// Base36 alphabet\nconst ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'\nconst LETTER_ALPHABET = 'abcdefghijklmnopqrstuvwxyz'\n\n// Reusable TextEncoder instance (stateless, safe to share)\nconst textEncoder = new TextEncoder()\n\n// CUID2's own pool - lazily initialized on first use\n// Uses 4 bytes per random() call\nlet pool: RandomPool | undefined\n\nfunction ensurePool(): RandomPool {\n if (!pool) pool = createPool(4)\n return pool\n}\n\n/**\n * Module-level state for counter and fingerprint.\n * Counter is initialized lazily on first call to prevent unnecessary crypto operations.\n * Fingerprint is also generated lazily on first call.\n */\nconst state: { counter: number | undefined; fingerprint: string | undefined } = {\n counter: undefined,\n fingerprint: undefined,\n}\n\n/**\n * Initialize counter using crypto for consistency with other entropy sources.\n */\nfunction initializeCounter(): number {\n const buffer = getRandomBytes(ensurePool(), 4)\n return (((buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]) >>> 0) % (INITIAL_COUNT_MAX + 1)\n}\n\n// --- Base36 utilities ---\n\nfunction bufToBigInt(buf: Uint8Array): bigint {\n let value = 0n\n for (const byte of buf) {\n value = value * 256n + BigInt(byte)\n }\n return value\n}\n\nfunction bigIntToBase36(value: bigint): string {\n if (value === 0n) return '0'\n const chars: string[] = []\n while (value > 0n) {\n chars.push(ALPHABET[Number(value % 36n)])\n value = value / 36n\n }\n return chars.reverse().join('')\n}\n\nfunction randomLetter(random: () => number): string {\n return LETTER_ALPHABET[Math.floor(random() * 26)]\n}\n\nfunction createEntropy(length: number, random: () => number): string {\n const chars = new Array<string>(length)\n for (let i = 0; i < length; i++) {\n chars[i] = ALPHABET[Math.floor(random() * 36)]\n }\n return chars.join('')\n}\n\n// --- SHA3 hash wrapper ---\n\nfunction hash(input: string): Uint8Array {\n return sha3_512(textEncoder.encode(input))\n}\n\n// --- Fingerprint generation ---\n\nconst BIG_LENGTH = 32\n\nfunction createFingerprint(): string {\n // Always use CSPRNG for fingerprint to ensure security regardless of custom random option\n const random = getCryptoRandom\n const globals = Object.keys(globalThis).toString()\n const sourceString = globals + createEntropy(BIG_LENGTH, random)\n const hashed = hash(sourceString)\n return bigIntToBase36(bufToBigInt(hashed)).slice(1, BIG_LENGTH + 1)\n}\n\n// --- Random function factory ---\n\n/**\n * Get a random number in [0, 1) using CUID2's own random pool.\n * Thread-safe when SharedArrayBuffer is available.\n */\nfunction getCryptoRandom(): number {\n const p = ensurePool()\n const offset = getPooledBytes(p, 4)\n const bytes = p.bytes\n const value =\n (bytes[offset] * 0x1000000 + bytes[offset + 1] * 0x10000 + bytes[offset + 2] * 0x100 + bytes[offset + 3]) /\n 0x100000000\n return value\n}\n\nfunction getRandomFn(random?: Uint8Array): () => number {\n if (random) {\n if (random.length === 0) {\n throw new Error('Random byte array cannot be empty')\n }\n let index = 0\n return () => {\n const value = random[index % random.length] / 256\n index += 1\n return value\n }\n }\n return getCryptoRandom\n}\n\n// --- Main generator ---\n\nfunction cuid2Fn(options?: Cuid2Options): string {\n const length = options?.length ?? DEFAULT_LENGTH\n\n if (length < MIN_LENGTH || length > MAX_LENGTH) {\n throw new RangeError(`CUID2 length must be between ${MIN_LENGTH} and ${MAX_LENGTH}. Received: ${length}`)\n }\n\n const random = getRandomFn(options?.random)\n\n // Initialize counter lazily on first call\n if (state.counter === undefined) {\n state.counter = initializeCounter()\n }\n\n // Initialize fingerprint lazily on first call (always uses CSPRNG)\n if (state.fingerprint === undefined) {\n state.fingerprint = createFingerprint()\n }\n\n const firstLetter = randomLetter(random)\n const time = Date.now().toString(36)\n state.counter += 1\n const count = state.counter.toString(36)\n const salt = createEntropy(length, random)\n\n const hashInput = time + salt + count + state.fingerprint\n const hashed = hash(hashInput)\n const base36Hash = bigIntToBase36(bufToBigInt(hashed))\n\n // Drop first char of hash to avoid histogram bias, prepend random letter\n return firstLetter + base36Hash.slice(1, length)\n}\n\n// --- Validation (type guard) ---\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && id.length >= MIN_LENGTH && id.length <= MAX_LENGTH && CUID2_REGEX.test(id)\n}\n\n/**\n * Generate a CUID2 string.\n *\n * CUID2 is a secure, collision-resistant identifier that hashes multiple\n * entropy sources using SHA3-512. Unlike time-ordered IDs (ULID, UUID v7),\n * CUID2 prevents enumeration attacks by making IDs non-predictable.\n *\n * Note: CUID2 does not provide toBytes/fromBytes because it is a string-native\n * format with no canonical binary representation (unlike UUID's 16-byte format).\n *\n * @example\n * ```ts\n * import { cuid2 } from 'uniku/cuid2'\n *\n * const id = cuid2()\n * // => \"pfh0haxfpzowht3oi213cqos\"\n *\n * // Custom length\n * const shortId = cuid2({ length: 10 })\n * // => \"tz4a98xxat\"\n *\n * // Validation (type guard)\n * const maybeId: unknown = getUserInput()\n * if (cuid2.isValid(maybeId)) {\n * console.log(maybeId.length) // TypeScript knows maybeId is string\n * }\n * ```\n */\nexport const cuid2: Cuid2 = Object.assign(cuid2Fn, {\n isValid,\n})\n"],"mappings":"iDAuCA,SAAgB,EAAW,EAA6B,CAEtD,IAAM,EAAY,EADD,EAAU,IAGvBA,EACA,EAAa,GAGjB,GAAI,OAAO,kBAAsB,IAC/B,GAAI,CACF,EAAS,IAAI,kBAAkB,EAAU,CACzC,EAAa,QACP,CAEN,EAAS,IAAI,YAAY,EAAU,MAGrC,EAAS,IAAI,YAAY,EAAU,CAIrC,IAAM,EAAS,IAAI,WAAW,EAAQ,EAAG,EAAE,CACrC,EAAQ,IAAI,WAAW,EAAQ,EAAiB,CAGtD,OAFA,OAAO,gBAAgB,EAAM,CAEtB,CAAE,SAAQ,QAAO,SAAQ,aAAY,CAmB9C,SAAgB,EAAe,EAAkB,EAAuB,CACtE,GAAIC,EAAK,WAAY,CAEnB,IAAM,EAAc,QAAQ,IAAIA,EAAK,OAAQ,EAAG,EAAM,CAgBtD,OAdI,EAAc,GAASA,EAAK,MAAM,OAC7B,EAKW,QAAQ,gBAAgBA,EAAK,OAAQ,EAAG,EAAc,EAAO,EAAM,GACnE,EAAc,GAEhC,OAAO,gBAAgBA,EAAK,MAAM,CAC3B,GAIF,EAAeA,EAAM,EAAM,KAC7B,CAEL,IAAI,EAAgBA,EAAK,OAAO,GAQhC,OANI,EAAgB,EAAQA,EAAK,MAAM,SACrC,OAAO,gBAAgBA,EAAK,MAAM,CAClC,EAAgB,GAGlB,EAAK,OAAO,GAAK,EAAgB,EAC1B,GAaX,SAAgB,EAAe,EAAkB,EAA2B,CAC1E,IAAM,EAAS,EAAeA,EAAM,EAAM,CAC1C,OAAOA,EAAK,MAAM,SAAS,EAAQ,EAAS,EAAM,CCzGpD,MASM,EAAc,mBAGd,EAAW,uCAIX,EAAc,IAAI,YAIxB,IAAIC,EAEJ,SAAS,GAAyB,CAEhC,MADA,CAAW,IAAO,EAAW,EAAE,CACxB,EAQT,MAAMC,EAA0E,CAC9E,QAAS,IAAA,GACT,YAAa,IAAA,GACd,CAKD,SAAS,GAA4B,CACnC,IAAM,EAAS,EAAe,GAAY,CAAE,EAAE,CAC9C,QAAU,EAAO,IAAM,GAAO,EAAO,IAAM,GAAO,EAAO,IAAM,EAAK,EAAO,MAAQ,GAAM,UAK3F,SAAS,EAAY,EAAyB,CAC5C,IAAI,EAAQ,GACZ,IAAK,IAAM,KAAQ,EACjB,EAAQ,EAAQ,KAAO,OAAO,EAAK,CAErC,OAAO,EAGT,SAAS,EAAe,EAAuB,CAC7C,GAAI,IAAU,GAAI,MAAO,IACzB,IAAMC,EAAkB,EAAE,CAC1B,KAAO,EAAQ,IACb,EAAM,KAAK,EAAS,OAAO,EAAQ,IAAI,EAAE,CACzC,GAAgB,IAElB,OAAO,EAAM,SAAS,CAAC,KAAK,GAAG,CAGjC,SAAS,EAAa,EAA8B,CAClD,MAAO,6BAAgB,KAAK,MAAM,GAAQ,CAAG,GAAG,EAGlD,SAAS,EAAc,EAAgB,EAA8B,CACnE,IAAM,EAAY,MAAc,EAAO,CACvC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,IAC1B,EAAM,GAAK,EAAS,KAAK,MAAM,GAAQ,CAAG,GAAG,EAE/C,OAAO,EAAM,KAAK,GAAG,CAKvB,SAAS,EAAK,EAA2B,CACvC,OAAO,EAAS,EAAY,OAAO,EAAM,CAAC,CAO5C,SAAS,GAA4B,CAEnC,IAAM,EAAS,EAIf,OAAO,EAAe,EADP,EAFC,OAAO,KAAK,WAAW,CAAC,UAAU,CACnB,EAAc,GAAY,EAAO,CAC/B,CACQ,CAAC,CAAC,MAAM,EAAG,GAAe,CASrE,SAAS,GAA0B,CACjC,IAAM,EAAI,GAAY,CAChB,EAAS,EAAe,EAAG,EAAE,CAC7B,EAAQ,EAAE,MAIhB,OAFG,EAAM,GAAU,SAAY,EAAM,EAAS,GAAK,MAAU,EAAM,EAAS,GAAK,IAAQ,EAAM,EAAS,IACtG,WAIJ,SAAS,EAAY,EAAmC,CACtD,GAAI,EAAQ,CACV,GAAI,EAAO,SAAW,EACpB,MAAU,MAAM,oCAAoC,CAEtD,IAAI,EAAQ,EACZ,UAAa,CACX,IAAM,EAAQ,EAAO,EAAQ,EAAO,QAAU,IAE9C,MADA,IAAS,EACF,GAGX,OAAO,EAKT,SAAS,EAAQ,EAAgC,CAC/C,IAAM,EAAS,GAAS,QAAU,GAElC,GAAI,EAAS,GAAc,EAAS,GAClC,MAAU,WAAW,oDAA2E,IAAS,CAG3G,IAAM,EAAS,EAAY,GAAS,OAAO,CAGvC,EAAM,UAAY,IAAA,KACpB,EAAM,QAAU,GAAmB,EAIjC,EAAM,cAAgB,IAAA,KACxB,EAAM,YAAc,GAAmB,EAGzC,IAAM,EAAc,EAAa,EAAO,CAClC,EAAO,KAAK,KAAK,CAAC,SAAS,GAAG,CACpC,EAAM,SAAW,EACjB,IAAM,EAAQ,EAAM,QAAQ,SAAS,GAAG,CAQxC,OAAO,EAHY,EAAe,EADnB,EADG,EAFL,EAAc,EAAQ,EAAO,CAEV,EAAQ,EAAM,YAChB,CACuB,CAAC,CAGtB,MAAM,EAAG,EAAO,CAKlD,SAAS,EAAQ,EAA2B,CAC1C,OAAO,OAAO,GAAO,UAAY,EAAG,QAAU,GAAc,EAAG,QAAU,IAAc,EAAY,KAAK,EAAG,CA+B7G,MAAaC,EAAe,OAAO,OAAO,EAAS,CACjD,UACD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
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: string): boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Generate a KSUID string or write the bytes into a buffer.
|
|
25
|
+
* Also includes helpers to convert to and from byte arrays.
|
|
26
|
+
*/
|
|
27
|
+
declare const ksuid: Ksuid;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { Ksuid, KsuidOptions, ksuid };
|
|
30
|
+
//# 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":";KAwBY,YAAA;EAAZ;AAaA;;EAE6B,MAAA,CAAA,EAXlB,UAWkB;EAAqB;;;;;EAG/B,IAAA,CAAA,EAAA,MAAA;CAAA;AAqKNA,KA1KD,KAAA,GA0KQ;;gBAxKJ,aAAa,qBAAqB,+BAA+B,wBAAwB;aAC5F;uBACU;mBACJ;;;;;;;;cAqKNA,OAAO"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"../random-CPlgK_62.mjs";const t=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,n=62n,r=new Uint8Array(128);r.fill(255);for(let e=0;e<62;e+=1)r[t.charCodeAt(e)]=e;function i(e){if(e.length<20)throw Error(`KSUID bytes must be at least 20 bytes, got ${e.length}`);let r=0n;for(let t=0;t<20;t+=1)r=r<<8n|BigInt(e[t]);let i=Array(27);for(let e=26;e>=0;--e)i[e]=t[Number(r%n)],r/=n;return i.join(``)}function a(e){if(e.length!==27)throw Error(`KSUID string must be 27 characters, got ${e.length}`);let t=0n;for(let i=0;i<27;i+=1){let a=e.charCodeAt(i);if(a>=128)throw Error(`Invalid KSUID character: ${e[i]}`);let o=r[a];if(o===255)throw Error(`Invalid KSUID character: ${e[i]}`);t=t*n+BigInt(o)}let i=new Uint8Array(20);for(let e=19;e>=0;--e)i[e]=Number(t&255n),t>>=8n;return i}const o=14e8,s=/^[0-9A-Za-z]{27}$/,c={secs:-1/0,lastPayload:new Uint8Array(16)};function l(e){let t=new Uint8Array(e);for(let e=t.length-1;e>=0;--e){if(t[e]<255)return t[e]+=1,t;t[e]=0}return t}function u(e,t,n,r=0){if(!n)n=new Uint8Array(20),r=0;else if(r<0||r+20>n.length)throw RangeError(`KSUID byte range ${r}:${r+20-1} is out of buffer bounds`);n[r]=e>>>24&255,n[r+1]=e>>>16&255,n[r+2]=e>>>8&255,n[r+3]=e&255;for(let e=0;e<16;e+=1)n[r+4+e]=t[e];return n}function d(t,n,r=0){let a,s,d=Date.now(),f=Math.floor(d/1e3)-o;return t?(a=t.secs===void 0?f:t.secs-o,s=t.random?t.random:e()):(a=f,a>c.secs?(s=e(),c.secs=a,c.lastPayload.set(s.subarray(0,16))):(s=l(c.lastPayload),c.lastPayload=s)),n?(u(a,s,n,r),n):i(u(a,s))}function f(e){return a(e)}function p(e){return i(e)}function m(e){let t=a(e);return(((t[0]<<24|t[1]<<16|t[2]<<8|t[3])>>>0)+o)*1e3}function h(e){return typeof e==`string`&&e.length===27&&s.test(e)}const g=Object.assign(d,{toBytes:f,fromBytes:p,timestamp:m,isValid:h});export{g 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/common/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 const result = new Array<string>(KSUID_STRING_LEN)\n\n for (let i = KSUID_STRING_LEN - 1; i >= 0; i -= 1) {\n result[i] = ALPHABET[Number(num % BASE)]\n num = num / BASE\n }\n\n return result.join('')\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 { rng } from '../uuid/common/random'\nimport { decodeBase62, encodeBase62 } from './common/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 (case-insensitive)\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: string): boolean\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 `msecs` 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 * Increment a 16-byte array by 1, propagating carry from LSB to MSB.\n * Returns a new array (does not modify input).\n */\nfunction incrementPayload(bytes: Uint8Array): Uint8Array {\n const result = new Uint8Array(bytes)\n for (let i = result.length - 1; i >= 0; i -= 1) {\n if (result[i] < 255) {\n result[i] += 1\n return result\n }\n result[i] = 0\n }\n return result\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 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 // 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 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 payload = incrementPayload(state.lastPayload)\n state.lastPayload = payload\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 * Case-insensitive: accepts uppercase, lowercase, or mixed case.\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.\n * Case-insensitive: accepts uppercase, lowercase, or mixed case.\n */\nfunction isValid(id: string): boolean {\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})\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nanoid.d.mts","names":["nanoid: Nanoid"],"sources":["../../src/nanoid/nanoid.ts"],"sourcesContent":[],"mappings":";;
|
|
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"}
|