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,90 @@
# restore-crypto-opinionated-layer — build the missing modules in `@suaveplan/crypto`
**Packages:** `@suaveplan/crypto`
**Status:** PROPOSED
**Type:** feature (additive; no breaking changes to existing modules)
**Carved from:** `restore-crypto-keychain-packer` umbrella (2026-05-03)
**Depends on:** `restore-crypto-cleanup-duplicates` and `restore-crypto-fold-keys-vault` must land first
## Why
After slices 1 and 2, `@suaveplan/crypto` holds the keychain, derivation, bundles, domain-tags, authority shell, errors, and folded keypair / BIP39 / legacy-Vault surface. The big gap is the *behavioural* opinionated layer — the actual signing hierarchy, signed envelopes, and append-only structures that turn primitives into a coherent product. Without these modules, the package is a label without a substance.
This slice ships the seven missing modules:
1. `Authority` — concrete in-memory backend behind the existing interface; signs under a `DerivationCoordinate` slot, rejects revoked slots, enforces validity windows, zeroises on `dispose()`
2. `RootIdentity` / `ServerIdentity` and `createRoot` / `createServer` / `rollServer` / `revokeServer` — typed wrappers around the keychain pair and the hierarchy operations that were modelled by upstream `nx/libs/keys/src/lib/keys-common.ts`
3. `Vault` (three-regime) — replaces the transitional `legacy-vault` from slice 2 with the spec-defined three-regime serialisation: public bundle (parent-signed, plaintext), private-key blob (password-AEAD, rotatable via `changePassword` without re-derivation), vault payload (auth-box-sealed under keypair, parent-signed under `DOMAIN_VAULT_V1`)
4. `Packer` / `Unpacker` — schema-driven encrypt-and-sign envelope over `@suaveplan/bitpacker`; `issuedAt` + `nonce` for replay-window protection
5. `ChunkedPacker` / `ChunkedUnpacker` — per-chunk-signed P2P fragmentation; tampered chunks fail at reassembly
6. `RevocationList` — root-signed CRL; append-only; verify-against-anchor
7. `AuditLog` — append-only Merkle log; root-signed root; tamper-detection on past entries
8. `wire-format/` — eight schemas committed with golden-bytes KAT fixtures: `DERIVATION_COORDINATE_SCHEMA`, `KEY_SLOT_PRIVATE_SCHEMA`, `KEY_SLOT_PUBLIC_SCHEMA`, `VAULT_PAYLOAD_SCHEMA`, `PUBLIC_BUNDLE_SCHEMA`, `CHAIN_BUNDLE_SCHEMA`, `CRL_ENTRY_SCHEMA`, `AUDIT_ENTRY_SCHEMA`
Property-based round-trip tests using `fast-check` accompany every schema.
## What changes
### Added — `@suaveplan/crypto/src/`
- `authority/in-memory-authority/` — concrete `InMemoryAuthority` implementing the existing `Authority` interface
- `identity/root-identity/`, `identity/server-identity/`, `identity/operations/` — `createRoot`, `createServer`, `rollServer`, `revokeServer`
- `vault/vault.ts` (three-regime), `vault/vault.types.ts`, `vault/changePassword.ts`, `vault/rotateApplicationData.ts` — replaces `legacy-vault/`
- `packer/packer.ts` (encrypt-and-sign envelope), `packer/replay-window.ts`
- `chunked-packer/chunked-packer.ts`, `chunked-packer/reassembly.ts`
- `revocation/revocation-list.ts`
- `audit-log/audit-log.ts`, `audit-log/merkle.ts`
- `wire-format/{derivation-coordinate,key-slot-private,key-slot-public,vault-payload,public-bundle,chain-bundle,crl-entry,audit-entry}-schema.ts` (eight schemas)
- `test-fixtures/wire-format/*.bin` — eight golden-bytes KAT fixtures
- Co-located `.md` for every implementation file (≥ 200 words, basic + advanced examples, full API reference)
### Modified
- `vault/legacy-vault/` — deleted in this slice once the three-regime `Vault` ships and any internal callers migrate
- `crypto/src/index.ts` — re-exports the new modules
- `crypto/src/errors/codes/codes.ts` — add codes for `PACKER.*`, `CHUNK.*`, `CRL.*`, `AUDIT.*`, `VAULT.PAYLOAD_*`, `AUTHORITY.SLOT_REVOKED`, etc.
### Tests added
- Per-feature `*.test.ts` for happy-path + every documented failure mode
- Per-feature `*.spec-conformance.test.ts` for type-level + observability + lifecycle scenarios
- `tests/wire-format-kat.test.ts` golden-bytes assertion against every committed fixture
- `tests/property-roundtrips.test.ts` — `fast-check` property tests for every pack/unpack pair
- Tampered-vault, tampered-chunk, expired-key, revoked-key, replay-rejection scenarios each pinned by a named `it(...)`
## Out of scope
- `@suaveplan/crypto-cli` package — slice 4
- `@suaveplan/slsa` migration to `Authority` — slice 4
- Import-allowlist conformance gate — slice 4
- HSM / threshold / YubiKey backends for `Authority` (the *interface* allows them; concrete drivers are dedicated future changes)
## Source mapping
The modules in this slice fold the contents of `restore-crypto-keychain-packer/specs/crypto/spec.md` requirements 7 (`Vault`), 8 (`Packer`), 9 (`ChunkedPacker`), 11 (`RevocationList`), 12 (`AuditLog`), 13 (`Authority` concrete backend), 14 (`Hierarchical identity`), 18 (`Wire-format KAT vectors`), and 19 (`Property-based round-trip`).
| Requirement | Umbrella spec source | Notes |
|---|---|---|
| `InMemoryAuthority` | umbrella `### Requirement: Authority — signing abstraction over hierarchical identity` | Concrete backend behind existing interface |
| `RootIdentity` / `ServerIdentity` / hierarchy ops | umbrella `### Requirement: Hierarchical identity — Root and Server` | Verbatim |
| Three-regime `Vault` | umbrella `### Requirement: Vault — three-regime serialisation with BIP39 restore and arbitrary application data` | Replaces transitional legacy-vault |
| `Packer` / `Unpacker` | umbrella `### Requirement: Packer — encrypt-and-sign envelope` | Verbatim |
| `ChunkedPacker` / `ChunkedUnpacker` | umbrella `### Requirement: ChunkedPacker — per-chunk-signed P2P fragmentation` | Verbatim |
| `RevocationList` | umbrella `### Requirement: RevocationList — root-signed CRL` | Verbatim |
| `AuditLog` | umbrella `### Requirement: AuditLog — append-only Merkle log` | Verbatim |
| Wire-format KAT vectors | umbrella `### Requirement: Wire-format KAT vectors` | Eight golden-bytes fixtures |
| Property-based round-trip | umbrella `### Requirement: Property-based round-trip coverage` | fast-check |
## Verification approach
- 100/100/100/100 coverage on every new module
- Per-feature `it(...)` names match the `**Verified by:**` lines in the spec deltas
- KAT fixtures committed and asserted byte-for-byte on every CI run
- Property tests run 100+ iterations per pack/unpack pair
- `bun run check` clean across monorepo
- `bunx turbo lint typecheck test:coverage build --filter=@suaveplan/crypto` green
## Migration impact
- The transitional `vault/legacy-vault/` is deleted in this slice. Any internal code (no out-of-package consumer today) that imported from `@suaveplan/crypto/vault/legacy-vault` migrates to the three-regime `Vault` at `@suaveplan/crypto/vault`.
- No public API breaks for anything else.

