Initial Commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# chain-bundle adopts EnvelopeSchema — first canary of the cycle-broken DI primitives
|
||||
|
||||
**Packages**: `@suaveplan/crypto`
|
||||
**Status**: PROPOSED
|
||||
**Type**: internal refactor (no public-API change, no wire-format change)
|
||||
|
||||
## Why
|
||||
|
||||
The just-archived `extract-shared-crypto-interfaces` (2026-05-03) eliminated the `crypto ↔ bitpacker-crypto` cycle by extracting `Authority` / `DomainTag` / `AeadOps` to `@suaveplan/types` and refactoring `bitpacker-crypto`'s primitives to dependency injection. That change unblocked the canary migrations that were originally deferred from `bitpacker-crypto-rewrite` (2026-05-02).
|
||||
|
||||
This is the **first** of those canary migrations. `@suaveplan/crypto::ChainBundle` currently re-implements the magic + formatVersion + linkCount header pattern inline using `defineBitSchema`:
|
||||
|
||||
```ts
|
||||
// chain-bundle.ts — current
|
||||
const HEADER_SCHEMA = defineBitSchema<ChainBundleHeaderRaw>([
|
||||
{ name: "magic", type: "bytes", byteLength: 4 },
|
||||
{ name: "formatVersion", type: "uint", bits: 8 },
|
||||
{ name: "linkCount", type: "uint", bits: 8 },
|
||||
]);
|
||||
```
|
||||
|
||||
…and re-implements the magic-mismatch / version-mismatch / length-mismatch error paths with hand-written checks. The same pattern lives in `crypto::PublicBundle`, the upstream `packer::chunkedPacketSchema`, and several other places. `@suaveplan/bitpacker-crypto::EnvelopeSchema` exists specifically to consolidate this pattern; this change proves the primitive works against a real consumer in the genesis tree (the canary use-case the rewrite was always supposed to deliver).
|
||||
|
||||
The migration is **byte-identical** — wire format does not change. This is enforced by a snapshot test pinning the bytes for a fixed input.
|
||||
|
||||
## What changes
|
||||
|
||||
### MODIFIED — `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.ts`
|
||||
|
||||
- **Drop** the inline `HEADER_SCHEMA = defineBitSchema([{magic}, {formatVersion}, {linkCount}])` declaration.
|
||||
- **Add** an `EnvelopeSchema`-based header definition:
|
||||
```ts
|
||||
const HEADER_BODY_SCHEMA = defineBitSchema<{ linkCount: number }>([
|
||||
{ name: "linkCount", type: "uint", bits: 8 },
|
||||
]);
|
||||
const CHAIN_BUNDLE_ENVELOPE = defineEnvelopeSchema<{ linkCount: number }>({
|
||||
magic: CHAIN_BUNDLE_MAGIC,
|
||||
formatVersion: CHAIN_BUNDLE_FORMAT_VERSION,
|
||||
bodyCodec: {
|
||||
pack: (b) => HEADER_BODY_SCHEMA.pack(b),
|
||||
unpack: (bytes) => HEADER_BODY_SCHEMA.unpack(bytes),
|
||||
},
|
||||
});
|
||||
```
|
||||
- **Update** `packChainBundle` to call `CHAIN_BUNDLE_ENVELOPE.pack({ linkCount: ... })` instead of `HEADER_SCHEMA.pack({...})`. The variable-length link payloads stay concatenated after the envelope header (the envelope's body is 1 byte = `linkCount`; the link payloads are not inside the envelope's body).
|
||||
- **Update** `parseChainBundle` to call `CHAIN_BUNDLE_ENVELOPE.parseStrict(...)` instead of inline `HEADER_SCHEMA.unpack(...)`. Map `BitpackerError` codes (`CRYPTO_ENVELOPE_MAGIC_MISMATCH`, `CRYPTO_ENVELOPE_VERSION_MISMATCH`) back to existing `CryptoError` codes (`CHAIN.WRONG_FORMAT` with `crypto.chain-bundle.bad-magic` / `crypto.chain-bundle.unsupported-version` messages) for **back-compat** — existing consumers catching by `CryptoError.code` continue to work unchanged.
|
||||
- **Preserve** every other behavior verbatim: `linkCount` range check (`CHAIN.TOO_DEEP`), `expectedTotal` length check (`CHAIN.WRONG_FORMAT` `length-mismatch`), `verifyChain` walk-and-verify logic.
|
||||
- **Add snapshot test** pinning the bytes of `packChainBundle([fixedLinkBytes])` so any future drift in the underlying primitive is caught immediately.
|
||||
|
||||
### MODIFIED — `packages/crypto/crypto/package.json`
|
||||
|
||||
- Add `@suaveplan/bitpacker-crypto: workspace:^` to `dependencies`. **No new cycle**: `bitpacker-crypto` no longer depends on `@suaveplan/crypto` at runtime (per the just-archived `extract-shared-crypto-interfaces`).
|
||||
|
||||
### MODIFIED — `packages/crypto/crypto/vite.config.ts`
|
||||
|
||||
- Add `"@suaveplan/bitpacker-crypto"` and `/^@suaveplan\/bitpacker-crypto\//` to externals.
|
||||
|
||||
## Out of scope (queued as follow-ups)
|
||||
|
||||
- **Canary of `crypto::PublicBundle → SignedBitSchema + EnvelopeSchema`** — same shape; queued as `public-bundle-adopt-envelope-schema`. Larger because PublicBundle uses both signing and envelope framing.
|
||||
- **Canary of upstream `packer::chunkedPacketSchema`** — out of genesis scope until `packer` is ported.
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Pre-refactor | Post-refactor | Notes |
|
||||
|---|---|---|---|
|
||||
| Public exports (`packChainBundle`, `parseChainBundle`, `verifyChain`, `CHAIN_BUNDLE_MAGIC`, `CHAIN_BUNDLE_FORMAT_VERSION`, `MAX_CHAIN_LINKS`, `CHAIN_BUNDLE_HEADER_BYTE_SIZE`, `ChainBundle`, `IsRevokedPredicate`, `VerifyChainOptions`) | ✅ | ✅ | Unchanged |
|
||||
| Wire format `[magic 4][version u8][linkCount u8][...links]` | ✅ | ✅ | **Byte-identical** — pinned by snapshot test |
|
||||
| `CHAIN_BUNDLE_HEADER_BYTE_SIZE = 6` | ✅ | ✅ | Same value, computed differently |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.bad-magic` on bad magic | ✅ | ✅ | Mapped from `BitpackerError CRYPTO_ENVELOPE_MAGIC_MISMATCH` |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.unsupported-version` on bad version | ✅ | ✅ | Mapped from `BitpackerError CRYPTO_ENVELOPE_VERSION_MISMATCH` |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.bytes-too-short` on too-short input | ✅ | ✅ | Pre-check before envelope.parseStrict |
|
||||
| `CryptoError CHAIN.TOO_DEEP crypto.chain-bundle.link-count-out-of-range` on bad linkCount | ✅ | ✅ | Unchanged |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.length-mismatch` on length mismatch | ✅ | ✅ | Unchanged |
|
||||
| All 14 existing `chain-bundle.test.ts` tests pass green | ✅ | ✅ | No test modifications needed |
|
||||
| Coverage of `chain-bundle.ts` | 100% | 100% | Reasserted by §5 |
|
||||
| New `crypto → bitpacker-crypto` dep edge | 🚫 (absent) | ✅ (added) | Acyclic (verified by manifest regen) |
|
||||
| `bitpacker-crypto → crypto` runtime dep | 🚫 | 🚫 | Stays absent — eliminated by `extract-shared-crypto-interfaces` |
|
||||
|
||||
## Source mapping
|
||||
|
||||
Every requirement in the spec delta is grounded in the existing chain-bundle test surface or in the just-archived `extract-shared-crypto-interfaces` requirements.
|
||||
|
||||
| Requirement | Source |
|
||||
|---|---|
|
||||
| Wire format byte-identical | Existing `chain-bundle.test.ts::"packs and parses a 1-link chain"` and `"packs and parses a 3-link chain"` |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT bad-magic` mapping | Existing `chain-bundle.test.ts::"parseChainBundle rejects bad magic"` |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT unsupported-version` mapping | Existing `chain-bundle.test.ts::"parseChainBundle rejects bad version"` |
|
||||
| `CryptoError CHAIN.WRONG_FORMAT bytes-too-short` mapping | Existing `chain-bundle.test.ts::"parseChainBundle rejects too-short payload"` |
|
||||
| `EnvelopeSchema` adoption proves the cycle-broken DI primitives compose against a real consumer | `extract-shared-crypto-interfaces/proposal.md` "Out of scope (queued as follow-ups)" — this is the first follow-up |
|
||||
|
||||
## Migration impact
|
||||
|
||||
### Internal (in this change)
|
||||
|
||||
- One file changed in `crypto`: `chain-bundle.ts` (~30 lines net).
|
||||
- One snapshot test added: `chain-bundle.test.ts::"chain-bundle wire format is byte-identical to v0x0001 snapshot"`.
|
||||
- `package.json` adds one workspace dep (`@suaveplan/bitpacker-crypto`).
|
||||
- `vite.config.ts` adds two externals entries.
|
||||
- Coverage stays 100% on `chain-bundle.ts`.
|
||||
|
||||
### External
|
||||
|
||||
- **Zero** consumer-side changes. `packChainBundle` / `parseChainBundle` / `verifyChain` signatures and behavior are unchanged. Existing consumers (which catch `CryptoError` by code, not by message) continue to work.
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `bunx turbo lint typecheck test --filter=@suaveplan/crypto` — all 265 existing tests stay green PLUS the new snapshot test passes.
|
||||
- `bunx turbo test:coverage --filter=@suaveplan/crypto` — coverage of `chain-bundle.ts` stays 100/100/100/100.
|
||||
- `bunx turbo build --filter=...@suaveplan/crypto` — every dependent compiles green.
|
||||
- `bun scripts/folder-reorg/generate-manifest.ts` — confirms the new `crypto → bitpacker-crypto` edge is acyclic.
|
||||
- Snapshot test: `chain-bundle.test.ts::"chain-bundle wire format is byte-identical to v0x0001 snapshot"` pins the exact bytes for a fixed input — any future drift caught immediately.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Wire-format drift**. Mitigated by snapshot test that pins the bytes for a fixed input. If `EnvelopeSchema` ever changes its byte layout (it has no reason to — the layout is locked by spec), the snapshot test will fail loudly.
|
||||
- **Error code remapping bug**. The `BitpackerError` → `CryptoError` mapping inside `parseChainBundle` is the only behavioral change. Mitigated by the four existing tests that assert specific `CryptoError` codes (bad-magic, bad-version, too-short, length-mismatch) on each error path.
|
||||
- **Cycle re-introduction**. The new `crypto → bitpacker-crypto` edge is safe ONLY because `bitpacker-crypto` no longer depends on `crypto` at runtime (just-archived `extract-shared-crypto-interfaces`). If a future PR re-adds `@suaveplan/crypto` to `bitpacker-crypto/dependencies`, the cycle reappears. Mitigated by the existing grep guard in `bitpacker-crypto/src/` and by `manifest.ts` regeneration as a CI step.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: crypto (chain-bundle-adopt-envelope-schema)
|
||||
|
||||
**Package**: `@suaveplan/crypto`
|
||||
**Status**: PROPOSED — gated on user sign-off
|
||||
|
||||
> Pins the byte-identical wire-format invariant of `ChainBundle` so the
|
||||
> just-completed migration to `@suaveplan/bitpacker-crypto::EnvelopeSchema`
|
||||
> cannot drift the on-the-wire layout. No public-API change; this
|
||||
> Requirement formalises an invariant that was previously only enforced
|
||||
> by the implementation reading itself.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ChainBundle wire format is byte-identical to v0x0001 layout
|
||||
|
||||
The `packChainBundle(linkBytes)` function SHALL produce bytes in the
|
||||
layout `[CHAIN_BUNDLE_MAGIC (4 bytes)][CHAIN_BUNDLE_FORMAT_VERSION (u8)][linkCount (u8)][...linkBytes concatenated]`. `CHAIN_BUNDLE_HEADER_BYTE_SIZE` SHALL be `6` (4 magic + 1 version + 1 linkCount). The implementation MAY consolidate the header construction through `@suaveplan/bitpacker-crypto::EnvelopeSchema`, but the resulting bytes MUST be byte-identical to the v0x0001 layout pinned by snapshot test. Any future drift in the underlying primitive that changes the byte layout is a wire-format break and MUST be caught by the snapshot test.
|
||||
|
||||
#### Scenario: snapshot bytes match v0x0001 layout
|
||||
|
||||
- **WHEN** `packChainBundle([fixedDeterministicLinkBytes])` runs with a fixed deterministic link payload
|
||||
- **THEN** the resulting bytes are byte-identical to the v0x0001 snapshot pinned in `chain-bundle.test.ts::"chain-bundle wire format is byte-identical to v0x0001 snapshot"`
|
||||
|
||||
#### Scenario: header size constant is 6
|
||||
|
||||
- **WHEN** `CHAIN_BUNDLE_HEADER_BYTE_SIZE` is read
|
||||
- **THEN** the value is exactly `6`
|
||||
|
||||
#### Scenario: bad magic surfaces as `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.bad-magic`
|
||||
|
||||
- **WHEN** `parseChainBundle(bytes)` is called on bytes whose first 4 bytes do not match `CHAIN_BUNDLE_MAGIC`
|
||||
- **THEN** the call throws `CryptoError` with code `CHAIN.WRONG_FORMAT` and message `crypto.chain-bundle.bad-magic`
|
||||
|
||||
#### Scenario: bad version surfaces as `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.unsupported-version`
|
||||
|
||||
- **WHEN** `parseChainBundle(bytes)` is called on bytes whose 5th byte (formatVersion) is not `CHAIN_BUNDLE_FORMAT_VERSION`
|
||||
- **THEN** the call throws `CryptoError` with code `CHAIN.WRONG_FORMAT` and message `crypto.chain-bundle.unsupported-version` and `context.version` carrying the actual version byte
|
||||
|
||||
#### Scenario: too-short bytes surface as `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.bytes-too-short`
|
||||
|
||||
- **WHEN** `parseChainBundle(bytes)` is called on a buffer shorter than `CHAIN_BUNDLE_HEADER_BYTE_SIZE`
|
||||
- **THEN** the call throws `CryptoError` with code `CHAIN.WRONG_FORMAT` and message `crypto.chain-bundle.bytes-too-short`
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.test.ts::"chain-bundle wire format is byte-identical to v0x0001 snapshot"` AND `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.test.ts::"parseChainBundle rejects bad magic"` AND `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.test.ts::"parseChainBundle rejects bad version"` AND `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.test.ts::"parseChainBundle rejects too-short payload"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,63 @@
|
||||
# Tasks — chain-bundle adopts EnvelopeSchema
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. No
|
||||
> retroactive ticking. Implementation gated on user sign-off (§1.5).
|
||||
|
||||
## 1. Spec authoring & sign-off
|
||||
|
||||
- [x] 1.1 Author `proposal.md` with Parity Checklist + Source mapping
|
||||
- [x] 1.2 Author this `tasks.md`
|
||||
- [x] 1.3 Author `specs/crypto/spec.md` ADDED Requirement pinning the byte-identical wire format
|
||||
- [ ] 1.5 **GATE — user sign-off on the API/wire-format invariants** before any implementation work
|
||||
- [ ] 1.6 `openspec change validate chain-bundle-adopt-envelope-schema --strict` exits clean
|
||||
|
||||
## 2. Package metadata
|
||||
|
||||
- [ ] 2.1 Add `@suaveplan/bitpacker-crypto: workspace:^` to `packages/crypto/crypto/package.json` `dependencies`
|
||||
- [ ] 2.2 Update `packages/crypto/crypto/vite.config.ts` externals: add `"@suaveplan/bitpacker-crypto"` and `/^@suaveplan\/bitpacker-crypto\//`
|
||||
- [ ] 2.3 Run `bun install` — lockfile updates clean
|
||||
- [ ] 2.4 Run `bun scripts/folder-reorg/generate-manifest.ts` — manifest regenerates clean (acyclic graph confirmed)
|
||||
|
||||
## 3. chain-bundle.ts refactor
|
||||
|
||||
- [ ] 3.1 Drop the inline `HEADER_SCHEMA = defineBitSchema(...)` declaration and the `ChainBundleHeaderRaw` interface
|
||||
- [ ] 3.2 Add `HEADER_BODY_SCHEMA = defineBitSchema<{ linkCount: number }>([{name: "linkCount", type: "uint", bits: 8}])`
|
||||
- [ ] 3.3 Add `CHAIN_BUNDLE_ENVELOPE = defineEnvelopeSchema<{ linkCount: number }>({ magic: CHAIN_BUNDLE_MAGIC, formatVersion: CHAIN_BUNDLE_FORMAT_VERSION, bodyCodec: { pack: HEADER_BODY_SCHEMA.pack, unpack: HEADER_BODY_SCHEMA.unpack } })`
|
||||
- [ ] 3.4 Update `CHAIN_BUNDLE_HEADER_BYTE_SIZE = CHAIN_BUNDLE_MAGIC.length + 1 + HEADER_BODY_SCHEMA.byteSize` (must equal 6)
|
||||
- [ ] 3.5 Refactor `packChainBundle` to use `CHAIN_BUNDLE_ENVELOPE.pack({ linkCount })` for the header, then concat link bytes
|
||||
- [ ] 3.6 Refactor `parseChainBundle` to use `CHAIN_BUNDLE_ENVELOPE.parseStrict(headerBytes)` for the header. Wrap in try/catch that maps `BitpackerError CRYPTO_ENVELOPE_MAGIC_MISMATCH` → `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.bad-magic`, and `BitpackerError CRYPTO_ENVELOPE_VERSION_MISMATCH` → `CryptoError CHAIN.WRONG_FORMAT crypto.chain-bundle.unsupported-version` (with `context.version: actual`).
|
||||
- [ ] 3.7 Preserve all other code paths verbatim: `linkCount` range check, `expectedTotal` length check, `verifyChain` walk-and-verify.
|
||||
- [ ] 3.8 No public API change — `packChainBundle`, `parseChainBundle`, `verifyChain`, `CHAIN_BUNDLE_MAGIC`, `CHAIN_BUNDLE_FORMAT_VERSION`, `MAX_CHAIN_LINKS`, `CHAIN_BUNDLE_HEADER_BYTE_SIZE`, `ChainBundle`, `IsRevokedPredicate`, `VerifyChainOptions` all remain identical.
|
||||
|
||||
## 4. Snapshot test for byte-identical wire format
|
||||
|
||||
- [ ] 4.1 Add a test in `chain-bundle.test.ts` that builds a fixed 1-link chain (deterministic keychain seed, deterministic nonce) and asserts the exact bytes of the resulting `bundleBytes`
|
||||
- [ ] 4.2 The snapshot bytes MUST be captured BEFORE the refactor (so the test pins the pre-refactor format) and PASS unchanged AFTER the refactor
|
||||
|
||||
## 5. 5-gate verification
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/crypto` — all existing 265 tests + the new snapshot test pass green
|
||||
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/crypto` — chain-bundle.ts coverage stays 100/100/100/100
|
||||
- [ ] 5.3 `bunx turbo build --filter=...@suaveplan/crypto` — every dependent compiles green
|
||||
- [ ] 5.4 Grep guard: `! grep -rE 'from "@suaveplan/crypto"' packages/bitpacker/bitpacker-crypto/src/ --include="*.ts" | grep -v "\\.test\\.ts"` — zero matches (cycle did not regress)
|
||||
- [ ] 5.5 `bun scripts/folder-reorg/generate-manifest.ts` — manifest regenerates clean
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [ ] 6.1 Generics threaded — `EnvelopeSchema<{ linkCount: number }>` is properly typed
|
||||
- [ ] 6.2 Zod schemas wired at every declared boundary — envelope validates options
|
||||
- [ ] 6.3 No new error codes introduced (this is a pure-refactor change)
|
||||
- [ ] 6.4 Strict-mode behaviors tested — all four error paths (bad magic, bad version, too short, length mismatch) covered by existing tests
|
||||
- [ ] 6.5 OTel spans emitted from production paths — envelope-pack / envelope-parse spans inherit from EnvelopeSchema
|
||||
- [ ] 6.6 Lifecycle dedup tested where claimed — N/A (pack is pure)
|
||||
- [ ] 6.7 No retroactive tick — every box reflects work done in this change
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold `specs/crypto/spec.md` ADDED Requirement into `openspec/specs/crypto/spec.md`
|
||||
- [ ] 7.2 Move `openspec/changes/chain-bundle-adopt-envelope-schema/` → `openspec/archive/<yyyy-mm-dd>-chain-bundle-adopt-envelope-schema/`
|
||||
- [ ] 7.3 Update CLAUDE.md (if needed) — note that `crypto` now depends on `bitpacker-crypto` at runtime
|
||||
|
||||
## 8. Follow-up changes (queue, not done in this change)
|
||||
|
||||
- [ ] 8.1 `public-bundle-adopt-envelope-schema` — migrate `crypto::PublicBundle` to use `EnvelopeSchema` + `SignedBitSchema` (same pattern, larger because it does both signing and envelope framing)
|
||||
Reference in New Issue
Block a user