Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export { generateUuid, uint8ArrayToUuid, uuidToUint8Array } from "./uuid.js";
export { UuidStringSchema } from "./uuid.schemas.js";

View File

@@ -0,0 +1,97 @@
# uuid
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `uuid` module provides three focused primitives for working with Universally Unique Identifiers in `@suaveplan/bytes`: generating cryptographically random UUIDv4 strings, converting canonical UUID strings to their compact 16-byte binary representation, and converting a 16-byte `Uint8Array` back to the canonical hyphen-separated string. These operations are essential for any system that stores entity identifiers as binary blobs in databases, network protocols, or binary serialisation formats while presenting them as human-readable strings at the API boundary.
All three functions validate their inputs strictly and throw typed `BytesError` values on malformed data, making them safe to use directly in request handlers and protocol parsers.
## Features
- **`generateUuid`** — generates a UUIDv4 string using the platform `crypto.randomUUID()` — no third-party uuid library, no custom random-number manipulation.
- **`uuidToUint8Array`** — parses a canonical UUID string into exactly 16 bytes; validates format with a regex before any byte parsing.
- **`uint8ArrayToUuid`** — formats a 16-byte `Uint8Array` as a canonical lowercase UUID string; validates length before formatting.
- **OTel tracing** — every function is wrapped in a named active span under the `@suaveplan/bytes` tracer.
- **Zod schema** — `UuidStringSchema` validates the canonical UUID format and is composable with any Zod object schema.
## Usage: Basic Example
```ts
import { generateUuid, uuidToUint8Array, uint8ArrayToUuid } from "@suaveplan/bytes/uuid";
// Generate a fresh UUIDv4
const id = generateUuid();
// → "550e8400-e29b-41d4-a716-446655440000" (random each call)
// Convert to binary for compact storage
const bytes = uuidToUint8Array(id);
// → Uint8Array of length 16
// Restore the canonical string
const restored = uint8ArrayToUuid(bytes);
// → same as `id`
```
## Usage: Advanced Example
```ts
import { uuidToUint8Array, uint8ArrayToUuid, BytesError } from "@suaveplan/bytes";
import { UuidStringSchema } from "@suaveplan/bytes/uuid";
import z from "zod";
// Validate and convert an incoming UUID header to binary for DB storage
const RequestSchema = z.object({
entityId: UuidStringSchema,
});
function handleRequest(raw: unknown): Uint8Array {
const { entityId } = RequestSchema.parse(raw);
try {
return uuidToUint8Array(entityId);
} catch (err) {
if (err instanceof BytesError) {
// err.code === "BYTES.UUID.INVALID_FORMAT"
throw new Error(`Malformed entity ID: ${err.message}`);
}
throw err;
}
}
// Convert back to string for API responses
function toApiResponse(raw: Uint8Array): string {
return uint8ArrayToUuid(raw);
}
```
## API Reference
### `generateUuid(): string`
Generate a UUID version 4 string using `crypto.randomUUID()`. Output is always lowercase and matches the pattern `/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/`.
### `uuidToUint8Array(uuid: string): Uint8Array`
Parse a canonical UUID string (upper- or lowercase) into a 16-byte `Uint8Array`. Strips the four hyphens, then reads each pair of hex digits as a byte.
**Throws** `BytesError(BYTES.UUID.INVALID_FORMAT)` when `uuid` does not match the canonical pattern `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
### `uint8ArrayToUuid(bytes: Uint8Array): string`
Format a 16-byte `Uint8Array` as a canonical lowercase UUID string in the form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
**Throws** `BytesError(BYTES.UUID.INVALID_LENGTH)` when `bytes.length !== 16`.
### `UuidStringSchema` (from `uuid.schemas.ts`)
Zod schema that validates the canonical UUID format. Accepts both upper- and lowercase hex digits. Does not verify UUID version or variant bits.
## Implementation Notes
`generateUuid` delegates entirely to the platform `crypto.randomUUID()` (Node 14.17+, Bun, all modern browsers), ensuring the UUID is generated from a cryptographically secure source of randomness with no custom bit-twiddling. A polyfill that sets `globalThis.crypto = require("node:crypto").webcrypto` is included in the package test setup so that Node `forks`-pool Vitest runs have access to the `crypto` global.
`uuidToUint8Array` validates the input with a compiled regex before any byte parsing. This rejects inputs that are structurally wrong (wrong group lengths, non-hex chars, missing hyphens) with a single check rather than per-byte error detection.
`uint8ArrayToUuid` materialises a hex string from the byte array, then splices in the four hyphens at the standard offsets (8, 12, 16, 20 hex chars). Output is always lowercase because `Number.prototype.toString(16)` produces lowercase hex on all platforms.

View File

@@ -0,0 +1,15 @@
import z from "zod";
/**
* Zod schema for a canonical UUID string.
*
* Validates the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x`
* is a hexadecimal digit (0–9, a–f, A–F). Does not verify the version or
* variant bits — use `generateUuid` to obtain a guaranteed UUIDv4.
*/
export const UuidStringSchema = z
.string()
.regex(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
"Must be a valid UUID string in canonical form"
);

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { generateUuid, uint8ArrayToUuid, uuidToUint8Array } from "./uuid.js";
describe("uuid round-trip", () => {
it("round-trips a known UUID", { timeout: 5000 }, () => {
const uuid = "550e8400-e29b-41d4-a716-446655440000";
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
});
it("uuidToUint8Array produces 16 bytes", { timeout: 5000 }, () => {
expect(
uuidToUint8Array("550e8400-e29b-41d4-a716-446655440000")
).toHaveLength(16);
});
it("throws on invalid UUID", { timeout: 5000 }, () => {
expect(() => uuidToUint8Array("not-a-uuid")).toThrow();
});
it("throws on UUID with wrong structure", { timeout: 5000 }, () => {
expect(() =>
uuidToUint8Array("550e8400e29b41d4a716446655440000")
).toThrow();
});
it("uint8ArrayToUuid throws on non-16-byte array", {
timeout: 5000,
}, () => {
expect(() => uint8ArrayToUuid(new Uint8Array(15))).toThrow();
});
it("uint8ArrayToUuid throws on empty array", { timeout: 5000 }, () => {
expect(() => uint8ArrayToUuid(new Uint8Array(0))).toThrow();
});
it("round-trips all-zeros UUID", { timeout: 5000 }, () => {
const uuid = "00000000-0000-0000-0000-000000000000";
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
});
it("round-trips all-fs UUID", { timeout: 5000 }, () => {
const uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff";
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
});
});
describe("generateUuid", () => {
it("generates a valid UUID v4 string", { timeout: 5000 }, () => {
const uuid = generateUuid();
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);
});
it("generates unique values", { timeout: 5000 }, () => {
expect(generateUuid()).not.toBe(generateUuid());
});
it("output is lowercase", { timeout: 5000 }, () => {
const uuid = generateUuid();
expect(uuid).toBe(uuid.toLowerCase());
});
it("generated UUID can round-trip through uint8Array", {
timeout: 5000,
}, () => {
const uuid = generateUuid();
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
});
});

