uniku 0.3.0 → 0.3.2

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.
Files changed (52) hide show
  1. package/build/cuid2/cuid2.d.mts +5 -5
  2. package/build/cuid2/cuid2.d.mts.map +1 -1
  3. package/build/cuid2/cuid2.mjs +1 -1
  4. package/build/cuid2/cuid2.mjs.map +1 -1
  5. package/build/ksuid/ksuid.d.mts +6 -6
  6. package/build/ksuid/ksuid.d.mts.map +1 -1
  7. package/build/ksuid/ksuid.mjs +1 -1
  8. package/build/ksuid/ksuid.mjs.map +1 -1
  9. package/build/nanoid/nanoid.d.mts +2 -2
  10. package/build/nanoid/nanoid.mjs.map +1 -1
  11. package/build/objectid/objectid.d.mts +6 -6
  12. package/build/objectid/objectid.d.mts.map +1 -1
  13. package/build/objectid/objectid.mjs +1 -1
  14. package/build/objectid/objectid.mjs.map +1 -1
  15. package/build/tsid/tsid.d.mts +8 -8
  16. package/build/tsid/tsid.d.mts.map +1 -1
  17. package/build/tsid/tsid.mjs +1 -1
  18. package/build/tsid/tsid.mjs.map +1 -1
  19. package/build/typeid/typeid.d.mts +8 -8
  20. package/build/typeid/typeid.d.mts.map +1 -1
  21. package/build/typeid/typeid.mjs.map +1 -1
  22. package/build/ulid/ulid.d.mts +10 -6
  23. package/build/ulid/ulid.d.mts.map +1 -1
  24. package/build/ulid/ulid.mjs +1 -1
  25. package/build/ulid/ulid.mjs.map +1 -1
  26. package/build/uuid/v4.d.mts +5 -6
  27. package/build/uuid/v4.d.mts.map +1 -1
  28. package/build/uuid/v4.mjs +1 -1
  29. package/build/uuid/v4.mjs.map +1 -1
  30. package/build/uuid/v7.d.mts +13 -6
  31. package/build/uuid/v7.d.mts.map +1 -1
  32. package/build/uuid/v7.mjs +1 -1
  33. package/build/uuid/v7.mjs.map +1 -1
  34. package/build/uuid-CnQIYoQi.mjs +2 -0
  35. package/build/uuid-CnQIYoQi.mjs.map +1 -0
  36. package/build/validation-CTNpXm94.mjs +2 -0
  37. package/build/validation-CTNpXm94.mjs.map +1 -0
  38. package/package.json +1 -1
  39. package/src/common/validation.ts +7 -0
  40. package/src/cuid2/cuid2.ts +13 -7
  41. package/src/ksuid/ksuid.ts +26 -17
  42. package/src/nanoid/nanoid.ts +3 -3
  43. package/src/objectid/objectid.ts +32 -17
  44. package/src/tsid/tsid.ts +92 -53
  45. package/src/typeid/typeid.ts +9 -0
  46. package/src/ulid/crockford.ts +2 -2
  47. package/src/ulid/ulid.ts +46 -24
  48. package/src/uuid/common/uuid.ts +12 -1
  49. package/src/uuid/v4.ts +31 -28
  50. package/src/uuid/v7.ts +86 -47
  51. package/build/uuid-BBjwHv7J.mjs +0 -2
  52. package/build/uuid-BBjwHv7J.mjs.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { writeTimestamp32 } from '../common/bytes'
2
2
  import { rng } from '../common/random'
3
+ import { isWritableRange } from '../common/validation'
3
4
  import { BufferError, InvalidInputError } from '../errors'
4
5
  import { decodeBase62, encodeBase62 } from './base62'
5
6
 
@@ -44,12 +45,19 @@ export type KsuidOptions = {
44
45
  }
45
46
 
