Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
# CRC32 + Adler32 Checksums
## Purpose
Fast non-cryptographic checksums for bit-packed payloads. `crc32` implements the IEEE-802.3 / zlib polynomial (`0xedb88320`); `adler32` implements the RFC 1950 algorithm. Both are designed to detect accidental corruption (single-bit flips, wire-level errors, dropped bytes), not adversarial tampering — for cryptographic integrity, use the AEAD or signed-bitschema primitives in `@suaveplan/bitpacker-crypto`'s `encrypted/` and `signed/` modules.
The four wrappers (`appendCrc32`, `verifyAndStripCrc32`, `appendAdler32`, `verifyAndStripAdler32`) bookend a payload with a 4-byte big-endian checksum so framed bitpacker output can be transmitted over a noisy channel and integrity-checked at the receiving end. The wrappers throw on truncated input or checksum mismatch; the bare `crc32` / `adler32` functions return the raw 32-bit value for callers that want to integrate the check elsewhere.
## Features
- `crc32(data)` — IEEE-802.3 / zlib polynomial CRC32 with a precomputed 256-entry table
- `adler32(data)` — RFC 1950 Adler32 with the standard `MOD_ADLER = 65521` prime
- `appendCrc32` / `appendAdler32` — append 4-byte big-endian checksum
- `verifyAndStripCrc32` / `verifyAndStripAdler32` — verify and strip 4-byte checksum, throwing on mismatch or short input
- Pure JS, no dependencies, deterministic output, browser- and Node-safe
## Usage: Basic Example
```ts
import { crc32, appendCrc32, verifyAndStripCrc32 } from "@suaveplan/bitpacker-crypto";
const payload = new Uint8Array([1, 2, 3, 4]);
const checksum = crc32(payload); // 0xb63cfbcd
const framed = appendCrc32(payload); // 8 bytes: payload + 4-byte CRC
const restored = verifyAndStripCrc32(framed); // -> [1, 2, 3, 4]
```
## Usage: Advanced Example
```ts
import {
appendCrc32,
verifyAndStripCrc32,
appendAdler32,
verifyAndStripAdler32,
} from "@suaveplan/bitpacker-crypto";
// Frame a bit-packed payload, transmit, and validate on receive.
function transmit(payload: Uint8Array): Uint8Array {
return appendCrc32(payload);
}
function receive(framed: Uint8Array): Uint8Array {
try {
return verifyAndStripCrc32(framed);
} catch (err) {
// Caller decides: retransmit, drop, or escalate to a higher-level
// integrity check (signed-bitschema for adversarial channels).
throw err;
}
}
// Adler32 is faster but weaker; pick it for short payloads on trusted channels.
const cheap = appendAdler32(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
const back = verifyAndStripAdler32(cheap);
```
## API Reference
### `crc32(data: Uint8Array): number`
Returns the 32-bit unsigned CRC32 value of `data` using the IEEE-802.3 / zlib polynomial (`0xedb88320`). Uses a precomputed 256-entry table for one-byte-per-iteration throughput.
### `adler32(data: Uint8Array): number`
Returns the 32-bit unsigned Adler32 value of `data` per RFC 1950 (`a` and `b` accumulators with `MOD_ADLER = 65521`).
### `appendCrc32(data: Uint8Array): Uint8Array`
Returns a new array containing `data` followed by 4 big-endian bytes of `crc32(data)`.
### `verifyAndStripCrc32(data: Uint8Array): Uint8Array`
Returns the leading `data.length - 4` bytes if the trailing 4 bytes match `crc32(payload)`. Throws `Error` with `"Data too short for CRC32 verification"` if `data.length < 4`, or `"CRC32 mismatch: expected X, got Y"` on mismatch.
### `appendAdler32(data: Uint8Array): Uint8Array`
Returns a new array containing `data` followed by 4 big-endian bytes of `adler32(data)`.
### `verifyAndStripAdler32(data: Uint8Array): Uint8Array`
Mirrors `verifyAndStripCrc32` for Adler32. Same error semantics.
## Implementation Notes
- **CRC32 polynomial = `0xedb88320`.** This is the bit-reversed form of the standard IEEE-802.3 polynomial (`0x04c11db7`). The reversal is encoded in the table-precomputation loop (`c & 1 ? POLY ^ (c >>> 1) : c >>> 1`) so the streaming inner loop processes least-significant bits first, matching zlib / gzip / PNG.
- **Big-endian checksum framing.** Both append wrappers write the 32-bit checksum as big-endian bytes, matching network byte order. The `verifyAndStrip` counterparts read with `getUint32(offset, false)` (false = big-endian).
- **Not cryptographically secure.** A motivated attacker can construct collisions for both CRC32 and Adler32 in O(1). Use these for accidental-corruption detection only; use AEAD or signature primitives elsewhere in `@suaveplan/bitpacker-crypto` for adversarial integrity.
- **No allocation in the hot path.** `crc32` / `adler32` only allocate via the precomputed `CRC32_TABLE`. `appendCrc32` / `appendAdler32` allocate one output buffer; `verifyAndStripCrc32` / `verifyAndStripAdler32` allocate via `slice` for the returned payload.

