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