@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.
Files changed (2) hide show
  1. package/README.md +94 -106
  2. 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 reaches your code as a `string`, and every layer below re-checks it because the type
6
- says nothing. This package checks once and hands back a `UUID`: the same string, carrying
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
- ```bash
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. [`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid) installs alongside it when you
18
- want prefixed identifiers built on these values.
15
+ Cloudflare Workers.
19
16
 
20
17
  ## Usage
21
18
 
22
- ### Generate An Identifier
19
+ ### Generate an identifier
23
20
 
24
- Each UUID version is its own entry point, and each exports the same `generateUUID()`. The
25
- import path picks the version, so switching an identifier to the other one changes only that
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" as UUID
28
+ // "019a6f3e-5c21-7a4b-9d3e-8f1a2b3c4d5e", typed UUID
33
29
  ```
34
30
 
35
- A v7 value starts with the millisecond it was created, so identifiers sort in creation order
36
- and new database rows append to the end of their primary-key index. Import from
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" as UUID
38
+ // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", typed UUID
44
39
  ```
45
40
 
46
- Either way the result is already branded, so it flows into anything typed as `UUID` without a
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 value passes when it is a string of exactly 36 characters shaped `8-4-4-4-12` in lowercase
62
- hex. Uppercase and the brace-wrapped form both fail, so the identifiers you store and compare
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
- `assertUUID` throws rather than returning, which suits the edge of a system: the value is an
78
- identifier or the request stops here.
56
+ ### Assert and tell the failures apart
79
57
 
80
- ### Tell The Failures Apart
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
- // something other than a string arrived
95
- }
96
-
97
- if (error instanceof InvalidUUIDLengthError) {
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
- Reports whether a string is a UUID, narrowing it to `UUID` inside the branch that takes it.
121
- Use it where an invalid value is an outcome you handle.
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 a string to `UUID` for the rest of the enclosing scope, throwing when it is not one:
126
- `InvalidUUIDTypeError` for a non-string, `InvalidUUIDLengthError` for a string that is not 36
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 new version 7 UUID as defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562):
132
- a 48-bit millisecond timestamp, a 12-bit counter and 62 random bits. Values sort in the order
133
- they were generated, including several created in the same millisecond, which matters on
134
- runtimes such as Cloudflare Workers where `Date.now()` holds still until the next I/O. The
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 new version 4 UUID from `crypto.randomUUID()`: 122 random bits, so two values carry
145
- no ordering and none reveals when it was created.
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
- ### Errors
108
+ Thrown for a non-string; the message names the type received.
148
109
 
149
- #### `InvalidUUIDTypeError`
110
+ ### `InvalidUUIDLengthError`
150
111
 
151
- Validation received something that is not a string. The message names the runtime type that
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
- #### `InvalidUUIDLengthError`
114
+ ### `InvalidUUIDFormatError`
155
115
 
156
- A string of the wrong length, reported as `Invalid UUID length: 10`. The check runs before
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
- #### `InvalidUUIDFormatError`
118
+ ### Types
160
119
 
161
- A 36-character string that is not lowercase `8-4-4-4-12` hex. The message repeats the value
162
- that failed, as in `Invalid UUID format: 550e8400_e29b_41d4_a716_446655440000`.
120
+ #### `UUID`
163
121
 
164
- ## Pattern: Narrowing Once At The Edge
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
- An identifier arrives as text — a path segment, a query parameter, a form field — and every
167
- function under the handler wants it typed. Assert it where it enters, and the signatures
168
- below can ask for a `UUID` and be sure they have one:
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 GET(request: Request) {
135
+ export async function handler(request: Request) {
176
136
  let accountId = new URL(request.url).pathname.split("/").at(-1) ?? "";
177
- assertUUID(accountId);
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
- // no re-validation: the type already carries the check
151
+ return { id: accountId };
184
152
  }
185
153
  ```
186
154
 
187
- The throw is the point: an identifier that never validated never reaches the query.
188
-
189
- ## Pattern: Branching Instead Of Throwing
155
+ ## Pattern: Validate A Form Field Without Throwing
190
156
 
191
- A form field is a value a person can get wrong, which makes an exception the wrong shape for
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(input: FormData): { ok: true; accountId: UUID } | { ok: false } {
200
- let value = input.get("accountId");
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
- `isUUID` is `assertUUID` with the throw turned into a `false`, so the two agree on exactly
207
- which strings are identifiers.
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
- A UUID says nothing about what it identifies, which is why an identifier pasted into a bug
212
- report is so hard to place. [`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid)
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.12",
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": "857ef87ddb0b2484e28b46431d0740fcc80e5f33",
12
+ "gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
13
13
  "publishConfig": {
14
14
  "access": "public"
15
15
  },