View File

@@ -0,0 +1,174 @@
# EncryptedBitSchema
Niche AEAD-encrypted envelope around a bit-packed body. Wire format:
`[version u8][algTag u8][nonceLen u8][nonce][ciphertext+tag]`.
**Intended scope: small encrypted payloads only.** Token bodies,
key-wrap envelopes inside `MessageEnvelope`, sub-MTU control messages.
**NOT a chunked-file encoder** — `@suaveplan/packer` owns chunked-file
encryption with its locked v0x0001 codec.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Several suaveplan surfaces need a small, self-describing AEAD-encrypted
envelope: a token body wrapped for transport, an inner
key-wrap inside a higher-level message envelope, an opaque control
record signed-then-encrypted by upstream layers. Re-implementing the
"version + algTag + nonce + ciphertext" frame each time invites
nonce-reuse mistakes and silent algorithm drift.
`EncryptedBitSchema` standardises the frame, picks safe defaults
(XChaCha20-Poly1305 with 24-byte random nonce, 32-byte key, no
`Math.random` fallback), and refuses to be misused as a chunked-file
encoder by being explicit in this README and the schema name.
## Features
- Three AEAD algorithms supported: `AES-256-GCM`, `ChaCha20-Poly1305`,
`XChaCha20-Poly1305` (default — its 24-byte random nonce avoids
nonce-collision concerns even at high message volume)
- Stable u8 algorithm tags (0/1/2) on the wire so an envelope encoded
under one algorithm can be detected (and rejected) by a schema
expecting another
- Pluggable `Rng`; default uses `globalThis.crypto.getRandomValues` and
throws `CRYPTO_RNG_UNAVAILABLE` if Web Crypto is missing — never
silently falls back to `Math.random`
- Optional `aad` (additional authenticated data) — held by reference;
participates in the integrity tag so binding the envelope to a
surrounding context (session id, conversation id) is one option away
- Wire-format constants: `version=1`, no tag changes once allocated.
Adding a new algorithm only appends to the tag table.
- Tampered ciphertext rejects with `CRYPTO_DECRYPT_FAILED`
- Wrong-algorithm envelope rejects with `CRYPTO_ENVELOPE_VERSION_MISMATCH`
- Wrong-key decrypt rejects with `CRYPTO_DECRYPT_FAILED`
- OTel spans `bitpacker-crypto.encrypted-pack` /
`bitpacker-crypto.encrypted-unpack`
## Intended scope
This primitive is for **small** payloads. The wire format prepends a 3-byte
header + the full nonce + the AEAD output (ciphertext + 16-byte tag). For
a 100-byte body, that's ~143 bytes total — fine. For a 50 MB chunked file,
this is the wrong shape: you want chunked encryption with a streaming
verifier, which is what `@suaveplan/packer::ChunkedPacker` provides.
If you find yourself reaching for `EncryptedBitSchema` to wrap large
payloads, stop and use `@suaveplan/packer` instead. This warning is also
asserted by a spec-conformance test.
## Usage: Basic Example
```ts
import { defineEncryptedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto";
interface Token { userId: number; expiry: number }
const tokenSchema = defineBitSchema<Token>([
{ name: "userId", type: "uint", bits: 32 },
{ name: "expiry", type: "uint", bits: 32 },
]);
const encryptedToken = defineEncryptedBitSchema<Token>(
{
pack: (t) => tokenSchema.pack(t),
unpack: (bytes) => tokenSchema.unpack(bytes),
},
{
key: aeadKey,
aead: { encrypt: aeadEncrypt, decrypt: aeadDecrypt },
}
);
const wrapped = await encryptedToken.pack({ userId: 7, expiry: 1700000000 });
const original = await encryptedToken.unpack(wrapped);
```
The `aead` option is required and accepts an `AeadOps` bundle
(`{ encrypt, decrypt }`). The canonical implementation is the
`aeadEncrypt` / `aeadDecrypt` functions from `@suaveplan/crypto`. Passing
them via DI keeps `@suaveplan/bitpacker-crypto` free of any runtime
dependency on `@suaveplan/crypto`. The `AEAD_KEY_LENGTH`,
`AEAD_NONCE_LENGTH`, and `AEAD_XCHACHA_NONCE_LENGTH` constants are
declared locally in this primitive (not imported from
`@suaveplan/crypto`); duplication is acceptable because these values
are part of the bit-packer-crypto wire format.
## Usage: Advanced Example — bind to a session id via AAD
```ts
const sessionId = new TextEncoder().encode("session:abc123");
const encrypted = defineEncryptedBitSchema<Token>(codec, {
key,
aad: sessionId,
});
// Only an unpack with the SAME aad will succeed; replays into a different
// session will reject with CRYPTO_DECRYPT_FAILED.
```
## API Reference
### `interface EncryptedBitSchemaOptions`
- `key: Uint8Array` — exactly 32 bytes (`AEAD_KEY_LENGTH`)
- `aead: AeadOps` (required) — AEAD operations bundle; typically
`{ encrypt: aeadEncrypt, decrypt: aeadDecrypt }` from
`@suaveplan/crypto`
- `algorithm?: AeadAlgorithm = "XChaCha20-Poly1305"`
- `rng?: Rng` — defaults to a `globalThis.crypto.getRandomValues`-backed Rng
- `aad?: Uint8Array` — optional additional authenticated data
### `defineEncryptedBitSchema<TBody>(bodyCodec, options): EncryptedBitSchema<TBody>`
Returns:
- `pack(data: TBody): Promise<Uint8Array>`
- throws `CRYPTO_ENCRYPT_FAILED` on AEAD failure (very rare — usually a
misbehaving Rng or wrong key length)
- throws `CRYPTO_RNG_UNAVAILABLE` on first call if Web Crypto is missing
and no Rng was supplied
- `unpack(bytes: Uint8Array): Promise<TBody>`
- throws `CRYPTO_DECRYPT_FAILED` on tag mismatch / wrong key / too-short
input
- throws `CRYPTO_ENVELOPE_VERSION_MISMATCH` on wire-format/algorithm
mismatch
- `algorithm`, `version`, `nonceLength` (read-only)
### Algorithm tags (wire format)
```text
0 AES-256-GCM (12-byte nonce)
1 ChaCha20-Poly1305 (12-byte nonce)
2 XChaCha20-Poly1305 (24-byte nonce, default)
```
These numbers are wire-format and never renumbered. New algorithms append
to the table.
## Implementation Notes
- Nonces are caller-Rng-driven, never derived from message contents. The
default Rng wraps `crypto.getRandomValues` directly. Tests use a
seeded mulberry32 Rng to assert reproducibility — production deployments
must use a CSPRNG.
- AEAD failures are wrapped with `BitpackerError` so consumers don't need
to import the underlying `@suaveplan/crypto` `CryptoError` to handle
decrypt failures uniformly with the other bit-packer-crypto primitives.
- The 3-byte header is intentionally minimal — version + alg + nonceLen
is enough to dispatch a future algorithm without breaking older
payloads. The `nonceLen` byte is technically derivable from the
`algTag`, but storing it explicitly makes the parser simpler and
catches an algorithm/nonce mismatch as a wire-format error instead of
a more confusing AEAD failure.
- The body codec runs on the _plaintext_ — `pack` calls `bodyCodec.pack`
before encrypting, `unpack` calls `bodyCodec.unpack` after decrypting.
This means the body codec sees full plaintext and must be trusted not
to leak side-channels.
- AEAD encrypt/decrypt are imported directly from `@suaveplan/crypto`'s
main barrel; no engine override is exposed. If you need an engine
override (e.g. for a hardware-accelerated AEAD), build a custom Rng
wrapper or use `@suaveplan/crypto`'s engine-pluggable API directly
outside this primitive.

