13 KiB
Bitpacker crypto port — close the 1,906-line gap
Package: @suaveplan/bitpacker-crypto
Status: SUPERSEDED by bitpacker-crypto-rewrite (2026-05-02). Both folders archive together so the decision history reads cleanly.
Type: spec-conformance + refactor (deprecate-and-route) OR full port
Why this change was superseded — A deeper consumer scan run after this proposal was authored found 8+ consumers across genesis and upstream manually re-implementing the same crypto-with-bitpacker patterns inline (sign-then-pack, encrypt-pack-frame, schema-versioned envelopes, schema fingerprints, stateless verifiers). The "Option A: deprecate-and-route" framing this change recommended is correct for the upstream provider abstraction but does not address the inline duplication.
bitpacker-crypto-rewrite(2026-05-02) replaces this change with a consumer-driven design that consolidates the patterns into five bit-packer-aware primitives:SignedBitSchema,EncryptedBitSchema,EnvelopeSchema,SchemaFingerprint,ChunkVerifier. TheSUPERSEDED.mdfile and audit test created by this change are PRESERVED under the rewrite (revised framing only); thebitpacker-modular-restructureparity-table flips this change introduced are PRESERVED (the upstream provider abstraction is still intentionally not ported).
Why
The 2026-05-02 audit of bitpacker-modular-restructure confirmed five files of upstream's bitpacker/src/crypto/ are absent from genesis: crypto/provider/provider.ts (296 L), crypto/registry/registry.ts (370 L), and three adapters under crypto/adapters/{noop,libsodium,elliptic}/ (337 + 331 + 572 L). Total: ~1,906 L of provider abstraction. The parity table in bitpacker-modular-restructure/proposal.md flags every one of them 🚫. That gap has to be closed before this restructure can archive cleanly.
But before authoring a 1,906-line port, this proposal documents three findings that change what "closing the gap" should mean:
- No upstream consumer depends on
BaseCryptoProvider/CryptoRegistryoutside the bitpacker package itself. Searchedgrep -rlE "BaseCryptoProvider|CryptoRegistry|LibsodiumCryptoProvider|EllipticCryptoProvider" /mnt/s/web/suaveplan/packages | grep -v /bitpacker/→ only one hit, and that hit is a documentation file (packages/universal/overview/bitpacker.md), not a code import. Upstream's crypto provider abstraction was speculative API surface; nothing actually called it. - Genesis already ships the underlying primitives in five focused packages:
@suaveplan/crypto(multi-algorithm façade),@suaveplan/crypto-sodium(libsodium engine),@suaveplan/crypto-browser(WebCrypto engine),@suaveplan/crypto-keys(keypair generation + Keychain),@suaveplan/crypto-vault(BIP39 + Argon2id HD vault + ChainSigner). Upstream'sLibsodiumCryptoProvider(line 1) literally doesimport from "@suaveplan/crypto"— it is not duplicate primitives, it is a provider-pattern wrapper around them. - Genesis already ships chunked AEAD at a higher level in
@suaveplan/packer(chunkedaead/verifier/unpacker/packer/manifest, 99 tests at 100% coverage). Any caller wanting "encrypt my bitpacked bytes" should use packer, not invent a parallel provider/registry.
A literal port would re-add a provider abstraction that no consumer asked for, while shadowing the crypto family that already exists. That is the opposite of the genesis directive.
Decision required
Two options. The proposal recommends Option A; Option B is fully scoped here so a reviewer can flip the call at review without restarting the change.
Option A — Deprecate-and-route (recommended)
Mark the upstream bitpacker-crypto provider/registry/adapters as superseded and route any future bitpacker-internal crypto need through the existing @suaveplan/crypto family. Specifically:
- DO NOT port
BaseCryptoProvider,CryptoRegistry,LibsodiumCryptoProvider,EllipticCryptoProvider,NoOpCryptoProvider, or their type companions. - DO NOT port
generateNonce/concatBytes/validateKeyLength/constantTimeEqual/secureWipefrom upstreamprovider.ts— every one of them already exists in@suaveplan/crypto-utils(bytes.concat,bytes.equalsConstantTime,random.bytes,bytes.zeroize). - DO add a
SUPERSEDED.mdfile atpackages/bitpacker/bitpacker-crypto/SUPERSEDED.mdrecording the decision, the upstream surface that's intentionally not ported, and the genesis replacement for each one. - DO close the parity-table 🚫 rows in
bitpacker-modular-restructure/proposal.mdby flipping their status to ⛔ "intentionally not ported" with a pointer to this change. - KEEP the existing
bitpacker-cryptochecksum surface (crc32,adler32,xxhash32,appendCrc32,verifyAndStripCrc32) — that's not the gap; it's a separate, non-controversial concern.
Why recommended: closes the parity gap honestly, avoids 1,906 L of duplicate abstraction, lets future bitpacker callers reach for the crypto family that's already documented in genesis, and matches the genesis directive of "no duplicate libraries."
Cost: zero implementation lines. The change is a decision document plus an audit-table flip.
Option B — Full port
Port the upstream crypto subsystem verbatim into bitpacker-crypto, with the genesis-required hardening:
- ADDED:
BaseCryptoProvider extends EventEmitterabstract class — encrypt / decrypt / sign / verify / deriveKey with event emission and capability advertisement. Replace upstream's plainErrorthrows withBitpackerErrorextending@suaveplan/error. - ADDED:
LibsodiumCryptoProvider— wraps@suaveplan/crypto-sodium(genesis package, not the upstream@suaveplan/cryptoimport); supports XChaCha20-Poly1305, AES-GCM, ed25519 sign / verify, X25519 agree, Argon2id key derivation. - ADDED:
EllipticCryptoProvider— wraps@suaveplan/crypto-curve(genesis package); supports secp256k1 / ed25519 / p256 sign / verify; key generation; ECDH agree. - ADDED:
NoOpCryptoProvider— for tests and "encryption disabled" code paths; passes data through unchanged with provenance markers;createSlowProvider,createUnreliableProviderfactories for fault-injection tests. - ADDED:
CryptoRegistry extends EventEmitter— registers and resolves providers by name; emitsprovider:registered,provider:unregistered,provider:replacedevents; supports a global registry singleton. - ADDED:
CryptoCapabilitiesinterface andEncryptedPayload,SignatureResult,KeyDerivationOptions,EncryptConfig,DecryptConfig,SignConfig,VerifyConfigtypes. - ADDED:
BITPACKER.CRYPTO_*codes (CRYPTO_KEY_LENGTH_INVALID,CRYPTO_PROVIDER_NOT_INITIALIZED,CRYPTO_PROVIDER_NOT_REGISTERED,CRYPTO_CAPABILITY_UNSUPPORTED,CRYPTO_DECRYPT_FAILED,CRYPTO_VERIFY_FAILED). - REMOVED (vs upstream): the
Math.random()fallback ingenerateNonce(line 244 upstream) — genesis routes through@suaveplan/crypto-utils::random.byteswhich usescrypto.getRandomValuesonly and throws on environments without it. - REMOVED (vs upstream): the duplicate
concatBytes/constantTimeEqual/secureWipe— re-export from@suaveplan/crypto-utilsinstead.
Why this might still be the right call: if genesis intends to ship a swappable crypto provider abstraction at the bitpacker layer specifically (e.g. so CompressedBitPacker can advertise "encryption enabled / disabled" without coupling to @suaveplan/crypto-vault), the port encodes that decision durably. It would also un-block any future feature where bitpacker's wire format wants to declare "this payload is signed by provider X with capabilities Y."
Cost: roughly 8 implementation files at ~200 L each, ~12 spec-conformance test files, plus the hardening and documentation; estimate ~3,500 L net new (port + hardening + tests + docs). Coverage gate is 100/100/100/100 per the genesis quality bar.
The remainder of this proposal is the spec delta if Option A is chosen. If Option B is chosen at review, the delta will be re-authored to match the ADDED/MODIFIED/REMOVED lists above and the source-mapping table will cite the upstream files line-by-line.
What changes (Option A)
- ADDED:
packages/bitpacker/bitpacker-crypto/SUPERSEDED.md— decision record listing every upstream surface intentionally not ported and naming its genesis replacement. - MODIFIED:
openspec/changes/bitpacker-modular-restructure/proposal.mdparity table — flip the five 🚫 rows for crypto-provider, crypto-registry, crypto-adapter-noop, crypto-adapter-libsodium, crypto-adapter-elliptic from "🚫 not ported" to "⛔ intentionally superseded by@suaveplan/crypto*; seebitpacker-crypto-port." - MODIFIED:
openspec/changes/bitpacker-modular-restructure/tasks.md— tick §14.6 with a pointer to this change. - MODIFIED:
packages/bitpacker/bitpacker-crypto/README.md— add a "Crypto provider needs" section pointing at@suaveplan/crypto,crypto-sodium,crypto-browser,crypto-vault,packer. - ADDED:
packages/bitpacker/bitpacker-crypto/SUPERSEDED.test.ts— pins the decision (SUPERSEDED.mdexists; the upstream symbol names listed in it do NOT exist as exports of@suaveplan/bitpacker-crypto).
Out of scope
- The
crc32/adler32/xxhash32checksum helpers already inbitpacker-crypto— those are not part of the upstream provider abstraction and are not affected by this decision. appendAdler32/verifyAdler32helper restoration (tracked inbitpacker-modular-restructure§14.7).- Adding event-emitting wrappers around
@suaveplan/cryptoin any other package — if a downstream consumer genuinely needs event emission on every encrypt / decrypt, that is a separate proposal scoped to that consumer. - Authoring
openspec/specs/bitpacker/spec.md(the canonical core spec) — covered by a follow-up change.
Source mapping
Every requirement in the spec delta below cites upstream-file evidence. Because Option A is a deliberate scope-down, the mapping is "what is intentionally NOT ported" rather than "what we ported".
| Upstream surface (intentionally not ported) | File | Line range | Genesis replacement |
|---|---|---|---|
BaseCryptoProvider extends EventEmitter |
crypto/provider/provider.ts |
L26-L232 | None — direct calls to @suaveplan/crypto façade |
generateNonce(length) |
crypto/provider/provider.ts |
L237-L248 | @suaveplan/crypto-utils::random.bytes(length) |
concatBytes(...arrays) |
crypto/provider/provider.ts |
L253-L262 | @suaveplan/crypto-utils::bytes.concat(...arrays) |
validateKeyLength(key, expected, type) |
crypto/provider/provider.ts |
L267-L277 | Caller-side guard; no central helper needed (genesis throws coded errors at the boundary) |
constantTimeEqual(a, b) |
crypto/provider/provider.ts |
L282-L289 | @suaveplan/crypto-utils::bytes.equalsConstantTime(a, b) |
secureWipe(buffer) |
crypto/provider/provider.ts |
L294-L296 | @suaveplan/crypto-utils::bytes.zeroize(buffer) |
LibsodiumCryptoProvider + createXChaCha20Provider + createAesGcmProvider |
crypto/adapters/libsodium/libsodium.ts |
L94-L335 | @suaveplan/crypto-sodium (libsodium engine) + @suaveplan/crypto::aead |
EllipticCryptoProvider + 3 factory helpers |
crypto/adapters/elliptic/elliptic.ts |
L36-L572 | @suaveplan/crypto-curve (secp256k1 / ed25519 / p256) |
NoOpCryptoProvider + createTestProvider + createSlowProvider + createUnreliableProvider |
crypto/adapters/noop/noop.ts |
L35-L335 | Test-only; consumers can stub @suaveplan/crypto calls directly with @suaveplan/testing fakes |
CryptoRegistry extends EventEmitter + createCryptoRegistry + getGlobalRegistry |
crypto/registry/registry.ts |
L21-L370 | None — no upstream consumer relied on it; the registry was self-referential |
Requirements with no source line
| Requirement | Justification |
|---|---|
SUPERSEDED.md audit trail |
Genesis policy: every removed surface needs a documented reason. Recording the decision durably in-repo is cheaper than re-litigating it at every future audit. |
| Test that pins the absence of upstream symbols | Defensive: prevents a future contributor from accidentally re-introducing the abstraction without re-running the decision. |
Verification approach
packages/bitpacker/bitpacker-crypto/SUPERSEDED.test.ts— assertsBaseCryptoProvider,CryptoRegistry,LibsodiumCryptoProvider,EllipticCryptoProvider,NoOpCryptoProvider,generateNonce,concatBytes,validateKeyLength,constantTimeEqual,secureWipeare NOT exported from@suaveplan/bitpacker-crypto. Each missing symbol is oneit(...).- A grep-based check (run in
tasks.md§3.3) confirming nothing inpackages/imports the names above from@suaveplan/bitpacker-crypto.
Migration impact
- Internal: zero. The bitpacker-crypto package today only exports checksum helpers; no genesis package depends on the provider abstraction.
- Future consumers: anyone wanting to encrypt or sign packed bytes should reach for
@suaveplan/crypto(façade),@suaveplan/crypto-sodium/crypto-browser(engines),@suaveplan/crypto-vault(HD vault + ChainSigner), or@suaveplan/packer(chunked AEAD). The decision record makes that routing visible. - Upstream port aspirants — anyone reading
bitpacker-modular-restructure/proposal.mdand seeing the 🚫 rows will be redirected to this decision instead of attempting a literal port.