# 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.