View File

@@ -0,0 +1,142 @@
# EnvelopeSchema
A typed, version-aware envelope schema for bit-packed payloads. Wraps a
caller-supplied `BodyCodec` with a `[magic][formatVersion][body]` frame so the
wire format is self-describing on the magic + version dimensions, while the
body remains pluggable. Replaces the manual
`defineBitSchema([{magic}, {formatVersion}, ...])` pattern that suaveplan
crypto, packer, and other consumers had been re-authoring.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Many suaveplan wire formats begin with a magic number + format version: chain
bundles (`SVCB`), public bundles, the chunked packer, and several network
encoders all do the same thing manually. The duplication isn't just code
churn — every callsite is a place where the magic/version contract can drift
out of sync with its parser. `EnvelopeSchema` consolidates the pattern and
makes "this byte string belongs to schema X version Y" a first-class
runtime check.
The body region is opaque to the envelope itself; encoding is delegated to a
`BodyCodec<TBody>` (`pack` + `unpack`). The bundled `rawBytesCodec()` is
provided for callers whose bodies are already-encoded bytes (typical when
composing with `SignedBitSchema` or `EncryptedBitSchema`); for structured
bodies, callers pass a codec built from the legacy `defineBitSchema(...)`
or from any encoder satisfying the two-method contract.
## Features
- `pack(body)` produces `[magic][formatVersion][bodyCodec.pack(body)]`
- `parse(bytes)` extracts magic + version + raw body bytes WITHOUT decoding
the body — enabling forward-compat dispatch when a future version arrives
- `parseStrict(bytes)` validates magic + version, then decodes the body to
`TBody` via `bodyCodec.unpack`
- Mismatched magic throws `BitpackerError` with code
`CRYPTO_ENVELOPE_MAGIC_MISMATCH` and `context.{expected,actual}`
- Mismatched version throws `BitpackerError` with code
`CRYPTO_ENVELOPE_VERSION_MISMATCH` and `context.{expected,actual}`
- Zod-validated options at construction; unknown keys rejected
- OTel spans `bitpacker-crypto.envelope-pack` and
`bitpacker-crypto.envelope-parse` carry `bitpacker.crypto.payload_bytes`
- Stays codec-agnostic so a future Protobuf-bodied envelope or a
schema-fingerprinted variant requires zero changes to envelope itself
## Usage: Basic Example
```ts
import {
type BodyCodec,
defineEnvelopeSchema,
} from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
interface Header {
foo: number;
bar: number;
}
const headerSchema = defineBitSchema<Header>([
{ name: "foo", type: "uint", bits: 8 },
{ name: "bar", type: "uint", bits: 16 },
]);
const codec: BodyCodec<Header> = {
pack: (body) => headerSchema.pack(body),
unpack: (bytes) => headerSchema.unpack(bytes),
};
const envelope = defineEnvelopeSchema<Header>({
magic: new Uint8Array([0x53, 0x56, 0x43, 0x42]), // "SVCB"
formatVersion: 1,
bodyCodec: codec,
});
const bytes = envelope.pack({ foo: 7, bar: 1234 });
const { foo, bar } = envelope.parseStrict(bytes); // typed: Header
```
## Usage: Advanced Example — forward-compat dispatch
Use `parse` to peek at magic + version before deciding which decoder to run,
so a v1 reader can detect v2 envelopes and route accordingly:
```ts
import { defineEnvelopeSchema } from "@suaveplan/bitpacker-crypto";
const envV1 = defineEnvelopeSchema({ magic, formatVersion: 1, bodyCodec: codecV1 });
const envV2 = defineEnvelopeSchema({ magic, formatVersion: 2, bodyCodec: codecV2 });
function decode(bytes: Uint8Array) {
const partial = envV1.parse(bytes);
if (partial.formatVersion === 1) return envV1.parseStrict(bytes);
if (partial.formatVersion === 2) return envV2.parseStrict(bytes);
throw new Error(`Unsupported envelope version: ${partial.formatVersion}`);
}
```
The body bytes are not consumed by `parse`, so the same input can be passed
back to `parseStrict` of the matching version without re-allocation cost on
the magic+version prefix.
## API Reference
### `interface BodyCodec<TBody>`
- `pack(body: TBody): Uint8Array` — encode the body
- `unpack(bytes: Uint8Array): TBody` — decode the body
### `interface EnvelopeSchemaOptions<TBody>`
- `magic: Uint8Array` — non-empty
- `formatVersion: number` — integer 0..255
- `bodyCodec: BodyCodec<TBody>` — body encoding hook
### `defineEnvelopeSchema<TBody>(options): EnvelopeSchema<TBody>`
Returns:
- `magic: Uint8Array`
- `formatVersion: number`
- `pack(body: TBody): Uint8Array`
- `parse(bytes: Uint8Array): { magic, formatVersion, bodyBytes }`
- `parseStrict(bytes: Uint8Array): TBody`
- throws `CRYPTO_ENVELOPE_MAGIC_MISMATCH` on magic mismatch
- throws `CRYPTO_ENVELOPE_VERSION_MISMATCH` on version mismatch
### `rawBytesCodec(): BodyCodec<Uint8Array>`
Pass-through codec for bodies that are already-encoded raw bytes — useful
when composing with `SignedBitSchema` or `EncryptedBitSchema` whose `pack`
output goes inside an envelope.
## Implementation Notes
- The envelope wire format is `[magic_bytes][1-byte u8 formatVersion][body_bytes]`. The body is NOT framed with a length prefix — the codec is expected to either be fixed-size or to consume the entire trailing buffer.
- `parse` throws `CRYPTO_ENVELOPE_MAGIC_MISMATCH` when input is shorter than `magic.length + 1` (the minimum header size). Magic and version validation in `parseStrict` happens against the parsed values, so a too-short input is caught at the parse stage.
- Magic comparison is constant-time-irrelevant — magic bytes are not secret. Equality is byte-wise. The error context includes hex-encoded `expected` and `actual` for diagnosability.
- The codec is held by reference; mutating the codec after `defineEnvelopeSchema(...)` returns will affect future `pack`/`parseStrict` calls. Callers should treat the codec as immutable in practice.
- OTel spans are emitted unconditionally (even when no SDK is wired) because they cost essentially nothing under the no-op tracer; production traces materialise once a tracer provider is installed.

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.

