Files
2026-05-27 23:47:53 +01:00

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

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 tagging
  • outputBytes: number — exact digest length
  • hash(data: Uint8Array): Promise<Uint8Array> — 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.