View File

@@ -0,0 +1,162 @@
# Capability Spec Delta: crypto (restore-crypto-opinionated-layer)
**Package:** `@suaveplan/crypto`
**Status:** PROPOSED
> Each ADDED requirement carries a `**Verified by:**` line. The full umbrella spec at `restore-crypto-keychain-packer/specs/crypto/spec.md` enumerates per-scenario detail; this slice's spec covers the seven new modules.
## ADDED Requirements
### Requirement: `InMemoryAuthority` concrete backend
The package SHALL ship `InMemoryAuthority`, a concrete implementation of the `Authority` interface that signs under a `DerivationCoordinate` slot, rejects revoked slots, enforces the slot's validity window against the injected `Clock`, and zeroises every secret on `dispose()`.
#### Scenario: signing with an in-window slot succeeds
- **WHEN** a caller asks `authority.sign(domainTag, payload)` against a slot whose `notBefore <= clock() < notAfter`
- **THEN** a verifiable signature is returned
#### Scenario: signing with a revoked slot fails
- **WHEN** the same call is issued against a slot whose `revokedAt` is in the past
- **THEN** `CryptoError(AUTHORITY.SLOT_REVOKED)` is thrown
#### Scenario: dispose zeroises secrets
- **WHEN** `authority.dispose()` is called
- **THEN** every slot's `secretKey` byte buffer is overwritten with zeros and subsequent sign calls throw `AUTHORITY.DISPOSED`
**Verified by:** `src/authority/in-memory-authority/in-memory-authority.test.ts::"sign-with-in-window-slot succeeds"` AND `::"sign-with-revoked-slot fails"` AND `::"dispose-zeroises-secrets"`
### Requirement: Hierarchical identity operations
The package SHALL ship `RootIdentity`, `ServerIdentity`, `createRoot`, `createServer`, `rollServer`, and `revokeServer`. `createServer` produces a server identity signed under the supplied root authority; `rollServer` increments the version, emits a CRL entry for the prior version, and appends an audit-log entry; `revokeServer` appends CRL + audit entries without re-issuing.
#### Scenario: createRoot self-verifies
- **WHEN** `createRoot(rng, clock, options)` runs
- **THEN** the produced root's public bundle verifies under its own sign slot
#### Scenario: rollServer increments version and emits CRL
- **WHEN** `rollServer(rootAuthority, serverIdentity, options)` runs
- **THEN** the new server's version is `prior.version + 1`, a CRL entry exists for `prior.version`, and an audit-log entry exists for the rotation
**Verified by:** `src/identity/operations/operations.test.ts::"createRoot self-verifies"` AND `::"createServer is signed by root"` AND `::"rollServer increments version + emits CRL"` AND `::"revokeServer appends without re-issue"`
### Requirement: Three-regime `Vault` replaces the legacy implementation
The package SHALL ship a three-regime `Vault`: (a) public bundle (parent-signed plaintext), (b) private-key blob (password-AEAD, rotatable via `changePassword` without re-derivation), (c) vault payload (auth-box-sealed under keypair, parent-signed under `DOMAIN_VAULT_V1`, carries `master`/`seedCount`/`keychain`/`data: Uint8Array`). The transitional `legacy-vault/` from slice 2 MUST be deleted in this slice.
#### Scenario: round-trip preserves payload data
- **WHEN** a `Vault` packs a payload containing `data: Uint8Array([1,2,3])` and is then unpacked
- **THEN** `data` round-trips byte-identically
#### Scenario: changePassword does not re-derive the keychain
- **WHEN** a `Vault` is unlocked, `changePassword(newPassword)` is called, and the vault is re-locked then re-unlocked with `newPassword`
- **THEN** the recovered keychain's slots are byte-identical to the original (no re-derivation occurred)
#### Scenario: lock zeroises secrets
- **WHEN** `vault.lock()` is called
- **THEN** every secret byte buffer reachable from the vault is overwritten with zeros
**Verified by:** `src/vault/vault.test.ts::"round-trip preserves payload data"` AND `::"changePassword without re-derive"` AND `::"lock zeroises secrets"` AND `::"BIP39 restore re-derives identical keychain"` AND `::"Argon2id parameter floor enforced"`
### Requirement: `Packer` / `Unpacker` envelope with replay protection
The package SHALL ship `Packer.pack(value, schema, recipientAuthority, senderAuthority, domainTag)` producing an encrypt-then-sign envelope including `issuedAt` and `nonce`. `Unpacker.unpack(envelope, recipientAuthority, senderPublicBundle, domainTag, options)` MUST reject envelopes whose `issuedAt` is older than `options.replayWindowMs` or whose `nonce` was already seen.
#### Scenario: round-trip preserves the value
- **WHEN** Packer.pack then Unpacker.unpack with matching parties and domain tag
- **THEN** the unpacked value is structurally equal to the original
#### Scenario: replay outside window rejected
- **WHEN** an envelope produced 1 hour ago is unpacked with `replayWindowMs = 60_000`
- **THEN** `CryptoError(PACKER.REPLAY_REJECTED)` is thrown
#### Scenario: cross-protocol domain tag rejected
- **WHEN** an envelope packed under `DOMAIN_PACKER_V1` is unpacked with `DOMAIN_VAULT_V1`
- **THEN** `CryptoError(PACKER.DOMAIN_TAG_MISMATCH)` is thrown
**Verified by:** `src/packer/packer.test.ts::"round-trip preserves the value"` AND `::"replay outside window rejected"` AND `::"cross-protocol domain tag rejected"`
### Requirement: `ChunkedPacker` per-chunk-signed fragmentation
The package SHALL ship `ChunkedPacker.packFileToChunks` and `ChunkedUnpacker.reassembleFile`. Each chunk MUST carry an independent signature; tampering with any single chunk's bytes MUST cause reassembly to fail.
#### Scenario: round-trip reassembles byte-identical
- **WHEN** a 10 KiB stream is packed into 4 chunks then reassembled
- **THEN** the reassembled stream is byte-identical to the input
#### Scenario: tampered chunk rejected at reassembly
- **WHEN** any byte of any single chunk is flipped before reassembly
- **THEN** `CryptoError(CHUNK.TAMPERED)` is thrown with the offending chunk index
**Verified by:** `src/chunked-packer/chunked-packer.test.ts::"round-trip reassembles byte-identical"` AND `::"tampered chunk rejected at reassembly"`
### Requirement: `RevocationList` root-signed CRL
The package SHALL ship a `RevocationList` that is append-only, root-signed, and `verify`-able against a root public bundle. Mutating any past entry MUST cause the list-level signature to fail verification.
#### Scenario: append then verify succeeds
- **WHEN** a CRL is appended to and then verified against the issuing root's public bundle
- **THEN** verification succeeds
#### Scenario: mutating a past entry invalidates the list
- **WHEN** any byte of an existing CRL entry is altered post-append
- **THEN** `CryptoError(CRL.MUTATED)` is thrown on next verify
**Verified by:** `src/revocation/revocation-list.test.ts::"append then verify succeeds"` AND `::"mutating a past entry invalidates the list"`
### Requirement: `AuditLog` append-only Merkle log
The package SHALL ship an `AuditLog` whose root is signed by a root authority. Tampering with any past entry MUST invalidate the root signature; an inclusion proof MUST be derivable for any appended entry.
#### Scenario: append then root signature verifies
- **WHEN** events are appended and the log root is signed
- **THEN** `auditLog.verify(rootPublicBundle)` returns true
#### Scenario: tampering with a past entry invalidates the root
- **WHEN** any byte of an appended event is flipped
- **THEN** `auditLog.verify(rootPublicBundle)` throws `CryptoError(AUDIT.ROOT_MISMATCH)`
**Verified by:** `src/audit-log/audit-log.test.ts::"append then root signature verifies"` AND `::"tampering with a past entry invalidates the root"` AND `::"inclusion proof verifies for appended entries"`
### Requirement: Eight wire-format schemas with golden-bytes KAT vectors
The package SHALL define and module-export eight wire-format schemas (`DERIVATION_COORDINATE_SCHEMA`, `KEY_SLOT_PRIVATE_SCHEMA`, `KEY_SLOT_PUBLIC_SCHEMA`, `VAULT_PAYLOAD_SCHEMA`, `PUBLIC_BUNDLE_SCHEMA`, `CHAIN_BUNDLE_SCHEMA`, `CRL_ENTRY_SCHEMA`, `AUDIT_ENTRY_SCHEMA`) and ship a golden-bytes KAT fixture per schema under `test-fixtures/wire-format/`. CI MUST assert byte-for-byte equality between the schema's encoding of canonical inputs and the committed fixtures.
#### Scenario: every schema is module-exported
- **WHEN** the test imports every named schema from `@suaveplan/crypto/wire-format`
- **THEN** every import resolves
#### Scenario: KAT bytes match committed fixtures
- **WHEN** each schema encodes its canonical input
- **THEN** the bytes are equal to the matching `test-fixtures/wire-format/*.bin`
**Verified by:** `tests/wire-format-kat.test.ts::"every wire-format schema is module-exported"` AND `tests/wire-format-kat.test.ts::"KAT bytes match committed fixtures for each schema"`
### Requirement: Property-based round-trip coverage
Every pack/unpack pair in the package SHALL be covered by a `fast-check` property test asserting `unpack(pack(v)) ≡ v` over at least 100 generated inputs per property.
#### Scenario: round-trip is identity for every pack/unpack pair
- **WHEN** a fast-check property runs over 100 generated inputs for any pack/unpack pair
- **THEN** every iteration round-trips structurally
**Verified by:** `tests/property-roundtrips.test.ts::"round-trip identity for keychain"` AND `::"round-trip identity for vault"` AND `::"round-trip identity for public-bundle"` AND `::"round-trip identity for chain-bundle"` AND `::"round-trip identity for packer-envelope"` AND `::"round-trip identity for chunk"` AND `::"round-trip identity for crl-entry"` AND `::"round-trip identity for audit-entry"`

