12 KiB
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 underpackages/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)— usesbtoa/atobin browser,Bufferfallback in Node. - ADDED:
utf8ToBytes(str)/bytesToUtf8(bytes)—TextEncoder/TextDecoderround-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 injectableRngfrom@suaveplan/types; defaults tocrypto.getRandomValues. Genesis policy bans module-scopeMath.random(). - ADDED:
uint8ArraysEqual(a, b)— non-constant-time equality (the constant-time variant lives in@suaveplan/crypto-utils::constant-time; documented inuint8-array.mdto prevent misuse in cryptographic contexts). - ADDED:
concatUint8Arrays(...arrays)— accepts both varargs and a single array argument; throws on non-Uint8Array elements viaUtilsError(UINT8_ARRAY.INVALID_ARGUMENT). - ADDED:
sliceUint8Array(data, splitAt)— returns[head, tail]tuple; throws on out-of-bounds viaUtilsError(UINT8_ARRAY.OUT_OF_BOUNDS). - ADDED:
DataFormatandInputTypetypes underuint8-array.types.ts. - ADDED:
uint8-array.schemas.ts— Zod schemas forDataFormatandconvertDataFormatoptions. - ADDED: subpath export
./uint8-arrayinpackage.json::exportsfor 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-jsonfrom@suaveplan/utils. Re-exporting them fromutilswould 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-timeto keep the dependency direction one-way (utils → no crypto). Theuint8-array.mddoc 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 thinstreamHexEncodeadapter can re-export from there. No streaming work in this change. InputType = ArrayBufferoverload. Legacy acceptsArrayBufferindirectly via a private detector. Genesis narrows the public type tostring | Uint8Array; callers withArrayBuffershouldnew Uint8Array(buf)before passing. Documented inuint8-array.md.- UUID v1 / v3 / v5 / v7. Only v4 generation is in scope. Other versions belong in a future
@suaveplan/uuidpackage or are obtainable fromcrypto.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—UtilsErrorcodes thrown from production paths; injectedRngused bygenerateUuid; Zod schemas reject invalid formats; subpath./uint8-arrayresolves 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.jsonreturning onlypackages/core/utils/package.jsonitself).
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.