233 lines
12 KiB
Markdown
233 lines
12 KiB
Markdown
# 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<TLayoutFields>`,
|
|
`KeySlotPrivate<TLayoutFields>`, `SlotId` (string alias), and the
|
|
`AuthorityVerifier` function-type. The `coordinate` field on
|
|
`KeySlotPublic` is typed as `unknown` here because its concrete shape
|
|
(`DerivationCoordinate<TLayoutFields>`) 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.
|