# 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` — branded `DomainTag` type alias (zero runtime). Source of truth for the type; the closed list of registered tag VALUES (`DOMAIN_VAULT_V1`, `DOMAIN_PUBLIC_KEYCHAIN_V1`, …) and the `encodeDomainTag` / `buildSigningInput` runtime helpers stay in `@suaveplan/crypto/domain-tags`. - `authority.ts` — `Authority` interface, `KeySlotPublic`, `KeySlotPrivate`, `SlotId` (string alias), and the `AuthorityVerifier` function-type. The `coordinate` field on `KeySlotPublic` is typed as `unknown` here because its concrete shape (`DerivationCoordinate`) lives in `@suaveplan/crypto` and is not part of the cross-package contract — verifiers only read `algorithm` and `publicKey`. - `aead-ops.ts` — `AeadAlgorithm` discriminator (`"AES-256-GCM"`, `"ChaCha20-Poly1305"`, `"XChaCha20-Poly1305"`), `AeadOpsOptions` (`nonce`, optional `aad`), and the `AeadOps` operations bundle (`encrypt` + `decrypt`). - Sibling `.md` per file (≥200 words each, basic + advanced examples). ### MODIFIED — `@suaveplan/crypto` - `domain-tags.ts` — re-exports `DomainTag` from `@suaveplan/types` instead of declaring it locally. The closed list of `DOMAIN_*` constants and the `encodeDomainTag` / `buildSigningInput` / `isDomainTag` runtime helpers stay here verbatim. - `authority/authority.ts` — re-exports `Authority`, `KeySlotPublic`, `SlotId` from `@suaveplan/types`. `InMemoryAuthority`, `inMemoryAuthority`, and `verifyAuthoritySignature` remain 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 `Authority` from `@suaveplan/crypto/authority` and `DomainTag` from `@suaveplan/crypto/domain-tags` — these subpaths re-export the canonical types so call-sites do not change. ### MODIFIED — `@suaveplan/bitpacker-crypto` - **Drops `@suaveplan/crypto` from runtime `dependencies`.** Moved to `devDependencies` (used only by tests and benchmarks). - All four DI-using primitives gain a required option: - `defineSignedBitSchema(bodyCodec, { domain, verifier, ... })` — `verifier: AuthorityVerifier` is required - `defineMultiSignedBitSchema(bodyCodec, { domain, verifier, signerCount, ... })` — `verifier` required - `defineHashChainSchema({ domain, verifier, payloadCodec, ... })` — `verifier` required - `defineEncryptedBitSchema(bodyCodec, { key, aead, ... })` — `aead: AeadOps` is required - Canonical wiring (caller-side): ```ts 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 in `encrypted-bitschema.ts`. They are algorithm-fixed values; duplication is acceptable because the values are part of the bit-packer-crypto wire format. - All `*.test.ts` files import the canonical wiring from `tests/helpers/crypto-deps.ts` (a new test-only file) and pass the bindings into every primitive constructor. - `tests/helpers/fake-authority.ts` updated to import `Authority` / `KeySlotPublic` / `SlotId` types from `@suaveplan/types` (runtime helpers `buildSigningInput` and `ed25519` continue 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 as `chain-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 as `public-bundle-adopt-envelope-schema`. - **Network-side adoption** (`network-message-signing-adopt-signed-bitschema`, `audit-log-adopt-hashchain-schema`, etc.) — already queued in `bitpacker-crypto-rewrite` archived tasks §19. None of those need to change as a result of this refactor; the type imports they will use resolve through `@suaveplan/types` and `@suaveplan/crypto` exactly 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/types` gains 3 new files (`domain-tag.ts`, `authority.ts`, `aead-ops.ts`) plus 3 sibling `.md` docs. Zero runtime emitted (per the existing `tests/zero-runtime.test.ts` byte-budget guard). - `@suaveplan/crypto`'s `domain-tags.ts` and `authority/authority.ts` re-export the canonical types from `@suaveplan/types`. No runtime change. Existing 265/265 tests stay green. - `@suaveplan/bitpacker-crypto` changes are larger: - 4 primitives gain required DI options (`verifier` or `aead`). - 16 test files updated to inject the canonical bindings via the new `tests/helpers/crypto-deps.ts`. - `package.json` deps shuffled (`@suaveplan/crypto` → `devDependencies`). - Must end at 100/100/100/100 coverage (the existing gate). - All 3 sibling `.md` docs 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`, `verifyAuthoritySignature` from `@suaveplan/crypto` continue to work unchanged.** Crypto's subpaths re-export the canonical types so all current call-sites compile without modification. - The canary migrations of `crypto::ChainBundle` and `PublicBundle` become 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-crypto` primitives without a `verifier` / `aead` option.** 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/types` zero-runtime byte budget.** New files emit only type declarations; the existing per-file `200-byte` budget guard is unaffected. Verified by running the existing `tests/zero-runtime.test.ts` after the change. - **Doc drift.** Four bitpacker-crypto primitive `.md` docs and the package `README.md` need updates to show the new DI options. Caught by the `AGENTS.md §4a` doc-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 in `tasks.md §14` correctly points at this follow-up.