Files
skills/docs/foundation/data/packer/src/integrity/integrity.md
2026-05-29 08:20:56 +01:00

4.4 KiB

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

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:

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 Uint8Arrays.
  • 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.