@sdxc/uuid 0.0.0-pre.1
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/LICENSE.md +21 -0
- package/README.md +230 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +29 -0
- package/dist/lib/errors.d.ts +37 -0
- package/dist/lib/errors.js +43 -0
- package/package.json +19 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Xalambrí
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# @sdxc/uuid
|
|
2
|
+
|
|
3
|
+
Validate, assert and generate UUIDs behind a branded UUID type.
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm add @sdxc/uuid
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`generateUUID` calls the platform's
|
|
17
|
+
[`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID).
|
|
18
|
+
[`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid) installs alongside it when you
|
|
19
|
+
want prefixed identifiers built on these values.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Generate An Identifier
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { generateUUID } from "@sdxc/uuid";
|
|
27
|
+
|
|
28
|
+
let id = generateUUID();
|
|
29
|
+
// "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" as UUID
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The longhand is `crypto.randomUUID()` followed by the check that brands its result, which is
|
|
33
|
+
what lets the value flow into anything typed as `UUID` without a cast.
|
|
34
|
+
|
|
35
|
+
### Narrow A String You Received
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { isUUID } from "@sdxc/uuid";
|
|
39
|
+
|
|
40
|
+
let value = "550e8400-e29b-41d4-a716-446655440000";
|
|
41
|
+
|
|
42
|
+
if (isUUID(value)) {
|
|
43
|
+
// value is UUID here
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A value passes when it is a string of exactly 36 characters shaped `8-4-4-4-12` in lowercase
|
|
48
|
+
hex. Uppercase and the brace-wrapped form both fail, so the identifiers you store and compare
|
|
49
|
+
have one spelling. The version and variant digits are read as hex like any other, so the
|
|
50
|
+
check is about the format rather than which UUID version produced it.
|
|
51
|
+
|
|
52
|
+
### Assert At A Boundary
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { assertUUID } from "@sdxc/uuid";
|
|
56
|
+
|
|
57
|
+
function readAccountId(value: string) {
|
|
58
|
+
assertUUID(value);
|
|
59
|
+
return value; // typed UUID from here on
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`assertUUID` throws rather than returning, which suits the edge of a system: the value is an
|
|
64
|
+
identifier or the request stops here.
|
|
65
|
+
|
|
66
|
+
### Tell The Failures Apart
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
assertUUID,
|
|
71
|
+
InvalidUUIDFormatError,
|
|
72
|
+
InvalidUUIDLengthError,
|
|
73
|
+
InvalidUUIDTypeError,
|
|
74
|
+
} from "@sdxc/uuid";
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
assertUUID(value);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof InvalidUUIDTypeError) {
|
|
80
|
+
// something other than a string arrived
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error instanceof InvalidUUIDLengthError) {
|
|
84
|
+
// a string of the wrong length: "Invalid UUID length: 10"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (error instanceof InvalidUUIDFormatError) {
|
|
88
|
+
// 36 characters in the wrong shape
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Three classes rather than one message, so a truncated identifier and a mistyped one are
|
|
94
|
+
distinguishable when you decide what to answer.
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### `UUID`
|
|
99
|
+
|
|
100
|
+
The UUID string type, branded. A plain `string` is not assignable to it, and a `UUID` is
|
|
101
|
+
assignable anywhere a `string` is, so the brand costs nothing at the call sites that only
|
|
102
|
+
read the value.
|
|
103
|
+
|
|
104
|
+
### `isUUID(value: string): value is UUID`
|
|
105
|
+
|
|
106
|
+
Reports whether a string is a UUID, narrowing it to `UUID` inside the branch that takes it.
|
|
107
|
+
Use it where an invalid value is an outcome you handle.
|
|
108
|
+
|
|
109
|
+
### `assertUUID(value: string): asserts value is UUID`
|
|
110
|
+
|
|
111
|
+
Narrows a string to `UUID` for the rest of the enclosing scope, throwing when it is not one:
|
|
112
|
+
`InvalidUUIDTypeError` for a non-string, `InvalidUUIDLengthError` for a string that is not 36
|
|
113
|
+
characters, and `InvalidUUIDFormatError` for 36 characters in the wrong shape.
|
|
114
|
+
|
|
115
|
+
### `generateUUID(): UUID`
|
|
116
|
+
|
|
117
|
+
Returns a new random UUID from `crypto.randomUUID()`, already narrowed to `UUID`. That is a
|
|
118
|
+
version 4 UUID as defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562), random in
|
|
119
|
+
every position the format leaves free.
|
|
120
|
+
|
|
121
|
+
### Errors
|
|
122
|
+
|
|
123
|
+
#### `InvalidUUIDTypeError`
|
|
124
|
+
|
|
125
|
+
Validation received something that is not a string. The message names the runtime type that
|
|
126
|
+
arrived, as in `Expected a string, got object`.
|
|
127
|
+
|
|
128
|
+
#### `InvalidUUIDLengthError`
|
|
129
|
+
|
|
130
|
+
A string of the wrong length, reported as `Invalid UUID length: 10`. The check runs before
|
|
131
|
+
the shape check, so a truncated identifier says so instead of reading as malformed.
|
|
132
|
+
|
|
133
|
+
#### `InvalidUUIDFormatError`
|
|
134
|
+
|
|
135
|
+
A 36-character string that is not lowercase `8-4-4-4-12` hex. The message repeats the value
|
|
136
|
+
that failed, as in `Invalid UUID format: 550e8400_e29b_41d4_a716_446655440000`.
|
|
137
|
+
|
|
138
|
+
## Pattern: Narrowing Once At The Edge
|
|
139
|
+
|
|
140
|
+
An identifier arrives as text — a path segment, a query parameter, a form field — and every
|
|
141
|
+
function under the handler wants it typed. Assert it where it enters, and the signatures
|
|
142
|
+
below can ask for a `UUID` and be sure they have one:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import type { UUID } from "@sdxc/uuid";
|
|
146
|
+
|
|
147
|
+
import { assertUUID } from "@sdxc/uuid";
|
|
148
|
+
|
|
149
|
+
export async function GET(request: Request) {
|
|
150
|
+
let accountId = new URL(request.url).pathname.split("/").at(-1) ?? "";
|
|
151
|
+
assertUUID(accountId);
|
|
152
|
+
|
|
153
|
+
return Response.json(await loadAccount(accountId));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function loadAccount(accountId: UUID) {
|
|
157
|
+
// no re-validation: the type already carries the check
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The throw is the point: an identifier that never validated never reaches the query.
|
|
162
|
+
|
|
163
|
+
## Pattern: Branching Instead Of Throwing
|
|
164
|
+
|
|
165
|
+
A form field is a value a person can get wrong, which makes an exception the wrong shape for
|
|
166
|
+
the answer. `isUUID` gives you the same narrowing in an expression you can branch on:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import type { UUID } from "@sdxc/uuid";
|
|
170
|
+
|
|
171
|
+
import { isUUID } from "@sdxc/uuid";
|
|
172
|
+
|
|
173
|
+
function readAccountId(input: FormData): { ok: true; accountId: UUID } | { ok: false } {
|
|
174
|
+
let value = input.get("accountId");
|
|
175
|
+
if (typeof value !== "string" || !isUUID(value)) return { ok: false };
|
|
176
|
+
return { ok: true, accountId: value };
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`isUUID` is `assertUUID` with the throw turned into a `false`, so the two agree on exactly
|
|
181
|
+
which strings are identifiers.
|
|
182
|
+
|
|
183
|
+
## Pattern: Prefixed Identifiers
|
|
184
|
+
|
|
185
|
+
A UUID says nothing about what it identifies, which is why an identifier pasted into a bug
|
|
186
|
+
report is so hard to place. [`@sdxc/typeid`](https://www.npmjs.com/package/@sdxc/typeid)
|
|
187
|
+
takes a `UUID` and a prefix and encodes them as one sortable, self-describing string:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { TypeID } from "@sdxc/typeid";
|
|
191
|
+
import { generateUUID } from "@sdxc/uuid";
|
|
192
|
+
|
|
193
|
+
let accountId = TypeID.fromUUID("account", generateUUID());
|
|
194
|
+
|
|
195
|
+
accountId.toString();
|
|
196
|
+
// "account_01h455vb4pex5vsknk084sn02q"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
It asks for a `UUID` rather than a `string`, so the value you generate here is accepted and a
|
|
200
|
+
random 36 characters is not.
|
|
201
|
+
|
|
202
|
+
## Versioning
|
|
203
|
+
|
|
204
|
+
Releases are dated rather than semantic. A version is the UTC date it was published,
|
|
205
|
+
written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one
|
|
206
|
+
release goes out per day.
|
|
207
|
+
|
|
208
|
+
Those numbers say when, not what: a later date means a later release and carries no
|
|
209
|
+
compatibility promise. Any release may change or remove an export.
|
|
210
|
+
|
|
211
|
+
Depend on one exact date, and move it when you are ready to take the change:
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"dependencies": {
|
|
216
|
+
"@sdxc/uuid": "2026.9.4"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every
|
|
222
|
+
later release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
227
|
+
|
|
228
|
+
## Author
|
|
229
|
+
|
|
230
|
+
[Sergio Xalambrí](https://sergiodxa.com)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
3
|
+
* @copyright Sergio Xalambrí 2026
|
|
4
|
+
*/
|
|
5
|
+
import { InvalidUUIDFormatError, InvalidUUIDLengthError, InvalidUUIDTypeError } from "./lib/errors.js";
|
|
6
|
+
import type { UUID as BaseUUID } from "node:crypto";
|
|
7
|
+
export type UUID = BaseUUID & {
|
|
8
|
+
__brand: "UUID";
|
|
9
|
+
};
|
|
10
|
+
export declare function isUUID(value: string): value is UUID;
|
|
11
|
+
export declare function assertUUID(value: string): asserts value is UUID;
|
|
12
|
+
export declare function generateUUID(): UUID;
|
|
13
|
+
export { InvalidUUIDFormatError, InvalidUUIDLengthError, InvalidUUIDTypeError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
3
|
+
* @copyright Sergio Xalambrí 2026
|
|
4
|
+
*/
|
|
5
|
+
import { InvalidUUIDFormatError, InvalidUUIDLengthError, InvalidUUIDTypeError, } from "./lib/errors.js";
|
|
6
|
+
export function isUUID(value) {
|
|
7
|
+
try {
|
|
8
|
+
assertUUID(value);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function assertUUID(value) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
throw new InvalidUUIDTypeError(typeof value);
|
|
18
|
+
if (value.length !== 36)
|
|
19
|
+
throw new InvalidUUIDLengthError(value.length);
|
|
20
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value)) {
|
|
21
|
+
throw new InvalidUUIDFormatError(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function generateUUID() {
|
|
25
|
+
let id = crypto.randomUUID();
|
|
26
|
+
assertUUID(id);
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
export { InvalidUUIDFormatError, InvalidUUIDLengthError, InvalidUUIDTypeError };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
3
|
+
* @copyright Sergio Xalambrí 2026
|
|
4
|
+
*/
|
|
5
|
+
/** Error thrown when a UUID string has an invalid length. */
|
|
6
|
+
export declare class InvalidUUIDLengthError extends Error {
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* Creates an invalid UUID length error.
|
|
10
|
+
* @param length Actual UUID length.
|
|
11
|
+
* @example
|
|
12
|
+
* throw new InvalidUUIDLengthError(10);
|
|
13
|
+
*/
|
|
14
|
+
constructor(length: number);
|
|
15
|
+
}
|
|
16
|
+
/** Error thrown when a UUID value is not a string. */
|
|
17
|
+
export declare class InvalidUUIDTypeError extends Error {
|
|
18
|
+
name: string;
|
|
19
|
+
/**
|
|
20
|
+
* Creates an invalid UUID type error.
|
|
21
|
+
* @param type Runtime type received instead of a UUID string.
|
|
22
|
+
* @example
|
|
23
|
+
* throw new InvalidUUIDTypeError("number");
|
|
24
|
+
*/
|
|
25
|
+
constructor(type: string);
|
|
26
|
+
}
|
|
27
|
+
/** Error thrown when a UUID string does not match the expected format. */
|
|
28
|
+
export declare class InvalidUUIDFormatError extends Error {
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* Creates an invalid UUID format error.
|
|
32
|
+
* @param uuid UUID value that failed format validation.
|
|
33
|
+
* @example
|
|
34
|
+
* throw new InvalidUUIDFormatError("not-a-uuid");
|
|
35
|
+
*/
|
|
36
|
+
constructor(uuid: string);
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
3
|
+
* @copyright Sergio Xalambrí 2026
|
|
4
|
+
*/
|
|
5
|
+
/** Error thrown when a UUID string has an invalid length. */
|
|
6
|
+
export class InvalidUUIDLengthError extends Error {
|
|
7
|
+
name = "InvalidUUIDLengthError";
|
|
8
|
+
/**
|
|
9
|
+
* Creates an invalid UUID length error.
|
|
10
|
+
* @param length Actual UUID length.
|
|
11
|
+
* @example
|
|
12
|
+
* throw new InvalidUUIDLengthError(10);
|
|
13
|
+
*/
|
|
14
|
+
constructor(length) {
|
|
15
|
+
super(`Invalid UUID length: ${length}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Error thrown when a UUID value is not a string. */
|
|
19
|
+
export class InvalidUUIDTypeError extends Error {
|
|
20
|
+
name = "InvalidUUIDTypeError";
|
|
21
|
+
/**
|
|
22
|
+
* Creates an invalid UUID type error.
|
|
23
|
+
* @param type Runtime type received instead of a UUID string.
|
|
24
|
+
* @example
|
|
25
|
+
* throw new InvalidUUIDTypeError("number");
|
|
26
|
+
*/
|
|
27
|
+
constructor(type) {
|
|
28
|
+
super(`Expected a string, got ${type}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Error thrown when a UUID string does not match the expected format. */
|
|
32
|
+
export class InvalidUUIDFormatError extends Error {
|
|
33
|
+
name = "InvalidUUIDFormatError";
|
|
34
|
+
/**
|
|
35
|
+
* Creates an invalid UUID format error.
|
|
36
|
+
* @param uuid UUID value that failed format validation.
|
|
37
|
+
* @example
|
|
38
|
+
* throw new InvalidUUIDFormatError("not-a-uuid");
|
|
39
|
+
*/
|
|
40
|
+
constructor(uuid) {
|
|
41
|
+
super(`Invalid UUID format: ${uuid}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sdxc/uuid",
|
|
3
|
+
"version": "0.0.0-pre.1",
|
|
4
|
+
"description": "Validate, assert and generate UUIDs behind a branded UUID type",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"gitHead": "e96a064f9208eb667923f57374e2cdaac73c72fb",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/sergiodxa/monorepo.git",
|
|
17
|
+
"directory": "packages/uuid"
|
|
18
|
+
}
|
|
19
|
+
}
|