# Capability Spec Delta: bitpacker-crypto (bitpacker-crypto-rewrite, slim) **Package**: `@suaveplan/bitpacker-crypto` **Status**: PROPOSED — slimmed 2026-05-02 after `restore-crypto-keychain-packer` discovery > Every requirement carries a `**Verified by:**` line at authoring time. > Slim version: only the two primitives that don't overlap `restore-crypto-keychain-packer`'s new `@suaveplan/crypto` layer. ## ADDED Requirements ### Requirement: SchemaFingerprint canonical hash of a BitPackerSchema The package SHALL expose `schemaFingerprint(schema, algorithm?, registry?) → Promise` that returns the chosen hash of a canonical serialization of the schema. Canonicalization SHALL sort object keys, omit whitespace, and produce identical bytes for any two schemas that describe the same wire layout regardless of property declaration order. Default `algorithm` SHALL be `"sha256"`. Default `registry` SHALL be the process-wide `defaultHashRegistry()` singleton. Unknown algorithm names SHALL throw `BitpackerError` with code `CRYPTO_HASH_ALGORITHM_UNKNOWN` and `context.algorithm` naming the missing algorithm. The package SHALL also expose `assertSchemaFingerprint(schema, expected, algorithm?, registry?) → Promise` that throws `BitpackerError` with code `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` on disagreement; the error context SHALL include both fingerprints (hex-encoded). #### Scenario: equal schemas in different declaration orders fingerprint identically - **WHEN** schema A is `{ a: "u8", b: "u16" }` and schema B is `{ b: "u16", a: "u8" }` - **THEN** `schemaFingerprint(A)` deep-equals `schemaFingerprint(B)` for every default algorithm #### Scenario: different schemas fingerprint differently - **WHEN** schema A is `{ a: "u8", b: "u16" }` and schema C is `{ a: "u8", b: "u32" }` - **THEN** `schemaFingerprint(A)` is NOT byte-identical to `schemaFingerprint(C)` #### Scenario: assertSchemaFingerprint rejects on mismatch - **WHEN** `assertSchemaFingerprint(localSchema, remoteFingerprint)` is called and the schemas disagree - **THEN** the call throws `BitpackerError` with code `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` #### Scenario: unknown algorithm rejected - **WHEN** `schemaFingerprint(schema, "magic-hash")` is called and `magic-hash` is not registered - **THEN** the call throws `BitpackerError` with code `CRYPTO_HASH_ALGORITHM_UNKNOWN` and `context.algorithm === "magic-hash"` #### Scenario: BLAKE3 plugin works after registration - **WHEN** a custom `HashAlgorithm` named `"blake3"` is registered into a fresh registry and `schemaFingerprint(schema, "blake3", registry)` is called - **THEN** the result is the BLAKE3 hash of the canonical schema bytes **Verified by:** `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.test.ts::"equal schemas in different orders fingerprint identically"` AND `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.test.ts::"different schemas fingerprint differently"` AND `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.test.ts::"assertSchemaFingerprint rejects on mismatch with CRYPTO_SCHEMA_FINGERPRINT_MISMATCH"` AND `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.spec-conformance.test.ts::"unknown algorithm throws CRYPTO_HASH_ALGORITHM_UNKNOWN"` AND `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.spec-conformance.test.ts::"plugged-in BLAKE3 algorithm produces BLAKE3 fingerprint"` --- ### Requirement: HashAlgorithmRegistry as pluggable extension point The package SHALL expose a `HashAlgorithmRegistry` interface with `register(alg)`, `resolve(name)`, `has(name)`, and `list()` methods, a `HashAlgorithm` interface with `{ name, outputBytes, hash(data) }`, a `defaultHashRegistry()` returning a process-wide singleton, and a `createHashRegistry()` returning a fresh registry. The default registry SHALL ship `xxhash32`, `crc32`, and `sha256` implementations sourced from the existing checksum surface in this package. Registering an algorithm with a name already present SHALL replace the existing entry (last-write-wins) AND emit an OTel span `bitpacker-crypto.hash-registry.replaced` with the replaced algorithm's `outputBytes` attribute. #### Scenario: defaults are present out of the box - **WHEN** `defaultHashRegistry()` is called and `has("sha256")`, `has("xxhash32")`, `has("crc32")` are checked - **THEN** all three return `true` #### Scenario: register replaces existing algorithm - **WHEN** a new `HashAlgorithm` named `"sha256"` (e.g. a faster impl) is registered into a fresh registry already containing the default `sha256` - **THEN** subsequent `resolve("sha256")` returns the new impl AND an OTel span `bitpacker-crypto.hash-registry.replaced` is emitted #### Scenario: createHashRegistry isolates from singleton - **WHEN** `createHashRegistry()` returns a fresh registry and a custom algorithm is registered into it - **THEN** the singleton's `list()` does NOT include the custom algorithm #### Scenario: list returns registered algorithm names - **WHEN** `defaultHashRegistry().list()` is called on a fresh process - **THEN** the result contains `"sha256"`, `"xxhash32"`, `"crc32"` (order not guaranteed) **Verified by:** `packages/bitpacker/bitpacker-crypto/src/hash-registry/hash-registry.test.ts::"default registry ships sha256, xxhash32, crc32"` AND `packages/bitpacker/bitpacker-crypto/src/hash-registry/hash-registry.test.ts::"register replaces existing algorithm with last-write-wins"` AND `packages/bitpacker/bitpacker-crypto/src/hash-registry/hash-registry.test.ts::"list returns registered algorithm names"` AND `packages/bitpacker/bitpacker-crypto/src/hash-registry/hash-registry.spec-conformance.test.ts::"createHashRegistry isolates from singleton"` AND `packages/bitpacker/bitpacker-crypto/src/hash-registry/hash-registry.spec-conformance.test.ts::"replacement emits OTel hash-registry.replaced span"` --- ### Requirement: Determinism — no module-scope Math.random / Date.now The package SHALL contain zero `Math.random()` and zero `Date.now()` calls in implementation files. Time-dependent code (e.g. OTel histogram timestamps) SHALL read from an injected `Clock` from `@suaveplan/types` (default `defaultClock` from `@suaveplan/bitpacker-network`'s convention). Random-dependent code (none currently) SHALL accept an injectable `Rng`. #### Scenario: no Math.random / Date.now in implementation files - **WHEN** `grep -rE "Math\\.random\\(\\)|Date\\.now\\(\\)" packages/bitpacker/bitpacker-crypto/src/` excluding `.test.ts` and `.spec-conformance.test.ts` is run - **THEN** there are zero matches **Verified by:** `packages/bitpacker/bitpacker-crypto/src/fingerprint/schema-fingerprint.spec-conformance.test.ts::"no Math.random or Date.now in implementation files"` ## REMOVED Requirements ### Requirement: Upstream provider/registry abstraction (BaseCryptoProvider, CryptoRegistry, three adapters) **Reason:** Upstream's 1,906-line `bitpacker/src/crypto/` provider abstraction has zero non-self consumers. Real consumers re-implement crypto-with-bitpacker patterns inline. Those patterns are now consolidated by `restore-crypto-keychain-packer`'s new `@suaveplan/crypto` layer (`Packer` / `Authority` / `AuditLog` / `RevocationList`), not by primitives in `bitpacker-crypto`. **Migration:** The auxiliary primitives in this slim rewrite (`SchemaFingerprint`, `HashAlgorithmRegistry`) provide bit-packer-side utilities that compose with `restore-crypto-keychain-packer` without overlap. `SUPERSEDED.md` records the per-symbol replacement table for the rejected upstream provider abstraction. ### Requirement: First-draft primitives now owned by restore-crypto-keychain-packer **Reason:** The first draft of this rewrite proposed seven primitives. Five of them (`SignedBitSchema`, `EnvelopeSchema`, `HashChainSchema`, `MultiSignedBitSchema`, `EncryptedBitSchema`) overlap with primitives proposed by `restore-crypto-keychain-packer` (`Packer`, `packPublicBundle`, `AuditLog`, `RevocationList`). Genesis policy explicitly forbids duplicate libraries; the slim rewrite drops the overlapping primitives. **Migration:** Future consumers needing those patterns reach for the `@suaveplan/crypto` primitives once `restore-crypto-keychain-packer` lands. The audit findings (eight inline consumers manually re-implementing the patterns) carry over as the consumer list for that change.