@sdxc/uuid 2026.10.12 → 2026.10.13
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 +94 -106
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,51 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
Validate and assert UUIDs behind a branded UUID type, and generate v4 or v7 values.
|
|
4
4
|
|
|
5
|
-
A UUID
|
|
6
|
-
|
|
7
|
-
proof of the check in its type, so a function that asks for a `UUID` cannot be handed the
|
|
8
|
-
raw path segment it came from.
|
|
5
|
+
A `UUID` is the same string, carrying proof of the check in its type: validate once where the
|
|
6
|
+
value enters, and every function below can ask for a `UUID` instead of re-checking a `string`.
|
|
9
7
|
|
|
10
8
|
## Installation
|
|
11
9
|
|
|
12
|
-
```
|
|
10
|
+
```sh
|
|
13
11
|
npm add @sdxc/uuid
|
|
14
12
|
```
|
|
15
13
|
|
|
16
14
|
The generators use the platform's Web Crypto API, available in browsers, Node, Bun, Deno and
|
|
17
|
-
Cloudflare Workers.
|
|
18
|
-
want prefixed identifiers built on these values.
|
|
15
|
+
Cloudflare Workers.
|
|
19
16
|
|
|
20
17
|
## Usage
|
|
21
18
|
|
|
22
|
-
### Generate
|
|
19
|
+
### Generate an identifier
|
|
23
20
|
|
|
24
|
-
Each UUID version is its own entry point
|
|
25
|
-
|
|
26
|
-
line.
|
|
21
|
+
Each UUID version is its own entry point exporting the same `generateUUID()`, so the import
|
|
22
|
+
path picks the version.
|
|
27
23
|
|
|
28
24
|
```typescript
|
|
29
25
|
import { generateUUID } from "@sdxc/uuid/v7";
|
|
30
26
|
|
|
31
27
|
let id = generateUUID();
|
|
32
|
-
// "019a6f3e-5c21-7a4b-9d3e-8f1a2b3c4d5e"
|
|
28
|
+
// "019a6f3e-5c21-7a4b-9d3e-8f1a2b3c4d5e", typed UUID
|
|
33
29
|
```
|
|
34
30
|
|
|
35
|
-
A v7 value starts with
|
|
36
|
-
|
|
37
|
-
`@sdxc/uuid/v4` instead when the identifier must not reveal when it was created:
|
|
31
|
+
A v7 value starts with its creation time, so values sort in the order they were generated.
|
|
32
|
+
Import from `@sdxc/uuid/v4` for a fully random value that reveals nothing:
|
|
38
33
|
|
|
39
34
|
```typescript
|
|
40
35
|
import { generateUUID } from "@sdxc/uuid/v4";
|
|
41
36
|
|
|
42
37
|
let id = generateUUID();
|
|
43
|
-
// "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
|
|
38
|
+
// "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", typed UUID
|
|
44
39
|
```
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
cast.
|
|
48
|
-
|
|
49
|
-
### Narrow A String You Received
|
|
41
|
+
### Check a string you received
|
|
50
42
|
|
|
51
43
|
```typescript
|
|
52
44
|
import { isUUID } from "@sdxc/uuid";
|
|
@@ -58,26 +50,12 @@ if (isUUID(value)) {
|
|
|
58
50
|
}
|
|
59
51
|
```
|
|
60
52
|
|
|
61
|
-
A
|
|
62
|
-
|
|
63
|
-
have one spelling. The version and variant digits are read as hex like any other, so the
|
|
64
|
-
check is about the format rather than which UUID version produced it.
|
|
65
|
-
|
|
66
|
-
### Assert At A Boundary
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { assertUUID } from "@sdxc/uuid";
|
|
70
|
-
|
|
71
|
-
function readAccountId(value: string) {
|
|
72
|
-
assertUUID(value);
|
|
73
|
-
return value; // typed UUID from here on
|
|
74
|
-
}
|
|
75
|
-
```
|
|
53
|
+
A UUID is exactly 36 characters of lowercase hex shaped `8-4-4-4-12`, so stored identifiers
|
|
54
|
+
have one spelling. Any version passes.
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
identifier or the request stops here.
|
|
56
|
+
### Assert and tell the failures apart
|
|
79
57
|
|
|
80
|
-
|
|
58
|
+
`assertUUID` narrows the value for the rest of the scope, or throws one of three error classes.
|
|
81
59
|
|
|
82
60
|
```typescript
|
|
83
61
|
import {
|
|
@@ -89,128 +67,142 @@ import {
|
|
|
89
67
|
|
|
90
68
|
try {
|
|
91
69
|
assertUUID(value);
|
|
70
|
+
// value is UUID from here on
|
|
92
71
|
} catch (error) {
|
|
93
72
|
if (error instanceof InvalidUUIDTypeError) {
|
|
94
|
-
//
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (error instanceof
|
|
98
|
-
// a string of the wrong length: "Invalid UUID length: 10"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (error instanceof InvalidUUIDFormatError) {
|
|
73
|
+
// not a string: "Expected a string, got object"
|
|
74
|
+
} else if (error instanceof InvalidUUIDLengthError) {
|
|
75
|
+
// wrong length: "Invalid UUID length: 10"
|
|
76
|
+
} else if (error instanceof InvalidUUIDFormatError) {
|
|
102
77
|
// 36 characters in the wrong shape
|
|
103
78
|
}
|
|
104
79
|
}
|
|
105
80
|
```
|
|
106
81
|
|
|
107
|
-
Three classes rather than one message, so a truncated identifier and a mistyped one are
|
|
108
|
-
distinguishable when you decide what to answer.
|
|
109
|
-
|
|
110
82
|
## API
|
|
111
83
|
|
|
112
|
-
### `UUID`
|
|
113
|
-
|
|
114
|
-
The UUID string type, branded. A plain `string` is not assignable to it, and a `UUID` is
|
|
115
|
-
assignable anywhere a `string` is, so the brand costs nothing at the call sites that only
|
|
116
|
-
read the value.
|
|
117
|
-
|
|
118
84
|
### `isUUID(value: string): value is UUID`
|
|
119
85
|
|
|
120
|
-
|
|
121
|
-
|
|
86
|
+
Returns `true` when the value is a lowercase `8-4-4-4-12` hex string, narrowing it to `UUID`.
|
|
87
|
+
Accepts exactly the values `assertUUID` accepts.
|
|
122
88
|
|
|
123
89
|
### `assertUUID(value: string): asserts value is UUID`
|
|
124
90
|
|
|
125
|
-
Narrows
|
|
126
|
-
`
|
|
127
|
-
characters, and `InvalidUUIDFormatError` for 36 characters in the wrong shape.
|
|
91
|
+
Narrows the value to `UUID`, or throws `InvalidUUIDTypeError` for a non-string,
|
|
92
|
+
`InvalidUUIDLengthError` for a length other than 36, and `InvalidUUIDFormatError` otherwise.
|
|
128
93
|
|
|
129
94
|
### `generateUUID(): UUID` from `@sdxc/uuid/v7`
|
|
130
95
|
|
|
131
|
-
Returns a
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
guarantee holds within one process or isolate; values from two processes in the same
|
|
136
|
-
millisecond are unique and share no defined order.
|
|
137
|
-
|
|
138
|
-
The timestamp is readable by anyone holding the value, including through a
|
|
139
|
-
[`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid) prefix. Use it for identifiers
|
|
140
|
-
whose creation time is fine to expose, and keep secrets on random values.
|
|
96
|
+
Returns a version 7 UUID ([RFC 9562](https://www.rfc-editor.org/rfc/rfc9562)): a 48-bit
|
|
97
|
+
millisecond timestamp, a 12-bit counter and 62 random bits. Each value sorts after the previous
|
|
98
|
+
one from the same process, including within one millisecond. Anyone holding the value can read
|
|
99
|
+
its creation time.
|
|
141
100
|
|
|
142
101
|
### `generateUUID(): UUID` from `@sdxc/uuid/v4`
|
|
143
102
|
|
|
144
|
-
Returns a
|
|
145
|
-
|
|
103
|
+
Returns a version 4 UUID from `crypto.randomUUID()`: 122 random bits, with no ordering and no
|
|
104
|
+
creation time.
|
|
105
|
+
|
|
106
|
+
### `InvalidUUIDTypeError`
|
|
146
107
|
|
|
147
|
-
|
|
108
|
+
Thrown for a non-string; the message names the type received.
|
|
148
109
|
|
|
149
|
-
|
|
110
|
+
### `InvalidUUIDLengthError`
|
|
150
111
|
|
|
151
|
-
|
|
152
|
-
arrived, as in `Expected a string, got object`.
|
|
112
|
+
Thrown for a string that is not 36 characters; the message names the length.
|
|
153
113
|
|
|
154
|
-
|
|
114
|
+
### `InvalidUUIDFormatError`
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
the shape check, so a truncated identifier says so instead of reading as malformed.
|
|
116
|
+
Thrown for 36 characters that are not lowercase `8-4-4-4-12` hex; the message repeats the value.
|
|
158
117
|
|
|
159
|
-
|
|
118
|
+
### Types
|
|
160
119
|
|
|
161
|
-
|
|
162
|
-
that failed, as in `Invalid UUID format: 550e8400_e29b_41d4_a716_446655440000`.
|
|
120
|
+
#### `UUID`
|
|
163
121
|
|
|
164
|
-
|
|
122
|
+
A branded UUID string. A plain `string` must pass `isUUID` or `assertUUID` to become one, and a
|
|
123
|
+
`UUID` is usable anywhere a `string` is.
|
|
165
124
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
125
|
+
## Pattern: Narrow Once At The Edge
|
|
126
|
+
|
|
127
|
+
Assert an identifier where it enters a request handler, and the functions below take a `UUID`
|
|
128
|
+
without re-validating.
|
|
169
129
|
|
|
170
130
|
```typescript
|
|
171
131
|
import type { UUID } from "@sdxc/uuid";
|
|
172
132
|
|
|
173
|
-
import { assertUUID } from "@sdxc/uuid";
|
|
133
|
+
import { assertUUID, InvalidUUIDLengthError, InvalidUUIDFormatError } from "@sdxc/uuid";
|
|
174
134
|
|
|
175
|
-
export async function
|
|
135
|
+
export async function handler(request: Request) {
|
|
176
136
|
let accountId = new URL(request.url).pathname.split("/").at(-1) ?? "";
|
|
177
|
-
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
assertUUID(accountId);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof InvalidUUIDLengthError || error instanceof InvalidUUIDFormatError) {
|
|
142
|
+
return new Response("Not Found", { status: 404 });
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
178
146
|
|
|
179
147
|
return Response.json(await loadAccount(accountId));
|
|
180
148
|
}
|
|
181
149
|
|
|
182
150
|
async function loadAccount(accountId: UUID) {
|
|
183
|
-
|
|
151
|
+
return { id: accountId };
|
|
184
152
|
}
|
|
185
153
|
```
|
|
186
154
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
## Pattern: Branching Instead Of Throwing
|
|
155
|
+
## Pattern: Validate A Form Field Without Throwing
|
|
190
156
|
|
|
191
|
-
A
|
|
192
|
-
the answer. `isUUID` gives you the same narrowing in an expression you can branch on:
|
|
157
|
+
A person can mistype a field, so branch on `isUUID` and answer with a result.
|
|
193
158
|
|
|
194
159
|
```typescript
|
|
195
160
|
import type { UUID } from "@sdxc/uuid";
|
|
196
161
|
|
|
197
162
|
import { isUUID } from "@sdxc/uuid";
|
|
198
163
|
|
|
199
|
-
function readAccountId(
|
|
200
|
-
let value =
|
|
164
|
+
function readAccountId(form: FormData): { ok: true; accountId: UUID } | { ok: false } {
|
|
165
|
+
let value = form.get("accountId");
|
|
201
166
|
if (typeof value !== "string" || !isUUID(value)) return { ok: false };
|
|
202
167
|
return { ok: true, accountId: value };
|
|
203
168
|
}
|
|
204
169
|
```
|
|
205
170
|
|
|
206
|
-
|
|
207
|
-
|
|
171
|
+
## Pattern: Time-Ordered Record Ids
|
|
172
|
+
|
|
173
|
+
Use v7 for primary keys: new rows append to the end of the index, and sorting by id sorts by
|
|
174
|
+
creation, so the last id of a page is enough of a cursor for the next one.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import type { UUID } from "@sdxc/uuid";
|
|
178
|
+
|
|
179
|
+
import { generateUUID } from "@sdxc/uuid/v7";
|
|
180
|
+
|
|
181
|
+
interface Note {
|
|
182
|
+
id: UUID;
|
|
183
|
+
body: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let notes: Note[] = [];
|
|
187
|
+
|
|
188
|
+
function createNote(body: string): Note {
|
|
189
|
+
let note = { id: generateUUID(), body };
|
|
190
|
+
notes.push(note);
|
|
191
|
+
return note;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function notesAfter(cursor: UUID | null, limit: number) {
|
|
195
|
+
return notes.filter((note) => cursor === null || note.id > cursor).slice(0, limit);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Reach for `@sdxc/uuid/v4` when an identifier must not reveal when it was created, such as a
|
|
200
|
+
public share id.
|
|
208
201
|
|
|
209
202
|
## Pattern: Prefixed Identifiers
|
|
210
203
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
takes a `UUID` and a prefix and encodes them as one sortable, self-describing string:
|
|
204
|
+
[`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid) takes a `UUID` and a prefix and
|
|
205
|
+
encodes them as one self-describing string that keeps the v7 sort order.
|
|
214
206
|
|
|
215
207
|
```typescript
|
|
216
208
|
import { TypeID } from "@sdxc/typeid";
|
|
@@ -222,10 +214,6 @@ accountId.toString();
|
|
|
222
214
|
// "account_01h455vb4pex5vsknk084sn02q"
|
|
223
215
|
```
|
|
224
216
|
|
|
225
|
-
It asks for a `UUID` rather than a `string`, so the value you generate here is accepted and a
|
|
226
|
-
random 36 characters is not. TypeID keeps the UUID's sort order, so a v7 identifier stays
|
|
227
|
-
time-ordered once it is prefixed.
|
|
228
|
-
|
|
229
217
|
## Versioning
|
|
230
218
|
|
|
231
219
|
Releases are dated rather than semantic. A version is the UTC date it was published,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/uuid",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.13",
|
|
4
4
|
"description": "Validate and assert UUIDs behind a branded UUID type, and generate v4 or v7 values",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"./v4": "./dist/v4.js",
|
|
10
10
|
"./v7": "./dist/v7.js"
|
|
11
11
|
},
|
|
12
|
-
"gitHead": "
|
|
12
|
+
"gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|