132 lines
12 KiB
Markdown
132 lines
12 KiB
Markdown
# Utils completion — Uint8Array helpers (countries / languages / safe-json placed elsewhere)
|
|
|
|
**Package**: `@suaveplan/utils`
|
|
**Status**: PROPOSED
|
|
**Type**: spec-conformance + feature
|
|
|
|
## Why
|
|
|
|
The 2026-05-02 audit of `packages/core/utils/src` against `../suaveplan/packages/universal/utils/src` flagged four legacy modules that genesis appears to lack: `countries/`, `languages/`, `json/safe-json.ts`, and `uint/`. Closer inspection shows three of the four are not gaps in `@suaveplan/utils` at all — they were intentionally hoisted into dedicated genesis packages (`@suaveplan/locale-data`, `@suaveplan/json`). The fourth, `uint/`, was dropped without a replacement and is the genuine gap.
|
|
|
|
The legacy `uint/` module (340 L) is misnamed — it is a Uint8Array / encoding helper bag covering hex/base64/UTF-8 round-trips, format auto-detection, UUID interop, equality, concatenation, and slicing. Crypto-utils already covers hex / base64 / concat / xor / equality for cryptographic call sites, but those helpers are deliberately scoped to the crypto plugin (no UUID dependency, no auto-detection). Application-level callers — drag-and-drop file readers, clipboard handlers, query-string parsers — need a dependency-light Uint8Array module they can pull from `@suaveplan/utils` without dragging the entire crypto stack. Without it, every consumer reimplements `bytesToHex` / `hexToBytes`, drift is inevitable, and we accumulate the kind of subtle off-by-one bugs the legacy harness already pinned tests for.
|
|
|
|
Hoisting `countries`, `languages`, and `safe-json` to dedicated packages was the right call (each has its own catalog, schemas, and OTel surface), but the parity table for `utils-completion` must say so explicitly so the next agent doesn't accidentally re-introduce them under `utils/` and create two copies that drift.
|
|
|
|
## What changes
|
|
|
|
### `@suaveplan/utils` (this change)
|
|
|
|
- ADDED: `uint8Array/` feature module under `packages/core/utils/src/uint8-array/` — encoding-format conversion + Uint8Array helpers tier appropriate for application-level (non-cryptographic) call sites.
|
|
- ADDED: `bytesToHex(bytes)` / `hexToBytes(hex)` — strict hex validation (rejects empty string, odd-length non-prefixed input, non-hex characters via opt-in strict mode).
|
|
- ADDED: `bytesToBase64(bytes)` / `base64ToBytes(base64)` — uses `btoa` / `atob` in browser, `Buffer` fallback in Node.
|
|
- ADDED: `utf8ToBytes(str)` / `bytesToUtf8(bytes)` — `TextEncoder` / `TextDecoder` round-trip.
|
|
- ADDED: `detectInputFormat(input)` — heuristic format auto-detection for `"hex" | "utf8" | "uint8array"`.
|
|
- ADDED: `validateInputTypeFormat(input, format)` — runtime check that the JS type matches the declared format.
|
|
- ADDED: `inputToUint8Array(input, format)` — universal coerce-to-bytes entry point.
|
|
- ADDED: `formatOutput(bytes, format)` — universal format-from-bytes entry point.
|
|
- ADDED: `convertDataFormat(input, outputFormat, inputFormat?)` — round-trip convertor with optional auto-detection.
|
|
- ADDED: `uint8ArrayToUuid(bytes)` / `uuidToUint8Array(uuid)` — RFC 4122 16-byte ↔ canonical string interop.
|
|
- ADDED: `generateUuid()` — UUID v4 emission backed by an injectable `Rng` from `@suaveplan/types`; defaults to `crypto.getRandomValues`. Genesis policy bans module-scope `Math.random()`.
|
|
- ADDED: `uint8ArraysEqual(a, b)` — non-constant-time equality (the constant-time variant lives in `@suaveplan/crypto-utils::constant-time`; documented in `uint8-array.md` to prevent misuse in cryptographic contexts).
|
|
- ADDED: `concatUint8Arrays(...arrays)` — accepts both varargs and a single array argument; throws on non-Uint8Array elements via `UtilsError` (`UINT8_ARRAY.INVALID_ARGUMENT`).
|
|
- ADDED: `sliceUint8Array(data, splitAt)` — returns `[head, tail]` tuple; throws on out-of-bounds via `UtilsError` (`UINT8_ARRAY.OUT_OF_BOUNDS`).
|
|
- ADDED: `DataFormat` and `InputType` types under `uint8-array.types.ts`.
|
|
- ADDED: `uint8-array.schemas.ts` — Zod schemas for `DataFormat` and `convertDataFormat` options.
|
|
- ADDED: subpath export `./uint8-array` in `package.json::exports` for tree-shaking.
|
|
|
|
### Out-of-scope mappings (NOT changed in this proposal — documented for parity)
|
|
|
|
The 2026-05-02 audit specifically asked whether `countries`, `languages`, and `safe-json` belong in `utils`. The answer is **no**, they live elsewhere; the parity checklist below is the source of truth so this question is not re-litigated.
|
|
|
|
| Capability | Upstream location | Genesis location | Status |
|
|
|---|---|---|---|
|
|
| Country code/name registry | `packages/universal/utils/src/countries/` (3021 L) | `packages/i18n/locale-data/src/countries/` | PRESENT-ELSEWHERE |
|
|
| Language code registry | `packages/universal/utils/src/languages/` (474 L) | `packages/i18n/locale-data/src/languages/` | PRESENT-ELSEWHERE |
|
|
| Safe JSON parse / stringify (proto-pollution guard) | `packages/universal/utils/src/json/safe-json.ts` (385 L) | `packages/data/json/src/safe-json/` | PRESENT-ELSEWHERE |
|
|
| Uint8Array / encoding helpers | `packages/universal/utils/src/uint/uint.ts` (340 L) | NONE | ADDED (this change) |
|
|
|
|
## Out of scope
|
|
|
|
- **Cross-publishing `countries` / `languages` / `safe-json` from `@suaveplan/utils`.** Re-exporting them from `utils` would re-introduce duplicate publish surface and break tree-shaking for non-i18n consumers. Callers wanting locale data import `@suaveplan/locale-data`; callers wanting safe JSON import `@suaveplan/json`. The entries in the parity table above are PRESENT-ELSEWHERE, not delegated.
|
|
- **Constant-time `uint8ArraysEqual`.** The cryptographic-grade comparison stays in `@suaveplan/crypto-utils::constant-time` to keep the dependency direction one-way (utils → no crypto). The `uint8-array.md` doc cross-links to it.
|
|
- **Streaming / chunked encoding.** Application-level callers wanting backpressure-aware streaming use `@suaveplan/streams` (not yet on genesis); when it lands, a thin `streamHexEncode` adapter can re-export from there. No streaming work in this change.
|
|
- **`InputType = ArrayBuffer` overload.** Legacy accepts `ArrayBuffer` indirectly via a private detector. Genesis narrows the public type to `string | Uint8Array`; callers with `ArrayBuffer` should `new Uint8Array(buf)` before passing. Documented in `uint8-array.md`.
|
|
- **UUID v1 / v3 / v5 / v7.** Only v4 generation is in scope. Other versions belong in a future `@suaveplan/uuid` package or are obtainable from `crypto.randomUUID` + format conversion already.
|
|
|
|
## Parity Checklist
|
|
|
|
Every legacy export traced, with disposition.
|
|
|
|
| Capability | Upstream | Migrated | Notes |
|
|
|---|---|---|---|
|
|
| `bytesToHex` | ✅ | ✅ | Verbatim port; default permissive (matches legacy), strict mode opt-in. |
|
|
| `hexToBytes` | ✅ | ✅ | Throws on empty input + non-hex; matches legacy error message. |
|
|
| `bytesToBase64` | ✅ | ✅ | `btoa` browser / `Buffer` Node fallback. |
|
|
| `base64ToBytes` | ✅ | ✅ | `atob` browser / `Buffer` Node fallback. |
|
|
| `utf8ToBytes` / `bytesToUtf8` | ✅ | ✅ | Native `TextEncoder` / `TextDecoder`. |
|
|
| `_detectFormat` (private) | ✅ | 🚫 | Replaced by `detectInputFormat` (the public version). Internal helper not needed once `ArrayBuffer` overload is dropped. |
|
|
| `detectInputFormat` | ✅ | ✅ | Public API stays. |
|
|
| `validateInputTypeFormat` | ✅ | ✅ | Throws via `UtilsError` instead of plain `Error`. |
|
|
| `inputToUint8Array` | ✅ | ✅ | Verbatim. |
|
|
| `formatOutput` | ✅ | ✅ | Verbatim. |
|
|
| `convertDataFormat` | ✅ | ✅ | Verbatim. |
|
|
| `uint8ArrayToUuid` | ✅ | ✅ | Backed by `uuid` catalog. |
|
|
| `uuidToUint8Array` | ✅ | ✅ | Backed by `uuid` catalog. |
|
|
| `generateUuid` | ✅ | ⬆️ | Enhanced: injectable `Rng`, defaults to `crypto.getRandomValues`. Genesis bans module-scope `Math.random()`. |
|
|
| `uint8ArraysEqual` | ✅ | ✅ | Non-constant-time (matches legacy). Crypto-grade comparison delegated to `@suaveplan/crypto-utils`. |
|
|
| `concatUint8Arrays` | ✅ | ✅ | Same dual signature (varargs OR single array). `UtilsError` instead of plain `Error`. |
|
|
| `sliceUint8Array` | ✅ | ✅ | `UtilsError` instead of plain `Error`. |
|
|
| `DataFormat` / `InputType` types | ✅ | ✅ | Co-located in `uint8-array.types.ts`. |
|
|
| `countries/*` (3021 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/locale-data::countries`. Cross-publishing rejected (see Out of scope). |
|
|
| `languages/*` (474 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/locale-data::languages`. Cross-publishing rejected. |
|
|
| `json/safe-json.ts` (385 L) | ✅ | 🚫 | PRESENT-ELSEWHERE: `@suaveplan/json::safe-json`. Cross-publishing rejected. |
|
|
|
|
## Source mapping
|
|
|
|
This change derives from `../suaveplan/packages/universal/utils/src/uint/`. There is no `enterprise/utils.md` for the encoding sub-feature — source mapping cites upstream source directly.
|
|
|
|
| Requirement | Source file | Source lines | Notes |
|
|
|---|---|---|---|
|
|
| `bytesToHex` / `hexToBytes` | `packages/universal/utils/src/uint/uint.ts` | L20-L58 | Verbatim; rewire `Error` → `UtilsError` with stable code. |
|
|
| `bytesToBase64` / `base64ToBytes` | `packages/universal/utils/src/uint/uint.ts` | L62-L91 | Verbatim. |
|
|
| `utf8ToBytes` / `bytesToUtf8` | `packages/universal/utils/src/uint/uint.ts` | L95-L105 | Verbatim. |
|
|
| `detectInputFormat` | `packages/universal/utils/src/uint/uint.ts` | L134-L149 | Verbatim. |
|
|
| `validateInputTypeFormat` | `packages/universal/utils/src/uint/uint.ts` | L154-L170 | Verbatim; `UtilsError` instead of `Error`. |
|
|
| `inputToUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L175-L194 | Verbatim. |
|
|
| `formatOutput` | `packages/universal/utils/src/uint/uint.ts` | L199-L216 | Verbatim. |
|
|
| `convertDataFormat` | `packages/universal/utils/src/uint/uint.ts` | L225-L252 | Verbatim. |
|
|
| `uint8ArrayToUuid` / `uuidToUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L257-L271 | Verbatim; backed by `uuid` catalog dep. |
|
|
| `generateUuid` | `packages/universal/utils/src/uint/uint.ts` | L275-L278 | Enhanced: injectable `Rng`. |
|
|
| `uint8ArraysEqual` | `packages/universal/utils/src/uint/uint.ts` | L283-L289 | Verbatim. |
|
|
| `concatUint8Arrays` | `packages/universal/utils/src/uint/uint.ts` | L294-L322 | Verbatim. |
|
|
| `sliceUint8Array` | `packages/universal/utils/src/uint/uint.ts` | L330-L340 | Verbatim. |
|
|
| `DataFormat` / `InputType` types | `packages/universal/utils/src/uint/uint.types.ts` | L1-L2 | Verbatim. |
|
|
|
|
### Requirements with no source line
|
|
|
|
| Requirement | Justification |
|
|
|---|---|
|
|
| `UtilsError` codes for `UINT8_ARRAY.INVALID_HEX`, `UINT8_ARRAY.INVALID_FORMAT`, `UINT8_ARRAY.INVALID_ARGUMENT`, `UINT8_ARRAY.OUT_OF_BOUNDS` | Genesis-wide policy that every thrown error route through `@suaveplan/error` (CLAUDE.md §Workflow rule 2). Legacy throws plain `Error`. |
|
|
| Injectable `Rng` for `generateUuid` | Genesis-wide determinism rule; module-scope `Math.random()` is banned. |
|
|
| Zod schemas for `DataFormat` and `convertDataFormat` options | Genesis-wide policy (CLAUDE.md "Zod schemas wired at every declared boundary"). |
|
|
| Module renamed from `uint/` to `uint8-array/` | The legacy name suggested typed-integer constructors (`uint8(n)`/`uint16(n)`/...) which it never delivered. The genesis name describes what the module actually does. The 2026-05-02 audit explicitly flagged this confusion. |
|
|
|
|
## Verification approach
|
|
|
|
- `src/uint8-array/uint8-array.test.ts` — every helper round-trip; error scenarios for invalid hex, invalid format, out-of-bounds slice; equality false on length mismatch; concat with mixed input shapes; UUID 16-byte validation.
|
|
- `src/uint8-array/uint8-array.spec-conformance.test.ts` — `UtilsError` codes thrown from production paths; injected `Rng` used by `generateUuid`; Zod schemas reject invalid formats; subpath `./uint8-array` resolves and tree-shakes.
|
|
|
|
## Impact
|
|
|
|
### Affected dependents
|
|
|
|
`@suaveplan/utils` currently has zero downstream packages on genesis (verified by `bunx turbo ls --filter=...@suaveplan/utils`). This is purely additive surface area — the change cannot break anything that doesn't yet exist.
|
|
|
|
- No dependents (verified by `grep -rln "@suaveplan/utils" /home/ubuntu/web/genesis/packages/ --include=package.json` returning only `packages/core/utils/package.json` itself).
|
|
|
|
When new consumers land, they import `@suaveplan/utils/uint8-array` or `@suaveplan/utils` (root barrel). Existing imports of `@suaveplan/utils/debug`, `./object`, etc. continue to work — this change adds a subpath, never removes one.
|
|
|
|
## Migration impact
|
|
|
|
None — additive only. No public surface is removed or renamed.
|