# 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` interface across sync and async backends - `register` is last-write-wins; replacing an existing algorithm emits a `bitpacker-crypto.hash-registry.replaced` OTel span carrying the previous and new `outputBytes` attributes - Process-wide singleton via `defaultHashRegistry()`; isolated registries via `createHashRegistry()` - Unknown algorithm name throws `BitpackerError` with code `CRYPTO_HASH_ALGORITHM_UNKNOWN` and `context.algorithm` for diagnosability ## Usage: Basic Example ```ts 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 ```ts 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 tagging - `outputBytes: number` — exact digest length - `hash(data: Uint8Array): Promise` — compute the digest; resolves to exactly `outputBytes` bytes ### `interface HashAlgorithmRegistry` - `register(algorithm: HashAlgorithm): void` — last-write-wins; emits `bitpacker-crypto.hash-registry.replaced` span when overwriting - `resolve(name: string): HashAlgorithm` — throws `CRYPTO_HASH_ALGORITHM_UNKNOWN` on miss - `has(name: string): boolean` - `list(): 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 `sha256` is implemented over Web Crypto's `crypto.subtle.digest`, which returns a Promise. Synchronous algorithms wrap their result with `Promise.resolve` for uniformity. - `crc32` digests are emitted big-endian (most-significant byte first) so the byte order matches the algorithm's natural representation; `xxhash32` digests are emitted little-endian to match the bit-packed checksums already produced by `appendXxhash32`. - Replacement intentionally does not throw. The OTel `replaced` span is the observability hook for swap-ins; tests asserting registry hygiene should monitor the span stream rather than rely on exception flow. - `unregister` is intentionally absent. If you need a clean slate, build a fresh registry with `createHashRegistry()` and use that one.