46
47
  export type Ksuid = {
48
+ /** Generate a time-ordered KSUID string. */
47
49
  (): string
50
+ /** Generate a KSUID with explicit options or write its 20 canonical bytes into a caller-owned buffer. */
48
51
  <TBuf extends Uint8Array = Uint8Array>(options: KsuidOptions | undefined, buf: TBuf, offset?: number): TBuf
52
+ /** Generate a KSUID string with optional timestamp or random payload bytes. */
49
53
  (options?: KsuidOptions, buf?: undefined, offset?: number): string
54
+ /** Convert a KSUID string to its canonical 20-byte representation. */
50
55
  toBytes(id: string): Uint8Array
56
+ /** Convert 20 canonical KSUID bytes to a KSUID string. */
51
57
  fromBytes(bytes: Uint8Array): string
58
+ /** Read the embedded Unix timestamp in milliseconds. */
52
59
  timestamp(id: string): number
60
+ /** Return whether a value is a syntactically valid KSUID string. */
53
61
  isValid(id: unknown): id is string
54
62
  /** The nil KSUID (all zeros) */
55
63
  NIL: string
@@ -58,19 +66,9 @@ export type Ksuid = {
58
66
  }
59
67
 
60
68
  /**
61
- * Write KSUID bytes to a buffer.
69
+ * Write already-validated KSUID fields to a buffer.
62
70
  */
63
- function ksuidBytes(timestamp: number, payload: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {
64
- if (!buf) {
65
- buf = new Uint8Array(KSUID_BYTES)
66
- offset = 0
67
- } else if (offset < 0 || offset + KSUID_BYTES > buf.length) {
68
- throw new BufferError(
69
- 'KSUID_BUFFER_OUT_OF_BOUNDS',
70
- `KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
71
- )
72
- }
73
-
71
+ function writeKsuidBytesUnchecked(timestamp: number, payload: Uint8Array, buf: Uint8Array, offset: number): void {
74
72
  // Timestamp (32-bit big-endian seconds since KSUID epoch) -> bytes 0-3
75
73
  writeTimestamp32(buf, offset, timestamp)
76
74
 
@@ -78,8 +76,6 @@ function ksuidBytes(timestamp: number, payload: Uint8Array, buf?: Uint8Array, of
78
76
  for (let i = 0; i < PAYLOAD_BYTES; i += 1) {
79
77
  buf[offset + TIMESTAMP_BYTES + i] = payload[i]
80
78
  }
81
-
82
- return buf
83
79
  }
84
80
 
85
81
  /*
@@ -103,7 +99,7 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
103
99
  let timestamp: number
104
100
  const secs = options?.secs
105
101
  if (secs !== undefined) {
106
- if (secs < KSUID_EPOCH) {
102
+ if (!Number.isInteger(secs) || secs < KSUID_EPOCH) {
107
103
  throw new InvalidInputError('KSUID_TIMESTAMP_TOO_LOW', 'Timestamp must be >= KSUID epoch')
108
104
  }
109
105
 
@@ -126,11 +122,18 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
126
122
  const payload = random ?? rng()
127
123
 
128
124
  if (buf) {
129
- ksuidBytes(timestamp, payload, buf, offset)
125
+ if (!isWritableRange(buf, offset, KSUID_BYTES)) {
126
+ throw new BufferError(
127
+ 'KSUID_BUFFER_OUT_OF_BOUNDS',
128
+ `KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
129
+ )
130
+ }
131
+ writeKsuidBytesUnchecked(timestamp, payload, buf, offset)
130
132
  return buf
131
133
  }
132
134
 
133
- const bytes = ksuidBytes(timestamp, payload)
135
+ const bytes = new Uint8Array(KSUID_BYTES)
136
+ writeKsuidBytesUnchecked(timestamp, payload, bytes, 0)
134
137
 
135
138
  // String mode: create bytes then encode to Base62
136
139
  return encodeBase62(bytes)
@@ -150,6 +153,12 @@ function toBytes(id: string): Uint8Array {
150
153
  * Convert 20 bytes to a KSUID string.
151
154
  */
152
155
  function fromBytes(bytes: Uint8Array): string {
156
+ if (bytes.length !== KSUID_BYTES) {
157
+ throw new BufferError(
158
+ 'KSUID_BYTES_INVALID_LENGTH',
159
+ `KSUID bytes must be exactly ${KSUID_BYTES} bytes, got ${bytes.length}`,
160
+ )
161
+ }
153
162
  return encodeBase62(bytes)
154
163
  }
155
164
 
@@ -55,11 +55,11 @@ export type NanoidOptions = {
55
55
  }
56
56
 
57
57
  export type Nanoid = {
58
- /** Generate nanoid with default settings */
58
+ /** Generate a Nanoid with the default URL-safe alphabet and 21-character length. */
59
59
  (): string
60
- /** Generate nanoid with custom size */
60
+ /** Generate a Nanoid with the default alphabet and a custom length. */
61
61
  (size: number): string
62
- /** Generate nanoid with options */
62
+ /** Generate a Nanoid with a custom alphabet, length, or deterministic random bytes. */
63
63
  (options: NanoidOptions): string
64
64
  /**
65
65
  * Validate a nanoid string against the default URL-safe alphabet.
@@ -1,5 +1,6 @@
1
1
  import { writeTimestamp32 } from '../common/bytes'
2
2
  import { randomBytes, randomUint32 } from '../common/random'
3
+ import { isIntegerInRange, isWritableRange } from '../common/validation'
3
4
  import { BufferError, InvalidInputError } from '../errors'
4
5
  import { decodeObjectIdHex, encodeObjectIdHex } from './hex'
5
6
 
@@ -41,12 +42,19 @@ export type ObjectIdOptions = {
41
42
  }
42
43
 
43
44
  export type ObjectId = {
45
+ /** Generate a MongoDB-compatible ObjectID string. */
44
46
  (): string
47
+ /** Generate an ObjectID with explicit options or write its 12 canonical bytes into a caller-owned buffer. */
45
48
  <TBuf extends Uint8Array = Uint8Array>(options: ObjectIdOptions | undefined, buf: TBuf, offset?: number): TBuf
49
+ /** Generate an ObjectID string with optional timestamp, random field, or counter. */
46
50
  (options?: ObjectIdOptions, buf?: undefined, offset?: number): string
51
+ /** Convert an ObjectID string to its canonical 12-byte representation. */
47
52
  toBytes(id: string): Uint8Array
53
+ /** Convert 12 canonical ObjectID bytes to an ObjectID string. */
48
54
  fromBytes(bytes: Uint8Array): string
55
+ /** Read the embedded Unix timestamp in milliseconds. */
49
56
  timestamp(id: string): number
57
+ /** Return whether a value is a syntactically valid ObjectID string. */
50
58
  isValid(id: unknown): id is string
51
59
  /** The nil ObjectID (all zeros) */
52
60
  NIL: string
@@ -85,17 +93,13 @@ function freshRandom(): Uint8Array {
85
93
  return bytes
86
94
  }
87
95
 
88
- function objectIdBytes(secs: number, random: Uint8Array, counter: number, buf?: Uint8Array, offset = 0): Uint8Array {
89
- if (!buf) {
90
- buf = new Uint8Array(OBJECTID_BYTES)
91
- offset = 0
92
- } else if (offset < 0 || offset + OBJECTID_BYTES > buf.length) {
93
- throw new BufferError(
94
- 'OBJECTID_BUFFER_OUT_OF_BOUNDS',
95
- `ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
96
- )
97
- }
98
-
96
+ function writeObjectIdBytesUnchecked(
97
+ secs: number,
98
+ random: Uint8Array,
99
+ counter: number,
100
+ buf: Uint8Array,
101
+ offset: number,
102
+ ): void {
99
103
  // Timestamp (32-bit big-endian seconds since Unix epoch) -> bytes 0-3
100
104
  writeTimestamp32(buf, offset, secs)
101
105
 
@@ -108,8 +112,6 @@ function objectIdBytes(secs: number, random: Uint8Array, counter: number, buf?:
108
112
  buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES] = (counter >>> 16) & 0xff
109
113
  buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + 1] = (counter >>> 8) & 0xff
110
114
  buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + COUNTER_BYTES - 1] = counter & 0xff
111
-
112
- return buf
113
115
  }
114
116
 
115
117
  /*
@@ -143,12 +145,12 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
143
145
  }
144
146
 
145
147
  const optSecs = options.secs
146
- if (optSecs !== undefined && (optSecs < 0 || optSecs > MAX_SECS)) {
148
+ if (optSecs !== undefined && !isIntegerInRange(optSecs, 0, MAX_SECS)) {
147
149
  throw new InvalidInputError('OBJECTID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
148
150
  }
149
151
 
150
152
  const optCounter = options.counter
151
- if (optCounter !== undefined && (optCounter < 0 || optCounter > MAX_COUNTER)) {
153
+ if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, MAX_COUNTER)) {
152
154
  throw new InvalidInputError('OBJECTID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)
153
155
  }
154
156
 
@@ -183,11 +185,18 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
183
185
  }
184
186
 
185
187
  if (buf) {
186
- objectIdBytes(secs, random, counter, buf, offset)
188
+ if (!isWritableRange(buf, offset, OBJECTID_BYTES)) {
189
+ throw new BufferError(
190
+ 'OBJECTID_BUFFER_OUT_OF_BOUNDS',
191
+ `ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
192
+ )
193
+ }
194
+ writeObjectIdBytesUnchecked(secs, random, counter, buf, offset)
187
195
  return buf
188
196
  }
189
197
 
190
- const bytes = objectIdBytes(secs, random, counter)
198
+ const bytes = new Uint8Array(OBJECTID_BYTES)
199
+ writeObjectIdBytesUnchecked(secs, random, counter, bytes, 0)
191
200
  return encodeObjectIdHex(bytes)
192
201
  }
193
202
 
@@ -202,6 +211,12 @@ function toBytes(id: string): Uint8Array {
202
211
  * Convert 12 bytes to an ObjectID hex string.
203
212
  */
204
213
  function fromBytes(bytes: Uint8Array): string {
214
+ if (bytes.length !== OBJECTID_BYTES) {
215
+ throw new BufferError(
216
+ 'OBJECTID_BYTES_INVALID_LENGTH',
217
+ `ObjectID bytes must be exactly ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
218
+ )
219
+ }
205
220
  return encodeObjectIdHex(bytes)
206
221
  }
207
222
 
package/src/tsid/tsid.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUint32 } from '../common/random'
2
+ import { isIntegerInRange, isWritableRange } from '../common/validation'
2
3
  import { BufferError, InvalidInputError } from '../errors'
3
4
  import { decodeTsidString, encodeTsidString } from './crockford64'
4
5
 
@@ -61,14 +62,23 @@ export type TsidOptions = {
61
62
  }
62
63
 
63
64
  export type Tsid = {
65
+ /** Generate a time-sorted TSID bigint. */
64
66
  (): bigint
67
+ /** Generate a TSID with explicit options or write its 8 canonical bytes into a caller-owned buffer. */
65
68
  <TBuf extends Uint8Array = Uint8Array>(options: TsidOptions | undefined, buf: TBuf, offset?: number): TBuf
69
+ /** Generate a TSID bigint with optional timestamp, epoch, node, or counter controls. */
66
70
  (options?: TsidOptions, buf?: undefined, offset?: number): bigint
71
+ /** Convert a TSID bigint to its canonical 8-byte representation. */
67
72
  toBytes(id: bigint): Uint8Array
73
+ /** Convert 8 canonical TSID bytes to a TSID bigint. */
68
74
  fromBytes(bytes: Uint8Array): bigint
75
+ /** Convert a TSID bigint to its canonical 13-character string. */
69
76
  toString(id: bigint): string
77
+ /** Convert a canonical TSID string to a bigint. */
70
78
  fromString(str: string): bigint
79
+ /** Read a TSID timestamp in milliseconds, using the supplied or default epoch. */
71
80
  timestamp(id: bigint, epoch?: number): number
81
+ /** Return whether a value is a valid 64-bit TSID bigint. */
72
82
  isValid(id: unknown): id is bigint
73
83
  /** The nil TSID (all zeros) */
74
84
  NIL: bigint
@@ -102,22 +112,60 @@ function pack(msecsDiff: bigint, node: number, counterBits: number, counter: num
102
112
  return (msecsDiff << BigInt(RANDOM_BITS)) | (BigInt(node) << BigInt(counterBits)) | BigInt(counter)
103
113
  }
104
114
 
105
- function writeTsidBytes(value: bigint, buf?: Uint8Array, offset = 0): Uint8Array {
106
- if (!buf) {
107
- buf = new Uint8Array(TSID_BYTES)
108
- offset = 0
109
- } else if (offset < 0 || offset + TSID_BYTES > buf.length) {
110
- throw new BufferError(
111
- 'TSID_BUFFER_OUT_OF_BOUNDS',
112
- `TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
113
- )
114
- }
115
-
115
+ function writeTsidBytesUnchecked(value: bigint, buf: Uint8Array, offset: number): void {
116
116
  for (let i = 0; i < TSID_BYTES; i += 1) {
117
117
  buf[offset + i] = Number((value >> BigInt((TSID_BYTES - 1 - i) * 8)) & 0xffn)
118
118
  }
119
+ }
120
+
121
+ type ResolvedTsidOptions = {
122
+ msecsDiff: bigint
123
+ node: number
124
+ counterBits: number
125
+ counter: number
126
+ }
127
+
128
+ function resolveTsidOptions(options: TsidOptions): ResolvedTsidOptions {
129
+ const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
130
+ if (!isIntegerInRange(nodeBits, 0, MAX_NODE_BITS)) {
131
+ throw new InvalidInputError('TSID_NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`)
132
+ }
119
133
 
120
- return buf
134
+ const counterBits = RANDOM_BITS - nodeBits
135
+ const nodeMask = (1 << nodeBits) - 1
136
+ const counterMask = (1 << counterBits) - 1
137
+ const optNode = options.node
138
+ if (optNode !== undefined && !isIntegerInRange(optNode, 0, nodeMask)) {
139
+ throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
140
+ }
141
+
142
+ const optCounter = options.counter
143
+ if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, counterMask)) {
144
+ throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
145
+ }
146
+
147
+ const epoch = options.epoch ?? TSID_EPOCH
148
+ if (!Number.isInteger(epoch)) {
149
+ throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
150
+ }
151
+ const msecs = options.msecs ?? Date.now()
152
+ if (!Number.isInteger(msecs)) {
153
+ throw new InvalidInputError('TSID_TIMESTAMP_INVALID', 'msecs must be a finite integer')
154
+ }
155
+ const msecsDiff = BigInt(msecs) - BigInt(epoch)
156
+ if (msecsDiff < 0n || msecsDiff > MAX_TIMESTAMP_DIFF) {
157
+ throw new InvalidInputError(
158
+ 'TSID_TIMESTAMP_OUT_OF_RANGE',
159
+ `msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
160
+ )
161
+ }
162
+
163
+ return {
164
+ msecsDiff,
165
+ node: optNode ?? randomUint32() & nodeMask,
166
+ counterBits,
167
+ counter: optCounter ?? randomUint32() & counterMask,
168
+ }
121
169
  }
122
170
 
123
171
  /*
@@ -139,44 +187,11 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
139
187
  let counter: number
140
188
 
141
189
  if (options) {
142
- const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
143
- if (nodeBits < 0 || nodeBits > MAX_NODE_BITS) {
144
- throw new InvalidInputError('TSID_NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`)
145
- }
146
- counterBits = RANDOM_BITS - nodeBits
147
- const nodeMask = (1 << nodeBits) - 1
148
- const counterMask = (1 << counterBits) - 1
149
-
150
- const optNode = options.node
151
- if (optNode !== undefined) {
152
- if (optNode < 0 || optNode > nodeMask) {
153
- throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
154
- }
155
- node = optNode
156
- } else {
157
- node = randomUint32() & nodeMask
158
- }
159
-
160
- const optCounter = options.counter
161
- if (optCounter !== undefined) {
162
- if (optCounter < 0 || optCounter > counterMask) {
163
- throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
164
- }
165
- counter = optCounter
166
- } else {
167
- counter = randomUint32() & counterMask
168
- }
169
-
170
- const epoch = options.epoch ?? TSID_EPOCH
171
- const msecs = options.msecs ?? Date.now()
172
- const diff = BigInt(msecs) - BigInt(epoch)
173
- if (diff < 0n || diff > MAX_TIMESTAMP_DIFF) {
174
- throw new InvalidInputError(
175
- 'TSID_TIMESTAMP_OUT_OF_RANGE',
176
- `msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
177
- )
178
- }
179
- msecsDiff = diff
190
+ const resolved = resolveTsidOptions(options)
191
+ msecsDiff = resolved.msecsDiff
192
+ node = resolved.node
193
+ counterBits = resolved.counterBits
194
+ counter = resolved.counter
180
195
  } else {
181
196
  // Lazily initialize the persistent node ID on first no-option call.
182
197
  if (state.node === undefined) {
@@ -212,7 +227,13 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
212
227
  const packed = pack(msecsDiff, node, counterBits, counter)
213
228
 
214
229
  if (buf) {
215
- writeTsidBytes(packed, buf, offset)
230
+ if (!isWritableRange(buf, offset, TSID_BYTES)) {
231
+ throw new BufferError(
232
+ 'TSID_BUFFER_OUT_OF_BOUNDS',
233
+ `TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
234
+ )
235
+ }
236
+ writeTsidBytesUnchecked(packed, buf, offset)
216
237
  return buf
217
238
  }
218
239
 
@@ -223,7 +244,10 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
223
244
  * Convert a TSID bigint to 8 bytes.
224
245
  */
225
246
  function toBytes(id: bigint): Uint8Array {
226
- return writeTsidBytes(id)
247
+ assertValidTsid(id)
248
+ const bytes = new Uint8Array(TSID_BYTES)
249
+ writeTsidBytesUnchecked(id, bytes, 0)
250
+ return bytes
227
251
  }
228
252
 
229
253
  /**
@@ -250,6 +274,10 @@ function fromBytes(bytes: Uint8Array): bigint {
250
274
  * `epoch` used at generation time if it was overridden from the default.
251
275
  */
252
276
  function timestamp(id: bigint, epoch: number = TSID_EPOCH): number {
277
+ assertValidTsid(id)
278
+ if (!Number.isInteger(epoch)) {
279
+ throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
280
+ }
253
281
  return epoch + Number(id >> BigInt(RANDOM_BITS))
254
282
  }
255
283
 
@@ -263,6 +291,17 @@ function isValid(id: unknown): id is bigint {
263
291
  const NIL = 0n
264
292
  const MAX = (1n << 64n) - 1n
265
293
 
294
+ function assertValidTsid(id: bigint): void {
295
+ if (id < NIL || id > MAX) {
296
+ throw new InvalidInputError('TSID_VALUE_OUT_OF_RANGE', `TSID value must be between ${NIL} and ${MAX}`)
297
+ }
298
+ }
299
+
300
+ function toString(id: bigint): string {
301
+ assertValidTsid(id)
302
+ return encodeTsidString(id)
303
+ }
304
+
266
305
  /**
267
306
  * Generate a TSID bigint or write the bytes into a buffer.
268
307
  *
@@ -299,7 +338,7 @@ const MAX = (1n << 64n) - 1n
299
338
  export const tsid: Tsid = Object.assign(tsidFn, {
300
339
  toBytes,
301
340
  fromBytes,
302
- toString: encodeTsidString,
341
+ toString,
303
342
  fromString: decodeTsidString,
304
343
  timestamp,
305
344
  isValid,
@@ -5,14 +5,23 @@ import { uuidv7 } from '../uuid/v7'
5
5
  export type TypeidOptions = UuidV7Options
6
6
 
7
7
  export type Typeid = {
8
+ /** Generate a TypeID from a lowercase entity prefix and a UUID v7 suffix. */
8
9
  (prefix: string, options?: TypeidOptions): string
10
+ /** Convert a TypeID's UUID v7 suffix to its canonical 16-byte representation. */
9
11
  toBytes(id: string): Uint8Array
12
+ /** Build a TypeID from a prefix and canonical UUID v7 bytes. */
10
13
  fromBytes(prefix: string, bytes: Uint8Array): string
14
+ /** Convert a TypeID to its UUID v7 string. */
11
15
  toUuid(id: string): string
16
+ /** Build a TypeID from a prefix and UUID v7 string. */
12
17
  fromUuid(prefix: string, uuid: string): string
18
+ /** Read the UUID v7 timestamp embedded in a TypeID, in milliseconds. */
13
19
  timestamp(id: string): number
20
+ /** Read a TypeID's entity prefix. */
14
21
  prefix(id: string): string
22
+ /** Read a TypeID's UUID v7 suffix. */
15
23
  suffix(id: string): string
24
+ /** Return whether a value is a syntactically valid TypeID. */
16
25
  isValid(id: unknown): id is string
17
26
  }
18
27
 
@@ -231,8 +231,8 @@ export function decodeToBytes(str: string): Uint8Array {
231
231
  * Encode 16 bytes to a 26-character ULID string.
232
232
  */
233
233
  export function bytesToUlid(bytes: Uint8Array): string {
234
- if (bytes.length < 16) {
235
- throw new BufferError('ULID_BYTES_TOO_SHORT', 'Byte array must be at least 16 bytes')
234
+ if (bytes.length !== 16) {
235
+ throw new BufferError('ULID_BYTES_INVALID_LENGTH', `ULID bytes must be exactly 16 bytes, got ${bytes.length}`)
236
236
  }
237
237
 
238
238
  // Timestamp: bytes 0-5 -> 10 characters
package/src/ulid/ulid.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { incrementBytesInPlace, writeTimestamp48 } from '../common/bytes'
2
2
  import { rng } from '../common/random'
3
+ import { isIntegerInRange, isWritableRange } from '../common/validation'
3
4
  import { BufferError, InvalidInputError } from '../errors'
4
5
  import { bytesToUlid, decodeToBytes, decodeUlidTime, encodeRandom, encodeTime } from './crockford'
5
6
 
@@ -17,12 +18,19 @@ export type UlidOptions = {
17
18
  }
18
19
 
19
20
  export type Ulid = {
21
+ /** Generate a time-ordered ULID string. */
20
22
  (): string
23
+ /** Generate a ULID with explicit options or write its 16 canonical bytes into a caller-owned buffer. */
21
24
  <TBuf extends Uint8Array = Uint8Array>(options: UlidOptions | undefined, buf: TBuf, offset?: number): TBuf
25
+ /** Generate a ULID string with optional timestamp or random bytes. */
22
26
  (options?: UlidOptions, buf?: undefined, offset?: number): string
27
+ /** Convert a ULID string to its canonical 16-byte representation. */
23
28
  toBytes(id: string): Uint8Array
29
+ /** Convert 16 canonical ULID bytes to a ULID string. */
24
30
  fromBytes(bytes: Uint8Array): string
31
+ /** Read the embedded Unix timestamp in milliseconds. */
25
32
  timestamp(id: string): number
33
+ /** Return whether a value is a syntactically valid ULID string. */
26
34
  isValid(id: unknown): id is string
27
35
  /** The nil ULID (all zeros) */
28
36
  NIL: string
@@ -32,6 +40,9 @@ export type Ulid = {
32
40
 
33
41
  // Validation regex: first char [0-7] to prevent overflow, rest from Crockford alphabet
34
42
  const ULID_REGEX = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i
43
+ const ULID_BYTES = 16
44
+ const RANDOM_BYTES = 10
45
+ const MAX_MSECS = 0xffffffffffff
35
46
 
36
47
  type UlidState = {
37
48
  msecs: number
@@ -48,29 +59,27 @@ type UlidState = {
48
59
  */
49
60
  const state: UlidState = {
50
61
  msecs: -Infinity,
51
- lastRandom: new Uint8Array(10),
62
+ lastRandom: new Uint8Array(RANDOM_BYTES),
52
63
  }
53
64
 
54
- function ulidBytes(time: number, random: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {
55
- if (!buf) {
56
- buf = new Uint8Array(16)
57
- offset = 0
58
- } else if (offset < 0 || offset + 16 > buf.length) {
59
- throw new BufferError(
60
- 'ULID_BUFFER_OUT_OF_BOUNDS',
61
- `ULID byte range ${offset}:${offset + 15} is out of buffer bounds`,
62
- )
63
- }
64
-
65
+ function writeUlidBytesUnchecked(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
65
66
  // Timestamp (48-bit big-endian milliseconds since Unix epoch) -> bytes 0-5
66
67
  writeTimestamp48(buf, offset, time)
67
68
 
68
69
  // Random (80 bits) -> bytes 6-15
69
- for (let i = 0; i < 10; i += 1) {
70
+ for (let i = 0; i < RANDOM_BYTES; i += 1) {
70
71
  buf[offset + 6 + i] = random[i]
71
72
  }
73
+ }
72
74
 
73
- return buf
75
+ function writeUlidBytes(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
76
+ if (!isWritableRange(buf, offset, ULID_BYTES)) {
77
+ throw new BufferError(
78
+ 'ULID_BUFFER_OUT_OF_BOUNDS',
79
+ `ULID byte range ${offset}:${offset + ULID_BYTES - 1} is out of buffer bounds`,
80
+ )
81
+ }
82
+ writeUlidBytesUnchecked(time, random, buf, offset)
74
83
  }
75
84
 
76
85
  /*
@@ -97,27 +106,36 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
97
106
  * - all ULIDs generated within a single request will have the same timestamp.
98
107
  * - the monotonic ordering will rely entirely on incrementing the random portion.
99
108
  */
100
- const defaultTime = Date.now()
101
-
102
109
  if (options) {
103
110
  // Explicit options provided - use them directly without monotonic state
104
- time = options.msecs ?? defaultTime
105
- if (options.random) {
106
- if (options.random.length < 10) {
107
- throw new InvalidInputError('ULID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 10 for ULID')
111
+ const optMsecs = options.msecs
112
+ if (optMsecs !== undefined && !isIntegerInRange(optMsecs, 0, MAX_MSECS)) {
113
+ throw new InvalidInputError(
114
+ 'ULID_TIMESTAMP_OUT_OF_RANGE',
115
+ `Timestamp must be an integer between 0 and ${MAX_MSECS}`,
116
+ )
117
+ }
118
+ time = optMsecs ?? Date.now()
119
+ const optRandom = options.random
120
+ if (optRandom) {
121
+ if (optRandom.length < RANDOM_BYTES) {
122
+ throw new InvalidInputError(
123
+ 'ULID_RANDOM_BYTES_TOO_SHORT',
124
+ `Random bytes length must be >= ${RANDOM_BYTES} for ULID`,
125
+ )
108
126
  }
109
- random = options.random
127
+ random = optRandom
110
128
  } else {
111
129
  random = rng()
112
130
  }
113
131
  } else {
114
- time = defaultTime
132
+ time = Date.now()
115
133
 
116
134
  if (time > state.msecs) {
117
135
  // New millisecond: generate fresh random
118
136
  random = rng()
119
137
  state.msecs = time
120
- state.lastRandom.set(random.subarray(0, 10))
138
+ state.lastRandom.set(random.subarray(0, RANDOM_BYTES))
121
139
  } else {
122
140
  // Same millisecond or clock rollback: preserve last timestamp and increment random portion.
123
141
  time = state.msecs
@@ -133,7 +151,7 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
133
151
  }
134
152
 
135
153
  if (buf) {
136
- ulidBytes(time, random, buf, offset)
154
+ writeUlidBytes(time, random, buf, offset)
137
155
  return buf
138
156
  }
139
157
 
@@ -172,6 +190,10 @@ function isValid(id: unknown): id is string {
172
190
  return typeof id === 'string' && ULID_REGEX.test(id)
173
191
  }
174
192
 
193
+ /**
194
+ * Generate a ULID string or write its 16 canonical bytes into a buffer.
195
+ * ULIDs are URL-safe, carry a millisecond timestamp, and sort by creation time.
196
+ */
175
197
  export const ulid: Ulid = Object.assign(ulidFn, {
176
198
  toBytes: (id: string) => decodeToBytes(id),
177
199
  fromBytes: (bytes: Uint8Array) => bytesToUlid(bytes),
@@ -1,4 +1,4 @@
1
- import { ParseError } from '../../errors'
1
+ import { BufferError, ParseError } from '../../errors'
2
2
  import { hexValue } from './hex'
3
3
 
4
4
  const UUID_BYTE_LENGTH = 16
@@ -93,6 +93,17 @@ const UUID_CHAR_IS_HIGH: boolean[] = [
93
93
  const HEX_TABLE: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
94
94
 
95
95
  export function formatUuid(bytes: Uint8Array): string {
96
+ if (bytes.length !== UUID_BYTE_LENGTH) {
97
+ throw new BufferError(
98
+ 'UUID_BYTES_INVALID_LENGTH',
99
+ `UUID bytes must be exactly ${UUID_BYTE_LENGTH} bytes, got ${bytes.length}`,
100
+ )
101
+ }
102
+
103
+ return formatUuidUnchecked(bytes)
104
+ }
105
+
106
+ export function formatUuidUnchecked(bytes: Uint8Array): string {
96
107
  // Direct string concatenation - optimized for V8's string builder.
97
108
  // This approach avoids loop overhead and intermediate allocations.
98
109
  // See: https://github.com/uuidjs/uuid/pull/434