Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user