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,127 @@
# SchemaFingerprint
Content-hash a `BitPackerSchema` so two parties can agree they're using
byte-identical schemas before they exchange payloads. The fingerprint is
computed over a canonical serialisation (sorted keys, no whitespace,
deterministic encoding), and the hash algorithm is pluggable via
`HashAlgorithmRegistry`.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Whenever a producer and consumer of bit-packed bytes are not co-located —
mesh peers, drive replicas, network plugins, or two services that pinned
their schemas independently — they need a way to confirm "we're talking
the same shape" before risking a parse error on a half-mismatched schema.
A schema fingerprint compresses that question to a fixed-size digest that
can be compared in a handshake.
The fingerprint must be _order-insensitive_: a producer that declared
fields in declaration order `(foo, bar)` and a consumer that declared them
in the order `(bar, foo)` should fingerprint identically, because the wire
format itself is order-independent at the bit-packer level (object keys
participate, but their declaration order does not). Achieving that
requires canonical-serialisation, which this module owns.
## Features
- Default algorithm: `sha256` (32 bytes, cryptographically strong)
- Built-in alternatives: `xxhash32` (4 bytes, fast non-crypto), `crc32`
(4 bytes, fast non-crypto)
- Pluggable: register `blake3` (or any custom hash) via
`HashAlgorithmRegistry` and pass it as `registry` argument
- `assertSchemaFingerprint(schema, expected, ...)` is the single-call
handshake check; throws `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` with a
context that includes hex `expected` and `actual` for diagnosability
- Unknown algorithm name throws `CRYPTO_HASH_ALGORITHM_UNKNOWN`
- Functions inside the schema (e.g. `ConditionalSchema._if`) are rejected;
v1 fingerprintable schemas are data-shape only
- Constant-time digest comparison in `assertSchemaFingerprint` (so timing
side channels can't leak partial-match information)
- OTel span `bitpacker-crypto.fingerprint` with algorithm + payload_bytes
## Usage: Basic Example
```ts
import {
assertSchemaFingerprint,
schemaFingerprint,
} from "@suaveplan/bitpacker-crypto";
const schema = { foo: 255, bar: 65535 };
const fp = await schemaFingerprint(schema); // sha256 by default
// On the other side of a handshake, validate the schemas match:
await assertSchemaFingerprint(localSchema, fp);
```
## Usage: Advanced Example — registry-backed BLAKE3
```ts
import {
type HashAlgorithm,
createHashRegistry,
schemaFingerprint,
} 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)),
};
const registry = createHashRegistry();
registry.register(blake3);
const fp = await schemaFingerprint(schema, "blake3", registry);
```
## API Reference
### `schemaFingerprint(schema, algorithm?, registry?): Promise<Uint8Array>`
- `schema: BitPackerSchema` — the schema to hash. Functions and
non-finite numbers are rejected.
- `algorithm: string = "sha256"` — name of a registered algorithm
- `registry: HashAlgorithmRegistry = defaultHashRegistry()` —
optional override
- **Throws** `CRYPTO_HASH_ALGORITHM_UNKNOWN` if the algorithm isn't
registered; `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` if the schema contains
unsupported value types (functions, NaN, Infinity).
### `assertSchemaFingerprint(schema, expected, algorithm?, registry?): Promise<void>`
- Computes the fingerprint and compares it byte-wise (constant-time) to
`expected`. Throws `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` on disagreement,
with `context.expected` and `context.actual` hex-encoded for
diagnosability.
### `canonicalize(schema): string`
Exposed for testing the canonical-serialisation invariant directly.
Returns the JSON-like canonical string (sorted keys, no whitespace).
## Implementation Notes
- The canonicalizer is recursive and supports: `null`, `undefined` (treated
as `null`), `number` (must be finite), `boolean`, `string`, `bigint`
(suffixed with `n`), `object` (keys sorted lexicographically), `array`
(order preserved). All other types throw `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH`.
- `bigint` values are serialised as `123n` (with the `n` suffix) to keep
them distinguishable from numbers in the canonical form. JSON does not
natively support bigint, so this is a deliberate extension.
- Constant-time comparison in `assertSchemaFingerprint` matters even though
fingerprints are not secret — the operation is cheap, and using a
constant-time check avoids becoming a habit-of-non-constant-time-compare
at higher layers.
- The OTel span carries `bitpacker.crypto.algorithm` and
`bitpacker.crypto.payload_bytes` so a deployment can observe schema
agreement frequency and the size of canonicalised schemas.
- Function-bearing schemas (`ConditionalSchema`, `VersionSchema`) are out
of scope for v1 fingerprinting because there is no canonical way to hash
a function. A future version can add a side-channel (e.g. a stable name
registry mapping function identity → string) if a real consumer needs it.