Initial Commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,150 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: bitpacker-crypto (extract-shared-crypto-interfaces)
|
||||
|
||||
**Package**: `@suaveplan/bitpacker-crypto`
|
||||
**Status**: PROPOSED — gated on user sign-off of the API shape
|
||||
|
||||
> Modifies the four DI-using primitives shipped by the archived
|
||||
> `bitpacker-crypto-rewrite` (2026-05-02) to accept `verifier` and
|
||||
> `aead` via dependency injection. No wire-format change.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: SignedBitSchema integrates with Authority + DomainTag (verifier injected)
|
||||
|
||||
The `defineSignedBitSchema<T>(bodyCodec, options)` function SHALL
|
||||
accept a required `verifier: AuthorityVerifier` field on its options
|
||||
bundle. The primitive SHALL call `verifier(domain, parentPublicSlot,
|
||||
body, signature)` instead of importing `verifyAuthoritySignature`
|
||||
directly. All other behavior (verify-then-decode order, `unpackSelf`
|
||||
trust-anchor flow, cross-domain replay rejection, `CRYPTO_VERIFY_FAILED`
|
||||
on tamper) remains identical to the archived `bitpacker-crypto-rewrite`
|
||||
spec.
|
||||
|
||||
The primitive SHALL import `Authority`, `AuthorityVerifier`, `DomainTag`,
|
||||
`KeySlotPublic`, and `SlotId` types from `@suaveplan/types` instead of
|
||||
from `@suaveplan/crypto/authority` and `@suaveplan/crypto/domain-tags`.
|
||||
|
||||
#### Scenario: missing `verifier` rejects at construction time
|
||||
|
||||
- **WHEN** `defineSignedBitSchema(codec, { domain: D })` is called
|
||||
WITHOUT a `verifier` field
|
||||
- **THEN** the Zod options-validation throws AND no schema instance is
|
||||
returned
|
||||
|
||||
#### Scenario: DI'd verifier produces same boolean answer as direct import
|
||||
|
||||
- **WHEN** `defineSignedBitSchema(codec, { domain, verifier:
|
||||
verifyAuthoritySignature })` packs a payload and the result is
|
||||
`unpack`-ed
|
||||
- **THEN** the round-trip matches the original AND the verify path
|
||||
emits the same OTel span as the archived rewrite
|
||||
|
||||
#### Scenario: bitpacker-crypto's compiled `dist/` contains zero references to `@suaveplan/crypto`
|
||||
|
||||
- **WHEN** `bunx turbo build --filter=@suaveplan/bitpacker-crypto` runs
|
||||
- **THEN** `grep -r "@suaveplan/crypto"
|
||||
packages/bitpacker/bitpacker-crypto/dist/` returns zero matches in
|
||||
`*.js` and `*.d.ts` files
|
||||
|
||||
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.test.ts::"missing verifier rejects at construction time"` AND `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.test.ts::"pack signs via Authority under the configured domain"` AND `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.spec-conformance.test.ts::"signed-pack emits OTel span with domain attribute"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MultiSignedBitSchema accepts injected verifier
|
||||
|
||||
The `defineMultiSignedBitSchema<T>(bodyCodec, options)` function SHALL
|
||||
accept a required `verifier: AuthorityVerifier` field on its options
|
||||
bundle. The primitive SHALL call `verifier(...)` for each signature
|
||||
verification instead of importing `verifyAuthoritySignature`. Wire
|
||||
format, count enforcement, `firstBadSignerIndex` reporting,
|
||||
`CRYPTO_MULTISIGN_COUNT_MISMATCH` semantics — all unchanged.
|
||||
|
||||
#### Scenario: missing `verifier` rejects at construction time
|
||||
|
||||
- **WHEN** `defineMultiSignedBitSchema(codec, { domain: D, signerCount:
|
||||
2 })` is called WITHOUT a `verifier` field
|
||||
- **THEN** the Zod options-validation throws
|
||||
|
||||
#### Scenario: DI'd verifier preserves firstBadSignerIndex semantics
|
||||
|
||||
- **WHEN** a 3-signer payload's second signature is flipped and
|
||||
`verify(bytes, [pubA, pubB, pubC])` is called with the DI'd
|
||||
`verifyAuthoritySignature`
|
||||
- **THEN** the result is `{ valid: false, firstBadSignerIndex: 1 }`
|
||||
identical to the archived rewrite
|
||||
|
||||
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/multisigned/multisigned-bitschema.test.ts::"missing verifier rejects at construction time"` AND `packages/bitpacker/bitpacker-crypto/src/multisigned/multisigned-bitschema.test.ts::"verify pinpoints which signer failed"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: HashChainSchema accepts injected verifier
|
||||
|
||||
The `defineHashChainSchema<TPayload>(options)` function SHALL accept
|
||||
a required `verifier: AuthorityVerifier` field on its options bundle.
|
||||
`verifyEntry` and `verifyChain` SHALL use the DI'd verifier. All
|
||||
chain-walk, prev-hash linkage, tamper detection (`firstBadIndex`),
|
||||
pluggable hash registry behavior — all unchanged.
|
||||
|
||||
#### Scenario: missing `verifier` rejects at construction time
|
||||
|
||||
- **WHEN** `defineHashChainSchema({ domain: D, payloadCodec: c })` is
|
||||
called WITHOUT a `verifier` field
|
||||
- **THEN** the Zod options-validation throws
|
||||
|
||||
#### Scenario: chain verification detects tamper at exact index with DI'd verifier
|
||||
|
||||
- **WHEN** a 5-entry chain is built using the DI'd verifier, then
|
||||
entry-2's payload is mutated and `verifyChain` is called
|
||||
- **THEN** the result is `{ valid: false, firstBadIndex: 2 }` identical
|
||||
to the archived rewrite
|
||||
|
||||
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/hashchain/hashchain-schema.test.ts::"missing verifier rejects at construction time"` AND `packages/bitpacker/bitpacker-crypto/src/hashchain/hashchain-schema.test.ts::"chain verification detects tamper at exact index"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: EncryptedBitSchema accepts injected AEAD operations
|
||||
|
||||
The `defineEncryptedBitSchema<T>(bodyCodec, options)` function SHALL
|
||||
accept a required `aead: AeadOps` field on its options bundle. The
|
||||
primitive SHALL call `aead.encrypt(...)` and `aead.decrypt(...)`
|
||||
instead of importing `aeadEncrypt` / `aeadDecrypt` directly.
|
||||
|
||||
The constants `AEAD_KEY_LENGTH = 32`, `AEAD_NONCE_LENGTH = 12`, and
|
||||
`AEAD_XCHACHA_NONCE_LENGTH = 24` SHALL be declared locally in
|
||||
`encrypted-bitschema.ts` (not imported from `@suaveplan/crypto`).
|
||||
Duplication is acceptable because these values are part of the
|
||||
bit-packer-crypto wire format.
|
||||
|
||||
The primitive SHALL import `AeadAlgorithm`, `AeadOps`, `Rng` from
|
||||
`@suaveplan/types`. Wire format `[version u8][algTag u8][nonceLen u8]
|
||||
[nonce][ciphertext + tag]` and the closed `ENCRYPTED_ALG_TAGS` enum
|
||||
remain unchanged.
|
||||
|
||||
#### Scenario: missing `aead` rejects at construction time
|
||||
|
||||
- **WHEN** `defineEncryptedBitSchema(codec, { key })` is called WITHOUT
|
||||
an `aead` field
|
||||
- **THEN** the Zod options-validation throws
|
||||
|
||||
#### Scenario: DI'd AEAD bundle preserves round-trip semantics
|
||||
|
||||
- **WHEN** `defineEncryptedBitSchema(codec, { key, aead: { encrypt:
|
||||
aeadEncrypt, decrypt: aeadDecrypt } })` packs and unpacks a payload
|
||||
- **THEN** the round-trip matches the original AND tampered ciphertext
|
||||
rejects with `CRYPTO_DECRYPT_FAILED` identical to the archived
|
||||
rewrite
|
||||
|
||||
#### Scenario: bitpacker-crypto's `package.json` lists `@suaveplan/crypto` only as devDependency
|
||||
|
||||
- **WHEN** `cat
|
||||
packages/bitpacker/bitpacker-crypto/package.json | jq -r
|
||||
'.dependencies | keys[]'` runs
|
||||
- **THEN** the output does NOT include `@suaveplan/crypto`
|
||||
- **AND** `cat packages/bitpacker/bitpacker-crypto/package.json | jq -r
|
||||
'.devDependencies | keys[]'` DOES include `@suaveplan/crypto`
|
||||
|
||||
**Verified by:** `packages/bitpacker/bitpacker-crypto/src/encrypted/encrypted-bitschema.test.ts::"missing aead rejects at construction time"` AND `packages/bitpacker/bitpacker-crypto/src/encrypted/encrypted-bitschema.test.ts::"round-trips through XChaCha20-Poly1305"` AND `packages/bitpacker/bitpacker-crypto/src/encrypted/encrypted-bitschema.test.ts::"tampered ciphertext rejects with CRYPTO_DECRYPT_FAILED"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,161 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: types (extract-shared-crypto-interfaces)
|
||||
|
||||
**Package**: `@suaveplan/types`
|
||||
**Status**: PROPOSED — gated on user sign-off of the API shape
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line at authoring time.
|
||||
> Each requirement is grounded in `AGENTS.md §0` rule 5 and the four
|
||||
> consumer call-sites in `@suaveplan/bitpacker-crypto`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: DomainTag is a branded type alias
|
||||
|
||||
The package SHALL expose `type DomainTag = string & { readonly __brand:
|
||||
"DomainTag" }`. The brand is a phantom type — it exists in the type
|
||||
system only and has no runtime representation. Consumers cannot
|
||||
construct a `DomainTag` from an arbitrary string at the type level;
|
||||
they MUST import a named constant (e.g. `DOMAIN_VAULT_V1`,
|
||||
`DOMAIN_PUBLIC_KEYCHAIN_V1`) from `@suaveplan/crypto/domain-tags`.
|
||||
The closed list of registered tag VALUES and the `encodeDomainTag` /
|
||||
`buildSigningInput` runtime helpers stay in `@suaveplan/crypto`.
|
||||
|
||||
#### Scenario: type-only import compiles without dragging in `@suaveplan/crypto`
|
||||
|
||||
- **WHEN** a package that does NOT depend on `@suaveplan/crypto`
|
||||
imports `DomainTag` from `@suaveplan/types`
|
||||
- **THEN** TypeScript compilation succeeds AND the dep graph remains
|
||||
acyclic AND the package's bundled output contains zero references to
|
||||
`@suaveplan/crypto`
|
||||
|
||||
#### Scenario: arbitrary string is not assignable to DomainTag
|
||||
|
||||
- **WHEN** a caller writes `const t: DomainTag = "free-form"`
|
||||
- **THEN** TypeScript reports a type error (the brand prevents implicit
|
||||
conversion from `string`)
|
||||
|
||||
#### Scenario: zero-runtime preserved
|
||||
|
||||
- **WHEN** the `@suaveplan/types` package is built and the per-file
|
||||
byte-budget guard runs
|
||||
- **THEN** the compiled `dist/domain-tag.js` file is ≤ 200 bytes
|
||||
(the existing `tests/zero-runtime.test.ts` budget)
|
||||
|
||||
**Verified by:** `packages/core/types/tests/zero-runtime.test.ts::"every .js file under dist/ is within the stub-size budget"` AND `packages/core/types/tests/cross-package-compat.test.ts::"DomainTag is structurally a branded string"` AND `packages/core/types/src/domain-tag.md` (documents the brand semantics)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Authority interface owns the cross-package signing contract
|
||||
|
||||
The package SHALL expose `interface Authority` with five members:
|
||||
`sign(domain: DomainTag, slotId: SlotId, payload: Uint8Array, signal?:
|
||||
AbortSignal): Promise<Uint8Array>`, `getPublicSlot(slotId: SlotId):
|
||||
KeySlotPublic<unknown> | undefined`, `hasSlot(slotId: SlotId):
|
||||
boolean`, `dispose(): void`, and `readonly disposed: boolean`. The
|
||||
package SHALL also expose `KeySlotPublic<TLayoutFields = unknown>`,
|
||||
`KeySlotPrivate<TLayoutFields = unknown>` (extends `KeySlotPublic`),
|
||||
and `type SlotId = string`. The `coordinate` field on `KeySlotPublic`
|
||||
is typed as `unknown` because its concrete shape lives in
|
||||
`@suaveplan/crypto`. The `_phantomLayout?: TLayoutFields` marker on
|
||||
`KeySlotPublic` carries the generic so callers can preserve typed
|
||||
layout fields.
|
||||
|
||||
#### Scenario: `@suaveplan/crypto::InMemoryAuthority` satisfies the interface
|
||||
|
||||
- **WHEN** the existing `InMemoryAuthority` class in
|
||||
`@suaveplan/crypto/authority` declares `implements Authority` against
|
||||
the interface from `@suaveplan/types`
|
||||
- **THEN** TypeScript structural compatibility check passes WITHOUT any
|
||||
shape change to `InMemoryAuthority`'s methods
|
||||
|
||||
#### Scenario: `bitpacker-crypto`'s `SignedBitSchema` accepts an `Authority` from `@suaveplan/types`
|
||||
|
||||
- **WHEN** `bitpacker-crypto::defineSignedBitSchema` declares its
|
||||
`pack(data, authority: Authority, ...)` parameter using the
|
||||
`Authority` import from `@suaveplan/types`
|
||||
- **THEN** a caller passing an `InMemoryAuthority` instance compiles
|
||||
AND the call-site has no transitive dep on `@suaveplan/crypto` for
|
||||
the type itself
|
||||
|
||||
#### Scenario: `KeySlotPublic.coordinate` typed as `unknown` keeps the contract narrow
|
||||
|
||||
- **WHEN** a verifier reads `slot.coordinate` from a `KeySlotPublic<unknown>`
|
||||
- **THEN** TypeScript requires the verifier to narrow the value before
|
||||
using it (no implicit `any`); the verifier is forced to acknowledge
|
||||
it is reading an opaque field
|
||||
|
||||
**Verified by:** `packages/core/types/tests/cross-package-compat.test.ts::"Authority interface is structurally satisfied by InMemoryAuthority"` AND `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.ts` (imports `Authority` from `@suaveplan/types`) AND `packages/core/types/src/authority.md` (documents the contract)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AuthorityVerifier function-type for DI
|
||||
|
||||
The package SHALL expose `type AuthorityVerifier = (domain: DomainTag,
|
||||
publicSlot: KeySlotPublic<unknown>, payload: Uint8Array, signature:
|
||||
Uint8Array) => boolean`. Returns `true` when the signature verifies,
|
||||
`false` for bytes that simply don't match. Implementations MAY throw
|
||||
on structural boundary errors (wrong-length signature, wrong-length
|
||||
public key, unsupported algorithm); primitives that need a boolean
|
||||
answer SHOULD swallow such throws and treat them as `false`.
|
||||
|
||||
#### Scenario: `verifyAuthoritySignature` from `@suaveplan/crypto` is assignable to `AuthorityVerifier`
|
||||
|
||||
- **WHEN** a caller writes `const v: AuthorityVerifier =
|
||||
verifyAuthoritySignature` (where `verifyAuthoritySignature` is the
|
||||
canonical implementation in `@suaveplan/crypto/authority`)
|
||||
- **THEN** TypeScript accepts the assignment with no shape adapter
|
||||
needed
|
||||
|
||||
#### Scenario: a `bitpacker-crypto` primitive's `verifier` option accepts the canonical impl
|
||||
|
||||
- **WHEN** `defineSignedBitSchema(codec, { domain, verifier:
|
||||
verifyAuthoritySignature })` is constructed
|
||||
- **THEN** TypeScript accepts the configuration AND the primitive's
|
||||
`unpack` / `verify` paths produce the same boolean answers as if the
|
||||
primitive had imported `verifyAuthoritySignature` directly
|
||||
|
||||
**Verified by:** `packages/core/types/tests/cross-package-compat.test.ts::"verifyAuthoritySignature is assignable to AuthorityVerifier"` AND `packages/bitpacker/bitpacker-crypto/src/signed/signed-bitschema.test.ts::"pack signs via Authority under the configured domain"` (uses the DI'd verifier end-to-end)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AeadOps interface for DI'd AEAD operations
|
||||
|
||||
The package SHALL expose `interface AeadOps` with two members:
|
||||
`encrypt(algorithm: AeadAlgorithm, key: Uint8Array, plaintext:
|
||||
Uint8Array, options: AeadOpsOptions): Uint8Array` and `decrypt(algorithm,
|
||||
key, ciphertext, options): Uint8Array`. The package SHALL expose
|
||||
`type AeadAlgorithm = "AES-256-GCM" | "ChaCha20-Poly1305" |
|
||||
"XChaCha20-Poly1305"` and `interface AeadOpsOptions { readonly nonce:
|
||||
Uint8Array; readonly aad?: Uint8Array }`. Both methods are synchronous
|
||||
because the canonical `@suaveplan/crypto::aeadEncrypt` / `aeadDecrypt`
|
||||
are synchronous.
|
||||
|
||||
#### Scenario: canonical `aeadEncrypt` / `aeadDecrypt` are assignable to `AeadOps`
|
||||
|
||||
- **WHEN** a caller writes `const ops: AeadOps = { encrypt: aeadEncrypt,
|
||||
decrypt: aeadDecrypt }` (where the right-hand-side functions come
|
||||
from `@suaveplan/crypto`)
|
||||
- **THEN** TypeScript accepts the assignment with no shape adapter
|
||||
needed
|
||||
|
||||
#### Scenario: `bitpacker-crypto::EncryptedBitSchema` accepts the canonical AEAD bundle
|
||||
|
||||
- **WHEN** `defineEncryptedBitSchema(codec, { key, aead: { encrypt:
|
||||
aeadEncrypt, decrypt: aeadDecrypt } })` is constructed and a payload
|
||||
is round-tripped
|
||||
- **THEN** the payload decrypts to the original value AND the primitive
|
||||
has no transitive dep on `@suaveplan/crypto`
|
||||
|
||||
#### Scenario: `AeadAlgorithm` discriminator matches `@suaveplan/crypto`'s union
|
||||
|
||||
- **WHEN** the three string literals (`"AES-256-GCM"`,
|
||||
`"ChaCha20-Poly1305"`, `"XChaCha20-Poly1305"`) are used in the type
|
||||
position of `AeadAlgorithm`
|
||||
- **THEN** they are byte-identical to the strings accepted by
|
||||
`@suaveplan/crypto::aeadEncrypt`'s first parameter
|
||||
|
||||
**Verified by:** `packages/core/types/tests/cross-package-compat.test.ts::"aeadEncrypt+aeadDecrypt are assignable to AeadOps"` AND `packages/bitpacker/bitpacker-crypto/src/encrypted/encrypted-bitschema.test.ts::"round-trips through XChaCha20-Poly1305"` (uses the DI'd AEAD end-to-end)
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,99 @@
|
||||
# Tasks — Extract shared crypto interfaces
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. No
|
||||
> retroactive ticking. Implementation is gated on user sign-off of the
|
||||
> API shape (§1.5).
|
||||
|
||||
## 1. Spec authoring & sign-off
|
||||
|
||||
- [x] 1.1 Author `proposal.md` with audit-revised consumer list and parity checklist
|
||||
- [x] 1.2 Author this `tasks.md`
|
||||
- [x] 1.3 Author `specs/types/spec.md` with one Requirement per added type
|
||||
- [x] 1.4 Author `specs/bitpacker-crypto/spec.md` with the MODIFIED requirements (DI options)
|
||||
- [ ] 1.5 **GATE — user sign-off on the API shape** before any further implementation work
|
||||
- [ ] 1.6 `openspec change validate extract-shared-crypto-interfaces --strict` exits clean
|
||||
|
||||
## 2. `@suaveplan/types` — add Authority/DomainTag/AeadOps interfaces
|
||||
|
||||
- [ ] 2.1 Author `packages/core/types/src/domain-tag.ts` — branded `DomainTag` type + sibling `domain-tag.md` (≥200 words, basic + advanced examples)
|
||||
- [ ] 2.2 Author `packages/core/types/src/authority.ts` — `Authority`, `KeySlotPublic<T>`, `KeySlotPrivate<T>`, `SlotId`, `AuthorityVerifier` + sibling `authority.md`
|
||||
- [ ] 2.3 Author `packages/core/types/src/aead-ops.ts` — `AeadAlgorithm`, `AeadOpsOptions`, `AeadOps` + sibling `aead-ops.md`
|
||||
- [ ] 2.4 Wire all three into `packages/core/types/src/index.ts` (alphabetical insertion)
|
||||
- [ ] 2.5 Run `bunx turbo lint typecheck test --filter=@suaveplan/types` — verify green and zero-runtime byte-budget guard still passes (no `.js` file > 200 bytes)
|
||||
|
||||
## 3. `@suaveplan/crypto` — re-export types from `@suaveplan/types`
|
||||
|
||||
- [ ] 3.1 Edit `packages/crypto/crypto/src/domain-tags/domain-tags.ts`: drop the local `DomainTag` declaration; re-export from `@suaveplan/types`. Keep all `DOMAIN_*` runtime constants and the `encodeDomainTag` / `buildSigningInput` / `isDomainTag` helpers.
|
||||
- [ ] 3.2 Edit `packages/crypto/crypto/src/authority/authority.ts`: drop the local `Authority`, `KeySlotPublic`, `SlotId` declarations; re-export from `@suaveplan/types`. Keep `InMemoryAuthority`, `inMemoryAuthority`, and `verifyAuthoritySignature` runtime helpers. Cast `slotId` to `PurposeTag` at the indexing site (one-line cast — type contract unchanged at the public surface).
|
||||
- [ ] 3.3 `packages/crypto/crypto/src/authority/index.ts` re-exports the canonical types from `@suaveplan/types` so `@suaveplan/crypto/authority` subpath continues to expose `Authority` / `KeySlotPublic` / `SlotId`.
|
||||
- [ ] 3.4 Run `bunx turbo lint typecheck test --filter=@suaveplan/crypto` — 265/265 tests stay green
|
||||
- [ ] 3.5 Verify no API regression: `grep -rE "import.*Authority.*from.*@suaveplan/crypto" /home/ubuntu/web/genesis/packages/` finds the same call-sites compiling unchanged
|
||||
|
||||
## 4. `@suaveplan/bitpacker-crypto` — drop crypto runtime dep, accept verifier+aead via DI
|
||||
|
||||
- [ ] 4.1 Edit `packages/bitpacker/bitpacker-crypto/package.json`: move `@suaveplan/crypto` from `dependencies` to `devDependencies`. Update package description.
|
||||
- [ ] 4.2 Edit `vite.config.ts` externals: drop `"@suaveplan/crypto"` and `/^@suaveplan\/crypto\//` entries
|
||||
- [ ] 4.3 Refactor `src/signed/signed-bitschema.ts`:
|
||||
- Import `Authority`, `AuthorityVerifier`, `DomainTag`, `KeySlotPublic`, `SlotId` from `@suaveplan/types`
|
||||
- Add required `verifier: AuthorityVerifier` to `SignedBitSchemaOptions`
|
||||
- Add `verifier` to Zod options schema
|
||||
- Replace `verifyAuthoritySignature(...)` calls with `verifier(...)`
|
||||
- [ ] 4.4 Refactor `src/multisigned/multisigned-bitschema.ts` — same shape
|
||||
- [ ] 4.5 Refactor `src/hashchain/hashchain-schema.ts` — same shape
|
||||
- [ ] 4.6 Refactor `src/encrypted/encrypted-bitschema.ts`:
|
||||
- Import `AeadAlgorithm`, `AeadOps`, `Rng` from `@suaveplan/types`
|
||||
- Move `AEAD_KEY_LENGTH`, `AEAD_NONCE_LENGTH`, `AEAD_XCHACHA_NONCE_LENGTH` constant declarations into this file (drop the `@suaveplan/crypto` import)
|
||||
- Add required `aead: AeadOps` to `EncryptedBitSchemaOptions`
|
||||
- Add `aead` to Zod options schema
|
||||
- Replace `aeadEncrypt(...)` / `aeadDecrypt(...)` with `aead.encrypt(...)` / `aead.decrypt(...)`
|
||||
- [ ] 4.7 Author `tests/helpers/crypto-deps.ts`: exports `cryptoVerifier: AuthorityVerifier` (= `verifyAuthoritySignature`) and `cryptoAead: AeadOps` (= `{ encrypt: aeadEncrypt, decrypt: aeadDecrypt }`). Single test-side import point for `@suaveplan/crypto` runtime functions.
|
||||
- [ ] 4.8 Update `tests/helpers/fake-authority.ts`: types from `@suaveplan/types`; runtime helpers (`buildSigningInput`, `ed25519`) continue from `@suaveplan/crypto`
|
||||
- [ ] 4.9 Update every `*.test.ts` and `*.spec-conformance.test.ts` that constructs a primitive: import `cryptoVerifier` / `cryptoAead` from `tests/helpers/crypto-deps.ts` and pass into the options bundle
|
||||
- `src/signed/signed-bitschema.test.ts` (16 sites)
|
||||
- `src/signed/signed-bitschema.spec-conformance.test.ts` (4 sites)
|
||||
- `src/multisigned/multisigned-bitschema.test.ts` (11 sites)
|
||||
- `src/hashchain/hashchain-schema.test.ts` (13 sites)
|
||||
- `src/encrypted/encrypted-bitschema.test.ts` (17 sites)
|
||||
- `src/encrypted/encrypted-bitschema.spec-conformance.test.ts` (5 sites)
|
||||
- `src/composition.test.ts` (6 sites)
|
||||
- `src/coverage.test.ts` (17 sites)
|
||||
- [ ] 4.10 Update sibling `.md` docs to reflect new DI options:
|
||||
- `src/signed/signed-bitschema.md` — `verifier` shown in basic + advanced examples
|
||||
- `src/multisigned/multisigned-bitschema.md` — `verifier` shown
|
||||
- `src/hashchain/hashchain-schema.md` — `verifier` shown
|
||||
- `src/encrypted/encrypted-bitschema.md` — `aead` shown; AEAD constants documented as locally-declared
|
||||
- Root `README.md` quick-start examples updated
|
||||
|
||||
## 5. Verification — 5-gate
|
||||
|
||||
- [ ] 5.1 `bun run check` — Biome 0/0/0 across the monorepo
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/types` — green; zero-runtime byte-budget guard satisfied
|
||||
- [ ] 5.3 `bunx turbo lint typecheck test --filter=@suaveplan/crypto` — 265/265 stays green
|
||||
- [ ] 5.4 `bunx turbo lint typecheck test --filter=@suaveplan/bitpacker-crypto` — green
|
||||
- [ ] 5.5 `bunx turbo test:coverage --filter=@suaveplan/bitpacker-crypto` — 100/100/100/100
|
||||
- [ ] 5.6 `bunx turbo build --filter=...@suaveplan/bitpacker-crypto` — every dependent compiles green
|
||||
- [ ] 5.7 `bun scripts/folder-reorg/generate-manifest.ts` — dep graph stays acyclic; manifest regenerates clean
|
||||
- [ ] 5.8 Grep guard: `! grep -rE 'from "@suaveplan/crypto"|from "@suaveplan/crypto/' packages/bitpacker/bitpacker-crypto/src/ --include="*.ts" | grep -v "\.test\.ts"` — zero matches in production source
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [ ] 6.1 Generics threaded — `Authority`, `KeySlotPublic<T>`, `AuthorityVerifier`, `AeadOps` all stay generic-friendly through the refactor
|
||||
- [ ] 6.2 Zod schemas wired at every declared boundary — 4 primitive options bundles updated to validate the new `verifier`/`aead` field
|
||||
- [ ] 6.3 Every error code touched in this change is still thrown by ≥1 production code path
|
||||
- [ ] 6.4 Strict-mode behaviors tested both on and off where applicable (DI rejection on missing `verifier` covered by Zod boundary)
|
||||
- [ ] 6.5 OTel spans emitted from production paths; asserted with the existing in-process recording tracer
|
||||
- [ ] 6.6 Lifecycle dedup tested where claimed
|
||||
- [ ] 6.7 No retroactive tick — every box reflects work done in this change
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold `specs/types/spec.md` into `openspec/specs/types/spec.md`
|
||||
- [ ] 7.2 Fold `specs/bitpacker-crypto/spec.md` into `openspec/specs/bitpacker-crypto/spec.md`
|
||||
- [ ] 7.3 Move `openspec/changes/extract-shared-crypto-interfaces/` → `openspec/archive/<yyyy-mm-dd>-extract-shared-crypto-interfaces/`
|
||||
- [ ] 7.4 STATUS.md update: refactor landed; cycle eliminated; `chain-bundle-adopt-envelope-schema` and `public-bundle-adopt-envelope-schema` follow-ups unblocked
|
||||
- [ ] 7.5 Update `CLAUDE.md` package listing for `@suaveplan/types` (note new shared crypto interfaces) and `@suaveplan/bitpacker-crypto` (note required DI options)
|
||||
|
||||
## 8. Follow-up changes (queue, not done in this change)
|
||||
|
||||
- [ ] 8.1 `chain-bundle-adopt-envelope-schema` — migrate `crypto::ChainBundle` to use `EnvelopeSchema` (now unblocked); wire format byte-identical pinned by snapshot test
|
||||
- [ ] 8.2 `public-bundle-adopt-envelope-schema` — migrate `crypto::PublicBundle` to use `EnvelopeSchema` + `SignedBitSchema` (now unblocked)
|
||||
Reference in New Issue
Block a user