# 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`. _(no submodules)_ ## 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` - `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` - 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.