View File

@@ -0,0 +1,87 @@
import { trace } from "@opentelemetry/api";
import { BYTES_CODES, BytesError } from "../errors/bytes-error.js";
const tracer = trace.getTracer("@suaveplan/bytes");
/** UUID canonical format: 8-4-4-4-12 lowercase hex groups separated by hyphens. */
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* Generate a UUID v4 string using the platform `crypto.randomUUID()`.
*
* The output is always lowercase and conforms to the canonical
* `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` format.
*/
export function generateUuid(): string {
return tracer.startActiveSpan("bytes.uuid.generateUuid", (span) => {
try {
return crypto.randomUUID();
} finally {
span.end();
}
});
}
/**
* Parse a canonical UUID string into a 16-byte `Uint8Array`.
*
* Accepts both upper- and lowercase hex. Dashes are stripped before parsing.
*
* @throws `BytesError(BYTES.UUID.INVALID_FORMAT)` when `uuid` does not
* match the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` pattern.
*/
export function uuidToUint8Array(uuid: string): Uint8Array {
return tracer.startActiveSpan("bytes.uuid.uuidToUint8Array", (span) => {
try {
if (!UUID_RE.test(uuid)) {
throw new BytesError(
BYTES_CODES["UUID.INVALID_FORMAT"],
`Invalid UUID format: "${uuid}". Expected canonical form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.`
);
}
const hex = uuid.replace(/-/g, "");
const result = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
result[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return result;
} finally {
span.end();
}
});
}
/**
* Format a 16-byte `Uint8Array` as a canonical lowercase UUID string.
*
* @throws `BytesError(BYTES.UUID.INVALID_LENGTH)` when `bytes` is not
* exactly 16 bytes long.
*/
export function uint8ArrayToUuid(bytes: Uint8Array): string {
return tracer.startActiveSpan("bytes.uuid.uint8ArrayToUuid", (span) => {
try {
if (bytes.length !== 16) {
throw new BytesError(
BYTES_CODES["UUID.INVALID_LENGTH"],
`Invalid UUID byte array length: ${bytes.length}. Expected exactly 16 bytes.`
);
}
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join("-");
} finally {
span.end();
}
});
}