View File

@@ -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.

View File

@@ -0,0 +1,175 @@
# HashChainSchema
Append-only signed hash chain. Each entry carries a sequence number, the
hash of the previous entry, an opaque payload, and a host signature over
the bit-packed `(seq, prevHash, payload)` triplet. Tampering anywhere in
the chain is detected at the first bad index by `verifyChain`.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Audit logs, replicated mesh state, and any "I cannot edit history,
only append to it" scenario need a chain shape: each new entry binds to
the previous via a hash, and the host signs the binding so a third party
can verify the chain wasn't forged. suaveplan has the building blocks —
`Authority`, `DomainTag`, `HashAlgorithmRegistry`, `BodyCodec` — but no
primitive that composes them into a chain. `HashChainSchema` is that
primitive.
The signing input is `(seq, prevHash, payload)`, NOT just `payload`. This
is load-bearing: a signature on a payload alone could be lifted into a
different position in the chain (different seq) or grafted onto a different
prev-link without invalidating the signature. By binding seq + prev-hash
into the signed bytes, the signature is non-portable across positions.
## Features
- `appendEntry(seq, prevHash, payload, authority)` — sign the triplet,
return a complete `HashChainEntry`
- `verifyEntry(entry, expectedPrevHash, hostPublicSlot)` — boolean
result, never throws on bad signature (callers may probe candidate
parents)
- `verifyChain(entries, hostPublicSlot)` — walks the chain in order;
returns `{valid: false, firstBadIndex}` on the first failure
- `packEntry(entry)` / `unpackEntry(bytes)` — wire-format codec for
storage / network transport
- `hashEntry(entry)` — compute the entry's digest, used as the next
entry's `prevHash`
- Pluggable hash algorithm via `HashAlgorithmRegistry` (default
`sha256` 32 bytes)
- Strict prev-hash length check at append time (must match algorithm's
`outputBytes`) — catches schema/algorithm mismatches before they
reach the wire
- AbortSignal threaded through `Authority.sign`
- OTel spans `bitpacker-crypto.hashchain-append` and
`bitpacker-crypto.hashchain-verify`
## Wire Format
```text
┌────────────┬────────────────────┬──────────────────────┬──────────────────┐
│ seq u64BE │ prevHash N bytes │ payload bit-packed │ signature 64 B │
└────────────┴────────────────────┴──────────────────────┴──────────────────┘
```
`N` is the chosen hash algorithm's `outputBytes` (32 for sha256). The
signature is 64 bytes (ed25519); other lengths are configurable via
`signatureLength`.
## Usage: Basic Example
```ts
import { defineHashChainSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_BITPACKER_HASHCHAIN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface AuditPayload { actorId: number; eventCode: number }
const auditSchema = defineBitSchema<AuditPayload>([
{ name: "actorId", type: "uint", bits: 32 },
{ name: "eventCode", type: "uint", bits: 16 },
]);
const audit = defineHashChainSchema<AuditPayload>({
domain: DOMAIN_BITPACKER_HASHCHAIN_V1,
verifier: verifyAuthoritySignature,
payloadCodec: {
pack: (b) => auditSchema.pack(b),
unpack: (bytes) => auditSchema.unpack(bytes),
},
});
let prevHash = new Uint8Array(audit.hashLength); // suaveplan sentinel
const e0 = await audit.appendEntry(0n, prevHash, { actorId: 1, eventCode: 100 }, auth);
prevHash = await audit.hashEntry(e0);
const e1 = await audit.appendEntry(1n, prevHash, { actorId: 1, eventCode: 200 }, auth);
const result = await audit.verifyChain([e0, e1], hostPublicSlot);
// → { valid: true }
```
## Usage: Advanced Example — BLAKE3-backed chain
```ts
import {
type HashAlgorithm,
createHashRegistry,
defineHashChainSchema,
} from "@suaveplan/bitpacker-crypto";
import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
const registry = createHashRegistry();
registry.register({
name: "blake3",
outputBytes: 32,
hash: (data) => Promise.resolve(nobleBlake3(data)),
});
const chain = defineHashChainSchema({
domain: DOMAIN_BITPACKER_HASHCHAIN_V1,
payloadCodec,
hashAlgorithm: "blake3",
hashRegistry: registry,
});
```
## API Reference
### `interface HashChainEntry<TPayload>`
- `seq: bigint` — non-negative sequence number, packed as u64 BE
- `prevHash: Uint8Array` — exactly `hashLength` bytes
- `payload: TPayload` — caller-typed body
- `hostSignature: Uint8Array` — exactly `signatureLength` bytes
### `interface HashChainSchemaOptions<TPayload>`
- `domain: DomainTag` — required
- `verifier: AuthorityVerifier` — required (typically
`verifyAuthoritySignature` from `@suaveplan/crypto/authority`)
- `slot?: SlotId = "sign"` — slot used by the host authority
- `payloadCodec: BodyCodec<TPayload>` — required
- `hashAlgorithm?: string = "sha256"` — must be registered
- `hashRegistry?: HashAlgorithmRegistry` — defaults to singleton
- `signatureLength?: number = 64`
### `defineHashChainSchema<TPayload>(options): HashChainSchema<TPayload>`
Returns:
- `hashEntry(entry): Promise<Uint8Array>` — `outputBytes`-byte digest
- `appendEntry(seq, prevHash, payload, authority, signal?): Promise<HashChainEntry<TPayload>>`
- throws `VALUE_OUT_OF_RANGE` if seq is negative or > 2^64-1
- throws `CRYPTO_KEY_INVALID` if prevHash length ≠ hashLength
- throws `CRYPTO_SIGN_FAILED` if signature length ≠ signatureLength
- `verifyEntry(entry, expectedPrevHash, hostPublicSlot): Promise<boolean>`
- `verifyChain(entries, hostPublicSlot): Promise<ChainVerifyResult>`
- `packEntry(entry): Uint8Array`
- `unpackEntry(bytes): HashChainEntry<TPayload>`
- throws `CRYPTO_VERIFY_FAILED` if bytes too short
## Implementation Notes
- The suaveplan entry's `prevHash` is caller-supplied — typically all-zero
bytes, but any deterministic value works as long as both producers and
consumers agree on it. `verifyChain` treats the first entry's
`prevHash` as the trust anchor (it is not derived from anything).
- Tamper detection is positional: `firstBadIndex` is the LOWEST index at
which either the prev-hash linkage or the signature failed. A chain
with multiple tamper points reports only the first; a strict caller
who needs to know every bad index should iterate `verifyEntry`
directly.
- The hash algorithm is resolved once at construction. Swapping the
registry's algorithm later does not affect already-built schemas. This
is deliberate: the chain's wire format is fixed at construction.
- `appendEntry` does NOT compute the new `prevHash` — it only appends.
Callers are responsible for calling `hashEntry(prev)` between appends.
This separation allows callers to batch-append from a known
starting hash without forcing every append to be sequential.
- `verifyChain` rebuilds the `expectedPrev` for index `i ≥ 1` by hashing
`entries[i-1]`. The suaveplan entry's `prevHash` is taken as the trust
anchor; if it does not match what the producer supplied, every chain
built atop it will fail at `firstBadIndex: 0`.

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-crypto/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,131 @@
# MultiSignedBitSchema
Bit-packed payload co-signed by N independent authorities. Wire format is
`[bit-packed body][sig_1][sig_2]…[sig_N]`. Pinpoints which signer failed
on verify so a partial-trust caller can recover gracefully.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Some suaveplan surfaces require multiple signatures to authorise a single
payload — multi-party transaction approval, threshold-equivalent ceremony
records, and any quorum-style action. Building this on top of single-signer
`SignedBitSchema` would force every callsite to roll its own concatenation
and per-signer verify loop. `MultiSignedBitSchema` consolidates the
pattern with strict count enforcement (a 3-signer schema cannot pack with
2 authorities) and with `firstBadSignerIndex` reporting on verify so the
error can carry the actionable detail of _which_ signer failed.
## Features
- N-of-N co-signing with `signerCount` baked into the schema
- Pack rejects wrong-count authority arrays with
`CRYPTO_MULTISIGN_COUNT_MISMATCH`
- Unpack rejects wrong-count public-slot arrays with
`CRYPTO_MULTISIGN_COUNT_MISMATCH`
- `verify` returns `{ valid, firstBadSignerIndex? }` so callers can
enumerate "which signature broke?" without exception flow
- `unpack` throws `CRYPTO_VERIFY_FAILED` whose context includes
`firstBadSignerIndex`
- Each authority signs the same payload bytes under the same domain;
signature ordering follows the authority list
- AbortSignal threaded through every `Authority.sign` call so a slow
signer can be cancelled
- Pluggable body codec (same `BodyCodec<T>` as `SignedBitSchema`)
- OTel spans `bitpacker-crypto.multisigned-pack` and
`bitpacker-crypto.multisigned-verify` with signer_count + payload_bytes
## Usage: Basic Example
```ts
import { defineMultiSignedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_BITPACKER_MULTISIGN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface VoteBody { proposalId: number; vote: number }
const voteSchema = defineBitSchema<VoteBody>([
{ name: "proposalId", type: "uint", bits: 32 },
{ name: "vote", type: "uint", bits: 1 },
]);
const ms = defineMultiSignedBitSchema<VoteBody>(
{
pack: (b) => voteSchema.pack(b),
unpack: (bytes) => voteSchema.unpack(bytes),
},
{
domain: DOMAIN_BITPACKER_MULTISIGN_V1,
verifier: verifyAuthoritySignature,
signerCount: 3,
}
);
const packed = await ms.pack(
{ proposalId: 7, vote: 1 },
[authA, authB, authC]
);
const decoded = ms.unpack(packed, [pubA, pubB, pubC]);
```
## Usage: Advanced Example — graceful partial-trust recovery
```ts
const result = ms.verify(bytes, [pubA, pubB, pubC]);
if (!result.valid) {
// Tell the user EXACTLY which signer rejected the payload.
const failingSigner = signerNames[result.firstBadSignerIndex!];
throw new Error(`Quorum verify failed at signer "${failingSigner}"`);
}
```
## API Reference
### `interface MultiVerifyResult`
- `valid: boolean`
- `firstBadSignerIndex?: number` — present when `valid` is false; zero-indexed
### `interface MultiSignedBitSchemaOptions`
- `domain: DomainTag` — required
- `verifier: AuthorityVerifier` — required (typically
`verifyAuthoritySignature` from `@suaveplan/crypto/authority`)
- `slot?: SlotId = "sign"` — slot used by every authority
- `signerCount: number` — required, range 1..64
- `signatureLength?: number = 64` — wire-format signature length
### `defineMultiSignedBitSchema<T>(bodyCodec, options): MultiSignedBitSchema<T>`
Returns:
- `pack(data: T, authorities: readonly Authority[], signal?): Promise<Uint8Array>`
- `unpack(bytes: Uint8Array, signerPublicSlots: readonly KeySlotPublic<unknown>[]): T`
- `verify(bytes: Uint8Array, signerPublicSlots: readonly KeySlotPublic<unknown>[]): MultiVerifyResult`
- `domain`, `slot`, `signerCount`, `signatureLength`
## Implementation Notes
- Signatures are appended in authority list order and verified in the
same order. Re-ordering the public slot list at verify time will fail
every signature (ed25519 binds key to signature). This is intentional —
the wire format does not permute, so the schema's contract is
positional.
- Pack short-circuits early if any authority returns a wrong-length
signature, throwing `CRYPTO_SIGN_FAILED` with `signerIndex`. A
misbehaving authority cannot poison a partially-signed payload because
bytes are not assembled until every signature is ready.
- Verify does NOT short-circuit on the first failure when called via
`verify(...)` — that's a lie. It DOES short-circuit, returning
`firstBadSignerIndex`. The naming is deliberate: callers should not
rely on every signer being verified just because they asked to verify
N signatures. If you need exhaustive reporting (which signers failed,
which passed), iterate yourself with `verifyAuthoritySignature`.
- The `signerCount` upper bound (64) is conservative; raising it requires
no schema changes, but the on-wire payload grows linearly so very
large signer sets should consider threshold signing instead.
- AbortSignal is checked once per authority — a slow signer at index 5
can be cancelled, but signers 0-4 will already have signed.