View File

@@ -0,0 +1,111 @@
# Tasks — restore-crypto-opinionated-layer
## 1. Authority concrete backend
- [ ] 1.1 Implement `InMemoryAuthority` against the existing `Authority` interface (sign under `DerivationCoordinate` slot, validity-window check, revoked-slot rejection)
- [ ] 1.2 `dispose()` zeroises every slot's secret material
- [ ] 1.3 Tests: sign-success, sign-with-revoked-slot-fails, sign-with-out-of-window-key-fails, dispose-zeroises, AbortSignal cancellation
- [ ] 1.4 Co-located `.md`
## 2. Identity hierarchy
- [ ] 2.1 `RootIdentity` and `ServerIdentity` typed wrappers
- [ ] 2.2 `createRoot(rng, clock, options)` — generates root keychain + signs its own public bundle
- [ ] 2.3 `createServer(rootAuthority, options)` — generates server keychain, signed under root
- [ ] 2.4 `rollServer(rootAuthority, serverIdentity, options)` — version-bump rotation; emits CRL entry for prior version; appends audit-log entry
- [ ] 2.5 `revokeServer(rootAuthority, serverRef, reason)` — appends CRL + audit entries; no re-issue
- [ ] 2.6 Tests: createRoot self-verifies; createServer parent-signed; rollServer increments version + emits CRL; revokeServer appends without re-issue
- [ ] 2.7 Co-located `.md` per file
## 3. Three-regime Vault
- [ ] 3.1 `Vault` class with three regimes: public bundle (parent-signed plaintext), private-key blob (password-AEAD), vault payload (auth-box-sealed under keypair, parent-signed under `DOMAIN_VAULT_V1`)
- [ ] 3.2 `pack` / `unpack` round-trip
- [ ] 3.3 `changePassword` re-encrypts the private-key blob without re-deriving the keychain
- [ ] 3.4 `rotateApplicationData` re-seals payload only (keys untouched)
- [ ] 3.5 `lock()` zeroises secrets
- [ ] 3.6 BIP39 restore re-derives identical keychain
- [ ] 3.7 Argon2id parameter floor enforced
- [ ] 3.8 Tests: round-trip, tamper-detect, wrong-password-distinct-from-tamper, BIP39-restore, lock-zeroises, Argon2id-floor, changePassword-without-re-derive, payload-data-round-trip, rotateApplicationData-keys-untouched
- [ ] 3.9 Delete `vault/legacy-vault/` after the new `Vault` ships and any internal callers migrate
- [ ] 3.10 Co-located `.md` (≥ 200 words) covering the three regimes
## 4. Packer / Unpacker
- [ ] 4.1 `Packer.pack(value, schema, recipientAuthority, senderAuthority, domainTag)` — encrypt-then-sign envelope
- [ ] 4.2 `Unpacker.unpack(envelope, recipientAuthority, senderPublicBundle, domainTag, options)` — verify-then-decrypt
- [ ] 4.3 Replay-window check via `issuedAt` + `nonce`; rejects envelopes outside window or with seen nonce
- [ ] 4.4 Tests: round-trip, recipient-verifies-sender, tamper-detect, replay-rejection, mismatched-domain-tag-fails
- [ ] 4.5 Co-located `.md`
## 5. ChunkedPacker / ChunkedUnpacker
- [ ] 5.1 `ChunkedPacker.packFileToChunks(stream, recipient, sender, options)` — per-chunk-signed fragmentation
- [ ] 5.2 `ChunkedUnpacker.reassembleFile(chunks, options)` — assemble; rejects on tampered chunk
- [ ] 5.3 Tests: 1-chunk, N-chunk, tampered-chunk-rejected, missing-chunk-rejected, out-of-order-chunks-handled
- [ ] 5.4 Co-located `.md`
## 6. RevocationList
- [ ] 6.1 `RevocationList` with `append(entry, rootAuthority)`, `verify(rootPublicBundle)`, `check(serverRef)`
- [ ] 6.2 Append-only invariant (mutating past entries invalidates the list)
- [ ] 6.3 Tests: append-then-verify, mutate-past-entry-fails, check-revoked-server-true, check-non-revoked-server-false
- [ ] 6.4 Co-located `.md`
## 7. AuditLog
- [ ] 7.1 `AuditLog` append-only Merkle log; root signed by `rootAuthority`
- [ ] 7.2 `append(event)`, `verify(rootPublicBundle)`, `inclusion(eventId)` proof
- [ ] 7.3 Tampering with any past entry invalidates the root signature
- [ ] 7.4 Tests: append-roots-correctly, tamper-past-entry-fails, inclusion-proof-verifies
- [ ] 7.5 Co-located `.md`
## 8. Wire-format schemas + KAT fixtures
- [ ] 8.1 Define `DERIVATION_COORDINATE_SCHEMA`
- [ ] 8.2 Define `KEY_SLOT_PRIVATE_SCHEMA`, `KEY_SLOT_PUBLIC_SCHEMA`
- [ ] 8.3 Define `VAULT_PAYLOAD_SCHEMA`
- [ ] 8.4 Define `PUBLIC_BUNDLE_SCHEMA` (`magic("SVPB") ‖ formatVersion ‖ issuerRef ‖ issuedAt ‖ nonce ‖ subjectRef ‖ slots[] ‖ parentSignature[64]`)
- [ ] 8.5 Define `CHAIN_BUNDLE_SCHEMA` (`magic("SVCB") ‖ formatVersion ‖ linkCount ‖ links[]`)
- [ ] 8.6 Define `CRL_ENTRY_SCHEMA`, `AUDIT_ENTRY_SCHEMA`
- [ ] 8.7 Commit eight golden-bytes fixtures under `test-fixtures/wire-format/`
- [ ] 8.8 `tests/wire-format-kat.test.ts` byte-for-byte assertion
- [ ] 8.9 Co-located `.md` per schema
## 9. Property-based round-trip
- [ ] 9.1 `tests/property-roundtrips.test.ts` using `fast-check`
- [ ] 9.2 Cover every pack/unpack pair: keychain, vault, public bundle, chain bundle, packer envelope, chunk, CRL entry, audit entry
## 10. Error codes
- [ ] 10.1 Add `PACKER.ENCRYPT_FAILED`, `PACKER.SIGN_FAILED`, `PACKER.REPLAY_REJECTED`, `PACKER.DOMAIN_TAG_MISMATCH`
- [ ] 10.2 Add `CHUNK.TAMPERED`, `CHUNK.MISSING`, `CHUNK.OUT_OF_ORDER`
- [ ] 10.3 Add `CRL.NOT_VERIFIED`, `CRL.MUTATED`
- [ ] 10.4 Add `AUDIT.ROOT_MISMATCH`, `AUDIT.INCLUSION_PROOF_FAILED`
- [ ] 10.5 Add `VAULT.PAYLOAD_DECRYPT_FAILED`, `VAULT.PAYLOAD_VERIFY_FAILED`
- [ ] 10.6 Add `AUTHORITY.SLOT_REVOKED`, `AUTHORITY.SLOT_OUT_OF_WINDOW`
- [ ] 10.7 Each code thrown by ≥ 1 production code path (asserted by `tests/codes-thrown.test.ts`)
## 11. Verification gates
- [ ] 11.1 `bun run check` — Biome 0/0/0
- [ ] 11.2 `bunx turbo lint typecheck test --filter=@suaveplan/crypto` green
- [ ] 11.3 `bunx turbo test:coverage --filter=@suaveplan/crypto` 100/100/100/100
- [ ] 11.4 `bunx turbo test --filter=...@suaveplan/crypto` (dependent sweep) green
- [ ] 11.5 OpenSpec conformance script clean
## 12. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 12.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim
- [ ] 12.2 Zod schemas wired at every declared boundary
- [ ] 12.3 Every error code thrown by at least one production path
- [ ] 12.4 Strict-mode behaviours tested both on and off
- [ ] 12.5 OTel instruments emitted from production paths and asserted with a real `MeterProvider`
- [ ] 12.6 Lifecycle dedup tested where claimed (replay-window nonces, CRL append-only)
- [ ] 12.7 No retroactive tick
## 13. Archive
- [ ] 13.1 Fold spec deltas into `openspec/specs/crypto/spec.md`
- [ ] 13.2 Move `openspec/changes/restore-crypto-opinionated-layer/` → `openspec/archive/<yyyy-mm-dd>-restore-crypto-opinionated-layer/`