uniku 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -6
- package/build/cuid2/cuid2.d.mts +1 -1
- package/build/cuid2/cuid2.d.mts.map +1 -1
- package/build/cuid2/cuid2.mjs +1 -1
- package/build/cuid2/cuid2.mjs.map +1 -1
- package/build/errors-Da-8dh4J.d.mts +67 -0
- package/build/errors-Da-8dh4J.d.mts.map +1 -0
- package/build/errors.d.mts +2 -2
- package/build/errors.mjs +1 -1
- package/build/errors.mjs.map +1 -1
- package/build/generators-BfJLt7Qf.d.mts +17 -0
- package/build/generators-BfJLt7Qf.d.mts.map +1 -0
- package/build/generators.d.mts +2 -17
- package/build/ksuid/ksuid.d.mts +10 -4
- package/build/ksuid/ksuid.d.mts.map +1 -1
- package/build/ksuid/ksuid.mjs +1 -1
- package/build/ksuid/ksuid.mjs.map +1 -1
- package/build/nanoid/nanoid.d.mts +13 -7
- package/build/nanoid/nanoid.d.mts.map +1 -1
- package/build/nanoid/nanoid.mjs +1 -1
- package/build/nanoid/nanoid.mjs.map +1 -1
- package/build/objectid/objectid.d.mts +10 -3
- package/build/objectid/objectid.d.mts.map +1 -1
- package/build/objectid/objectid.mjs +1 -1
- package/build/objectid/objectid.mjs.map +1 -1
- package/build/timestamp-ChrSuQCR.mjs +2 -0
- package/build/timestamp-ChrSuQCR.mjs.map +1 -0
- package/build/tsid/tsid.d.mts +1 -1
- package/build/tsid/tsid.d.mts.map +1 -1
- package/build/tsid/tsid.mjs +1 -1
- package/build/tsid/tsid.mjs.map +1 -1
- package/build/typeid/typeid.d.mts +4 -2
- package/build/typeid/typeid.d.mts.map +1 -1
- package/build/typeid/typeid.mjs +1 -1
- package/build/typeid/typeid.mjs.map +1 -1
- package/build/ulid/ulid.d.mts +1 -1
- package/build/ulid/ulid.d.mts.map +1 -1
- package/build/ulid/ulid.mjs +1 -1
- package/build/ulid/ulid.mjs.map +1 -1
- package/build/uuid/v4.d.mts +1 -1
- package/build/uuid/v4.d.mts.map +1 -1
- package/build/uuid/v4.mjs +1 -1
- package/build/uuid/v4.mjs.map +1 -1
- package/build/uuid/v7.d.mts +9 -3
- package/build/uuid/v7.d.mts.map +1 -1
- package/build/uuid/v7.mjs +1 -1
- package/build/uuid/v7.mjs.map +1 -1
- package/build/uuid-BPebYihz.mjs +2 -0
- package/build/uuid-BPebYihz.mjs.map +1 -0
- package/build/xid/xid.d.mts +12 -2
- package/build/xid/xid.d.mts.map +1 -1
- package/build/xid/xid.mjs +1 -1
- package/build/xid/xid.mjs.map +1 -1
- package/package.json +1 -1
- package/src/common/timestamp.ts +70 -0
- package/src/cuid2/cuid2.ts +5 -2
- package/src/errors.ts +75 -10
- package/src/ksuid/base62.ts +7 -7
- package/src/ksuid/ksuid.ts +19 -15
- package/src/nanoid/nanoid.ts +41 -18
- package/src/objectid/hex.ts +6 -4
- package/src/objectid/objectid.ts +24 -11
- package/src/tsid/crockford64.ts +7 -6
- package/src/tsid/tsid.ts +20 -13
- package/src/typeid/typeid.ts +106 -16
- package/src/ulid/crockford.ts +8 -6
- package/src/ulid/ulid.ts +11 -7
- package/src/uuid/common/uuid.ts +12 -4
- package/src/uuid/v4.ts +5 -2
- package/src/uuid/v7.ts +26 -10
- package/src/xid/base32hex.ts +8 -7
- package/src/xid/xid.ts +25 -12
- package/build/errors-Dgoyi-ci.d.mts +0 -38
- package/build/errors-Dgoyi-ci.d.mts.map +0 -1
- package/build/generators.d.mts.map +0 -1
- package/build/uuid-CnQIYoQi.mjs +0 -2
- package/build/uuid-CnQIYoQi.mjs.map +0 -1
package/src/nanoid/nanoid.ts
CHANGED
|
@@ -41,8 +41,8 @@ function defaultAlphabetFromPool(size: number): string {
|
|
|
41
41
|
export type NanoidOptions = {
|
|
42
42
|
/**
|
|
43
43
|
* Random bytes for deterministic output (testing).
|
|
44
|
-
* For power-of-2 alphabets (2, 4, 8, 16, 32, 64, 128, 256): exactly `
|
|
45
|
-
* For other alphabets: ~
|
|
44
|
+
* For power-of-2 alphabets (2, 4, 8, 16, 32, 64, 128, 256): exactly `length` bytes needed.
|
|
45
|
+
* For other alphabets: ~length * 2 bytes needed (rejection sampling).
|
|
46
46
|
*/
|
|
47
47
|
random?: Uint8Array
|
|
48
48
|
/**
|
|
@@ -53,6 +53,13 @@ export type NanoidOptions = {
|
|
|
53
53
|
/**
|
|
54
54
|
* Length of generated ID. Default: 21. Maximum: 2048.
|
|
55
55
|
*/
|
|
56
|
+
length?: number
|
|
57
|
+
/**
|
|
58
|
+
* Deprecated alias for `length`.
|
|
59
|
+
*
|
|
60
|
+
* @deprecated Use `length` instead. Will be removed in `1.0.0-rc.1`.
|
|
61
|
+
*/
|
|
62
|
+
// TODO(v1-rc.1): remove this alias (tracked in docs/STABILITY.md).
|
|
56
63
|
size?: number
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -75,36 +82,45 @@ export type Nanoid = {
|
|
|
75
82
|
*/
|
|
76
83
|
function validateAlphabet(alphabet: string): void {
|
|
77
84
|
if (alphabet.length < 2) {
|
|
78
|
-
throw new InvalidInputError('
|
|
85
|
+
throw new InvalidInputError('ALPHABET_OUT_OF_RANGE', 'Alphabet must contain at least 2 characters', {
|
|
86
|
+
strategy: 'nanoid',
|
|
87
|
+
})
|
|
79
88
|
}
|
|
80
89
|
if (alphabet.length > 256) {
|
|
81
|
-
throw new InvalidInputError('
|
|
90
|
+
throw new InvalidInputError('ALPHABET_OUT_OF_RANGE', 'Alphabet must not exceed 256 characters', {
|
|
91
|
+
strategy: 'nanoid',
|
|
92
|
+
})
|
|
82
93
|
}
|
|
83
94
|
const seen = new Set<string>()
|
|
84
95
|
for (const char of alphabet) {
|
|
85
96
|
const code = char.charCodeAt(0)
|
|
86
97
|
if (code < 32 || code > 126) {
|
|
87
98
|
throw new InvalidInputError(
|
|
88
|
-
'
|
|
99
|
+
'ALPHABET_INVALID_CHAR',
|
|
89
100
|
'Alphabet must contain only printable ASCII characters (32-126)',
|
|
101
|
+
{
|
|
102
|
+
strategy: 'nanoid',
|
|
103
|
+
},
|
|
90
104
|
)
|
|
91
105
|
}
|
|
92
106
|
if (seen.has(char)) {
|
|
93
|
-
throw new InvalidInputError('
|
|
107
|
+
throw new InvalidInputError('ALPHABET_DUPLICATE', `Duplicate character in alphabet: "${char}"`, {
|
|
108
|
+
strategy: 'nanoid',
|
|
109
|
+
})
|
|
94
110
|
}
|
|
95
111
|
seen.add(char)
|
|
96
112
|
}
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
/**
|
|
100
|
-
* Validate
|
|
116
|
+
* Validate length parameter
|
|
101
117
|
*/
|
|
102
|
-
function
|
|
118
|
+
function validateLength(size: number): void {
|
|
103
119
|
if (!Number.isInteger(size) || size < 0) {
|
|
104
|
-
throw new InvalidInputError('
|
|
120
|
+
throw new InvalidInputError('LENGTH_OUT_OF_RANGE', 'Length must be a non-negative integer', { strategy: 'nanoid' })
|
|
105
121
|
}
|
|
106
122
|
if (size > MAX_SIZE) {
|
|
107
|
-
throw new InvalidInputError('
|
|
123
|
+
throw new InvalidInputError('LENGTH_OUT_OF_RANGE', `Length must not exceed ${MAX_SIZE}`, { strategy: 'nanoid' })
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
|
|
@@ -126,7 +142,12 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
126
142
|
if (typeof sizeOrOptions === 'number') {
|
|
127
143
|
size = sizeOrOptions
|
|
128
144
|
} else {
|
|
129
|
-
|
|
145
|
+
if (sizeOrOptions.length !== undefined && sizeOrOptions.size !== undefined) {
|
|
146
|
+
throw new InvalidInputError('CONFLICTING_OPTIONS', 'Pass only one of `length` or `size`, not both', {
|
|
147
|
+
strategy: 'nanoid',
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
size = sizeOrOptions.length ?? sizeOrOptions.size ?? DEFAULT_SIZE
|
|
130
151
|
alphabet = sizeOrOptions.alphabet ?? URL_ALPHABET
|
|
131
152
|
randomBytes = sizeOrOptions.random
|
|
132
153
|
if (sizeOrOptions.alphabet !== undefined) {
|
|
@@ -134,7 +155,7 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
134
155
|
}
|
|
135
156
|
}
|
|
136
157
|
|
|
137
|
-
|
|
158
|
+
validateLength(size)
|
|
138
159
|
|
|
139
160
|
if (size === 0) return ''
|
|
140
161
|
|
|
@@ -150,8 +171,9 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
150
171
|
const mask = alphabetLen - 1
|
|
151
172
|
if (randomBytes && randomBytes.length < size) {
|
|
152
173
|
throw new InvalidInputError(
|
|
153
|
-
'
|
|
174
|
+
'RANDOM_BYTES_TOO_SHORT',
|
|
154
175
|
`Insufficient random bytes: need ${size}, have ${randomBytes.length}`,
|
|
176
|
+
{ strategy: 'nanoid' },
|
|
155
177
|
)
|
|
156
178
|
}
|
|
157
179
|
const bytes = randomBytes?.subarray(0, size) ?? globalThis.crypto.getRandomValues(new Uint8Array(size))
|
|
@@ -176,8 +198,9 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
176
198
|
if (randomBytes) {
|
|
177
199
|
if (randomBytes.length - randomOffset < step) {
|
|
178
200
|
throw new InvalidInputError(
|
|
179
|
-
'
|
|
201
|
+
'RANDOM_BYTES_TOO_SHORT',
|
|
180
202
|
`Insufficient random bytes: need at least ${step} more, have ${randomBytes.length - randomOffset}`,
|
|
203
|
+
{ strategy: 'nanoid' },
|
|
181
204
|
)
|
|
182
205
|
}
|
|
183
206
|
bytes = randomBytes.subarray(randomOffset, randomOffset + step)
|
|
@@ -227,7 +250,7 @@ function isValid(id: unknown): id is string {
|
|
|
227
250
|
* // => "V1StGXR8_Z5jdHi6B-myT"
|
|
228
251
|
* ```
|
|
229
252
|
*
|
|
230
|
-
* @example Custom
|
|
253
|
+
* @example Custom length
|
|
231
254
|
* ```ts
|
|
232
255
|
* const shortId = nanoid(10)
|
|
233
256
|
* // => "IRFa-VaY2b"
|
|
@@ -235,7 +258,7 @@ function isValid(id: unknown): id is string {
|
|
|
235
258
|
*
|
|
236
259
|
* @example Custom alphabet (hex)
|
|
237
260
|
* ```ts
|
|
238
|
-
* const hexId = nanoid({ alphabet: '0123456789abcdef',
|
|
261
|
+
* const hexId = nanoid({ alphabet: '0123456789abcdef', length: 12 })
|
|
239
262
|
* // => "4f90d13a42bc"
|
|
240
263
|
* ```
|
|
241
264
|
*
|
|
@@ -248,9 +271,9 @@ function isValid(id: unknown): id is string {
|
|
|
248
271
|
* }
|
|
249
272
|
* ```
|
|
250
273
|
*
|
|
251
|
-
* @throws {InvalidInputError}
|
|
274
|
+
* @throws {InvalidInputError} Length must be between 0 and 2048
|
|
252
275
|
* @throws {InvalidInputError} Alphabet must contain 2-256 unique printable ASCII characters
|
|
253
|
-
* @throws {InvalidInputError} Insufficient random bytes for requested
|
|
276
|
+
* @throws {InvalidInputError} Insufficient random bytes for requested length
|
|
254
277
|
*/
|
|
255
278
|
export const nanoid: Nanoid = Object.assign(nanoidFn, {
|
|
256
279
|
isValid,
|
package/src/objectid/hex.ts
CHANGED
|
@@ -45,8 +45,9 @@ for (let i = 0; i < 6; i += 1) {
|
|
|
45
45
|
export function encodeObjectIdHex(bytes: Uint8Array): string {
|
|
46
46
|
if (bytes.length < OBJECTID_BYTES) {
|
|
47
47
|
throw new BufferError(
|
|
48
|
-
'
|
|
48
|
+
'BYTES_INVALID_LENGTH',
|
|
49
49
|
`ObjectID bytes must be at least ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
|
|
50
|
+
{ strategy: 'objectid' },
|
|
50
51
|
)
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -63,8 +64,9 @@ export function encodeObjectIdHex(bytes: Uint8Array): string {
|
|
|
63
64
|
export function decodeObjectIdHex(str: string): Uint8Array {
|
|
64
65
|
if (str.length !== OBJECTID_STRING_LEN) {
|
|
65
66
|
throw new ParseError(
|
|
66
|
-
'
|
|
67
|
+
'INVALID_LENGTH',
|
|
67
68
|
`ObjectID string must be ${OBJECTID_STRING_LEN} characters, got ${str.length}`,
|
|
69
|
+
{ strategy: 'objectid' },
|
|
68
70
|
)
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -76,10 +78,10 @@ export function decodeObjectIdHex(str: string): Uint8Array {
|
|
|
76
78
|
const lo = DECODING[str.charCodeAt(loIndex)]
|
|
77
79
|
|
|
78
80
|
if (hi === 255) {
|
|
79
|
-
throw new ParseError('
|
|
81
|
+
throw new ParseError('INVALID_CHAR', `Invalid ObjectID character: ${str[hiIndex]}`, { strategy: 'objectid' })
|
|
80
82
|
}
|
|
81
83
|
if (lo === 255) {
|
|
82
|
-
throw new ParseError('
|
|
84
|
+
throw new ParseError('INVALID_CHAR', `Invalid ObjectID character: ${str[loIndex]}`, { strategy: 'objectid' })
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
bytes[i] = (hi << 4) | lo
|
package/src/objectid/objectid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTimestamp32 } from '../common/bytes'
|
|
2
2
|
import { randomBytes, randomUint32 } from '../common/random'
|
|
3
|
+
import { resolveTimestampSecs } from '../common/timestamp'
|
|
3
4
|
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
4
5
|
import { BufferError, InvalidInputError } from '../errors'
|
|
5
6
|
import { decodeObjectIdHex, encodeObjectIdHex } from './hex'
|
|
@@ -31,9 +32,17 @@ export type ObjectIdOptions = {
|
|
|
31
32
|
*/
|
|
32
33
|
random?: Uint8Array
|
|
33
34
|
/**
|
|
34
|
-
* Timestamp in
|
|
35
|
-
* Defaults to
|
|
35
|
+
* Timestamp in milliseconds since the Unix epoch.
|
|
36
|
+
* Defaults to Date.now().
|
|
37
|
+
* ObjectID stores whole seconds, so sub-second precision is truncated.
|
|
36
38
|
*/
|
|
39
|
+
msecs?: number
|
|
40
|
+
/**
|
|
41
|
+
* Deprecated alias for `msecs`; timestamp in seconds since the Unix epoch.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated Use `msecs` instead. Will be removed in `1.0.0-rc.1`.
|
|
44
|
+
*/
|
|
45
|
+
// TODO(v1-rc.1): remove this alias (tracked in docs/STABILITY.md).
|
|
37
46
|
secs?: number
|
|
38
47
|
/**
|
|
39
48
|
* 24-bit counter value (0 to 0xFFFFFF).
|
|
@@ -74,7 +83,7 @@ type ObjectIdState = {
|
|
|
74
83
|
* - In serverless/edge functions with warm starts, state persists between invocations.
|
|
75
84
|
* Unlike ULID/UUIDv7's per-timestamp sequence reset, the counter continuing to climb
|
|
76
85
|
* across warm starts is exactly the anti-collision behavior the ObjectID spec intends.
|
|
77
|
-
* - For isolated state, pass explicit `random`, `
|
|
86
|
+
* - For isolated state, pass explicit `random`, `msecs`, or `counter` via options.
|
|
78
87
|
* - Tests should mock Date.now() or provide explicit options for deterministic behavior.
|
|
79
88
|
*/
|
|
80
89
|
const state: ObjectIdState = {
|
|
@@ -139,19 +148,21 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
139
148
|
const optRandom = options.random
|
|
140
149
|
if (optRandom && optRandom.length < RANDOM_BYTES) {
|
|
141
150
|
throw new InvalidInputError(
|
|
142
|
-
'
|
|
151
|
+
'RANDOM_BYTES_TOO_SHORT',
|
|
143
152
|
`Random bytes length must be >= ${RANDOM_BYTES} for ObjectID`,
|
|
153
|
+
{
|
|
154
|
+
strategy: 'objectid',
|
|
155
|
+
},
|
|
144
156
|
)
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
const optSecs = options
|
|
148
|
-
if (optSecs !== undefined && !isIntegerInRange(optSecs, 0, MAX_SECS)) {
|
|
149
|
-
throw new InvalidInputError('OBJECTID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
|
|
150
|
-
}
|
|
159
|
+
const optSecs = resolveTimestampSecs(options, 0, MAX_SECS, 'objectid')
|
|
151
160
|
|
|
152
161
|
const optCounter = options.counter
|
|
153
162
|
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, MAX_COUNTER)) {
|
|
154
|
-
throw new InvalidInputError('
|
|
163
|
+
throw new InvalidInputError('COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`, {
|
|
164
|
+
strategy: 'objectid',
|
|
165
|
+
})
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
// Options bypass persistent state entirely: every field is sourced fresh (given
|
|
@@ -187,8 +198,9 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
187
198
|
if (buf) {
|
|
188
199
|
if (!isWritableRange(buf, offset, OBJECTID_BYTES)) {
|
|
189
200
|
throw new BufferError(
|
|
190
|
-
'
|
|
201
|
+
'BUFFER_OUT_OF_BOUNDS',
|
|
191
202
|
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
203
|
+
{ strategy: 'objectid' },
|
|
192
204
|
)
|
|
193
205
|
}
|
|
194
206
|
writeObjectIdBytesUnchecked(secs, random, counter, buf, offset)
|
|
@@ -213,8 +225,9 @@ function toBytes(id: string): Uint8Array {
|
|
|
213
225
|
function fromBytes(bytes: Uint8Array): string {
|
|
214
226
|
if (bytes.length !== OBJECTID_BYTES) {
|
|
215
227
|
throw new BufferError(
|
|
216
|
-
'
|
|
228
|
+
'BYTES_INVALID_LENGTH',
|
|
217
229
|
`ObjectID bytes must be exactly ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
|
|
230
|
+
{ strategy: 'objectid' },
|
|
218
231
|
)
|
|
219
232
|
}
|
|
220
233
|
return encodeObjectIdHex(bytes)
|
package/src/tsid/crockford64.ts
CHANGED
|
@@ -53,7 +53,9 @@ export function encodeTsidString(value: bigint): string {
|
|
|
53
53
|
*/
|
|
54
54
|
export function decodeTsidString(str: string): bigint {
|
|
55
55
|
if (str.length !== TSID_STRING_LEN) {
|
|
56
|
-
throw new ParseError('
|
|
56
|
+
throw new ParseError('INVALID_LENGTH', `TSID string must be ${TSID_STRING_LEN} characters, got ${str.length}`, {
|
|
57
|
+
strategy: 'tsid',
|
|
58
|
+
})
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
let value = 0n
|
|
@@ -61,13 +63,12 @@ export function decodeTsidString(str: string): bigint {
|
|
|
61
63
|
const code = str.charCodeAt(i)
|
|
62
64
|
const group = code < 128 ? DECODING[code] : 255
|
|
63
65
|
if (group === 255) {
|
|
64
|
-
throw new ParseError('
|
|
66
|
+
throw new ParseError('INVALID_CHAR', `Invalid TSID character: ${str[i]}`, { strategy: 'tsid' })
|
|
65
67
|
}
|
|
66
68
|
if (i === 0 && group > TSID_LEADING_MAX) {
|
|
67
|
-
throw new ParseError(
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
)
|
|
69
|
+
throw new ParseError('VALUE_OUT_OF_RANGE', `TSID leading character must be one of 0-9, A-F, got: ${str[i]}`, {
|
|
70
|
+
strategy: 'tsid',
|
|
71
|
+
})
|
|
71
72
|
}
|
|
72
73
|
value = (value << 5n) | BigInt(group)
|
|
73
74
|
}
|
package/src/tsid/tsid.ts
CHANGED
|
@@ -128,7 +128,9 @@ type ResolvedTsidOptions = {
|
|
|
128
128
|
function resolveTsidOptions(options: TsidOptions): ResolvedTsidOptions {
|
|
129
129
|
const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
|
|
130
130
|
if (!isIntegerInRange(nodeBits, 0, MAX_NODE_BITS)) {
|
|
131
|
-
throw new InvalidInputError('
|
|
131
|
+
throw new InvalidInputError('NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`, {
|
|
132
|
+
strategy: 'tsid',
|
|
133
|
+
})
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
const counterBits = RANDOM_BITS - nodeBits
|
|
@@ -136,28 +138,29 @@ function resolveTsidOptions(options: TsidOptions): ResolvedTsidOptions {
|
|
|
136
138
|
const counterMask = (1 << counterBits) - 1
|
|
137
139
|
const optNode = options.node
|
|
138
140
|
if (optNode !== undefined && !isIntegerInRange(optNode, 0, nodeMask)) {
|
|
139
|
-
throw new InvalidInputError('
|
|
141
|
+
throw new InvalidInputError('NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`, { strategy: 'tsid' })
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
const optCounter = options.counter
|
|
143
145
|
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, counterMask)) {
|
|
144
|
-
throw new InvalidInputError('
|
|
146
|
+
throw new InvalidInputError('COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`, {
|
|
147
|
+
strategy: 'tsid',
|
|
148
|
+
})
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
const epoch = options.epoch ?? TSID_EPOCH
|
|
148
152
|
if (!Number.isInteger(epoch)) {
|
|
149
|
-
throw new InvalidInputError('
|
|
153
|
+
throw new InvalidInputError('EPOCH_INVALID', 'epoch must be a finite integer', { strategy: 'tsid' })
|
|
150
154
|
}
|
|
151
155
|
const msecs = options.msecs ?? Date.now()
|
|
152
156
|
if (!Number.isInteger(msecs)) {
|
|
153
|
-
throw new InvalidInputError('
|
|
157
|
+
throw new InvalidInputError('TIMESTAMP_OUT_OF_RANGE', 'msecs must be a finite integer', { strategy: 'tsid' })
|
|
154
158
|
}
|
|
155
159
|
const msecsDiff = BigInt(msecs) - BigInt(epoch)
|
|
156
160
|
if (msecsDiff < 0n || msecsDiff > MAX_TIMESTAMP_DIFF) {
|
|
157
|
-
throw new InvalidInputError(
|
|
158
|
-
'
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
+
throw new InvalidInputError('TIMESTAMP_OUT_OF_RANGE', `msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`, {
|
|
162
|
+
strategy: 'tsid',
|
|
163
|
+
})
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
return {
|
|
@@ -229,8 +232,9 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
229
232
|
if (buf) {
|
|
230
233
|
if (!isWritableRange(buf, offset, TSID_BYTES)) {
|
|
231
234
|
throw new BufferError(
|
|
232
|
-
'
|
|
235
|
+
'BUFFER_OUT_OF_BOUNDS',
|
|
233
236
|
`TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
|
|
237
|
+
{ strategy: 'tsid' },
|
|
234
238
|
)
|
|
235
239
|
}
|
|
236
240
|
writeTsidBytesUnchecked(packed, buf, offset)
|
|
@@ -256,8 +260,9 @@ function toBytes(id: bigint): Uint8Array {
|
|
|
256
260
|
function fromBytes(bytes: Uint8Array): bigint {
|
|
257
261
|
if (bytes.length !== TSID_BYTES) {
|
|
258
262
|
throw new BufferError(
|
|
259
|
-
'
|
|
263
|
+
'BYTES_INVALID_LENGTH',
|
|
260
264
|
`TSID bytes must be exactly ${TSID_BYTES} bytes, got ${bytes.length}`,
|
|
265
|
+
{ strategy: 'tsid' },
|
|
261
266
|
)
|
|
262
267
|
}
|
|
263
268
|
|
|
@@ -276,7 +281,7 @@ function fromBytes(bytes: Uint8Array): bigint {
|
|
|
276
281
|
function timestamp(id: bigint, epoch: number = TSID_EPOCH): number {
|
|
277
282
|
assertValidTsid(id)
|
|
278
283
|
if (!Number.isInteger(epoch)) {
|
|
279
|
-
throw new InvalidInputError('
|
|
284
|
+
throw new InvalidInputError('EPOCH_INVALID', 'epoch must be a finite integer', { strategy: 'tsid' })
|
|
280
285
|
}
|
|
281
286
|
return epoch + Number(id >> BigInt(RANDOM_BITS))
|
|
282
287
|
}
|
|
@@ -293,7 +298,9 @@ const MAX = (1n << 64n) - 1n
|
|
|
293
298
|
|
|
294
299
|
function assertValidTsid(id: bigint): void {
|
|
295
300
|
if (id < NIL || id > MAX) {
|
|
296
|
-
throw new InvalidInputError('
|
|
301
|
+
throw new InvalidInputError('VALUE_OUT_OF_RANGE', `TSID value must be between ${NIL} and ${MAX}`, {
|
|
302
|
+
strategy: 'tsid',
|
|
303
|
+
})
|
|
297
304
|
}
|
|
298
305
|
}
|
|
299
306
|
|
package/src/typeid/typeid.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
2
|
+
import { BufferError, InvalidInputError, ParseError } from '../errors'
|
|
2
3
|
import type { UuidV7Options } from '../uuid/v7'
|
|
3
4
|
import { uuidv7 } from '../uuid/v7'
|
|
4
5
|
|
|
@@ -7,6 +8,15 @@ export type TypeidOptions = UuidV7Options
|
|
|
7
8
|
export type Typeid = {
|
|
8
9
|
/** Generate a TypeID from a lowercase entity prefix and a UUID v7 suffix. */
|
|
9
10
|
(prefix: string, options?: TypeidOptions): string
|
|
11
|
+
/** Generate a TypeID with explicit options or write its 16 canonical UUID v7 bytes into a caller-owned buffer. */
|
|
12
|
+
<TBuf extends Uint8Array = Uint8Array>(
|
|
13
|
+
prefix: string,
|
|
14
|
+
options: TypeidOptions | undefined,
|
|
15
|
+
buf: TBuf,
|
|
16
|
+
offset?: number,
|
|
17
|
+
): TBuf
|
|
18
|
+
/** Generate a TypeID string with optional timestamp, counter, or random bytes. */
|
|
19
|
+
(prefix: string, options?: TypeidOptions, buf?: undefined, offset?: number): string
|
|
10
20
|
/** Convert a TypeID's UUID v7 suffix to its canonical 16-byte representation. */
|
|
11
21
|
toBytes(id: string): Uint8Array
|
|
12
22
|
/** Build a TypeID from a prefix and canonical UUID v7 bytes. */
|
|
@@ -27,6 +37,11 @@ export type Typeid = {
|
|
|
27
37
|
|
|
28
38
|
const TYPEID_SUFFIX_LENGTH = 26
|
|
29
39
|
const TYPEID_UUID_BYTE_LENGTH = 16
|
|
40
|
+
// 48-bit UUID v7 timestamp capacity, mirrored from uuid/v7 alongside the
|
|
41
|
+
// 32-bit counter capacity, so option validation is attributed to the typeid
|
|
42
|
+
// boundary instead of leaking `strategy: 'uuid'` through delegation.
|
|
43
|
+
const MAX_MSECS = 0xffffffffffff
|
|
44
|
+
const MAX_COUNTER = 0xffffffff
|
|
30
45
|
const TYPEID_ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz'
|
|
31
46
|
|
|
32
47
|
const BASE32_DECODE: number[] = Array.from({ length: 128 }, () => -1)
|
|
@@ -46,7 +61,9 @@ function assertValidPrefix(prefix: string): void {
|
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
if (prefix.length > 63) {
|
|
49
|
-
throw new InvalidInputError('
|
|
64
|
+
throw new InvalidInputError('PREFIX_TOO_LONG', 'TypeID prefix must be at most 63 characters', {
|
|
65
|
+
strategy: 'typeid',
|
|
66
|
+
})
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
for (let i = 0; i < prefix.length; i += 1) {
|
|
@@ -56,13 +73,16 @@ function assertValidPrefix(prefix: string): void {
|
|
|
56
73
|
|
|
57
74
|
if (!isLowercaseLetter && !isUnderscore) {
|
|
58
75
|
throw new InvalidInputError(
|
|
59
|
-
'
|
|
76
|
+
'PREFIX_INVALID_CHAR',
|
|
60
77
|
'TypeID prefix must contain only lowercase ASCII letters and underscores',
|
|
78
|
+
{ strategy: 'typeid' },
|
|
61
79
|
)
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
if ((i === 0 || i === prefix.length - 1) && !isLowercaseLetter) {
|
|
65
|
-
throw new InvalidInputError('
|
|
83
|
+
throw new InvalidInputError('PREFIX_INVALID_BOUNDARY', 'TypeID prefix must start and end with a-z', {
|
|
84
|
+
strategy: 'typeid',
|
|
85
|
+
})
|
|
66
86
|
}
|
|
67
87
|
}
|
|
68
88
|
}
|
|
@@ -72,10 +92,9 @@ function decodeSuffixValue(suffix: string, index: number): number {
|
|
|
72
92
|
const value = code < BASE32_DECODE.length ? BASE32_DECODE[code] : -1
|
|
73
93
|
|
|
74
94
|
if (value === -1) {
|
|
75
|
-
throw new ParseError(
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
)
|
|
95
|
+
throw new ParseError('INVALID_CHAR', `TypeID suffix contains invalid character at position ${index}`, {
|
|
96
|
+
strategy: 'typeid',
|
|
97
|
+
})
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
return value
|
|
@@ -83,17 +102,23 @@ function decodeSuffixValue(suffix: string, index: number): number {
|
|
|
83
102
|
|
|
84
103
|
function assertValidSuffixShape(suffix: string): void {
|
|
85
104
|
if (suffix.length !== TYPEID_SUFFIX_LENGTH) {
|
|
86
|
-
throw new ParseError('
|
|
105
|
+
throw new ParseError('INVALID_LENGTH', `TypeID suffix must be 26 characters, got ${suffix.length}`, {
|
|
106
|
+
strategy: 'typeid',
|
|
107
|
+
})
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
if (suffix[0] > '7') {
|
|
90
|
-
throw new ParseError('
|
|
111
|
+
throw new ParseError('VALUE_OUT_OF_RANGE', 'TypeID suffix must encode a 128-bit value', {
|
|
112
|
+
strategy: 'typeid',
|
|
113
|
+
})
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
116
|
|
|
94
117
|
function encodeBytesToSuffix(bytes: Uint8Array): string {
|
|
95
118
|
if (bytes.length !== TYPEID_UUID_BYTE_LENGTH) {
|
|
96
|
-
throw new InvalidInputError('
|
|
119
|
+
throw new InvalidInputError('BYTES_INVALID_LENGTH', `UUID bytes must be 16 bytes, got ${bytes.length}`, {
|
|
120
|
+
strategy: 'typeid',
|
|
121
|
+
})
|
|
97
122
|
}
|
|
98
123
|
|
|
99
124
|
return (
|
|
@@ -157,11 +182,15 @@ function decodeSuffixToBytes(suffix: string): Uint8Array {
|
|
|
157
182
|
|
|
158
183
|
function assertUuidV7Bytes(bytes: Uint8Array): void {
|
|
159
184
|
if (bytes.length !== TYPEID_UUID_BYTE_LENGTH) {
|
|
160
|
-
throw new InvalidInputError('
|
|
185
|
+
throw new InvalidInputError('BYTES_INVALID_LENGTH', `UUID bytes must be 16 bytes, got ${bytes.length}`, {
|
|
186
|
+
strategy: 'typeid',
|
|
187
|
+
})
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
if (bytes[6] >> 4 !== 7 || (bytes[8] & 0xc0) !== 0x80) {
|
|
164
|
-
throw new InvalidInputError('
|
|
191
|
+
throw new InvalidInputError('UUID_NOT_V7', 'TypeID UUID bytes must encode a UUID v7 value', {
|
|
192
|
+
strategy: 'typeid',
|
|
193
|
+
})
|
|
165
194
|
}
|
|
166
195
|
}
|
|
167
196
|
|
|
@@ -173,7 +202,7 @@ function uuidV7BytesFromSuffix(suffix: string): Uint8Array {
|
|
|
173
202
|
|
|
174
203
|
function uuidV7BytesFromUuid(uuid: string): Uint8Array {
|
|
175
204
|
if (!uuidv7.isValid(uuid)) {
|
|
176
|
-
throw new InvalidInputError('
|
|
205
|
+
throw new InvalidInputError('UUID_NOT_V7', 'TypeID can only wrap UUID v7 values', { strategy: 'typeid' })
|
|
177
206
|
}
|
|
178
207
|
|
|
179
208
|
return uuidv7.toBytes(uuid)
|
|
@@ -183,7 +212,7 @@ function parseTypeid(id: string): ParsedTypeid {
|
|
|
183
212
|
const separatorIndex = id.lastIndexOf('_')
|
|
184
213
|
|
|
185
214
|
if (separatorIndex === 0) {
|
|
186
|
-
throw new ParseError('
|
|
215
|
+
throw new ParseError('INVALID_FORMAT', 'TypeID must not start with "_"', { strategy: 'typeid' })
|
|
187
216
|
}
|
|
188
217
|
|
|
189
218
|
const prefix = separatorIndex === -1 ? '' : id.slice(0, separatorIndex)
|
|
@@ -213,7 +242,68 @@ function timestampFromUuidV7Bytes(bytes: Uint8Array): number {
|
|
|
213
242
|
return msecs
|
|
214
243
|
}
|
|
215
244
|
|
|
216
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Mirror uuid/v7's option validation at the typeid boundary, so failures are
|
|
247
|
+
* attributed to `strategy: 'typeid'` instead of leaking `strategy: 'uuid'`
|
|
248
|
+
* through delegation.
|
|
249
|
+
*/
|
|
250
|
+
function validateTypeidOptions(options: TypeidOptions): void {
|
|
251
|
+
const msecs = options.msecs
|
|
252
|
+
if (msecs !== undefined && !isIntegerInRange(msecs, 0, MAX_MSECS)) {
|
|
253
|
+
throw new InvalidInputError('TIMESTAMP_OUT_OF_RANGE', `Timestamp must be an integer between 0 and ${MAX_MSECS}`, {
|
|
254
|
+
strategy: 'typeid',
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.counter !== undefined && options.seq !== undefined) {
|
|
259
|
+
throw new InvalidInputError('CONFLICTING_OPTIONS', 'Pass only one of `counter` or `seq`, not both', {
|
|
260
|
+
strategy: 'typeid',
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
const counter = options.counter ?? options.seq
|
|
264
|
+
if (counter !== undefined && !isIntegerInRange(counter, 0, MAX_COUNTER)) {
|
|
265
|
+
throw new InvalidInputError('COUNTER_OUT_OF_RANGE', `Counter must be an integer between 0 and ${MAX_COUNTER}`, {
|
|
266
|
+
strategy: 'typeid',
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const random = options.random
|
|
271
|
+
if (random && random.length < TYPEID_UUID_BYTE_LENGTH) {
|
|
272
|
+
throw new InvalidInputError('RANDOM_BYTES_TOO_SHORT', `Random bytes length must be >= ${TYPEID_UUID_BYTE_LENGTH}`, {
|
|
273
|
+
strategy: 'typeid',
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function typeidFn(prefix: string, options?: TypeidOptions, buf?: undefined, offset?: number): string
|
|
279
|
+
function typeidFn<TBuf extends Uint8Array = Uint8Array>(
|
|
280
|
+
prefix: string,
|
|
281
|
+
options: TypeidOptions | undefined,
|
|
282
|
+
buf: TBuf,
|
|
283
|
+
offset?: number,
|
|
284
|
+
): TBuf
|
|
285
|
+
function typeidFn<TBuf extends Uint8Array = Uint8Array>(
|
|
286
|
+
prefix: string,
|
|
287
|
+
options?: TypeidOptions,
|
|
288
|
+
buf?: TBuf,
|
|
289
|
+
offset = 0,
|
|
290
|
+
): string | TBuf {
|
|
291
|
+
if (options !== undefined) {
|
|
292
|
+
validateTypeidOptions(options)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (buf) {
|
|
296
|
+
if (!isWritableRange(buf, offset, TYPEID_UUID_BYTE_LENGTH)) {
|
|
297
|
+
throw new BufferError(
|
|
298
|
+
'BUFFER_OUT_OF_BOUNDS',
|
|
299
|
+
`TypeID byte range ${offset}:${offset + TYPEID_UUID_BYTE_LENGTH - 1} is out of buffer bounds`,
|
|
300
|
+
{ strategy: 'typeid' },
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
assertValidPrefix(prefix)
|
|
304
|
+
return uuidv7(options, buf, offset)
|
|
305
|
+
}
|
|
306
|
+
|
|
217
307
|
const bytes = uuidv7(options, new Uint8Array(TYPEID_UUID_BYTE_LENGTH))
|
|
218
308
|
return formatTypeidFromUuidV7Bytes(prefix, bytes)
|
|
219
309
|
}
|
package/src/ulid/crockford.ts
CHANGED
|
@@ -25,11 +25,11 @@ const TIME_LEN = 10
|
|
|
25
25
|
const ULID_LEN = 26
|
|
26
26
|
|
|
27
27
|
function invalidCharError(str: string, index: number): ParseError {
|
|
28
|
-
return new ParseError('
|
|
28
|
+
return new ParseError('INVALID_CHAR', `Invalid ULID character: ${str[index]}`, { strategy: 'ulid' })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function timestampOverflowError(): ParseError {
|
|
32
|
-
return new ParseError('
|
|
32
|
+
return new ParseError('TIMESTAMP_OUT_OF_RANGE', 'ULID timestamp exceeds 48 bits', { strategy: 'ulid' })
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function decodeTimeChars(str: string): number {
|
|
@@ -106,7 +106,7 @@ export function encodeRandom(bytes: Uint8Array): string {
|
|
|
106
106
|
*/
|
|
107
107
|
export function decodeTime(str: string): number {
|
|
108
108
|
if (str.length !== TIME_LEN) {
|
|
109
|
-
throw new ParseError('
|
|
109
|
+
throw new ParseError('INVALID_LENGTH', `ULID timestamp must be ${TIME_LEN} characters`, { strategy: 'ulid' })
|
|
110
110
|
}
|
|
111
111
|
return decodeTimeChars(str)
|
|
112
112
|
}
|
|
@@ -116,7 +116,7 @@ export function decodeTime(str: string): number {
|
|
|
116
116
|
*/
|
|
117
117
|
export function decodeUlidTime(str: string): number {
|
|
118
118
|
if (str.length !== ULID_LEN) {
|
|
119
|
-
throw new ParseError('
|
|
119
|
+
throw new ParseError('INVALID_LENGTH', `ULID string must be ${ULID_LEN} characters`, { strategy: 'ulid' })
|
|
120
120
|
}
|
|
121
121
|
return decodeTimeChars(str)
|
|
122
122
|
}
|
|
@@ -127,7 +127,7 @@ export function decodeUlidTime(str: string): number {
|
|
|
127
127
|
*/
|
|
128
128
|
export function decodeToBytes(str: string): Uint8Array {
|
|
129
129
|
if (str.length !== ULID_LEN) {
|
|
130
|
-
throw new ParseError('
|
|
130
|
+
throw new ParseError('INVALID_LENGTH', `ULID string must be ${ULID_LEN} characters`, { strategy: 'ulid' })
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const bytes = new Uint8Array(16)
|
|
@@ -232,7 +232,9 @@ export function decodeToBytes(str: string): Uint8Array {
|
|
|
232
232
|
*/
|
|
233
233
|
export function bytesToUlid(bytes: Uint8Array): string {
|
|
234
234
|
if (bytes.length !== 16) {
|
|
235
|
-
throw new BufferError('
|
|
235
|
+
throw new BufferError('BYTES_INVALID_LENGTH', `ULID bytes must be exactly 16 bytes, got ${bytes.length}`, {
|
|
236
|
+
strategy: 'ulid',
|
|
237
|
+
})
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
// Timestamp: bytes 0-5 -> 10 characters
|