View File

@@ -0,0 +1,158 @@
# SignedBitSchema
Bit-packed payload signed by an `Authority` under a fixed `DomainTag`. Wire
format is `[bit-packed body][signature]`. Verifies before decoding so a
tampered payload never even reaches the body codec. Replaces the ad-hoc
`BitPacker.pack(...) → sign(...) → concat(...)` pattern that crypto
chain bundles, public bundles, packer chunks, and game-server state-sync
encoders had been re-authoring.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Every signed wire format in the stack follows the same recipe:
bit-pack a structured body, sign the bytes with an authority slot, frame
`[body][signature]`, then on the verify side strip the signature, verify it
against a parent's public slot, and only then decode the body. Five plus
suaveplan/upstream call-sites had been manually re-authoring this dance, each
slightly differently. `SignedBitSchema` consolidates the pattern so the
domain separation, signature length, sign-slot dispatch, and verify-then-decode
order are all driven by a single primitive.
The domain `DomainTag` is required, not optional, so a signature produced
under one domain (e.g. `DOMAIN_PUBLIC_KEYCHAIN_V1`) cannot be replayed
under a different domain (e.g. `DOMAIN_AUDIT_V1`) — the tag bytes
participate in the signing input via
`@suaveplan/crypto::buildSigningInput`. Cross-protocol replay is rejected
at the bytes-don't-match level rather than at any application-layer check.
## Features
- Pluggable body codec (`BodyCodec<T>`) so the body can be a legacy
`defineBitSchema` codec, a modern `BitPacker`-backed codec, an outer
envelope, or any future encoder satisfying the two-method contract
- `pack(data, authority, signal?)` is async — delegates signing to
`Authority.sign(domain, slot, payload, signal)` so HSM/threshold backends
Just Work without the primitive having to know
- `unpack(bytes, parentPublicSlot)` enforces verify-then-decode order —
body codec is NEVER invoked on a tampered payload
- `verify(bytes, parentPublicSlot)` returns `boolean` so callers can
enumerate candidate parent keys without exception flow
- `unpackSelf(bytes, getEmbeddedSlot)` supports trust-anchor bundles whose
own embedded sign slot is the verifier
- AbortSignal threaded through `Authority.sign` per suaveplan cancellation
rules
- `signatureLength` configurable (defaults to ed25519's 64) so future
primitives with different signature shapes inherit the schema for free
- OTel spans `bitpacker-crypto.signed-pack` and
`bitpacker-crypto.signed-verify` with `domain` + `payload_bytes` attrs
## Usage: Basic Example
```ts
import { defineSignedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_PUBLIC_KEYCHAIN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface LinkBody {
parentSeq: number;
purposeBits: number;
}
const linkSchema = defineBitSchema<LinkBody>([
{ name: "parentSeq", type: "uint", bits: 32 },
{ name: "purposeBits", type: "uint", bits: 16 },
]);
const link = defineSignedBitSchema<LinkBody>(
{
pack: (b) => linkSchema.pack(b),
unpack: (bytes) => linkSchema.unpack(bytes),
},
{
domain: DOMAIN_PUBLIC_KEYCHAIN_V1,
verifier: verifyAuthoritySignature,
}
);
const packed = await link.pack(
{ parentSeq: 7, purposeBits: 0xf0 },
authority
);
const decoded = link.unpack(packed, parentPublicSlot);
```
The `verifier` option is required and accepts an `AuthorityVerifier`
function (`(domain, publicSlot, payload, signature) => boolean`). The
canonical implementation is `verifyAuthoritySignature` from
`@suaveplan/crypto/authority`. Passing it via DI keeps
`@suaveplan/bitpacker-crypto` free of any runtime dependency on
`@suaveplan/crypto`, which is what allows future
`crypto::ChainBundle`-style consumers to adopt this primitive without
inducing a circular package dependency.
## Usage: Advanced Example — trust anchor via `unpackSelf`
```ts
const trustAnchor = defineSignedBitSchema<TrustAnchorBundle>(codec, {
domain: DOMAIN_BITPACKER_SIGNED_V1,
});
// Verify against the bundle's OWN embedded sign slot.
const bundle = trustAnchor.unpackSelf(bytes, (decoded) => decoded.embeddedSlot);
```
`unpackSelf` decodes the body once to extract the embedded sign slot, then
verifies the signature against that slot. Only used for the root link of a
chain (every other link verifies against its parent).
## API Reference
### `interface SignedBitSchemaOptions`
- `domain: DomainTag` (required) — the domain under which the body is signed
- `verifier: AuthorityVerifier` (required) — pure verifier function;
typically `verifyAuthoritySignature` from `@suaveplan/crypto/authority`
- `slot?: SlotId = "sign"` — the authority slot used for signing
- `signatureLength?: number = 64` — wire-format signature length
### `defineSignedBitSchema<T>(bodyCodec, options): SignedBitSchema<T>`
Returns:
- `pack(data: T, authority: Authority, signal?: AbortSignal): Promise<Uint8Array>`
- throws `CRYPTO_SIGN_FAILED` if the authority returns a wrong-length
signature
- `unpack(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): T`
- throws `CRYPTO_VERIFY_FAILED` on verify failure (BEFORE body decode)
- `verify(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): boolean`
- `unpackSelf(bytes: Uint8Array, getEmbeddedSlot: (decoded: T) => KeySlotPublic<unknown>): T`
- `domain: DomainTag`, `slot: SlotId`, `signatureLength: number`
## Implementation Notes
- The order `verify → decode` is load-bearing. A tampered byte string must
never reach the body codec because the codec may have side effects, may
log unexpected payloads, or may be slow. Verify-then-decode is asserted
by a spec-conformance test that injects a tracking codec and checks
`decodeCalls === 0` after a tampered unpack.
- `verify(bytes, slot)` deliberately swallows structural exceptions and
returns `false` so callers can probe candidate parent keys against an
arbitrary byte string without try/catch boilerplate. `unpack(...)` is
the throw-on-failure variant.
- The body codec is held by reference; mutating it after construction
changes future pack/unpack behavior. Treat the codec as immutable.
- `Authority.sign` is async because real backends (HSM, TPM, threshold
signing, network-attached signers) are async. Even the in-memory v1
authority returns a Promise to preserve the contract.
- Signature length is enforced on pack to surface a misbehaving authority
early. The check is identity-checking the `Promise<Uint8Array>.length`,
not the algorithm — a future ECDSA-backed authority signing into an
ed25519-shaped slot would be caught here.
- OTel spans cost essentially nothing under the no-op tracer; they
materialise once a real provider is wired. The spans deliberately do
NOT carry the signature itself (sensitive material) or the body bytes
(potentially sensitive); only metadata: domain, slot, payload_bytes.

View File

@@ -0,0 +1,31 @@
## xxhash32
**Path:** `src/xxhash32.ts`
**Export:** `calculateXxhash32`, `verifyXxhash32`, `packXxhash32`, `unpackXxhash32`, `appendXxhash32`, `verifyAndStripXxhash32`
### What it does
Pure TypeScript implementation of the XXHash32 non-cryptographic hash algorithm (spec v0.8.2). No external dependencies.
### Key API
| Function | Description |
|----------|-------------|
| `calculateXxhash32(data, seed?)` | Compute 32-bit hash of a `Uint8Array` |
| `verifyXxhash32(data, expected, seed?)` | Verify a hash matches |
| `packXxhash32(checksum)` | Pack hash as 4 little-endian bytes |
| `unpackXxhash32(bytes, offset?)` | Unpack 4 bytes to hash value |
| `appendXxhash32(data, seed?)` | Append 4-byte hash to data |
| `verifyAndStripXxhash32(data, seed?)` | Verify and strip appended hash |
### Constants
- `PRIME1` = `0x9e3779b1`
- `PRIME2` = `0x85ebca77`
- `PRIME3` = `0xc2b2ae3d`
- `PRIME4` = `0x27d4eb2f`
- `PRIME5` = `0x165667b1`
### Used by
- Checksum validation in downstream network protocols