12 KiB
Extract shared crypto interfaces — break crypto ↔ bitpacker-crypto cycle
Packages: @suaveplan/types, @suaveplan/crypto, @suaveplan/bitpacker-crypto
Status: PROPOSED
Type: architectural refactor (no wire-format change)
Why
The just-archived bitpacker-crypto-rewrite (2026-05-02) shipped seven
bit-packer-aware primitives (SignedBitSchema, MultiSignedBitSchema,
HashChainSchema, EncryptedBitSchema, EnvelopeSchema,
SchemaFingerprint, HashAlgorithmRegistry) but deferred the
canary migration of crypto::ChainBundle → EnvelopeSchema because that
migration would have created a circular package dependency:
@suaveplan/crypto → @suaveplan/bitpacker-crypto → @suaveplan/crypto
The → @suaveplan/crypto edge from bitpacker-crypto exists because
the rewrite's primitives directly import Authority, DomainTag,
KeySlotPublic, verifyAuthoritySignature, aeadEncrypt, and
aeadDecrypt from @suaveplan/crypto's subpath exports. With those
direct imports in place, crypto cannot adopt any of the primitives
without inducing a cycle.
AGENTS.md §0 rule 5 already mandates the resolution:
If the new type or abstraction is shared across ≥2 packages, it belongs in
@suaveplan/types(zero deps, types-only). Add it there first, then reference it. This prevents circular dependencies and keeps the public contract in one place.
This change applies that rule retroactively: extracts the cross-package
type contract (Authority, DomainTag, KeySlotPublic/Private,
SlotId, AuthorityVerifier, AeadOps, AeadAlgorithm) into
@suaveplan/types and refactors the four bitpacker-crypto primitives
that need a verifier or AEAD engine to accept them via dependency
injection. After this change, bitpacker-crypto has zero runtime
dependency on @suaveplan/crypto; the canary migration of
crypto::ChainBundle becomes unblocked (queued as a follow-up so the
two changes review independently).
What changes
ADDED — @suaveplan/types
domain-tag.ts— brandedDomainTagtype alias (zero runtime). Source of truth for the type; the closed list of registered tag VALUES (DOMAIN_VAULT_V1,DOMAIN_PUBLIC_KEYCHAIN_V1, …) and theencodeDomainTag/buildSigningInputruntime helpers stay in@suaveplan/crypto/domain-tags.authority.ts—Authorityinterface,KeySlotPublic<TLayoutFields>,KeySlotPrivate<TLayoutFields>,SlotId(string alias), and theAuthorityVerifierfunction-type. Thecoordinatefield onKeySlotPublicis typed asunknownhere because its concrete shape (DerivationCoordinate<TLayoutFields>) lives in@suaveplan/cryptoand is not part of the cross-package contract — verifiers only readalgorithmandpublicKey.aead-ops.ts—AeadAlgorithmdiscriminator ("AES-256-GCM","ChaCha20-Poly1305","XChaCha20-Poly1305"),AeadOpsOptions(nonce, optionalaad), and theAeadOpsoperations bundle (encrypt+decrypt).- Sibling
.mdper file (≥200 words each, basic + advanced examples).
MODIFIED — @suaveplan/crypto
domain-tags.ts— re-exportsDomainTagfrom@suaveplan/typesinstead of declaring it locally. The closed list ofDOMAIN_*constants and theencodeDomainTag/buildSigningInput/isDomainTagruntime helpers stay here verbatim.authority/authority.ts— re-exportsAuthority,KeySlotPublic,SlotIdfrom@suaveplan/types.InMemoryAuthority,inMemoryAuthority, andverifyAuthoritySignatureremain unchanged in shape; their signatures now reference the canonical types from@suaveplan/types.- No wire-format change. No public API removal. Existing consumers
continue to import
Authorityfrom@suaveplan/crypto/authorityandDomainTagfrom@suaveplan/crypto/domain-tags— these subpaths re-export the canonical types so call-sites do not change.
MODIFIED — @suaveplan/bitpacker-crypto
- Drops
@suaveplan/cryptofrom runtimedependencies. Moved todevDependencies(used only by tests and benchmarks). - All four DI-using primitives gain a required option:
defineSignedBitSchema(bodyCodec, { domain, verifier, ... })—verifier: AuthorityVerifieris requireddefineMultiSignedBitSchema(bodyCodec, { domain, verifier, signerCount, ... })—verifierrequireddefineHashChainSchema({ domain, verifier, payloadCodec, ... })—verifierrequireddefineEncryptedBitSchema(bodyCodec, { key, aead, ... })—aead: AeadOpsis required
- Canonical wiring (caller-side):
import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto"; import { verifyAuthoritySignature } from "@suaveplan/crypto/authority"; // pass { verifier: verifyAuthoritySignature }, { aead: { encrypt: aeadEncrypt, decrypt: aeadDecrypt } } - Constants the AEAD primitive previously imported from
@suaveplan/crypto(AEAD_KEY_LENGTH,AEAD_NONCE_LENGTH,AEAD_XCHACHA_NONCE_LENGTH) are now declared locally inencrypted-bitschema.ts. They are algorithm-fixed values; duplication is acceptable because the values are part of the bit-packer-crypto wire format. - All
*.test.tsfiles import the canonical wiring fromtests/helpers/crypto-deps.ts(a new test-only file) and pass the bindings into every primitive constructor. tests/helpers/fake-authority.tsupdated to importAuthority/KeySlotPublic/SlotIdtypes from@suaveplan/types(runtime helpersbuildSigningInputanded25519continue to come from@suaveplan/crypto— the fake is a test asset and depending on crypto's runtime there is fine).
Out of scope (queued as follow-ups)
- Canary migration of
crypto::ChainBundle → EnvelopeSchema. Becomes unblocked by this change; queued aschain-bundle-adopt-envelope-schema. Wire format stays byte-identical and is pinned by snapshot test in that follow-up. - Canary migration of
crypto::PublicBundle → SignedBitSchema + EnvelopeSchema. Same shape; queued aspublic-bundle-adopt-envelope-schema. - Network-side adoption (
network-message-signing-adopt-signed-bitschema,audit-log-adopt-hashchain-schema, etc.) — already queued inbitpacker-crypto-rewritearchived tasks §19. None of those need to change as a result of this refactor; the type imports they will use resolve through@suaveplan/typesand@suaveplan/cryptoexactly as before.
Parity Checklist
| Capability | Pre-refactor (bitpacker-crypto-rewrite archive) |
Post-refactor | Notes |
|---|---|---|---|
SignedBitSchema pack/unpack/verify/unpackSelf |
✅ | ✅ | Adds required verifier option |
MultiSignedBitSchema co-signing |
✅ | ✅ | Adds required verifier option |
HashChainSchema append-only chain |
✅ | ✅ | Adds required verifier option |
EncryptedBitSchema AEAD envelope |
✅ | ✅ | Adds required aead: AeadOps option |
EnvelopeSchema magic+version+body |
✅ | ✅ | Unchanged |
SchemaFingerprint canonical-hash |
✅ | ✅ | Unchanged |
HashAlgorithmRegistry pluggable hashes |
✅ | ✅ | Unchanged |
| OTel spans on all primitives | ✅ | ✅ | Unchanged |
| 100/100/100/100 coverage | ✅ | ✅ | Re-asserted by Z4 gate |
bitpacker-crypto → crypto runtime edge |
✅ (present) | 🚫 (eliminated) | The whole point of this refactor |
Authority / DomainTag / KeySlotPublic / AuthorityVerifier / AeadOps interfaces |
Local to @suaveplan/crypto |
⬆️ Promoted to @suaveplan/types |
Single source of truth |
Closed list of DOMAIN_* runtime constants |
✅ in @suaveplan/crypto |
✅ in @suaveplan/crypto |
Unchanged — runtime values stay where they are |
Source mapping
Every requirement in the spec deltas is grounded in either an existing
consumer call-site or an AGENTS.md rule.
| Requirement | Source |
|---|---|
Authority lives in @suaveplan/types |
AGENTS.md §0 rule 5; consumer: bitpacker-crypto's 4 DI primitives |
DomainTag lives in @suaveplan/types |
AGENTS.md §0 rule 5; consumer: bitpacker-crypto's 4 DI primitives |
KeySlotPublic/Private live in @suaveplan/types |
AGENTS.md §0 rule 5; consumer: bitpacker-crypto's SignedBitSchema/MultiSignedBitSchema/HashChainSchema |
AuthorityVerifier function-type lives in @suaveplan/types |
DI shape needed for bitpacker-crypto's 3 sign-using primitives to drop the @suaveplan/crypto runtime edge |
AeadOps lives in @suaveplan/types |
DI shape needed for bitpacker-crypto::EncryptedBitSchema to drop the @suaveplan/crypto runtime edge |
AeadAlgorithm lives in @suaveplan/types |
Co-located with AeadOps; bitpacker-crypto::EncryptedBitSchema's wire format pins these three names |
SignedBitSchema.verifier required option |
Removes the previous import { verifyAuthoritySignature } from "@suaveplan/crypto" runtime dep |
MultiSignedBitSchema.verifier required option |
Same |
HashChainSchema.verifier required option |
Same |
EncryptedBitSchema.aead required option |
Removes the previous import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto" runtime dep |
bitpacker-crypto package.json drops @suaveplan/crypto from dependencies |
Confirms cycle is eliminated; turbo build verifies |
Migration impact
Internal (in this change)
@suaveplan/typesgains 3 new files (domain-tag.ts,authority.ts,aead-ops.ts) plus 3 sibling.mddocs. Zero runtime emitted (per the existingtests/zero-runtime.test.tsbyte-budget guard).@suaveplan/crypto'sdomain-tags.tsandauthority/authority.tsre-export the canonical types from@suaveplan/types. No runtime change. Existing 265/265 tests stay green.@suaveplan/bitpacker-cryptochanges are larger:- 4 primitives gain required DI options (
verifieroraead). - 16 test files updated to inject the canonical bindings via the new
tests/helpers/crypto-deps.ts. package.jsondeps shuffled (@suaveplan/crypto→devDependencies).- Must end at 100/100/100/100 coverage (the existing gate).
- 4 primitives gain required DI options (
- All 3 sibling
.mddocs in bitpacker-crypto's primitive files updated to reflect the new DI option.
External (outside this change, queued as follow-ups)
- Existing consumers of
Authority,DomainTag,verifyAuthoritySignaturefrom@suaveplan/cryptocontinue to work unchanged. Crypto's subpaths re-export the canonical types so all current call-sites compile without modification. - The canary migrations of
crypto::ChainBundleandPublicBundlebecome possible. Both queued as separate OpenSpec changes.
Verification approach
bun scripts/folder-reorg/generate-manifest.ts— confirms the dep graph is acyclic after the refactor.bunx turbo lint typecheck test --filter=@suaveplan/types— types package green; zero-runtime guard satisfied.bunx turbo lint typecheck test --filter=@suaveplan/crypto— 265 existing tests stay green; no behavioral regression.bunx turbo lint typecheck test --filter=@suaveplan/bitpacker-crypto— green after DI updates.bunx turbo test:coverage --filter=@suaveplan/bitpacker-crypto— re-asserts 100/100/100/100.bunx turbo build --filter=...@suaveplan/bitpacker-crypto— every dependent compiles green.- Grep guard:
! grep -rE "from \"@suaveplan/crypto\"|from \"@suaveplan/crypto/" packages/bitpacker/bitpacker-crypto/src/— zero matches in production source files (test helpers may import; production cannot).
Risks
- Breaking change for any consumer that already wired
bitpacker-cryptoprimitives without averifier/aeadoption. Risk is theoretical because no genesis package consumed the rewrite primitives yet — this change ships before any consumer adoption. Mitigated by making the option required (TypeScript catches missing wiring at compile time, not at runtime). @suaveplan/typeszero-runtime byte budget. New files emit only type declarations; the existing per-file200-bytebudget guard is unaffected. Verified by running the existingtests/zero-runtime.test.tsafter the change.- Doc drift. Four bitpacker-crypto primitive
.mddocs and the packageREADME.mdneed updates to show the new DI options. Caught by theAGENTS.md §4adoc-drift gate before the change ships. - Re-archive coordination. The just-archived
bitpacker-crypto-rewrite(2026-05-02) deferred the canary; this change unblocks it. We do NOT re-archive the rewrite — the deferral note intasks.md §14correctly points at this follow-up.