Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# 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.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```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<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.