Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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 -->

View File

@@ -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 -->