# `integrity/integrity` — BLAKE3 per-entry digest and bundle-level Merkle root _(no submodules)_ ## Purpose `@suaveplan/packer/integrity` provides the content-addressable integrity layer the rest of the package uses to detect any tampering or accidental corruption of packed bundles. It computes a per-entry BLAKE3-256 digest and a bundle-level Merkle root over the sorted entry digests; both are recorded in the produced envelope (whole-file `PackedDataMetadata.integrity`, chunked `Manifest.merkleRoot` plus per-entry `ManifestChunkEntry.entryDigest`) and verified on read. Verification coexists with the existing ed25519 signing pipeline — neither replaces the other — because BLAKE3 is fast enough to run on every chunk without measurable overhead while ed25519 still anchors the publisher's identity. ## Features - BLAKE3-256 one-shot digest of arbitrary bytes via `@noble/hashes/blake3`. - `merkleRootBlake3(digests)` over already-sorted entry digests. - `computeWholeFileIntegrity(payload)` convenience producing the `{ entryDigest, merkleRoot }` pair stored on `PackedDataMetadata.integrity`. - Constant-time `bytesEqual` for use in verify paths. - Strict input validation: every digest must be exactly 32 bytes; mismatches raise `TypeError` (programmer error — not a packer error code). ## Usage: Basic Example ```ts import { entryDigestBlake3, merkleRootBlake3, computeWholeFileIntegrity, } from "@suaveplan/packer/integrity"; const payload = new TextEncoder().encode("hello world"); const digest = entryDigestBlake3(payload); console.log(digest.byteLength); // 32 const integrity = computeWholeFileIntegrity(payload); console.log(integrity.entryDigest.byteLength); // 32 console.log(integrity.merkleRoot.byteLength); // 32 ``` ## Usage: Advanced Example Build a Merkle root over a chunked bundle's entries, sorted by `chunkId`: ```ts import { entryDigestBlake3, merkleRootBlake3, bytesEqual, } from "@suaveplan/packer/integrity"; const chunks = [/* wire bytes from packChunks(...) */]; const digests = chunks .map((c) => ({ id: c.chunkId, d: entryDigestBlake3(c.bytes) })) .sort((a, b) => a.id - b.id) .map((x) => x.d); const root = merkleRootBlake3(digests); // Later, on read: const recomputed = merkleRootBlake3(digests); if (!bytesEqual(root, recomputed)) { throw new Error("Merkle root drifted between pack and verify"); } ``` ## API Reference - `entryDigestBlake3(data: Uint8Array): Uint8Array` — BLAKE3-256 of `data`. Returns a fresh 32-byte array. - `merkleRootBlake3(digests: readonly Uint8Array[]): Uint8Array` — BLAKE3-256 of the byte concatenation of `digests` (caller supplies sort order). Empty input maps to `blake3("")`. Each digest must be 32 bytes; mismatches throw `TypeError`. - `computeWholeFileIntegrity(payload): PackedDataIntegrity` — single-entry convenience returning `{ entryDigest: blake3(payload), merkleRoot: blake3(blake3(payload)) }`. - `bytesEqual(a, b): boolean` — constant-time byte-equality test on two `Uint8Array`s. - `BLAKE3_DIGEST_LEN: 32` — exported constant for callers that need the digest length without recomputing. - `PackedDataIntegrity` — `{ entryDigest, merkleRoot }` shape stored on `PackedDataMetadata.integrity`. ## Implementation Notes - BLAKE3 is sourced from `@noble/hashes/blake3`. The package adds `@noble/hashes` as a direct dependency (catalog `crypto`) rather than going through `@suaveplan/crypto-hash`, which does not yet surface BLAKE3 — exposing it there is tracked separately and is out of scope for this change. - The Merkle root scheme is intentionally flat (single-level hash of sorted concatenated leaves) rather than a binary tree. The bundle-level root's job is to detect any leaf substitution, which a flat root accomplishes; binary Merkle proofs are not in the public surface and adding them later does not invalidate the flat root. - Domain separation: the whole-file Merkle root wraps the lone entry digest in a second BLAKE3 call so that a verifier never confuses a leaf digest with a root. The same applies to chunked manifests — `merkleRoot` is always `blake3(concat(...))`, never the bare concatenation. - `bytesEqual` is intentionally constant-time. Integrity verification is not a cryptographic primitive (the signature does that), but the verify path sits on the request hot path and constant-time comparisons keep the semantics consistent with the rest of the package.