Files
suaveplan/openspec/archive/2026-05-02-bitpacker-crypto-rewrite/specs/bitpacker-crypto/spec.md
2026-05-27 23:29:53 +01:00

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