8.3 KiB
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 overlaprestore-crypto-keychain-packer's new@suaveplan/cryptolayer.
ADDED Requirements
Requirement: SchemaFingerprint canonical hash of a BitPackerSchema
The package SHALL expose schemaFingerprint(schema, algorithm?, registry?) → Promise<Uint8Array> 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<void> 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-equalsschemaFingerprint(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 toschemaFingerprint(C)
Scenario: assertSchemaFingerprint rejects on mismatch
- WHEN
assertSchemaFingerprint(localSchema, remoteFingerprint)is called and the schemas disagree - THEN the call throws
BitpackerErrorwith codeCRYPTO_SCHEMA_FINGERPRINT_MISMATCH
Scenario: unknown algorithm rejected
- WHEN
schemaFingerprint(schema, "magic-hash")is called andmagic-hashis not registered - THEN the call throws
BitpackerErrorwith codeCRYPTO_HASH_ALGORITHM_UNKNOWNandcontext.algorithm === "magic-hash"
Scenario: BLAKE3 plugin works after registration
- WHEN a custom
HashAlgorithmnamed"blake3"is registered into a fresh registry andschemaFingerprint(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 andhas("sha256"),has("xxhash32"),has("crc32")are checked - THEN all three return
true
Scenario: register replaces existing algorithm
- WHEN a new
HashAlgorithmnamed"sha256"(e.g. a faster impl) is registered into a fresh registry already containing the defaultsha256 - THEN subsequent
resolve("sha256")returns the new impl AND an OTel spanbitpacker-crypto.hash-registry.replacedis 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.tsand.spec-conformance.test.tsis 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.