4.6 KiB
HashAlgorithmRegistry
A pluggable registry of named hash algorithms used by every hashing-aware
primitive in @suaveplan/bitpacker-crypto (notably SchemaFingerprint and
HashChainSchema). Ships with three defaults out of the box: sha256 (32
bytes, Web Crypto), xxhash32 (4 bytes, in-tree), and crc32 (4 bytes,
in-tree). Consumers register additional algorithms — e.g. blake3 — via
register. Last-write-wins replacement is permitted and emits an OpenTelemetry
span so swap-ins are observable.
(no submodules)
Purpose
The crypto stack avoids hard-coding a single hash function inside every
primitive that needs one. Instead, hashing is a first-class extension point:
primitives accept an optional algorithm name and an optional registry, and
the registry resolves the name to a concrete HashAlgorithm. This lets a
deployment swap in a faster sha256 (e.g. native node:crypto) or register an
entirely new algorithm without forking the primitive.
The registry shape is intentionally tiny — register, resolve, has,
list — because the surface area is also a maintenance surface area. There is
no unregister: defaults are guaranteed present on every fresh registry, and
test isolation is achieved by calling createHashRegistry() to get a private
instance instead of mutating the singleton.
Features
- Three defaults bundled:
sha256(Web Crypto async),xxhash32(in-tree sync),crc32(in-tree sync, big-endian 4-byte digest) - Uniform async
hash(data) → Promise<Uint8Array>interface across sync and async backends registeris last-write-wins; replacing an existing algorithm emits abitpacker-crypto.hash-registry.replacedOTel span carrying the previous and newoutputBytesattributes- Process-wide singleton via
defaultHashRegistry(); isolated registries viacreateHashRegistry() - Unknown algorithm name throws
BitpackerErrorwith codeCRYPTO_HASH_ALGORITHM_UNKNOWNandcontext.algorithmfor diagnosability
Usage: Basic Example
import { defaultHashRegistry } from "@suaveplan/bitpacker-crypto";
const sha256 = defaultHashRegistry().resolve("sha256");
const digest = await sha256.hash(new TextEncoder().encode("hello"));
console.log(digest.length); // 32
Usage: Advanced Example — registering BLAKE3
import {
type HashAlgorithm,
createHashRegistry,
} from "@suaveplan/bitpacker-crypto";
import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
const blake3: HashAlgorithm = {
name: "blake3",
outputBytes: 32,
hash: (data) => Promise.resolve(nobleBlake3(data)),
};
// Build an isolated registry so we don't pollute the singleton.
const registry = createHashRegistry();
registry.register(blake3);
// Use with SchemaFingerprint / HashChainSchema:
// schemaFingerprint(schema, "blake3", registry)
API Reference
interface HashAlgorithm
name: string— stable identifier used for registry lookup and wire-format taggingoutputBytes: number— exact digest lengthhash(data: Uint8Array): Promise<Uint8Array>— compute the digest; resolves to exactlyoutputBytesbytes
interface HashAlgorithmRegistry
register(algorithm: HashAlgorithm): void— last-write-wins; emitsbitpacker-crypto.hash-registry.replacedspan when overwritingresolve(name: string): HashAlgorithm— throwsCRYPTO_HASH_ALGORITHM_UNKNOWNon misshas(name: string): booleanlist(): readonly string[]— names in stable sort order
createHashRegistry(): HashAlgorithmRegistry
Fresh registry pre-populated with the defaults. Independent of the singleton; mutations stay local.
defaultHashRegistry(): HashAlgorithmRegistry
Process-wide singleton. Lazily constructed on first call. Subsequent calls return the same instance.
Implementation Notes
- The interface is asynchronous because
sha256is implemented over Web Crypto'scrypto.subtle.digest, which returns a Promise. Synchronous algorithms wrap their result withPromise.resolvefor uniformity. crc32digests are emitted big-endian (most-significant byte first) so the byte order matches the algorithm's natural representation;xxhash32digests are emitted little-endian to match the bit-packed checksums already produced byappendXxhash32.- Replacement intentionally does not throw. The OTel
replacedspan is the observability hook for swap-ins; tests asserting registry hygiene should monitor the span stream rather than rely on exception flow. unregisteris intentionally absent. If you need a clean slate, build a fresh registry withcreateHashRegistry()and use that one.