5.2 KiB
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) viaHashAlgorithmRegistryand pass it asregistryargument assertSchemaFingerprint(schema, expected, ...)is the single-call handshake check; throwsCRYPTO_SCHEMA_FINGERPRINT_MISMATCHwith a context that includes hexexpectedandactualfor 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.fingerprintwith algorithm + payload_bytes
Usage: Basic Example
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
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 algorithmregistry: HashAlgorithmRegistry = defaultHashRegistry()— optional override- Throws
CRYPTO_HASH_ALGORITHM_UNKNOWNif the algorithm isn't registered;CRYPTO_SCHEMA_FINGERPRINT_MISMATCHif 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. ThrowsCRYPTO_SCHEMA_FINGERPRINT_MISMATCHon disagreement, withcontext.expectedandcontext.actualhex-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 asnull),number(must be finite),boolean,string,bigint(suffixed withn),object(keys sorted lexicographically),array(order preserved). All other types throwCRYPTO_SCHEMA_FINGERPRINT_MISMATCH. bigintvalues are serialised as123n(with thensuffix) 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
assertSchemaFingerprintmatters 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.algorithmandbitpacker.crypto.payload_bytesso 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.