uniku 0.0.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ import { InvalidInputError, ParseError } from '../errors'
2
+ import type { UuidV7Options } from '../uuid/v7'
3
+ import { uuidv7 } from '../uuid/v7'
4
+
5
+ export type TypeidOptions = UuidV7Options
6
+
7
+ export type Typeid = {
8
+ (prefix: string, options?: TypeidOptions): string
9
+ toBytes(id: string): Uint8Array
10
+ fromBytes(prefix: string, bytes: Uint8Array): string
11
+ toUuid(id: string): string
12
+ fromUuid(prefix: string, uuid: string): string
13
+ timestamp(id: string): number
14
+ prefix(id: string): string
15
+ suffix(id: string): string
16
+ isValid(id: unknown): id is string
17
+ }
18
+
19
+ const TYPEID_SUFFIX_LENGTH = 26
20
+ const TYPEID_UUID_BYTE_LENGTH = 16
21
+ const TYPEID_ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz'
22
+
23
+ const BASE32_DECODE: number[] = Array.from({ length: 128 }, () => -1)
24
+ for (let i = 0; i < TYPEID_ALPHABET.length; i += 1) {
25
+ BASE32_DECODE[TYPEID_ALPHABET.charCodeAt(i)] = i
26
+ }
27
+
28
+ type ParsedTypeid = {
29
+ prefix: string
30
+ suffix: string
31
+ bytes: Uint8Array
32
+ }
33
+
34
+ function assertValidPrefix(prefix: string): void {
35
+ if (prefix.length === 0) {
36
+ return
37
+ }
38
+
39
+ if (prefix.length > 63) {
40
+ throw new InvalidInputError('TYPEID_PREFIX_TOO_LONG', 'TypeID prefix must be at most 63 characters')
41
+ }
42
+
43
+ for (let i = 0; i < prefix.length; i += 1) {
44
+ const code = prefix.charCodeAt(i)
45
+ const isLowercaseLetter = code >= 97 && code <= 122
46
+ const isUnderscore = code === 95
47
+
48
+ if (!isLowercaseLetter && !isUnderscore) {
49
+ throw new InvalidInputError(
50
+ 'TYPEID_PREFIX_INVALID_CHARACTER',
51
+ 'TypeID prefix must contain only lowercase ASCII letters and underscores',
52
+ )
53
+ }
54
+
55
+ if ((i === 0 || i === prefix.length - 1) && !isLowercaseLetter) {
56
+ throw new InvalidInputError('TYPEID_PREFIX_INVALID_BOUNDARY', 'TypeID prefix must start and end with a-z')
57
+ }
58
+ }
59
+ }
60
+
61
+ function decodeSuffixValue(suffix: string, index: number): number {
62
+ const code = suffix.charCodeAt(index)
63
+ const value = code < BASE32_DECODE.length ? BASE32_DECODE[code] : -1
64
+
65
+ if (value === -1) {
66
+ throw new ParseError(
67
+ 'TYPEID_SUFFIX_INVALID_CHARACTER',
68
+ `TypeID suffix contains invalid character at position ${index}`,
69
+ )
70
+ }
71
+
72
+ return value
73
+ }
74
+
75
+ function assertValidSuffixShape(suffix: string): void {
76
+ if (suffix.length !== TYPEID_SUFFIX_LENGTH) {
77
+ throw new ParseError('TYPEID_SUFFIX_INVALID_LENGTH', `TypeID suffix must be 26 characters, got ${suffix.length}`)
78
+ }
79
+
80
+ if (suffix[0] > '7') {
81
+ throw new ParseError('TYPEID_SUFFIX_OVERFLOW', 'TypeID suffix must encode a 128-bit value')
82
+ }
83
+ }
84
+
85
+ function encodeBytesToSuffix(bytes: Uint8Array): string {
86
+ if (bytes.length !== TYPEID_UUID_BYTE_LENGTH) {
87
+ throw new InvalidInputError('TYPEID_UUID_BYTES_INVALID_LENGTH', `UUID bytes must be 16 bytes, got ${bytes.length}`)
88
+ }
89
+
90
+ return (
91
+ TYPEID_ALPHABET[(bytes[0] & 0xe0) >> 5] +
92
+ TYPEID_ALPHABET[bytes[0] & 0x1f] +
93
+ TYPEID_ALPHABET[(bytes[1] & 0xf8) >> 3] +
94
+ TYPEID_ALPHABET[((bytes[1] & 0x07) << 2) | ((bytes[2] & 0xc0) >> 6)] +
95
+ TYPEID_ALPHABET[(bytes[2] & 0x3e) >> 1] +
96
+ TYPEID_ALPHABET[((bytes[2] & 0x01) << 4) | ((bytes[3] & 0xf0) >> 4)] +
97
+ TYPEID_ALPHABET[((bytes[3] & 0x0f) << 1) | ((bytes[4] & 0x80) >> 7)] +
98
+ TYPEID_ALPHABET[(bytes[4] & 0x7c) >> 2] +
99
+ TYPEID_ALPHABET[((bytes[4] & 0x03) << 3) | ((bytes[5] & 0xe0) >> 5)] +
100
+ TYPEID_ALPHABET[bytes[5] & 0x1f] +
101
+ TYPEID_ALPHABET[(bytes[6] & 0xf8) >> 3] +
102
+ TYPEID_ALPHABET[((bytes[6] & 0x07) << 2) | ((bytes[7] & 0xc0) >> 6)] +
103
+ TYPEID_ALPHABET[(bytes[7] & 0x3e) >> 1] +
104
+ TYPEID_ALPHABET[((bytes[7] & 0x01) << 4) | ((bytes[8] & 0xf0) >> 4)] +
105
+ TYPEID_ALPHABET[((bytes[8] & 0x0f) << 1) | ((bytes[9] & 0x80) >> 7)] +
106
+ TYPEID_ALPHABET[(bytes[9] & 0x7c) >> 2] +
107
+ TYPEID_ALPHABET[((bytes[9] & 0x03) << 3) | ((bytes[10] & 0xe0) >> 5)] +
108
+ TYPEID_ALPHABET[bytes[10] & 0x1f] +
109
+ TYPEID_ALPHABET[(bytes[11] & 0xf8) >> 3] +
110
+ TYPEID_ALPHABET[((bytes[11] & 0x07) << 2) | ((bytes[12] & 0xc0) >> 6)] +
111
+ TYPEID_ALPHABET[(bytes[12] & 0x3e) >> 1] +
112
+ TYPEID_ALPHABET[((bytes[12] & 0x01) << 4) | ((bytes[13] & 0xf0) >> 4)] +
113
+ TYPEID_ALPHABET[((bytes[13] & 0x0f) << 1) | ((bytes[14] & 0x80) >> 7)] +
114
+ TYPEID_ALPHABET[(bytes[14] & 0x7c) >> 2] +
115
+ TYPEID_ALPHABET[((bytes[14] & 0x03) << 3) | ((bytes[15] & 0xe0) >> 5)] +
116
+ TYPEID_ALPHABET[bytes[15] & 0x1f]
117
+ )
118
+ }
119
+
120
+ function decodeSuffixToBytes(suffix: string): Uint8Array {
121
+ assertValidSuffixShape(suffix)
122
+
123
+ const values = new Array<number>(TYPEID_SUFFIX_LENGTH)
124
+ for (let i = 0; i < TYPEID_SUFFIX_LENGTH; i += 1) {
125
+ values[i] = decodeSuffixValue(suffix, i)
126
+ }
127
+
128
+ const bytes = new Uint8Array(TYPEID_UUID_BYTE_LENGTH)
129
+ bytes[0] = (values[0] << 5) | values[1]
130
+ bytes[1] = (values[2] << 3) | (values[3] >> 2)
131
+ bytes[2] = ((values[3] & 0x03) << 6) | (values[4] << 1) | (values[5] >> 4)
132
+ bytes[3] = ((values[5] & 0x0f) << 4) | (values[6] >> 1)
133
+ bytes[4] = ((values[6] & 0x01) << 7) | (values[7] << 2) | (values[8] >> 3)
134
+ bytes[5] = ((values[8] & 0x07) << 5) | values[9]
135
+ bytes[6] = (values[10] << 3) | (values[11] >> 2)
136
+ bytes[7] = ((values[11] & 0x03) << 6) | (values[12] << 1) | (values[13] >> 4)
137
+ bytes[8] = ((values[13] & 0x0f) << 4) | (values[14] >> 1)
138
+ bytes[9] = ((values[14] & 0x01) << 7) | (values[15] << 2) | (values[16] >> 3)
139
+ bytes[10] = ((values[16] & 0x07) << 5) | values[17]
140
+ bytes[11] = (values[18] << 3) | (values[19] >> 2)
141
+ bytes[12] = ((values[19] & 0x03) << 6) | (values[20] << 1) | (values[21] >> 4)
142
+ bytes[13] = ((values[21] & 0x0f) << 4) | (values[22] >> 1)
143
+ bytes[14] = ((values[22] & 0x01) << 7) | (values[23] << 2) | (values[24] >> 3)
144
+ bytes[15] = ((values[24] & 0x07) << 5) | values[25]
145
+
146
+ return bytes
147
+ }
148
+
149
+ function assertUuidV7Bytes(bytes: Uint8Array): void {
150
+ if (bytes.length !== TYPEID_UUID_BYTE_LENGTH) {
151
+ throw new InvalidInputError('TYPEID_UUID_BYTES_INVALID_LENGTH', `UUID bytes must be 16 bytes, got ${bytes.length}`)
152
+ }
153
+
154
+ if (bytes[6] >> 4 !== 7 || (bytes[8] & 0xc0) !== 0x80) {
155
+ throw new InvalidInputError('TYPEID_UUID_NOT_V7', 'TypeID UUID bytes must encode a UUID v7 value')
156
+ }
157
+ }
158
+
159
+ function uuidV7BytesFromSuffix(suffix: string): Uint8Array {
160
+ const bytes = decodeSuffixToBytes(suffix)
161
+ assertUuidV7Bytes(bytes)
162
+ return bytes
163
+ }
164
+
165
+ function uuidV7BytesFromUuid(uuid: string): Uint8Array {
166
+ if (!uuidv7.isValid(uuid)) {
167
+ throw new InvalidInputError('TYPEID_UUID_NOT_V7', 'TypeID can only wrap UUID v7 values')
168
+ }
169
+
170
+ return uuidv7.toBytes(uuid)
171
+ }
172
+
173
+ function parseTypeid(id: string): ParsedTypeid {
174
+ const separatorIndex = id.lastIndexOf('_')
175
+
176
+ if (separatorIndex === 0) {
177
+ throw new ParseError('TYPEID_INVALID_FORMAT', 'TypeID must not start with "_"')
178
+ }
179
+
180
+ const prefix = separatorIndex === -1 ? '' : id.slice(0, separatorIndex)
181
+ const suffix = separatorIndex === -1 ? id : id.slice(separatorIndex + 1)
182
+
183
+ assertValidPrefix(prefix)
184
+ const bytes = uuidV7BytesFromSuffix(suffix)
185
+
186
+ return { prefix, suffix, bytes }
187
+ }
188
+
189
+ function joinPrefixAndSuffix(prefix: string, suffix: string): string {
190
+ return prefix === '' ? suffix : `${prefix}_${suffix}`
191
+ }
192
+
193
+ function formatTypeidFromUuidV7Bytes(prefix: string, bytes: Uint8Array): string {
194
+ assertValidPrefix(prefix)
195
+ assertUuidV7Bytes(bytes)
196
+ return joinPrefixAndSuffix(prefix, encodeBytesToSuffix(bytes))
197
+ }
198
+
199
+ function timestampFromUuidV7Bytes(bytes: Uint8Array): number {
200
+ let msecs = 0
201
+ for (let i = 0; i < 6; i += 1) {
202
+ msecs = msecs * 256 + bytes[i]
203
+ }
204
+ return msecs
205
+ }
206
+
207
+ function typeidFn(prefix: string, options?: TypeidOptions): string {
208
+ const bytes = uuidv7(options, new Uint8Array(TYPEID_UUID_BYTE_LENGTH))
209
+ return formatTypeidFromUuidV7Bytes(prefix, bytes)
210
+ }
211
+
212
+ function typeidToBytes(id: string): Uint8Array {
213
+ return parseTypeid(id).bytes
214
+ }
215
+
216
+ function typeidFromBytes(prefix: string, bytes: Uint8Array): string {
217
+ return formatTypeidFromUuidV7Bytes(prefix, bytes)
218
+ }
219
+
220
+ function typeidToUuid(id: string): string {
221
+ return uuidv7.fromBytes(typeidToBytes(id))
222
+ }
223
+
224
+ function typeidFromUuid(prefix: string, uuid: string): string {
225
+ return formatTypeidFromUuidV7Bytes(prefix, uuidV7BytesFromUuid(uuid))
226
+ }
227
+
228
+ function timestamp(id: string): number {
229
+ return timestampFromUuidV7Bytes(parseTypeid(id).bytes)
230
+ }
231
+
232
+ function prefix(id: string): string {
233
+ return parseTypeid(id).prefix
234
+ }
235
+
236
+ function suffix(id: string): string {
237
+ return parseTypeid(id).suffix
238
+ }
239
+
240
+ function isValid(id: unknown): id is string {
241
+ if (typeof id !== 'string') {
242
+ return false
243
+ }
244
+
245
+ try {
246
+ parseTypeid(id)
247
+ } catch {
248
+ return false
249
+ }
250
+
251
+ return true
252
+ }
253
+
254
+ /**
255
+ * Generate a TypeID string backed by UUID v7.
256
+ *
257
+ * TypeID combines a lowercase snake_case type prefix with a 26-character
258
+ * modified base32 encoding of a UUID v7, producing sortable domain identifiers
259
+ * such as `user_01h2xcejqtf2nbrexx3vqjhp41`.
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * import { typeid } from 'uniku/typeid'
264
+ *
265
+ * const id = typeid('user')
266
+ * // => "user_01h2xcejqtf2nbrexx3vqjhp41"
267
+ *
268
+ * const uuid = typeid.toUuid(id)
269
+ * const restored = typeid.fromUuid('user', uuid)
270
+ * ```
271
+ */
272
+ export const typeid: Typeid = Object.assign(typeidFn, {
273
+ toBytes: typeidToBytes,
274
+ fromBytes: typeidFromBytes,
275
+ toUuid: typeidToUuid,
276
+ fromUuid: typeidFromUuid,
277
+ timestamp,
278
+ prefix,
279
+ suffix,
280
+ isValid,
281
+ })
282
+
283
+ export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
@@ -1,2 +0,0 @@
1
- const e=globalThis.crypto.getRandomValues.bind(globalThis.crypto),t=new Uint8Array(256);let n=256;function r(){e(t),n=0}function i(){n>252&&r();let e=t[n]*16777216+t[n+1]*65536+t[n+2]*256+t[n+3]>>>0;return n+=4,e}function a(){n>240&&r();let e=n;return n+=16,t.subarray(e,n)}export{a as n,i as t};
2
- //# sourceMappingURL=random-CWCVvNLb.mjs.map